1use byteorder::{BigEndian, ByteOrder};
2#[cfg(feature = "rust173-polyfill")]
3use disk_core::rust173::IntIsMultipleOf as _;
4use std::cmp::Ordering;
5use std::collections::{HashMap, HashSet};
6use std::io::{Read, Seek, SeekFrom, Write};
7
8use crate::hfs_common::{
9 self, bitmap_clear_bit_be, bitmap_collect_clear_runs_be, bitmap_set_bit_be, bitmap_test_bit_be,
10 btree_detach_freed_node_and_refresh, btree_free_node, btree_grow_blocks, btree_map_chain_tail,
11 btree_refresh_index_keys, btree_remove_record, btree_retire_empty_leaf, BTreeHeader,
12 BTreeKeyFormat,
13};
14use disk_core::entry::{EntryType, FileEntry};
15use disk_core::filesystem::CompactResult;
16use disk_core::filesystem::{
17 CreateDirectoryOptions, CreateFileOptions, EditableFilesystem, Filesystem, FilesystemError,
18};
19
20const HFS_PLUS_SIGNATURE: u16 = 0x482B;
21const HFSX_SIGNATURE: u16 = 0x4858;
22
23const HFSPLUS_EXTENTS_FILE_ID: u32 = 3;
25const HFSPLUS_CATALOG_FILE_ID: u32 = 4;
26const HFSPLUS_ALLOCATION_FILE_ID: u32 = 6;
27const HFSPLUS_ATTRIBUTES_FILE_ID: u32 = 8;
28
29#[derive(Clone, Copy, PartialEq, Eq, Debug)]
34enum BTreeFork {
35 Catalog,
36 Extents,
37 Attributes,
38}
39
40impl BTreeFork {
41 fn file_id(self) -> u32 {
45 match self {
46 BTreeFork::Catalog => HFSPLUS_CATALOG_FILE_ID,
47 BTreeFork::Extents => HFSPLUS_EXTENTS_FILE_ID,
48 BTreeFork::Attributes => HFSPLUS_ATTRIBUTES_FILE_ID,
49 }
50 }
51
52 fn label(self) -> &'static str {
53 match self {
54 BTreeFork::Catalog => "catalog",
55 BTreeFork::Extents => "extents-overflow",
56 BTreeFork::Attributes => "attributes",
57 }
58 }
59}
60
61pub(crate) const HFSPLUS_VOLUME_JOURNALED_BIT: u32 = 0x2000;
65
66pub(crate) const HFSPLUS_VOLUME_UNMOUNTED_BIT: u32 = 0x100;
70
71pub(crate) const HFSPLUS_BOOT_VOLUME_INCONSISTENT_BIT: u32 = 0x800;
75
76pub(crate) const HFSPLUS_VOLUME_INCONSISTENT_BIT: u32 = 0x4000;
81
82const KEY_COMPARE_CASE_FOLDING: u8 = 0xCF;
86const KEY_COMPARE_BINARY: u8 = 0xBC;
87
88const HFSPLUS_FORK_DATA: u8 = 0x00;
90#[allow(dead_code)]
91const HFSPLUS_FORK_RESOURCE: u8 = 0xFF;
92
93pub(crate) const HFSPLUS_ATTR_INLINE_DATA: u32 = 0x10;
95#[allow(dead_code)] const HFSPLUS_ATTR_FORK_DATA: u32 = 0x20;
97#[allow(dead_code)]
98const HFSPLUS_ATTR_EXTENTS: u32 = 0x30;
99
100#[derive(Debug, Clone, Copy)]
102pub(crate) struct ExtentDescriptor {
103 pub(crate) start_block: u32,
104 pub(crate) block_count: u32,
105}
106
107impl ExtentDescriptor {
108 fn parse(data: &[u8]) -> Self {
109 ExtentDescriptor {
110 start_block: BigEndian::read_u32(&data[0..4]),
111 block_count: BigEndian::read_u32(&data[4..8]),
112 }
113 }
114
115 fn is_empty(&self) -> bool {
116 self.block_count == 0
117 }
118
119 fn serialize(&self, out: &mut [u8]) {
120 BigEndian::write_u32(&mut out[0..4], self.start_block);
121 BigEndian::write_u32(&mut out[4..8], self.block_count);
122 }
123}
124
125#[derive(Debug, Clone)]
127pub(crate) struct ForkData {
128 pub(crate) logical_size: u64,
129 #[allow(dead_code)]
133 pub(crate) clump_size: u32,
134 pub(crate) total_blocks: u32,
135 pub(crate) extents: [ExtentDescriptor; 8],
136}
137
138impl ForkData {
139 fn parse(data: &[u8]) -> Self {
140 let mut extents = [ExtentDescriptor {
141 start_block: 0,
142 block_count: 0,
143 }; 8];
144 for i in 0..8 {
145 extents[i] = ExtentDescriptor::parse(&data[16 + i * 8..24 + i * 8]);
146 }
147 ForkData {
148 logical_size: BigEndian::read_u64(&data[0..8]),
149 clump_size: BigEndian::read_u32(&data[8..12]),
150 total_blocks: BigEndian::read_u32(&data[12..16]),
151 extents,
152 }
153 }
154
155 fn serialize(&self, out: &mut [u8]) {
156 BigEndian::write_u64(&mut out[0..8], self.logical_size);
157 BigEndian::write_u32(&mut out[8..12], self.clump_size);
158 BigEndian::write_u32(&mut out[12..16], self.total_blocks);
159 for i in 0..8 {
160 self.extents[i].serialize(&mut out[16 + i * 8..24 + i * 8]);
161 }
162 }
163
164 pub(crate) fn empty() -> Self {
165 ForkData {
166 logical_size: 0,
167 clump_size: 0,
168 total_blocks: 0,
169 extents: [ExtentDescriptor {
170 start_block: 0,
171 block_count: 0,
172 }; 8],
173 }
174 }
175}
176
177#[derive(Debug, Clone)]
184#[allow(dead_code)] pub(crate) struct XattrRecord {
186 pub name: String,
187 pub start_block: u32,
191 pub kind: XattrKind,
192}
193
194#[derive(Debug, Clone)]
195#[allow(dead_code)] pub(crate) enum XattrKind {
197 Inline(Vec<u8>),
200 Fork(ForkData),
203 Extents(Vec<ExtentDescriptor>),
206}
207
208#[derive(Debug, Clone)]
210#[allow(dead_code)]
211struct HfsPlusVolumeHeader {
212 signature: u16,
213 version: u16,
214 attributes: u32,
215 last_mounted_version: u32,
216 journal_info_block: u32,
217 create_date: u32,
218 modify_date: u32,
219 backup_date: u32,
220 checked_date: u32,
221 file_count: u32,
222 folder_count: u32,
223 block_size: u32,
224 total_blocks: u32,
225 free_blocks: u32,
226 next_allocation: u32,
227 rsrc_clump_size: u32,
228 data_clump_size: u32,
229 next_catalog_id: u32,
230 write_count: u32,
231 encodings_bitmap: u64,
232 finder_info: [u32; 8],
233 allocation_file: ForkData,
234 extents_file: ForkData,
235 catalog_file: ForkData,
236 attributes_file: ForkData,
237 startup_file: ForkData,
238}
239
240impl HfsPlusVolumeHeader {
241 fn parse(data: &[u8]) -> Result<Self, FilesystemError> {
242 if data.len() < 512 {
243 return Err(FilesystemError::Parse("volume header too short".into()));
244 }
245 let sig = BigEndian::read_u16(&data[0..2]);
246 if sig != HFS_PLUS_SIGNATURE && sig != HFSX_SIGNATURE {
247 return Err(FilesystemError::Parse(format!(
248 "bad HFS+ volume header signature: 0x{sig:04X}"
249 )));
250 }
251
252 let mut finder_info = [0u32; 8];
253 for i in 0..8 {
254 finder_info[i] = BigEndian::read_u32(&data[80 + i * 4..84 + i * 4]);
255 }
256
257 Ok(HfsPlusVolumeHeader {
258 signature: sig,
259 version: BigEndian::read_u16(&data[2..4]),
260 attributes: BigEndian::read_u32(&data[4..8]),
261 last_mounted_version: BigEndian::read_u32(&data[8..12]),
262 journal_info_block: BigEndian::read_u32(&data[12..16]),
263 create_date: BigEndian::read_u32(&data[16..20]),
264 modify_date: BigEndian::read_u32(&data[20..24]),
265 backup_date: BigEndian::read_u32(&data[24..28]),
266 checked_date: BigEndian::read_u32(&data[28..32]),
267 file_count: BigEndian::read_u32(&data[32..36]),
268 folder_count: BigEndian::read_u32(&data[36..40]),
269 block_size: BigEndian::read_u32(&data[40..44]),
270 total_blocks: BigEndian::read_u32(&data[44..48]),
271 free_blocks: BigEndian::read_u32(&data[48..52]),
272 next_allocation: BigEndian::read_u32(&data[52..56]),
273 rsrc_clump_size: BigEndian::read_u32(&data[56..60]),
274 data_clump_size: BigEndian::read_u32(&data[60..64]),
275 next_catalog_id: BigEndian::read_u32(&data[64..68]),
276 write_count: BigEndian::read_u32(&data[68..72]),
277 encodings_bitmap: BigEndian::read_u64(&data[72..80]),
278 finder_info,
279 allocation_file: ForkData::parse(&data[112..192]),
280 extents_file: ForkData::parse(&data[192..272]),
281 catalog_file: ForkData::parse(&data[272..352]),
282 attributes_file: ForkData::parse(&data[352..432]),
283 startup_file: ForkData::parse(&data[432..512]),
284 })
285 }
286
287 fn serialize(&self) -> [u8; 512] {
288 let mut out = [0u8; 512];
289 BigEndian::write_u16(&mut out[0..2], self.signature);
290 BigEndian::write_u16(&mut out[2..4], self.version);
291 BigEndian::write_u32(&mut out[4..8], self.attributes);
292 BigEndian::write_u32(&mut out[8..12], self.last_mounted_version);
293 BigEndian::write_u32(&mut out[12..16], self.journal_info_block);
294 BigEndian::write_u32(&mut out[16..20], self.create_date);
295 BigEndian::write_u32(&mut out[20..24], self.modify_date);
296 BigEndian::write_u32(&mut out[24..28], self.backup_date);
297 BigEndian::write_u32(&mut out[28..32], self.checked_date);
298 BigEndian::write_u32(&mut out[32..36], self.file_count);
299 BigEndian::write_u32(&mut out[36..40], self.folder_count);
300 BigEndian::write_u32(&mut out[40..44], self.block_size);
301 BigEndian::write_u32(&mut out[44..48], self.total_blocks);
302 BigEndian::write_u32(&mut out[48..52], self.free_blocks);
303 BigEndian::write_u32(&mut out[52..56], self.next_allocation);
304 BigEndian::write_u32(&mut out[56..60], self.rsrc_clump_size);
305 BigEndian::write_u32(&mut out[60..64], self.data_clump_size);
306 BigEndian::write_u32(&mut out[64..68], self.next_catalog_id);
307 BigEndian::write_u32(&mut out[68..72], self.write_count);
308 BigEndian::write_u64(&mut out[72..80], self.encodings_bitmap);
309 for i in 0..8 {
310 BigEndian::write_u32(&mut out[80 + i * 4..84 + i * 4], self.finder_info[i]);
311 }
312 self.allocation_file.serialize(&mut out[112..192]);
313 self.extents_file.serialize(&mut out[192..272]);
314 self.catalog_file.serialize(&mut out[272..352]);
315 self.attributes_file.serialize(&mut out[352..432]);
316 self.startup_file.serialize(&mut out[432..512]);
317 out
318 }
319
320 fn is_hfsx(&self) -> bool {
321 self.signature == HFSX_SIGNATURE
322 }
323}
324
325#[derive(Debug)]
327struct BTreeNodeDescriptor {
328 next: u32,
329 #[allow(dead_code)]
330 prev: u32,
331 kind: i8,
332 #[allow(dead_code)]
333 height: u8,
334 num_records: u16,
335}
336
337impl BTreeNodeDescriptor {
338 fn parse(data: &[u8]) -> Self {
339 BTreeNodeDescriptor {
340 next: BigEndian::read_u32(&data[0..4]),
341 prev: BigEndian::read_u32(&data[4..8]),
342 kind: data[8] as i8,
343 height: data[9],
344 num_records: BigEndian::read_u16(&data[10..12]),
345 }
346 }
347}
348
349#[derive(Debug, Clone)]
351#[allow(dead_code)]
352struct BTreeHeaderRecord {
353 depth: u16,
354 root_node: u32,
355 leaf_records: u32,
356 first_leaf_node: u32,
357 last_leaf_node: u32,
358 node_size: u16,
359 max_key_len: u16,
360 total_nodes: u32,
361 free_nodes: u32,
362 key_compare_type: u8,
368}
369
370impl BTreeHeaderRecord {
371 fn parse(data: &[u8]) -> Self {
372 let key_compare_type = if data.len() >= 38 { data[37] } else { 0 };
377 BTreeHeaderRecord {
378 depth: BigEndian::read_u16(&data[0..2]),
379 root_node: BigEndian::read_u32(&data[2..6]),
380 leaf_records: BigEndian::read_u32(&data[6..10]),
381 first_leaf_node: BigEndian::read_u32(&data[10..14]),
382 last_leaf_node: BigEndian::read_u32(&data[14..18]),
383 node_size: BigEndian::read_u16(&data[18..20]),
384 max_key_len: BigEndian::read_u16(&data[20..22]),
385 total_nodes: BigEndian::read_u32(&data[22..26]),
386 free_nodes: BigEndian::read_u32(&data[26..30]),
387 key_compare_type,
388 }
389 }
390}
391
392const CATALOG_FOLDER: i16 = 1;
394const CATALOG_FILE: i16 = 2;
395
396fn validate_hfsplus_create_name(name: &str) -> Result<(), FilesystemError> {
398 if name.is_empty() {
399 return Err(FilesystemError::InvalidData(
400 "filename is empty — pick a non-blank name".into(),
401 ));
402 }
403 if name.contains(':') {
404 return Err(FilesystemError::InvalidData(
405 "filename contains ':', which classic Mac OS uses as a path separator — \
406 rename the file (try '-' or '_' instead)"
407 .into(),
408 ));
409 }
410 let utf16_len = fs_mac_unicode::hfs_unicode::decompose_str(name).len();
411 if utf16_len > 255 {
412 return Err(FilesystemError::InvalidData(format!(
413 "filename is too long ({utf16_len} UTF-16 units); HFS+ allows up to 255 — \
414 shorten the name (note: some emoji and rare characters count as 2 units)"
415 )));
416 }
417 Ok(())
418}
419#[allow(dead_code)]
420const CATALOG_FOLDER_THREAD: i16 = 3;
421#[allow(dead_code)]
422const CATALOG_FILE_THREAD: i16 = 4;
423
424#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
436pub(crate) struct HfsPlusBsdInfo {
437 pub(crate) owner_id: u32,
438 pub(crate) group_id: u32,
439 pub(crate) file_mode: u16,
440}
441
442impl HfsPlusBsdInfo {
443 pub(crate) const OFFSET: usize = 32;
445
446 fn parse(rec: &[u8]) -> Self {
448 if rec.len() < Self::OFFSET + 16 {
449 return Self::default();
450 }
451 Self {
452 owner_id: BigEndian::read_u32(&rec[Self::OFFSET..Self::OFFSET + 4]),
453 group_id: BigEndian::read_u32(&rec[Self::OFFSET + 4..Self::OFFSET + 8]),
454 file_mode: BigEndian::read_u16(&rec[Self::OFFSET + 10..Self::OFFSET + 12]),
457 }
458 }
459
460 fn is_set(&self) -> bool {
462 self.file_mode != 0
463 }
464
465 fn apply_to(&self, fe: &mut FileEntry) {
468 if !self.is_set() {
469 return;
470 }
471 fe.mode = Some(self.file_mode as u32);
472 fe.uid = Some(self.owner_id);
473 fe.gid = Some(self.group_id);
474 }
475}
476
477#[derive(Debug, Clone)]
479#[allow(clippy::large_enum_variant)] enum CatalogEntry {
481 Folder {
482 folder_id: u32,
483 name: String,
484 content_mod_date: u32,
487 bsd: HfsPlusBsdInfo,
488 },
489 File {
490 file_id: u32,
491 name: String,
492 data_size: u64,
493 data_fork: ForkData,
494 rsrc_size: u64,
495 rsrc_fork: ForkData,
496 type_code: [u8; 4],
499 creator_code: [u8; 4],
500 finder_flags: u16,
502 link_inode_num: Option<u32>,
507 dir_link_inode_num: Option<u32>,
513 content_mod_date: u32,
516 bsd: HfsPlusBsdInfo,
517 },
518}
519
520const HFSPLUS_HARDLINK_FILE_TYPE: &[u8; 4] = b"hlnk";
522const HFSPLUS_HARDLINK_FILE_CREATOR: &[u8; 4] = b"hfs+";
524const HFSPLUS_HARDLINK_DIR_TYPE: &[u8; 4] = b"fdrp";
526const HFSPLUS_HARDLINK_DIR_CREATOR: &[u8; 4] = b"MACS";
528
529pub(crate) fn catalog_compare_keys(a: &[u8], b: &[u8], case_sensitive: bool) -> Ordering {
533 if a.len() < 8 || b.len() < 8 {
534 return a.len().cmp(&b.len());
535 }
536 let parent_a = BigEndian::read_u32(&a[2..6]);
537 let parent_b = BigEndian::read_u32(&b[2..6]);
538 let name_len_a = BigEndian::read_u16(&a[6..8]) as usize;
539 let name_len_b = BigEndian::read_u16(&b[6..8]) as usize;
540 let name_a: Vec<u16> = a[8..8 + name_len_a.min((a.len() - 8) / 2) * 2]
541 .chunks_exact(2)
542 .map(BigEndian::read_u16)
543 .collect();
544 let name_b: Vec<u16> = b[8..8 + name_len_b.min((b.len() - 8) / 2) * 2]
545 .chunks_exact(2)
546 .map(BigEndian::read_u16)
547 .collect();
548 hfs_common::compare_hfsplus_keys(parent_a, &name_a, parent_b, &name_b, case_sensitive)
549}
550
551fn hfsplus_private_dir_name() -> String {
556 let mut s = String::with_capacity(4 + 17);
557 for _ in 0..4 {
558 s.push('\u{0}');
559 }
560 s.push_str("HFS+ Private Data");
561 s
562}
563
564fn hfsplus_dir_private_dir_name() -> String {
568 let mut s = String::with_capacity(28 + 1);
569 s.push_str(".HFS+ Private Directory Data");
570 s.push('\u{D}');
571 s
572}
573
574pub struct HfsPlusFilesystem<R: Read + Seek> {
576 reader: R,
577 partition_offset: u64,
578 partition_size: Option<u64>,
581 vh: HfsPlusVolumeHeader,
582 catalog_data: Vec<u8>,
584 catalog_header: BTreeHeaderRecord,
586 label: String,
588 bitmap: Option<Vec<u8>>,
590 extents_overflow_data: Option<Vec<u8>>,
593 attributes_data: Option<Vec<u8>>,
597 xattr_cnids: Option<HashSet<u32>>,
603 hardlink_inode_map: Option<HashMap<u32, u32>>,
610 dir_hardlink_inode_map: Option<HashMap<u32, u32>>,
613 pending_boot_blocks: Option<Box<[u8; 1024]>>,
619}
620
621#[derive(Debug, Clone)]
630pub struct HfsPlusSnapshot {
631 vh: HfsPlusVolumeHeader,
632 catalog_data: Vec<u8>,
633 catalog_header: BTreeHeaderRecord,
634 bitmap: Option<Vec<u8>>,
635 extents_overflow_data: Option<Vec<u8>>,
636 attributes_data: Option<Vec<u8>>,
637 xattr_cnids: Option<HashSet<u32>>,
638 hardlink_inode_map: Option<HashMap<u32, u32>>,
639 dir_hardlink_inode_map: Option<HashMap<u32, u32>>,
640}
641
642impl<R: Read + Seek> HfsPlusFilesystem<R> {
643 pub fn open(reader: R, partition_offset: u64) -> Result<Self, FilesystemError> {
644 Self::open_sized(reader, partition_offset, None)
645 }
646
647 pub fn open_sized(
650 mut reader: R,
651 partition_offset: u64,
652 partition_size: Option<u64>,
653 ) -> Result<Self, FilesystemError> {
654 reader.seek(SeekFrom::Start(partition_offset + 1024))?;
656 let mut vh_buf = [0u8; 512];
657 reader.read_exact(&mut vh_buf)?;
658 let vh = HfsPlusVolumeHeader::parse(&vh_buf)?;
659
660 if vh.block_size == 0 || !vh.block_size.is_power_of_two() {
661 return Err(FilesystemError::Parse(format!(
662 "invalid block size: {}",
663 vh.block_size
664 )));
665 }
666
667 let volume_bytes = vh.total_blocks as u64 * vh.block_size as u64;
671 if vh.catalog_file.logical_size > volume_bytes {
672 return Err(FilesystemError::Parse(format!(
673 "catalog file logical_size {} exceeds volume size {}",
674 vh.catalog_file.logical_size, volume_bytes
675 )));
676 }
677 if vh.extents_file.logical_size > volume_bytes {
678 return Err(FilesystemError::Parse(format!(
679 "extents-overflow file logical_size {} exceeds volume size {}",
680 vh.extents_file.logical_size, volume_bytes
681 )));
682 }
683
684 log::debug!(
685 "[HFS+ open @ {partition_offset}] vh ok: block_size={}, total_blocks={} ({} bytes), \
686 catalog={} bytes, extents_file={} bytes, alloc_file={} bytes",
687 vh.block_size,
688 vh.total_blocks,
689 volume_bytes,
690 vh.catalog_file.logical_size,
691 vh.extents_file.logical_size,
692 vh.allocation_file.logical_size,
693 );
694
695 log::debug!("[HFS+ open] reading extents-overflow file...");
702 let extents_overflow_data = if vh.extents_file.logical_size > 0 {
703 Some(read_fork(
704 &mut reader,
705 partition_offset,
706 vh.block_size,
707 &vh.extents_file,
708 )?)
709 } else {
710 None
711 };
712 log::debug!(
713 "[HFS+ open] extents-overflow loaded: {} bytes",
714 extents_overflow_data.as_ref().map(|d| d.len()).unwrap_or(0)
715 );
716
717 log::debug!("[HFS+ open] reading catalog file...");
718 let catalog_data = read_fork_with_overflow(
719 &mut reader,
720 partition_offset,
721 vh.block_size,
722 &vh.catalog_file,
723 HFSPLUS_CATALOG_FILE_ID,
724 HFSPLUS_FORK_DATA,
725 extents_overflow_data.as_deref(),
726 )?;
727 log::debug!("[HFS+ open] catalog loaded: {} bytes", catalog_data.len());
728
729 if catalog_data.len() < 14 + 106 {
731 return Err(FilesystemError::Parse(
732 "catalog file too small for B-tree header".into(),
733 ));
734 }
735 let catalog_header = BTreeHeaderRecord::parse(&catalog_data[14..14 + 106]);
736 log::debug!(
737 "[HFS+ open] catalog btree: depth={}, root_node={}, first_leaf={}, last_leaf={}, \
738 node_size={}, total_nodes={}, free_nodes={}",
739 catalog_header.depth,
740 catalog_header.root_node,
741 catalog_header.first_leaf_node,
742 catalog_header.last_leaf_node,
743 catalog_header.node_size,
744 catalog_header.total_nodes,
745 catalog_header.free_nodes,
746 );
747
748 log::debug!("[HFS+ open] scanning catalog for volume label...");
750 let label = find_volume_label(&catalog_data, &catalog_header);
751 log::debug!("[HFS+ open] volume label: {:?}", label);
752
753 Ok(HfsPlusFilesystem {
754 reader,
755 partition_offset,
756 partition_size,
757 vh,
758 catalog_data,
759 catalog_header,
760 label,
761 bitmap: None,
762 extents_overflow_data,
763 attributes_data: None,
764 xattr_cnids: None,
765 hardlink_inode_map: None,
766 dir_hardlink_inode_map: None,
767 pending_boot_blocks: None,
768 })
769 }
770
771 #[allow(dead_code)] pub(crate) fn partition_offset(&self) -> u64 {
778 self.partition_offset
779 }
780
781 pub fn block_size(&self) -> u32 {
782 self.vh.block_size
783 }
784
785 pub(crate) fn total_blocks(&self) -> u32 {
786 self.vh.total_blocks
787 }
788
789 pub(crate) fn signature(&self) -> u16 {
790 self.vh.signature
791 }
792
793 pub(crate) fn vh_attributes(&self) -> u32 {
794 self.vh.attributes
795 }
796
797 pub fn is_journaled(&self) -> bool {
799 self.vh.attributes & HFSPLUS_VOLUME_JOURNALED_BIT != 0
800 }
801
802 fn locate_journal(
811 &mut self,
812 ) -> Result<
813 Option<(
814 crate::hfsplus_journal::JournalInfoBlock,
815 crate::hfsplus_journal::JournalHeader,
816 )>,
817 FilesystemError,
818 > {
819 use crate::hfsplus_journal::{JournalHeader, JournalInfoBlock};
820
821 if !self.is_journaled() {
822 return Ok(None);
823 }
824 let to_fs = |e: crate::hfsplus_journal::JournalError| {
825 FilesystemError::InvalidData(format!("journal: {e}"))
826 };
827
828 let jib_byte =
830 self.partition_offset + self.vh.journal_info_block as u64 * self.vh.block_size as u64;
831 self.reader.seek(SeekFrom::Start(jib_byte))?;
832 let mut jib_buf = vec![0u8; 512];
833 self.reader.read_exact(&mut jib_buf)?;
834 let jib = JournalInfoBlock::parse(&jib_buf).map_err(to_fs)?;
835
836 self.reader
838 .seek(SeekFrom::Start(self.partition_offset + jib.offset))?;
839 let mut jh_buf = vec![0u8; 512];
840 self.reader.read_exact(&mut jh_buf)?;
841 let header = JournalHeader::parse(&jh_buf).map_err(to_fs)?;
842 Ok(Some((jib, header)))
843 }
844
845 fn metadata_regions(&self) -> Vec<crate::hfsplus_journal::MetadataRegion> {
848 use crate::hfsplus_journal::MetadataRegion;
849 let bs = self.vh.block_size as u64;
850 let mut regions = vec![MetadataRegion {
851 name: "volume header",
852 start: 1024,
853 end: 1536,
854 }];
855 let fork = |name: &'static str, fork: &ForkData| -> Option<MetadataRegion> {
856 let first = fork.extents.iter().find(|e| !e.is_empty())?;
857 let start = first.start_block as u64 * bs;
858 let bytes = fork.total_blocks as u64 * bs;
859 Some(MetadataRegion {
860 name,
861 start,
862 end: start + bytes,
863 })
864 };
865 for (name, f) in [
866 ("allocation bitmap", &self.vh.allocation_file),
867 ("extents btree", &self.vh.extents_file),
868 ("catalog btree", &self.vh.catalog_file),
869 ("attributes btree", &self.vh.attributes_file),
870 ("startup file", &self.vh.startup_file),
871 ] {
872 if let Some(r) = fork(name, f) {
873 regions.push(r);
874 }
875 }
876 regions
877 }
878
879 pub fn journal_detail(
883 &mut self,
884 ) -> Result<Option<crate::hfsplus_journal::JournalDetail>, FilesystemError> {
885 let Some((jib, header)) = self.locate_journal()? else {
886 return Ok(None);
887 };
888 let regions = self.metadata_regions();
889 let detail = crate::hfsplus_journal::read_journal_detail(
890 &mut self.reader,
891 self.partition_offset,
892 jib,
893 header,
894 regions,
895 );
896 Ok(Some(detail))
897 }
898
899 pub fn read_journal(
900 &mut self,
901 ) -> Result<Option<crate::hfsplus_journal::JournalState>, FilesystemError> {
902 use crate::hfsplus_journal::JournalWalker;
903
904 let Some((jib, header)) = self.locate_journal()? else {
905 return Ok(None);
906 };
907
908 let mut transactions = Vec::new();
909 let mut partial = false;
910 let mut checksum_mismatch = None;
911 let mut sequence_jumps = Vec::new();
912 {
913 let mut walker =
914 JournalWalker::new(&mut self.reader, self.partition_offset, &jib, &header);
915 let mut prev_seq: Option<u32> = None;
916 while let Some(item) = walker.next() {
917 match item {
918 Ok(view) => {
919 let seq = view.sequence_num;
920 if let Some(prev) = prev_seq {
921 if seq != prev && seq != prev.wrapping_add(1) {
924 sequence_jumps.push((prev, seq));
925 }
926 }
927 prev_seq = Some(seq);
928 transactions.push(view.summary());
929 }
930 Err(crate::hfsplus_journal::JournalError::CorruptTransaction {
931 sequence_num,
932 }) => {
933 checksum_mismatch = Some(sequence_num);
934 partial = true;
935 break;
936 }
937 Err(_) => {
938 partial = true;
939 break;
940 }
941 }
942 }
943 }
944
945 Ok(Some(crate::hfsplus_journal::JournalState {
946 info: jib,
947 header,
948 transactions,
949 partial,
950 checksum_mismatch,
951 sequence_jumps,
952 }))
953 }
954
955 pub(crate) fn vh_create_date(&self) -> u32 {
956 self.vh.create_date
957 }
958
959 pub(crate) fn vh_modify_date(&self) -> u32 {
960 self.vh.modify_date
961 }
962
963 pub(crate) fn vh_backup_date(&self) -> u32 {
964 self.vh.backup_date
965 }
966
967 pub(crate) fn vh_checked_date(&self) -> u32 {
968 self.vh.checked_date
969 }
970
971 pub(crate) fn vh_file_count(&self) -> u32 {
972 self.vh.file_count
973 }
974
975 pub(crate) fn vh_folder_count(&self) -> u32 {
976 self.vh.folder_count
977 }
978
979 pub(crate) fn vh_finder_info(&self) -> [u32; 8] {
980 self.vh.finder_info
981 }
982
983 pub(crate) fn vh_free_blocks(&self) -> u32 {
984 self.vh.free_blocks
985 }
986
987 #[allow(dead_code)] pub(crate) fn vh_next_catalog_id(&self) -> u32 {
989 self.vh.next_catalog_id
990 }
991
992 pub(crate) fn read_allocation_bitmap_for_fsck(&mut self) -> Result<Vec<u8>, FilesystemError> {
996 self.read_allocation_bitmap()
997 }
998
999 pub(crate) fn vh_catalog_total_blocks(&self) -> u32 {
1004 self.vh.catalog_file.total_blocks
1005 }
1006
1007 pub(crate) fn vh_extents_total_blocks(&self) -> u32 {
1011 self.vh.extents_file.total_blocks
1012 }
1013
1014 pub(crate) fn vh_attributes_total_blocks(&self) -> u32 {
1017 self.vh.attributes_file.total_blocks
1018 }
1019
1020 pub(crate) fn label(&self) -> &str {
1021 &self.label
1022 }
1023
1024 pub(crate) fn catalog_data(&self) -> &[u8] {
1025 &self.catalog_data
1026 }
1027
1028 pub(crate) fn catalog_node_size(&self) -> usize {
1029 self.catalog_header.node_size as usize
1030 }
1031
1032 pub(crate) fn catalog_first_leaf(&self) -> u32 {
1033 self.catalog_header.first_leaf_node
1034 }
1035
1036 pub(crate) fn extents_overflow_data(&self) -> Option<&[u8]> {
1038 self.extents_overflow_data.as_deref()
1039 }
1040
1041 pub(crate) fn partition_size(&self) -> Option<u64> {
1043 self.partition_size
1044 }
1045
1046 pub(crate) fn attributes_data_for_fsck(&mut self) -> Result<Option<&[u8]>, FilesystemError> {
1048 if self.vh.attributes_file.logical_size == 0 {
1049 return Ok(None);
1050 }
1051 self.ensure_attributes_loaded()?;
1052 Ok(self.attributes_data.as_deref())
1053 }
1054
1055 pub(crate) fn case_sensitive_catalog(&self) -> bool {
1056 self.case_sensitive()
1057 }
1058
1059 pub(crate) fn read_boot_blocks(&mut self) -> Result<[u8; 1024], FilesystemError> {
1062 self.reader.seek(SeekFrom::Start(self.partition_offset))?;
1063 let mut buf = [0u8; 1024];
1064 self.reader.read_exact(&mut buf)?;
1065 Ok(buf)
1066 }
1067
1068 pub(crate) fn fork_stream_reader(
1074 &mut self,
1075 file_id: u32,
1076 fork_type: u8,
1077 fork_record: &[u8; 80],
1078 ) -> Result<ForkStreamReader<'_, R>, FilesystemError> {
1079 let fork = ForkData::parse(fork_record);
1080 let logical_size = fork.logical_size;
1081
1082 let block_size = self.vh.block_size as u64;
1086 let mut extents: Vec<(u64, u64)> = Vec::new();
1087 let mut covered: u64 = 0;
1088 for ext in fork.extents.iter() {
1089 if ext.block_count == 0 {
1090 continue;
1091 }
1092 let off = self.partition_offset + ext.start_block as u64 * block_size;
1093 let len = ext.block_count as u64 * block_size;
1094 extents.push((off, len));
1095 covered += len;
1096 }
1097 if covered < logical_size {
1098 let inline_blocks: u32 = fork.extents.iter().map(|e| e.block_count).sum();
1099 let overflow = match self.extents_overflow_data.as_deref() {
1100 Some(data) => {
1101 collect_hfsplus_overflow_extents(data, file_id, fork_type, inline_blocks)
1102 }
1103 None => {
1104 return Err(FilesystemError::InvalidData(format!(
1105 "file {file_id} fork {fork_type:#x}: {} bytes requested but only \
1106 {} bytes in inline extents and no extents-overflow B-tree available",
1107 logical_size, covered
1108 )));
1109 }
1110 };
1111 for ext in overflow {
1112 if ext.block_count == 0 {
1113 continue;
1114 }
1115 let off = self.partition_offset + ext.start_block as u64 * block_size;
1116 let len = ext.block_count as u64 * block_size;
1117 extents.push((off, len));
1118 covered += len;
1119 if covered >= logical_size {
1120 break;
1121 }
1122 }
1123 if covered < logical_size {
1124 return Err(FilesystemError::InvalidData(format!(
1125 "file {file_id} fork {fork_type:#x}: extents (inline + overflow) \
1126 cover {covered} of {logical_size} bytes"
1127 )));
1128 }
1129 }
1130
1131 Ok(ForkStreamReader {
1132 reader: &mut self.reader,
1133 extents,
1134 logical_size,
1135 pos: 0,
1136 cur_extent: 0,
1137 cur_extent_consumed: 0,
1138 })
1139 }
1140
1141 pub fn snapshot(&self) -> HfsPlusSnapshot {
1145 HfsPlusSnapshot {
1146 vh: self.vh.clone(),
1147 catalog_data: self.catalog_data.clone(),
1148 catalog_header: self.catalog_header.clone(),
1149 bitmap: self.bitmap.clone(),
1150 extents_overflow_data: self.extents_overflow_data.clone(),
1151 attributes_data: self.attributes_data.clone(),
1152 xattr_cnids: self.xattr_cnids.clone(),
1153 hardlink_inode_map: self.hardlink_inode_map.clone(),
1154 dir_hardlink_inode_map: self.dir_hardlink_inode_map.clone(),
1155 }
1156 }
1157
1158 pub fn restore_snapshot(&mut self, snap: HfsPlusSnapshot) {
1162 self.vh = snap.vh;
1163 self.catalog_data = snap.catalog_data;
1164 self.catalog_header = snap.catalog_header;
1165 self.bitmap = snap.bitmap;
1166 self.extents_overflow_data = snap.extents_overflow_data;
1167 self.attributes_data = snap.attributes_data;
1168 self.xattr_cnids = snap.xattr_cnids;
1169 self.hardlink_inode_map = snap.hardlink_inode_map;
1170 self.dir_hardlink_inode_map = snap.dir_hardlink_inode_map;
1171 }
1172
1173 pub fn prepare_for_edit(&mut self) -> Result<(), FilesystemError> {
1191 if self.vh.attributes & HFSPLUS_VOLUME_JOURNALED_BIT != 0 {
1192 match self.locate_journal() {
1193 Ok(Some((_jib, header))) if !header.is_empty() => {
1194 return Err(FilesystemError::Unsupported(
1195 "dirty journaled HFS+ volume — mount and cleanly unmount it in macOS \
1196 to flush the journal, then retry (or open read-only)"
1197 .into(),
1198 ));
1199 }
1200 Ok(_) => {
1201 }
1204 Err(e) => {
1205 return Err(FilesystemError::Unsupported(format!(
1206 "journaled HFS+ volume with an unreadable journal ({e}) — \
1207 open read-only"
1208 )));
1209 }
1210 }
1211 }
1212
1213 if self.vh.attributes_file.logical_size == 0 {
1216 self.xattr_cnids = Some(HashSet::new());
1217 return Ok(());
1218 }
1219
1220 let (attr_data, attr_header) = self
1223 .ensure_attributes_loaded()?
1224 .expect("attributes_file.logical_size > 0 must produce a header (checked just above)");
1225 let node_size = attr_header.node_size as usize;
1226 let mut cnids: HashSet<u32> = HashSet::new();
1227 hfs_common::walk_leaf_records::<(), _>(
1228 attr_data,
1229 attr_header.first_leaf_node,
1230 node_size,
1231 |_node_idx, _rec_idx, _abs_off, rec_bytes| {
1232 if rec_bytes.len() < 2 {
1233 return None;
1234 }
1235 let key_len = BigEndian::read_u16(&rec_bytes[0..2]) as usize;
1236 if key_len < 6 || 2 + 6 > rec_bytes.len() {
1238 return None;
1239 }
1240 let file_id = BigEndian::read_u32(&rec_bytes[4..8]);
1241 cnids.insert(file_id);
1242 None
1243 },
1244 );
1245 log::debug!(
1246 "[HFS+ open_editable] attributes B-tree carries records for {} CNIDs",
1247 cnids.len()
1248 );
1249 self.xattr_cnids = Some(cnids);
1250 Ok(())
1251 }
1252
1253 fn ensure_attributes_loaded(
1260 &mut self,
1261 ) -> Result<Option<(&[u8], BTreeHeaderRecord)>, FilesystemError> {
1262 if self.vh.attributes_file.logical_size == 0 {
1263 return Ok(None);
1264 }
1265 if self.attributes_data.is_none() {
1266 let data = read_fork_with_overflow(
1267 &mut self.reader,
1268 self.partition_offset,
1269 self.vh.block_size,
1270 &self.vh.attributes_file,
1271 HFSPLUS_ATTRIBUTES_FILE_ID,
1272 HFSPLUS_FORK_DATA,
1273 self.extents_overflow_data.as_deref(),
1274 )?;
1275 if data.len() < 14 + 106 {
1276 return Err(FilesystemError::Parse(
1277 "attributes file too small for B-tree header".into(),
1278 ));
1279 }
1280 self.attributes_data = Some(data);
1281 }
1282 let data = self.attributes_data.as_deref().expect("just populated");
1283 let header = BTreeHeaderRecord::parse(&data[14..14 + 106]);
1284 Ok(Some((data, header)))
1285 }
1286
1287 #[allow(dead_code)] pub(crate) fn list_xattrs(&mut self, cnid: u32) -> Result<Vec<XattrRecord>, FilesystemError> {
1303 let Some((data, header)) = self.ensure_attributes_loaded()? else {
1304 return Ok(Vec::new());
1305 };
1306 let node_size = header.node_size as usize;
1307 let first_leaf = header.first_leaf_node;
1308 let mut out: Vec<XattrRecord> = Vec::new();
1309 hfs_common::walk_leaf_records::<(), _>(
1310 data,
1311 first_leaf,
1312 node_size,
1313 |_node_idx, _rec_idx, _abs_off, rec_bytes| {
1314 if rec_bytes.len() < 2 {
1315 return None;
1316 }
1317 let key_len = BigEndian::read_u16(&rec_bytes[0..2]) as usize;
1318 if key_len < 12 || 2 + key_len > rec_bytes.len() {
1320 return None;
1321 }
1322 let file_id = BigEndian::read_u32(&rec_bytes[4..8]);
1323 if file_id != cnid {
1324 return None;
1325 }
1326 let start_block = BigEndian::read_u32(&rec_bytes[8..12]);
1327 let name_len = BigEndian::read_u16(&rec_bytes[12..14]) as usize;
1328 let name_bytes_end = 14 + name_len * 2;
1329 if name_bytes_end > 2 + key_len {
1330 return None;
1331 }
1332 let name = decode_utf16be(&rec_bytes[14..name_bytes_end]);
1333
1334 let data_off = 2 + key_len;
1338 if data_off + 4 > rec_bytes.len() {
1339 return None;
1340 }
1341 let record_type = BigEndian::read_u32(&rec_bytes[data_off..data_off + 4]);
1342 let kind = match record_type {
1343 HFSPLUS_ATTR_INLINE_DATA => {
1344 let header_end = data_off + 4 + 8 + 4;
1346 if header_end > rec_bytes.len() {
1347 return None;
1348 }
1349 let attr_size =
1350 BigEndian::read_u32(&rec_bytes[header_end - 4..header_end]) as usize;
1351 if header_end + attr_size > rec_bytes.len() {
1352 return None;
1353 }
1354 XattrKind::Inline(rec_bytes[header_end..header_end + attr_size].to_vec())
1355 }
1356 HFSPLUS_ATTR_FORK_DATA => {
1357 let fork_off = data_off + 4 + 4;
1359 if fork_off + 80 > rec_bytes.len() {
1360 return None;
1361 }
1362 XattrKind::Fork(ForkData::parse(&rec_bytes[fork_off..fork_off + 80]))
1363 }
1364 HFSPLUS_ATTR_EXTENTS => {
1365 let ext_off = data_off + 4 + 4;
1367 if ext_off + 64 > rec_bytes.len() {
1368 return None;
1369 }
1370 let mut extents = Vec::with_capacity(8);
1371 for i in 0..8 {
1372 let e = ExtentDescriptor::parse(
1373 &rec_bytes[ext_off + i * 8..ext_off + (i + 1) * 8],
1374 );
1375 extents.push(e);
1376 }
1377 XattrKind::Extents(extents)
1378 }
1379 _ => {
1380 log::warn!(
1381 "[HFS+ list_xattrs] unknown attribute record type 0x{:x} \
1382 on cnid={} name={:?}; skipping",
1383 record_type,
1384 cnid,
1385 name
1386 );
1387 return None;
1388 }
1389 };
1390 out.push(XattrRecord {
1391 name,
1392 start_block,
1393 kind,
1394 });
1395 None
1396 },
1397 );
1398 Ok(out)
1399 }
1400
1401 fn list_children(&self, parent_cnid: u32) -> Result<Vec<CatalogEntry>, FilesystemError> {
1410 let node_size = self.catalog_header.node_size as usize;
1411 if node_size == 0 {
1412 return Ok(vec![]);
1413 }
1414
1415 let mut results = Vec::new();
1416 let header = hfs_common::BTreeHeader::read(&self.catalog_data);
1422 let search_key = Self::build_catalog_key(parent_cnid, "");
1423 let cs = self.case_sensitive();
1424 let cmp = |a: &[u8], b: &[u8]| Self::catalog_compare(a, b, cs);
1425 let (start_leaf, _chain) =
1426 hfs_common::btree_find_insert_leaf(&self.catalog_data, &header, &search_key, &cmp);
1427 let mut node_idx = if start_leaf != 0 {
1428 start_leaf
1429 } else {
1430 self.catalog_header.first_leaf_node
1431 };
1432 let mut visited: std::collections::HashSet<u32> = std::collections::HashSet::new();
1433 let mut seen_target = false;
1434
1435 'outer: while node_idx != 0 {
1436 if !visited.insert(node_idx) {
1437 break;
1438 }
1439 let offset = node_idx as usize * node_size;
1440 if offset + node_size > self.catalog_data.len() {
1441 break;
1442 }
1443 let node = &self.catalog_data[offset..offset + node_size];
1444
1445 let desc = BTreeNodeDescriptor::parse(node);
1446 if desc.kind != -1 {
1448 node_idx = desc.next;
1449 continue;
1450 }
1451
1452 for i in 0..desc.num_records as usize {
1453 let offset_pos = node_size - 2 * (i + 1);
1455 if offset_pos + 2 > node.len() {
1456 break;
1457 }
1458 let rec_offset = BigEndian::read_u16(&node[offset_pos..offset_pos + 2]) as usize;
1459 if rec_offset + 6 > node.len() {
1460 continue;
1461 }
1462
1463 let key_len = BigEndian::read_u16(&node[rec_offset..rec_offset + 2]) as usize;
1465 if key_len < 6 || rec_offset + 2 + key_len > node.len() {
1466 continue;
1467 }
1468 let key_data = &node[rec_offset + 2..rec_offset + 2 + key_len];
1469
1470 let key_parent_id = BigEndian::read_u32(&key_data[0..4]);
1472 if key_parent_id == parent_cnid {
1473 seen_target = true;
1474 } else if key_parent_id > parent_cnid {
1475 break 'outer;
1482 } else {
1483 if seen_target {
1485 break 'outer;
1487 }
1488 continue;
1489 }
1490
1491 let name_length = BigEndian::read_u16(&key_data[4..6]) as usize;
1492 let name = if name_length > 0 && 6 + name_length * 2 <= key_data.len() {
1493 decode_utf16be(&key_data[6..6 + name_length * 2])
1494 } else {
1495 String::new()
1496 };
1497
1498 let mut rec_data_start = rec_offset + 2 + key_len;
1500 if !rec_data_start.is_multiple_of(2) {
1501 rec_data_start += 1;
1502 }
1503 if rec_data_start + 2 > node.len() {
1504 continue;
1505 }
1506
1507 let record_type = BigEndian::read_i16(&node[rec_data_start..rec_data_start + 2]);
1508 let rec = &node[rec_data_start..];
1509
1510 match record_type {
1511 CATALOG_FOLDER => {
1512 if rec.len() < 88 {
1513 continue;
1514 }
1515 let folder_id = BigEndian::read_u32(&rec[8..12]);
1516 let content_mod_date = BigEndian::read_u32(&rec[20..24]);
1520 results.push(CatalogEntry::Folder {
1521 folder_id,
1522 name,
1523 content_mod_date,
1524 bsd: HfsPlusBsdInfo::parse(rec),
1525 });
1526 }
1527 CATALOG_FILE => {
1528 if rec.len() < 248 {
1529 continue;
1530 }
1531 let file_id = BigEndian::read_u32(&rec[8..12]);
1532 let type_code: [u8; 4] = rec[48..52].try_into().unwrap();
1534 let creator_code: [u8; 4] = rec[52..56].try_into().unwrap();
1535 let finder_flags = BigEndian::read_u16(&rec[56..58]);
1536 let link_inode_num = if &rec[48..52] == HFSPLUS_HARDLINK_FILE_TYPE
1541 && &rec[52..56] == HFSPLUS_HARDLINK_FILE_CREATOR
1542 {
1543 Some(BigEndian::read_u32(&rec[44..48]))
1544 } else {
1545 None
1546 };
1547 let dir_link_inode_num = if &rec[48..52] == HFSPLUS_HARDLINK_DIR_TYPE
1548 && &rec[52..56] == HFSPLUS_HARDLINK_DIR_CREATOR
1549 {
1550 Some(BigEndian::read_u32(&rec[44..48]))
1551 } else {
1552 None
1553 };
1554 let data_fork = ForkData::parse(&rec[88..168]);
1556 let rsrc_fork = ForkData::parse(&rec[168..248]);
1558 let content_mod_date = BigEndian::read_u32(&rec[20..24]);
1561 results.push(CatalogEntry::File {
1562 file_id,
1563 name,
1564 data_size: data_fork.logical_size,
1565 data_fork,
1566 rsrc_size: rsrc_fork.logical_size,
1567 rsrc_fork,
1568 type_code,
1569 creator_code,
1570 finder_flags,
1571 link_inode_num,
1572 dir_link_inode_num,
1573 content_mod_date,
1574 bsd: HfsPlusBsdInfo::parse(rec),
1575 });
1576 }
1577 _ => {}
1578 }
1579 }
1580
1581 node_idx = desc.next;
1582 }
1583
1584 Ok(results)
1585 }
1586
1587 fn ensure_hardlink_inode_map(&mut self) -> Result<(), FilesystemError> {
1592 if self.hardlink_inode_map.is_some() {
1593 return Ok(());
1594 }
1595 let mut map: HashMap<u32, u32> = HashMap::new();
1596 let private_name = hfsplus_private_dir_name();
1597 let private_cnid = self.list_children(2)?.into_iter().find_map(|c| match c {
1598 CatalogEntry::Folder {
1599 folder_id, name, ..
1600 } if name == private_name => Some(folder_id),
1601 _ => None,
1602 });
1603 if let Some(cnid) = private_cnid {
1604 for child in self.list_children(cnid)? {
1605 if let CatalogEntry::File { file_id, name, .. } = child {
1606 if let Some(rest) = name.strip_prefix("iNode") {
1607 if let Ok(inode_num) = rest.parse::<u32>() {
1608 map.insert(inode_num, file_id);
1609 }
1610 }
1611 }
1612 }
1613 }
1614 self.hardlink_inode_map = Some(map);
1615 Ok(())
1616 }
1617
1618 fn resolve_hardlink_inode(&mut self, inode_num: u32) -> Result<Option<u32>, FilesystemError> {
1623 self.ensure_hardlink_inode_map()?;
1624 Ok(self
1625 .hardlink_inode_map
1626 .as_ref()
1627 .and_then(|m| m.get(&inode_num).copied()))
1628 }
1629
1630 fn find_private_dir_cnid(&self) -> Result<Option<u32>, FilesystemError> {
1633 let private_name = hfsplus_private_dir_name();
1634 Ok(self.list_children(2)?.into_iter().find_map(|c| match c {
1635 CatalogEntry::Folder {
1636 folder_id, name, ..
1637 } if name == private_name => Some(folder_id),
1638 _ => None,
1639 }))
1640 }
1641
1642 fn find_dir_private_dir_cnid(&self) -> Result<Option<u32>, FilesystemError> {
1645 let dir_private_name = hfsplus_dir_private_dir_name();
1646 Ok(self.list_children(2)?.into_iter().find_map(|c| match c {
1647 CatalogEntry::Folder {
1648 folder_id, name, ..
1649 } if name == dir_private_name => Some(folder_id),
1650 _ => None,
1651 }))
1652 }
1653
1654 fn ensure_dir_hardlink_inode_map(&mut self) -> Result<(), FilesystemError> {
1658 if self.dir_hardlink_inode_map.is_some() {
1659 return Ok(());
1660 }
1661 let mut map: HashMap<u32, u32> = HashMap::new();
1662 if let Some(cnid) = self.find_dir_private_dir_cnid()? {
1663 for child in self.list_children(cnid)? {
1664 if let CatalogEntry::Folder {
1665 folder_id, name, ..
1666 } = child
1667 {
1668 if let Some(rest) = name.strip_prefix("dir_") {
1669 if let Ok(inode_num) = rest.parse::<u32>() {
1670 map.insert(inode_num, folder_id);
1671 }
1672 }
1673 }
1674 }
1675 }
1676 self.dir_hardlink_inode_map = Some(map);
1677 Ok(())
1678 }
1679
1680 fn resolve_dir_hardlink_inode(
1682 &mut self,
1683 inode_num: u32,
1684 ) -> Result<Option<u32>, FilesystemError> {
1685 self.ensure_dir_hardlink_inode_map()?;
1686 Ok(self
1687 .dir_hardlink_inode_map
1688 .as_ref()
1689 .and_then(|m| m.get(&inode_num).copied()))
1690 }
1691
1692 fn read_file_record_link_inode_num(
1697 &self,
1698 parent_cnid: u32,
1699 name: &str,
1700 ) -> Result<Option<u32>, FilesystemError> {
1701 let Some((_, _, abs_off)) = self.find_catalog_record(parent_cnid, name) else {
1702 return Ok(None);
1703 };
1704 if abs_off + 2 > self.catalog_data.len() {
1705 return Ok(None);
1706 }
1707 let key_len = BigEndian::read_u16(&self.catalog_data[abs_off..abs_off + 2]) as usize;
1708 let mut body_start = abs_off + 2 + key_len;
1709 if !body_start.is_multiple_of(2) {
1710 body_start += 1;
1711 }
1712 if body_start + 56 > self.catalog_data.len() {
1713 return Ok(None);
1714 }
1715 let record_type = BigEndian::read_i16(&self.catalog_data[body_start..body_start + 2]);
1716 if record_type != CATALOG_FILE {
1717 return Ok(None);
1718 }
1719 if &self.catalog_data[body_start + 48..body_start + 52] != HFSPLUS_HARDLINK_FILE_TYPE
1720 || &self.catalog_data[body_start + 52..body_start + 56] != HFSPLUS_HARDLINK_FILE_CREATOR
1721 {
1722 return Ok(None);
1723 }
1724 Ok(Some(BigEndian::read_u32(
1725 &self.catalog_data[body_start + 44..body_start + 48],
1726 )))
1727 }
1728
1729 fn find_file_by_id(&self, file_id: u32) -> Option<(ForkData, ForkData)> {
1732 let node_size = self.catalog_header.node_size as usize;
1733 let first_leaf = self.catalog_header.first_leaf_node;
1734
1735 hfs_common::walk_leaf_records(
1736 &self.catalog_data,
1737 first_leaf,
1738 node_size,
1739 |_node_idx, _rec_idx, _abs_off, rec| {
1740 if rec.len() < 8 {
1741 return None;
1742 }
1743 let key_len = BigEndian::read_u16(&rec[0..2]) as usize;
1744 if key_len < 6 || 2 + key_len > rec.len() {
1745 return None;
1746 }
1747 let mut data_rel = 2 + key_len;
1748 if !data_rel.is_multiple_of(2) {
1749 data_rel += 1;
1750 }
1751 if data_rel + 2 > rec.len() {
1752 return None;
1753 }
1754 let record_type = BigEndian::read_i16(&rec[data_rel..data_rel + 2]);
1755 if record_type != CATALOG_FILE {
1756 return None;
1757 }
1758 let body = &rec[data_rel..];
1759 if body.len() < 248 {
1760 return None;
1761 }
1762 let rec_file_id = BigEndian::read_u32(&body[8..12]);
1763 if rec_file_id != file_id {
1764 return None;
1765 }
1766 Some((
1767 ForkData::parse(&body[88..168]),
1768 ForkData::parse(&body[168..248]),
1769 ))
1770 },
1771 )
1772 }
1773
1774 fn read_allocation_bitmap(&mut self) -> Result<Vec<u8>, FilesystemError> {
1776 read_fork_with_overflow(
1779 &mut self.reader,
1780 self.partition_offset,
1781 self.vh.block_size,
1782 &self.vh.allocation_file,
1783 HFSPLUS_ALLOCATION_FILE_ID,
1784 HFSPLUS_FORK_DATA,
1785 self.extents_overflow_data.as_deref(),
1786 )
1787 }
1788
1789 fn read_user_fork(
1792 &mut self,
1793 file_id: u32,
1794 fork_type: u8,
1795 fork: &ForkData,
1796 ) -> Result<Vec<u8>, FilesystemError> {
1797 read_fork_with_overflow(
1798 &mut self.reader,
1799 self.partition_offset,
1800 self.vh.block_size,
1801 fork,
1802 file_id,
1803 fork_type,
1804 self.extents_overflow_data.as_deref(),
1805 )
1806 }
1807
1808 fn ensure_bitmap(&mut self) -> Result<(), FilesystemError> {
1810 if self.bitmap.is_none() {
1811 self.bitmap = Some(self.read_allocation_bitmap()?);
1812 }
1813 Ok(())
1814 }
1815
1816 pub(crate) fn build_catalog_key(parent_cnid: u32, name: &str) -> Vec<u8> {
1818 let utf16: Vec<u16> = fs_mac_unicode::hfs_unicode::decompose_str(name);
1819 let key_len = 4 + 2 + utf16.len() * 2;
1820 let mut key = Vec::with_capacity(2 + key_len);
1821 let mut buf = [0u8; 2];
1822 BigEndian::write_u16(&mut buf, key_len as u16);
1823 key.extend_from_slice(&buf);
1824 let mut buf4 = [0u8; 4];
1825 BigEndian::write_u32(&mut buf4, parent_cnid);
1826 key.extend_from_slice(&buf4);
1827 BigEndian::write_u16(&mut buf, utf16.len() as u16);
1828 key.extend_from_slice(&buf);
1829 for &ch in &utf16 {
1830 BigEndian::write_u16(&mut buf, ch);
1831 key.extend_from_slice(&buf);
1832 }
1833 key
1834 }
1835
1836 pub(crate) fn case_sensitive(&self) -> bool {
1843 self.catalog_header.key_compare_type == KEY_COMPARE_BINARY
1844 }
1845
1846 fn catalog_compare(a: &[u8], b: &[u8], case_sensitive: bool) -> Ordering {
1847 if a.len() < 8 || b.len() < 8 {
1849 return a.len().cmp(&b.len());
1850 }
1851 let parent_a = BigEndian::read_u32(&a[2..6]);
1852 let parent_b = BigEndian::read_u32(&b[2..6]);
1853 let name_len_a = BigEndian::read_u16(&a[6..8]) as usize;
1854 let name_len_b = BigEndian::read_u16(&b[6..8]) as usize;
1855 let name_a: Vec<u16> = a[8..8 + name_len_a.min((a.len() - 8) / 2) * 2]
1856 .chunks_exact(2)
1857 .map(BigEndian::read_u16)
1858 .collect();
1859 let name_b: Vec<u16> = b[8..8 + name_len_b.min((b.len() - 8) / 2) * 2]
1860 .chunks_exact(2)
1861 .map(BigEndian::read_u16)
1862 .collect();
1863 hfs_common::compare_hfsplus_keys(parent_a, &name_a, parent_b, &name_b, case_sensitive)
1864 }
1865
1866 fn find_catalog_record(&self, parent_cnid: u32, name: &str) -> Option<(u32, usize, usize)> {
1870 let search_key = Self::build_catalog_key(parent_cnid, name);
1871 let node_size = self.catalog_header.node_size as usize;
1872 let first_leaf = self.catalog_header.first_leaf_node;
1873 let cs = self.case_sensitive();
1874
1875 hfs_common::walk_leaf_records(
1876 &self.catalog_data,
1877 first_leaf,
1878 node_size,
1879 |node_idx, rec_idx, abs_off, rec| {
1880 if rec.len() < 8 {
1881 return None;
1882 }
1883 let key_len = BigEndian::read_u16(&rec[0..2]) as usize;
1884 let key_portion = &rec[..2 + key_len.min(rec.len() - 2)];
1885 if Self::catalog_compare(key_portion, &search_key, cs) == Ordering::Equal {
1886 Some((node_idx, rec_idx, abs_off))
1887 } else {
1888 None
1889 }
1890 },
1891 )
1892 }
1893
1894 fn find_catalog_record_by_cnid(&self, cnid: u32) -> Option<(u32, usize, usize)> {
1896 self.find_catalog_record(cnid, "")
1897 }
1898
1899 fn update_parent_valence(
1901 &mut self,
1902 parent_cnid: u32,
1903 delta: i32,
1904 ) -> Result<(), FilesystemError> {
1905 let (node_idx, _rec_idx, _offset) = self
1907 .find_catalog_record_by_cnid(parent_cnid)
1908 .ok_or_else(|| {
1909 FilesystemError::NotFound(format!("thread record for CNID {parent_cnid} not found"))
1910 })?;
1911
1912 let node_size = self.catalog_header.node_size as usize;
1914 let first_leaf = self.catalog_header.first_leaf_node;
1915 let val_offset = hfs_common::walk_leaf_records(
1916 &self.catalog_data,
1917 first_leaf,
1918 node_size,
1919 |_node_idx, _rec_idx, abs_off, rec| {
1920 if rec.len() < 8 {
1921 return None;
1922 }
1923 let key_len = BigEndian::read_u16(&rec[0..2]) as usize;
1924 let mut data_off = abs_off + 2 + key_len;
1925 if !data_off.is_multiple_of(2) {
1926 data_off += 1;
1927 }
1928 let abs_end = abs_off + rec.len();
1929 if data_off + 12 > abs_end {
1930 return None;
1931 }
1932 let record_type = BigEndian::read_i16(&self.catalog_data[data_off..data_off + 2]);
1933 if record_type != CATALOG_FOLDER {
1934 return None;
1935 }
1936 let folder_id =
1937 BigEndian::read_u32(&self.catalog_data[data_off + 8..data_off + 12]);
1938 if folder_id == parent_cnid {
1939 Some(data_off + 4)
1940 } else {
1941 None
1942 }
1943 },
1944 );
1945 let val_offset = val_offset.ok_or_else(|| {
1946 FilesystemError::NotFound(format!(
1947 "folder record for parent CNID {parent_cnid} not found in catalog"
1948 ))
1949 })?;
1950 let old_val = BigEndian::read_u32(&self.catalog_data[val_offset..val_offset + 4]);
1951 let new_val = (old_val as i64 + delta as i64).max(0) as u32;
1952 BigEndian::write_u32(&mut self.catalog_data[val_offset..val_offset + 4], new_val);
1953 let _ = node_idx; Ok(())
1955 }
1956
1957 fn insert_catalog_record(&mut self, key_record: &[u8]) -> Result<(), FilesystemError>
1968 where
1969 R: std::io::Write,
1970 {
1971 let cs = self.case_sensitive();
1972 let mut grown = false;
1978 loop {
1979 let cmp = |a: &[u8], b: &[u8]| Self::catalog_compare(a, b, cs);
1980 match hfs_common::btree_insert_full(
1981 &mut self.catalog_data,
1982 key_record,
1983 &BTreeKeyFormat::HFSPLUS_CATALOG,
1984 &cmp,
1985 ) {
1986 Ok(()) => break,
1987 Err(FilesystemError::DiskFull(_)) if !grown => {
1988 self.grow_btree_fork(BTreeFork::Catalog)?;
1989 grown = true;
1990 }
1991 Err(e) => return Err(e),
1992 }
1993 }
1994 self.catalog_header = BTreeHeaderRecord::parse(&self.catalog_data[14..14 + 106]);
1995 Ok(())
1996 }
1997
1998 fn remove_catalog_record(
2000 &mut self,
2001 node_idx: u32,
2002 rec_idx: usize,
2003 ) -> Result<(), FilesystemError> {
2004 let node_size = self.catalog_header.node_size as usize;
2005 let offset = node_idx as usize * node_size;
2006 let cs = self.case_sensitive();
2007 let cmp = |a: &[u8], b: &[u8]| Self::catalog_compare(a, b, cs);
2008 btree_remove_record(
2009 &mut self.catalog_data[offset..offset + node_size],
2010 node_size,
2011 rec_idx,
2012 );
2013
2014 let num = BigEndian::read_u16(&self.catalog_data[offset + 10..offset + 12]);
2016 if num > 0 && rec_idx == 0 {
2017 btree_refresh_index_keys(
2020 &mut self.catalog_data,
2021 node_size,
2022 node_idx,
2023 &BTreeKeyFormat::HFSPLUS_CATALOG,
2024 &cmp,
2025 )?;
2026 }
2027 if num == 0 {
2028 let prev = BigEndian::read_u32(&self.catalog_data[offset + 4..offset + 8]);
2030 let next = BigEndian::read_u32(&self.catalog_data[offset..offset + 4]);
2031 if prev != 0 {
2032 let prev_off = prev as usize * node_size;
2033 BigEndian::write_u32(&mut self.catalog_data[prev_off..prev_off + 4], next);
2034 }
2035 if next != 0 {
2036 let next_off = next as usize * node_size;
2037 BigEndian::write_u32(&mut self.catalog_data[next_off + 4..next_off + 8], prev);
2038 }
2039 btree_free_node(&mut self.catalog_data, node_size, node_idx);
2040
2041 let mut h = BTreeHeader::read(&self.catalog_data);
2042 h.free_nodes += 1;
2043 if h.first_leaf_node == node_idx {
2044 h.first_leaf_node = next;
2045 }
2046 if h.last_leaf_node == node_idx {
2047 h.last_leaf_node = prev;
2048 }
2049 h.write(&mut self.catalog_data);
2050 btree_detach_freed_node_and_refresh(
2053 &mut self.catalog_data,
2054 node_size,
2055 node_idx,
2056 &BTreeKeyFormat::HFSPLUS_CATALOG,
2057 &cmp,
2058 )?;
2059 }
2060
2061 let mut h = BTreeHeader::read(&self.catalog_data);
2063 h.leaf_records = h.leaf_records.saturating_sub(1);
2064 h.write(&mut self.catalog_data);
2065 self.catalog_header = BTreeHeaderRecord::parse(&self.catalog_data[14..14 + 106]);
2066 Ok(())
2067 }
2068}
2069
2070impl<R: Read + Seek> HfsPlusFilesystem<R> {
2073 pub(crate) fn extents_compare(a: &[u8], b: &[u8]) -> Ordering {
2080 if a.len() < 12 || b.len() < 12 {
2081 return a.len().cmp(&b.len());
2082 }
2083 let a_file = BigEndian::read_u32(&a[4..8]);
2084 let b_file = BigEndian::read_u32(&b[4..8]);
2085 a_file.cmp(&b_file).then(a[2].cmp(&b[2])).then_with(|| {
2086 let a_sb = BigEndian::read_u32(&a[8..12]);
2087 let b_sb = BigEndian::read_u32(&b[8..12]);
2088 a_sb.cmp(&b_sb)
2089 })
2090 }
2091
2092 fn build_extents_overflow_record(
2099 file_id: u32,
2100 fork_type: u8,
2101 file_rel_start: u32,
2102 chunk: &[ExtentDescriptor],
2103 ) -> Vec<u8> {
2104 let mut rec = vec![0u8; 2 + 10 + 64];
2105 BigEndian::write_u16(&mut rec[0..2], 10); rec[2] = fork_type;
2107 rec[3] = 0; BigEndian::write_u32(&mut rec[4..8], file_id);
2109 BigEndian::write_u32(&mut rec[8..12], file_rel_start);
2110 for (i, ext) in chunk.iter().take(8).enumerate() {
2111 ext.serialize(&mut rec[12 + i * 8..12 + i * 8 + 8]);
2112 }
2113 rec
2114 }
2115}
2116
2117impl<R: Read + Write + Seek> HfsPlusFilesystem<R> {
2118 fn insert_extents_overflow_record(&mut self, key_record: &[u8]) -> Result<(), FilesystemError> {
2129 {
2130 let data = self.extents_overflow_data.as_ref().ok_or_else(|| {
2131 FilesystemError::Unsupported(
2132 "volume has no extents-overflow B-tree — fragmented files cannot be created"
2133 .into(),
2134 )
2135 })?;
2136 if BTreeHeader::read(data).node_size == 0 {
2137 return Err(FilesystemError::InvalidData(
2138 "extents-overflow B-tree has zero node_size".into(),
2139 ));
2140 }
2141 }
2142 let mut grown = false;
2145 loop {
2146 let data = self.extents_overflow_data.as_mut().unwrap();
2147 match hfs_common::btree_insert_full(
2148 data,
2149 key_record,
2150 &BTreeKeyFormat::HFSPLUS_EXTENTS,
2151 &Self::extents_compare,
2152 ) {
2153 Ok(()) => return Ok(()),
2154 Err(FilesystemError::DiskFull(_)) if !grown => {
2155 self.grow_btree_fork(BTreeFork::Extents)?;
2156 grown = true;
2157 }
2158 Err(e) => return Err(e),
2159 }
2160 }
2161 }
2162
2163 fn remove_extents_overflow_records_for(
2167 &mut self,
2168 file_id: u32,
2169 fork_type: u8,
2170 ) -> Result<(), FilesystemError> {
2171 let Some(ref data) = self.extents_overflow_data else {
2172 return Ok(());
2173 };
2174 let header = BTreeHeader::read(data);
2175 let node_size = header.node_size as usize;
2176 if node_size == 0 {
2177 return Ok(());
2178 }
2179
2180 let mut victims: Vec<(u32, usize)> = Vec::new();
2183 hfs_common::walk_leaf_records::<(), _>(
2184 data,
2185 header.first_leaf_node,
2186 node_size,
2187 |node_idx, rec_idx, _abs_off, rec| {
2188 if rec.len() < 12 {
2189 return None;
2190 }
2191 let key_len = BigEndian::read_u16(&rec[0..2]) as usize;
2192 if key_len < 10 {
2193 return None;
2194 }
2195 let rec_fork = rec[2];
2196 let rec_file = BigEndian::read_u32(&rec[4..8]);
2197 if rec_fork == fork_type && rec_file == file_id {
2198 victims.push((node_idx, rec_idx));
2199 }
2200 None
2201 },
2202 );
2203 if victims.is_empty() {
2204 return Ok(());
2205 }
2206
2207 let data = self.extents_overflow_data.as_mut().unwrap();
2210 victims.sort_by(|a, b| b.0.cmp(&a.0).then(b.1.cmp(&a.1)));
2211 let mut removed: u32 = 0;
2212 for &(node_idx, rec_idx) in &victims {
2213 let off = node_idx as usize * node_size;
2214 btree_remove_record(&mut data[off..off + node_size], node_size, rec_idx);
2215 removed += 1;
2216 }
2217 let mut h = BTreeHeader::read(data);
2218 h.leaf_records = h.leaf_records.saturating_sub(removed);
2219 h.write(data);
2220 let mut nodes: Vec<u32> = victims.iter().map(|v| v.0).collect();
2223 nodes.dedup();
2224 for node_idx in nodes {
2225 let off = node_idx as usize * node_size;
2226 let kf = BTreeKeyFormat::HFSPLUS_EXTENTS;
2227 if BigEndian::read_u16(&data[off + 10..off + 12]) == 0 {
2228 btree_retire_empty_leaf(data, node_size, node_idx, &kf, &Self::extents_compare)?;
2229 } else if victims.iter().any(|&(n, r)| n == node_idx && r == 0) {
2230 btree_refresh_index_keys(data, node_size, node_idx, &kf, &Self::extents_compare)?;
2231 }
2232 }
2233 Ok(())
2234 }
2235}
2236
2237impl<R: Read + Seek> HfsPlusFilesystem<R> {
2240 pub(crate) fn attr_compare(a: &[u8], b: &[u8]) -> Ordering {
2248 if a.len() < 14 || b.len() < 14 {
2249 return a.len().cmp(&b.len());
2250 }
2251 let af = BigEndian::read_u32(&a[4..8]);
2252 let bf = BigEndian::read_u32(&b[4..8]);
2253 if af != bf {
2254 return af.cmp(&bf);
2255 }
2256 let an_len = BigEndian::read_u16(&a[12..14]) as usize;
2257 let bn_len = BigEndian::read_u16(&b[12..14]) as usize;
2258 let a_name_end = (14 + an_len * 2).min(a.len());
2259 let b_name_end = (14 + bn_len * 2).min(b.len());
2260 let cmp = a[14..a_name_end].cmp(&b[14..b_name_end]);
2263 if cmp != Ordering::Equal {
2264 return cmp;
2265 }
2266 let asb = BigEndian::read_u32(&a[8..12]);
2267 let bsb = BigEndian::read_u32(&b[8..12]);
2268 asb.cmp(&bsb)
2269 }
2270
2271 pub(crate) fn build_inline_attr_record(cnid: u32, name: &str, value: &[u8]) -> Vec<u8> {
2278 let utf16: Vec<u16> = fs_mac_unicode::hfs_unicode::decompose_str(name);
2279 let key_body_len = 12 + utf16.len() * 2;
2280 let data_section_len = 4 + 8 + 4 + value.len();
2281 let mut rec = vec![0u8; 2 + key_body_len + data_section_len];
2282 BigEndian::write_u16(&mut rec[0..2], key_body_len as u16);
2283 BigEndian::write_u32(&mut rec[4..8], cnid);
2285 BigEndian::write_u32(&mut rec[8..12], 0); BigEndian::write_u16(&mut rec[12..14], utf16.len() as u16);
2287 for (i, ch) in utf16.iter().enumerate() {
2288 BigEndian::write_u16(&mut rec[14 + i * 2..16 + i * 2], *ch);
2289 }
2290 let data_off = 2 + key_body_len;
2291 BigEndian::write_u32(&mut rec[data_off..data_off + 4], HFSPLUS_ATTR_INLINE_DATA);
2292 BigEndian::write_u32(&mut rec[data_off + 12..data_off + 16], value.len() as u32);
2294 rec[data_off + 16..data_off + 16 + value.len()].copy_from_slice(value);
2295 rec
2296 }
2297}
2298
2299impl<R: Read + Write + Seek> HfsPlusFilesystem<R> {
2300 fn insert_xattr_record(&mut self, key_record: &[u8]) -> Result<(), FilesystemError> {
2303 if self.vh.attributes_file.logical_size == 0 {
2304 return Err(FilesystemError::Unsupported(
2305 "volume has no attributes B-tree — extended attribute writes \
2306 require an existing attributes file"
2307 .into(),
2308 ));
2309 }
2310 self.ensure_attributes_loaded()?;
2311 let data = self
2312 .attributes_data
2313 .as_mut()
2314 .expect("ensure_attributes_loaded populated the buffer");
2315 let header = BTreeHeader::read(data);
2316 let node_size = header.node_size as usize;
2317 if node_size == 0 {
2318 return Err(FilesystemError::InvalidData(
2319 "attributes B-tree has zero node_size".into(),
2320 ));
2321 }
2322 if key_record.len() + 2 > node_size.saturating_sub(14 + 2) {
2325 return Err(FilesystemError::XattrTooLarge(format!(
2326 "xattr record of {} bytes exceeds attributes node capacity ({} bytes); \
2327 fork-style attribute writes are not yet implemented",
2328 key_record.len(),
2329 node_size,
2330 )));
2331 }
2332 let mut grown = false;
2338 loop {
2339 let data = self
2340 .attributes_data
2341 .as_mut()
2342 .expect("ensure_attributes_loaded populated the buffer");
2343 match hfs_common::btree_insert_full(
2344 data,
2345 key_record,
2346 &BTreeKeyFormat::HFSPLUS_ATTRIBUTES,
2347 &Self::attr_compare,
2348 ) {
2349 Ok(()) => return Ok(()),
2350 Err(FilesystemError::DiskFull(_)) if !grown => {
2351 self.grow_btree_fork(BTreeFork::Attributes)?;
2352 grown = true;
2353 }
2354 Err(e) => return Err(e),
2355 }
2356 }
2357 }
2358
2359 fn remove_xattr_records(
2365 &mut self,
2366 cnid: u32,
2367 name: Option<&str>,
2368 ) -> Result<u32, FilesystemError> {
2369 if self.vh.attributes_file.logical_size == 0 {
2370 return Ok(0);
2371 }
2372 self.ensure_attributes_loaded()?;
2373 let data = self
2374 .attributes_data
2375 .as_mut()
2376 .expect("ensure_attributes_loaded populated the buffer");
2377 let header = BTreeHeader::read(data);
2378 let node_size = header.node_size as usize;
2379 if node_size == 0 {
2380 return Ok(0);
2381 }
2382 let target_utf16: Option<Vec<u16>> = name.map(fs_mac_unicode::hfs_unicode::decompose_str);
2383
2384 let mut victims: Vec<(u32, usize)> = Vec::new();
2385 hfs_common::walk_leaf_records::<(), _>(
2386 data,
2387 header.first_leaf_node,
2388 node_size,
2389 |node_idx, rec_idx, _abs_off, rec| {
2390 if rec.len() < 14 {
2391 return None;
2392 }
2393 let key_len = BigEndian::read_u16(&rec[0..2]) as usize;
2394 if key_len < 12 || 2 + key_len > rec.len() {
2395 return None;
2396 }
2397 let file_id = BigEndian::read_u32(&rec[4..8]);
2398 if file_id != cnid {
2399 return None;
2400 }
2401 if let Some(target) = target_utf16.as_ref() {
2402 let nl = BigEndian::read_u16(&rec[12..14]) as usize;
2403 if nl != target.len() {
2404 return None;
2405 }
2406 if 14 + nl * 2 > 2 + key_len {
2407 return None;
2408 }
2409 for (i, ch) in target.iter().enumerate() {
2410 let off = 14 + i * 2;
2411 if BigEndian::read_u16(&rec[off..off + 2]) != *ch {
2412 return None;
2413 }
2414 }
2415 }
2416 victims.push((node_idx, rec_idx));
2417 None
2418 },
2419 );
2420 if victims.is_empty() {
2421 return Ok(0);
2422 }
2423 victims.sort_by(|a, b| b.0.cmp(&a.0).then(b.1.cmp(&a.1)));
2425 let mut removed: u32 = 0;
2426 for (node_idx, rec_idx) in &victims {
2427 let off = *node_idx as usize * node_size;
2428 btree_remove_record(&mut data[off..off + node_size], node_size, *rec_idx);
2429 removed += 1;
2430 }
2431 let mut h = BTreeHeader::read(data);
2432 h.leaf_records = h.leaf_records.saturating_sub(removed);
2433 h.write(data);
2434 let mut nodes: Vec<u32> = victims.iter().map(|v| v.0).collect();
2437 nodes.dedup();
2438 for node_idx in nodes {
2439 let off = node_idx as usize * node_size;
2440 let kf = BTreeKeyFormat::HFSPLUS_ATTRIBUTES;
2441 if BigEndian::read_u16(&data[off + 10..off + 12]) == 0 {
2442 btree_retire_empty_leaf(data, node_size, node_idx, &kf, &Self::attr_compare)?;
2443 } else if victims.iter().any(|&(n, r)| n == node_idx && r == 0) {
2444 btree_refresh_index_keys(data, node_size, node_idx, &kf, &Self::attr_compare)?;
2445 }
2446 }
2447 Ok(removed)
2448 }
2449}
2450
2451impl<R: Read + Write + Seek> HfsPlusFilesystem<R> {
2454 fn write_block(&mut self, block: u32, data: &[u8]) -> Result<(), FilesystemError> {
2456 let offset = self.partition_offset + block as u64 * self.vh.block_size as u64;
2457 self.reader.seek(SeekFrom::Start(offset))?;
2458 self.reader.write_all(data)?;
2459 Ok(())
2460 }
2461
2462 fn write_volume_header(&mut self) -> Result<(), FilesystemError> {
2464 let vh_bytes = self.vh.serialize();
2465 self.reader
2467 .seek(SeekFrom::Start(self.partition_offset + 1024))?;
2468 self.reader.write_all(&vh_bytes)?;
2469 let volume_end = self
2472 .partition_size
2473 .filter(|&s| s >= 2048)
2474 .unwrap_or(self.vh.total_blocks as u64 * self.vh.block_size as u64);
2475 if volume_end > 1024 {
2476 self.reader
2477 .seek(SeekFrom::Start(self.partition_offset + volume_end - 1024))?;
2478 self.reader.write_all(&vh_bytes)?;
2479 }
2480 Ok(())
2481 }
2482
2483 fn write_extents_overflow(&mut self) -> Result<(), FilesystemError> {
2487 if self.vh.extents_file.logical_size == 0 {
2488 return Ok(());
2489 }
2490 let Some(ref data) = self.extents_overflow_data else {
2491 return Ok(());
2492 };
2493 write_fork_data(
2494 &mut self.reader,
2495 self.partition_offset,
2496 self.vh.block_size,
2497 &self.vh.extents_file,
2498 data,
2499 )
2500 }
2501
2502 fn write_attributes(&mut self) -> Result<(), FilesystemError> {
2506 if self.vh.attributes_file.logical_size == 0 {
2507 return Ok(());
2508 }
2509 if self.attributes_data.is_none() {
2510 return Ok(());
2511 }
2512 write_fork_data_with_overflow(
2513 &mut self.reader,
2514 self.partition_offset,
2515 self.vh.block_size,
2516 &self.vh.attributes_file,
2517 HFSPLUS_ATTRIBUTES_FILE_ID,
2518 0,
2519 self.extents_overflow_data.as_deref(),
2520 self.attributes_data.as_deref().unwrap(),
2521 )
2522 }
2523
2524 fn write_catalog(&mut self) -> Result<(), FilesystemError> {
2528 write_fork_data_with_overflow(
2529 &mut self.reader,
2530 self.partition_offset,
2531 self.vh.block_size,
2532 &self.vh.catalog_file,
2533 HFSPLUS_CATALOG_FILE_ID,
2534 0,
2535 self.extents_overflow_data.as_deref(),
2536 &self.catalog_data,
2537 )
2538 }
2539
2540 fn write_allocation_bitmap(&mut self) -> Result<(), FilesystemError> {
2542 if let Some(ref bm) = self.bitmap {
2543 write_fork_data(
2544 &mut self.reader,
2545 self.partition_offset,
2546 self.vh.block_size,
2547 &self.vh.allocation_file,
2548 bm,
2549 )?;
2550 }
2551 Ok(())
2552 }
2553
2554 fn allocate_extents(
2567 &mut self,
2568 blocks_needed: u32,
2569 ) -> Result<Vec<ExtentDescriptor>, FilesystemError> {
2570 if blocks_needed == 0 {
2571 return Ok(Vec::new());
2572 }
2573 self.ensure_bitmap()?;
2574 let total_blocks = self.vh.total_blocks;
2575 let bitmap = self.bitmap.as_mut().unwrap();
2576
2577 if self.vh.free_blocks < blocks_needed {
2580 return Err(FilesystemError::DiskFull(format!(
2581 "{} blocks free, {} requested",
2582 self.vh.free_blocks, blocks_needed
2583 )));
2584 }
2585
2586 let mut runs = bitmap_collect_clear_runs_be(bitmap, total_blocks);
2587 runs.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
2589
2590 let mut extents: Vec<ExtentDescriptor> = Vec::new();
2591 let mut remaining = blocks_needed;
2592 let mut last_end = 0u32;
2593 for (start, len) in runs {
2594 if remaining == 0 {
2595 break;
2596 }
2597 let take = len.min(remaining);
2598 extents.push(ExtentDescriptor {
2599 start_block: start,
2600 block_count: take,
2601 });
2602 for i in 0..take {
2603 bitmap_set_bit_be(bitmap, start + i);
2604 }
2605 remaining -= take;
2606 last_end = start + take;
2607 }
2608
2609 if remaining > 0 {
2610 for ext in &extents {
2613 for i in 0..ext.block_count {
2614 bitmap_clear_bit_be(bitmap, ext.start_block + i);
2615 }
2616 }
2617 return Err(FilesystemError::DiskFull(format!(
2618 "needed {blocks_needed} blocks, short by {remaining} after gathering all free runs"
2619 )));
2620 }
2621
2622 self.vh.free_blocks -= blocks_needed;
2623 self.vh.next_allocation = last_end;
2624 Ok(extents)
2625 }
2626
2627 fn free_blocks(&mut self, start: u32, count: u32) {
2629 self.ensure_bitmap().ok();
2630 if let Some(ref mut bitmap) = self.bitmap {
2631 for i in 0..count {
2632 bitmap_clear_bit_be(bitmap, start + i);
2633 }
2634 }
2635 self.vh.free_blocks += count;
2636 }
2637
2638 fn free_fork_blocks(&mut self, fork: &ForkData) {
2640 for ext in &fork.extents {
2641 if ext.is_empty() {
2642 break;
2643 }
2644 self.free_blocks(ext.start_block, ext.block_count);
2645 }
2646 }
2647
2648 fn free_fork_overflow_extents(&mut self, file_id: u32, fork_type: u8, fork: &ForkData) {
2654 let Some(ref ext_data) = self.extents_overflow_data else {
2655 return;
2656 };
2657 let inline_blocks: u32 = fork.extents.iter().map(|e| e.block_count).sum();
2658 let overflow =
2659 collect_hfsplus_overflow_extents(ext_data, file_id, fork_type, inline_blocks);
2660 for ext in overflow {
2661 if ext.block_count == 0 {
2662 continue;
2663 }
2664 self.free_blocks(ext.start_block, ext.block_count);
2665 }
2666 }
2667
2668 fn tree_buf_mut(&mut self, kind: BTreeFork) -> Result<&mut Vec<u8>, FilesystemError> {
2674 match kind {
2675 BTreeFork::Catalog => Ok(&mut self.catalog_data),
2676 BTreeFork::Extents => self.extents_overflow_data.as_mut().ok_or_else(|| {
2677 FilesystemError::Unsupported("volume has no extents-overflow B-tree".into())
2678 }),
2679 BTreeFork::Attributes => self.attributes_data.as_mut().ok_or_else(|| {
2680 FilesystemError::Unsupported("volume has no attributes B-tree".into())
2681 }),
2682 }
2683 }
2684
2685 fn fork_mut(&mut self, kind: BTreeFork) -> &mut ForkData {
2687 match kind {
2688 BTreeFork::Catalog => &mut self.vh.catalog_file,
2689 BTreeFork::Extents => &mut self.vh.extents_file,
2690 BTreeFork::Attributes => &mut self.vh.attributes_file,
2691 }
2692 }
2693
2694 fn fork_last_inline_block(&self, kind: BTreeFork) -> u32 {
2702 let fork = match kind {
2703 BTreeFork::Catalog => &self.vh.catalog_file,
2704 BTreeFork::Extents => &self.vh.extents_file,
2705 BTreeFork::Attributes => &self.vh.attributes_file,
2706 };
2707 let mut last = 0u32;
2708 for e in &fork.extents {
2709 if e.block_count == 0 {
2710 break;
2711 }
2712 last = e.start_block + e.block_count;
2713 }
2714 last
2715 }
2716
2717 fn allocate_contiguous_after(
2725 &mut self,
2726 after_block: u32,
2727 blocks: u32,
2728 ) -> Option<ExtentDescriptor> {
2729 if blocks == 0 || after_block == 0 {
2730 return None;
2731 }
2732 self.ensure_bitmap().ok()?;
2733 if after_block.checked_add(blocks)? > self.vh.total_blocks {
2734 return None;
2735 }
2736 if self.vh.free_blocks < blocks {
2737 return None;
2738 }
2739 let bitmap = self.bitmap.as_mut()?;
2740 for i in 0..blocks {
2741 if bitmap_test_bit_be(bitmap, after_block + i) {
2742 return None; }
2744 }
2745 for i in 0..blocks {
2746 bitmap_set_bit_be(bitmap, after_block + i);
2747 }
2748 self.vh.free_blocks -= blocks;
2749 self.vh.next_allocation = (after_block + blocks).max(self.vh.next_allocation);
2750 Some(ExtentDescriptor {
2751 start_block: after_block,
2752 block_count: blocks,
2753 })
2754 }
2755
2756 fn attach_extents_to_fork(
2763 &mut self,
2764 kind: BTreeFork,
2765 new_extents: &[ExtentDescriptor],
2766 ) -> Result<(), FilesystemError> {
2767 let block_size = self.vh.block_size as u64;
2768 let fork = self.fork_mut(kind);
2769 let mut added_blocks = 0u32;
2770 let mut overflow: Vec<ExtentDescriptor> = Vec::new();
2771 for ext in new_extents {
2772 if ext.block_count == 0 {
2773 continue;
2774 }
2775 added_blocks += ext.block_count;
2776 let last_idx = (0..8)
2778 .take_while(|&i| fork.extents[i].block_count != 0)
2779 .last();
2780 let mut placed = false;
2781 if let Some(i) = last_idx {
2782 let le = fork.extents[i];
2783 if le.start_block + le.block_count == ext.start_block {
2784 fork.extents[i].block_count += ext.block_count;
2785 placed = true;
2786 }
2787 }
2788 if !placed {
2789 if let Some(i) = (0..8).find(|&i| fork.extents[i].block_count == 0) {
2790 fork.extents[i] = *ext;
2791 placed = true;
2792 }
2793 }
2794 if !placed {
2795 overflow.push(*ext);
2796 }
2797 }
2798 fork.total_blocks += added_blocks;
2799 fork.logical_size += added_blocks as u64 * block_size;
2800 if !overflow.is_empty() {
2801 self.spill_fork_extents_to_overflow(kind, &overflow)?;
2802 }
2803 Ok(())
2804 }
2805
2806 fn spill_fork_extents_to_overflow(
2813 &mut self,
2814 kind: BTreeFork,
2815 extents: &[ExtentDescriptor],
2816 ) -> Result<(), FilesystemError> {
2817 if kind == BTreeFork::Extents {
2824 return Err(FilesystemError::DiskFull(
2825 "extents-overflow B-tree fork exceeded its 8 inline extents".into(),
2826 ));
2827 }
2828 let file_id = kind.file_id();
2829 let fork = match kind {
2830 BTreeFork::Catalog => &self.vh.catalog_file,
2831 BTreeFork::Attributes => &self.vh.attributes_file,
2832 BTreeFork::Extents => unreachable!(),
2833 };
2834 let inline_blocks: u32 = fork.extents.iter().map(|e| e.block_count).sum();
2835 let existing_overflow: u32 = match self.extents_overflow_data.as_deref() {
2836 Some(ed) => collect_hfsplus_overflow_extents(ed, file_id, 0, inline_blocks)
2837 .iter()
2838 .map(|e| e.block_count)
2839 .sum(),
2840 None => 0,
2841 };
2842 let mut file_rel = inline_blocks + existing_overflow;
2843 for chunk in extents.chunks(8) {
2845 let rec = Self::build_extents_overflow_record(file_id, 0, file_rel, chunk);
2846 self.insert_extents_overflow_record(&rec)?;
2847 file_rel += chunk.iter().map(|e| e.block_count).sum::<u32>();
2848 }
2849 Ok(())
2850 }
2851
2852 fn grow_btree_fork(&mut self, kind: BTreeFork) -> Result<(), FilesystemError> {
2861 let block_size = self.vh.block_size;
2862
2863 let (node_size, total_nodes) = {
2864 let buf = self.tree_buf_mut(kind)?;
2865 let h = BTreeHeader::read(buf);
2866 (h.node_size as usize, h.total_nodes)
2867 };
2868 if node_size == 0 || node_size < 256 {
2869 return Err(FilesystemError::InvalidData(format!(
2870 "{} B-tree has invalid node_size {node_size}",
2871 kind.label()
2872 )));
2873 }
2874
2875 let clump_nodes = (self.vh.data_clump_size as usize)
2880 .div_ceil(node_size)
2881 .max(1) as u32;
2882 let grow_nodes = clump_nodes.max(8);
2883
2884 let header_cap = ((node_size - 256) * 8) as u32;
2892 let per_map = (hfs_common::map_node_bitmap_bytes(node_size) * 8) as u32;
2893 let existing_map = {
2894 let buf = self.tree_buf_mut(kind)?;
2895 (hfs_common::btree_bitmap_segments(buf, node_size).len() as u32).saturating_sub(1)
2896 };
2897 let current_capacity = header_cap + existing_map * per_map;
2898 let need_map = total_nodes + grow_nodes > current_capacity;
2899 if need_map && per_map == 0 {
2900 return Err(FilesystemError::DiskFull(format!(
2901 "{} B-tree node bitmap full and node size too small for map nodes",
2902 kind.label()
2903 )));
2904 }
2905 let add_slots = grow_nodes + u32::from(need_map);
2906 let grow_blocks = btree_grow_blocks(total_nodes, add_slots, node_size, block_size);
2907 if grow_blocks > 0 {
2910 let last_block = self.fork_last_inline_block(kind);
2911 let new_extents = match self.allocate_contiguous_after(last_block, grow_blocks) {
2912 Some(e) => vec![e],
2913 None => self.allocate_extents(grow_blocks)?,
2914 };
2915 self.attach_extents_to_fork(kind, &new_extents)?;
2916 }
2917
2918 {
2922 let buf = self.tree_buf_mut(kind)?;
2923 buf.resize(buf.len() + add_slots as usize * node_size, 0);
2924
2925 if need_map {
2926 let map_idx = total_nodes;
2932 let tail = btree_map_chain_tail(buf, node_size); hfs_common::init_map_node(buf, node_size, map_idx, tail, 0);
2934 let linker_off = tail as usize * node_size; BigEndian::write_u32(&mut buf[linker_off..linker_off + 4], map_idx);
2936 hfs_common::btree_bitmap_set(buf, node_size, map_idx);
2937 }
2938
2939 let mut h = BTreeHeader::read(buf);
2940 h.total_nodes += add_slots;
2941 h.free_nodes += grow_nodes; h.write(buf);
2943 }
2944 self.vh.write_count = self.vh.write_count.wrapping_add(1);
2945 if kind == BTreeFork::Catalog {
2946 self.catalog_header = BTreeHeaderRecord::parse(&self.catalog_data[14..14 + 106]);
2947 }
2948 Ok(())
2949 }
2950
2951 fn write_data_to_blocks(
2963 &mut self,
2964 data: &mut dyn std::io::Read,
2965 data_len: u64,
2966 file_id: u32,
2967 fork_type: u8,
2968 ) -> Result<ForkData, FilesystemError> {
2969 if data_len == 0 {
2970 return Ok(ForkData::empty());
2971 }
2972 let block_size = self.vh.block_size as u64;
2973 let blocks_needed = data_len.div_ceil(block_size) as u32;
2974 let extents = self.allocate_extents(blocks_needed)?;
2975
2976 let mut buf = vec![0u8; block_size as usize];
2978 let mut remaining = data_len;
2979 'outer: for ext in &extents {
2980 for i in 0..ext.block_count {
2981 if remaining == 0 {
2982 break 'outer;
2983 }
2984 let to_read = remaining.min(block_size) as usize;
2985 data.read_exact(&mut buf[..to_read]).map_err(|e| {
2986 FilesystemError::Io(std::io::Error::new(
2987 e.kind(),
2988 format!("reading file data: {e}"),
2989 ))
2990 })?;
2991 if to_read < block_size as usize {
2992 buf[to_read..].fill(0);
2993 }
2994 self.write_block(ext.start_block + i, &buf)?;
2995 remaining -= to_read as u64;
2996 }
2997 }
2998
2999 let mut fork = ForkData::empty();
3001 fork.logical_size = data_len;
3002 fork.total_blocks = blocks_needed;
3003 for (slot, ext) in fork.extents.iter_mut().zip(extents.iter().take(8)) {
3004 *slot = *ext;
3005 }
3006
3007 if extents.len() > 8 {
3011 let inline_blocks: u32 = extents.iter().take(8).map(|e| e.block_count).sum();
3012 let mut file_rel_start = inline_blocks;
3013 for chunk in extents[8..].chunks(8) {
3014 let rec =
3015 Self::build_extents_overflow_record(file_id, fork_type, file_rel_start, chunk);
3016 self.insert_extents_overflow_record(&rec)?;
3017 file_rel_start += chunk.iter().map(|e| e.block_count).sum::<u32>();
3018 }
3019 }
3020
3021 Ok(fork)
3022 }
3023
3024 fn build_file_record_with_dates(
3030 file_id: u32,
3031 data_fork: &ForkData,
3032 rsrc_fork: &ForkData,
3033 type_code: &[u8; 4],
3034 creator_code: &[u8; 4],
3035 mtime_secs: Option<u64>,
3036 finder_flags: u16,
3037 ) -> [u8; 248] {
3038 let mut rec = [0u8; 248];
3039 let now = match mtime_secs {
3040 Some(s) => disk_core::times::unix_to_mac_epoch(s),
3041 None => hfs_common::hfs_now(),
3042 };
3043 BigEndian::write_i16(&mut rec[0..2], CATALOG_FILE);
3044 BigEndian::write_u32(&mut rec[8..12], file_id);
3045 BigEndian::write_u32(&mut rec[12..16], now); BigEndian::write_u32(&mut rec[16..20], now); BigEndian::write_u32(&mut rec[20..24], now); BigEndian::write_u32(&mut rec[24..28], now); rec[48..52].copy_from_slice(type_code);
3051 rec[52..56].copy_from_slice(creator_code);
3052 BigEndian::write_u16(&mut rec[56..58], finder_flags); data_fork.serialize(&mut rec[88..168]);
3055 rsrc_fork.serialize(&mut rec[168..248]);
3057 rec
3058 }
3059
3060 fn stamp_record_dates(&mut self, frec_start: usize, update_content: bool) {
3070 let now = hfs_common::hfs_now();
3071 if update_content {
3072 BigEndian::write_u32(
3073 &mut self.catalog_data[frec_start + 16..frec_start + 20],
3074 now,
3075 );
3076 }
3077 BigEndian::write_u32(
3078 &mut self.catalog_data[frec_start + 20..frec_start + 24],
3079 now,
3080 );
3081 BigEndian::write_u32(
3082 &mut self.catalog_data[frec_start + 24..frec_start + 28],
3083 now,
3084 );
3085 }
3086
3087 fn build_folder_record_with_dates(folder_id: u32, mtime_secs: Option<u64>) -> [u8; 88] {
3093 let mut rec = [0u8; 88];
3094 let now = match mtime_secs {
3095 Some(s) => disk_core::times::unix_to_mac_epoch(s),
3096 None => hfs_common::hfs_now(),
3097 };
3098 BigEndian::write_i16(&mut rec[0..2], CATALOG_FOLDER);
3099 BigEndian::write_u32(&mut rec[8..12], folder_id);
3101 BigEndian::write_u32(&mut rec[12..16], now);
3102 BigEndian::write_u32(&mut rec[16..20], now);
3103 BigEndian::write_u32(&mut rec[20..24], now);
3104 BigEndian::write_u32(&mut rec[24..28], now);
3105 rec
3106 }
3107
3108 fn build_thread_record(record_type: i16, parent_cnid: u32, name: &str) -> (Vec<u8>, Vec<u8>) {
3110 let key = Self::build_catalog_key(parent_cnid, "");
3116 let utf16: Vec<u16> = fs_mac_unicode::hfs_unicode::decompose_str(name);
3122 let mut rec = Vec::with_capacity(10 + utf16.len() * 2);
3123 let mut buf2 = [0u8; 2];
3124 let mut buf4 = [0u8; 4];
3125 BigEndian::write_i16(&mut buf2, record_type);
3126 rec.extend_from_slice(&buf2); rec.extend_from_slice(&[0, 0]); BigEndian::write_u32(&mut buf4, parent_cnid);
3129 rec.extend_from_slice(&buf4); BigEndian::write_u16(&mut buf2, utf16.len() as u16);
3131 rec.extend_from_slice(&buf2); for &ch in &utf16 {
3133 BigEndian::write_u16(&mut buf2, ch);
3134 rec.extend_from_slice(&buf2);
3135 }
3136 (key, rec)
3137 }
3138
3139 fn do_sync_metadata(&mut self) -> Result<(), FilesystemError> {
3141 self.vh.modify_date = hfs_common::hfs_now();
3142 self.vh.attributes |= HFSPLUS_VOLUME_UNMOUNTED_BIT;
3150 self.vh.attributes &= !HFSPLUS_BOOT_VOLUME_INCONSISTENT_BIT;
3151 self.vh.attributes &= !HFSPLUS_VOLUME_INCONSISTENT_BIT;
3152 self.write_catalog()?;
3153 self.write_extents_overflow()?;
3154 self.write_attributes()?;
3155 self.write_allocation_bitmap()?;
3156 self.write_volume_header()?;
3157 if let Some(ref boot) = self.pending_boot_blocks {
3158 self.reader.seek(SeekFrom::Start(self.partition_offset))?;
3159 self.reader.write_all(boot.as_ref())?;
3160 }
3161 self.reader.flush()?;
3162 Ok(())
3163 }
3164}
3165
3166impl<R: Read + Seek + Send> Filesystem for HfsPlusFilesystem<R> {
3167 fn root(&mut self) -> Result<FileEntry, FilesystemError> {
3168 Ok(FileEntry {
3169 name: "/".into(),
3170 path: "/".into(),
3171 entry_type: EntryType::Directory,
3172 size: 0,
3173 location: 2, modified: None,
3175 modified_unix: None,
3176 type_code: None,
3177 creator_code: None,
3178 symlink_target: None,
3179 special_type: None,
3180 mode: None,
3181 uid: None,
3182 gid: None,
3183 resource_fork_size: None,
3184 aux_type: None,
3185 link_target_cnid: None,
3186 amiga_protection: None,
3187 amiga_comment: None,
3188 amiga_date: None,
3189 dos_attributes: None,
3190 finder_flags: None,
3191 prodos_file_type: None,
3192 mac_dates: None,
3193 owner_label: None,
3194 native_attrs: None,
3195 qnx2_attrs: None,
3196 })
3197 }
3198
3199 fn list_directory(&mut self, entry: &FileEntry) -> Result<Vec<FileEntry>, FilesystemError> {
3200 let parent_cnid = entry
3206 .link_target_cnid
3207 .map(|c| c as u32)
3208 .unwrap_or(entry.location as u32);
3209 let children = self.list_children(parent_cnid)?;
3210
3211 let mut entries = Vec::new();
3212 for child in children {
3213 match child {
3214 CatalogEntry::Folder {
3215 folder_id,
3216 name,
3217 content_mod_date,
3218 bsd,
3219 } => {
3220 let path = if entry.path == "/" {
3221 format!("/{name}")
3222 } else {
3223 format!("{}/{name}", entry.path)
3224 };
3225 let mut fe = FileEntry::new_directory(name, path, folder_id as u64);
3226 bsd.apply_to(&mut fe);
3227 fe.modified = crate::hfs_common::format_mac_date(content_mod_date);
3228 fe.modified_unix = crate::hfs_common::mac_date_to_unix(content_mod_date);
3229 entries.push(fe);
3230 }
3231 CatalogEntry::File {
3232 file_id,
3233 name,
3234 data_size,
3235 data_fork,
3236 rsrc_size,
3237 rsrc_fork,
3238 type_code,
3239 creator_code,
3240 finder_flags,
3241 link_inode_num,
3242 dir_link_inode_num,
3243 content_mod_date,
3244 bsd,
3245 } => {
3246 let path = if entry.path == "/" {
3247 format!("/{name}")
3248 } else {
3249 format!("{}/{name}", entry.path)
3250 };
3251 if let Some(num) = dir_link_inode_num {
3256 let mut fe = FileEntry::new_directory(name, path, file_id as u64);
3257 fe.type_code = Some(type_code);
3258 fe.creator_code = Some(creator_code);
3259 bsd.apply_to(&mut fe);
3260 if let Some(target_cnid) = self.resolve_dir_hardlink_inode(num)? {
3261 fe.link_target_cnid = Some(target_cnid as u64);
3262 }
3263 let _ = (data_fork, rsrc_fork, finder_flags);
3264 entries.push(fe);
3265 continue;
3266 }
3267 let mut display_size = data_size;
3271 let mut display_rsrc = rsrc_size;
3272 let resolved_target = match link_inode_num {
3273 Some(num) => self.resolve_hardlink_inode(num)?,
3274 None => None,
3275 };
3276 if let Some(target_cnid) = resolved_target {
3277 if let Some((d, r)) = self.find_file_by_id(target_cnid) {
3278 display_size = d.logical_size;
3279 display_rsrc = r.logical_size;
3280 }
3281 }
3282 let mut fe = FileEntry::new_file(name, path, display_size, file_id as u64);
3283 fe.type_code = Some(type_code);
3284 fe.creator_code = Some(creator_code);
3285 fe.finder_flags = Some(finder_flags);
3286 bsd.apply_to(&mut fe);
3287 fe.modified = crate::hfs_common::format_mac_date(content_mod_date);
3288 fe.modified_unix = crate::hfs_common::mac_date_to_unix(content_mod_date);
3289 if display_rsrc > 0 {
3290 fe.resource_fork_size = Some(display_rsrc);
3291 }
3292 if let Some(target) = resolved_target {
3293 fe.link_target_cnid = Some(target as u64);
3294 }
3295 let _ = (data_fork, rsrc_fork);
3317 entries.push(fe);
3318 }
3319 }
3320 }
3321
3322 entries.sort_by_key(|a| a.name.to_lowercase());
3323 Ok(entries)
3324 }
3325
3326 fn read_file(
3327 &mut self,
3328 entry: &FileEntry,
3329 max_bytes: usize,
3330 ) -> Result<Vec<u8>, FilesystemError> {
3331 let file_id = entry
3334 .link_target_cnid
3335 .map(|c| c as u32)
3336 .unwrap_or(entry.location as u32);
3337 let (data_fork, _rsrc_fork) = self.find_file_by_id(file_id).ok_or_else(|| {
3338 FilesystemError::NotFound(format!("file id {file_id} not found in catalog"))
3339 })?;
3340
3341 let mut data = self.read_user_fork(file_id, HFSPLUS_FORK_DATA, &data_fork)?;
3342 data.truncate(max_bytes);
3343 Ok(data)
3344 }
3345
3346 fn write_file_to(
3347 &mut self,
3348 entry: &FileEntry,
3349 writer: &mut dyn std::io::Write,
3350 ) -> Result<u64, FilesystemError> {
3351 if entry.is_directory() {
3352 return Err(FilesystemError::NotADirectory(entry.path.clone()));
3353 }
3354 let file_id = entry
3356 .link_target_cnid
3357 .map(|c| c as u32)
3358 .unwrap_or(entry.location as u32);
3359 let (data_fork, _rsrc_fork) = self.find_file_by_id(file_id).ok_or_else(|| {
3360 FilesystemError::NotFound(format!("file id {file_id} not found in catalog"))
3361 })?;
3362 write_fork_to(
3363 &mut self.reader,
3364 self.partition_offset,
3365 self.vh.block_size,
3366 &data_fork,
3367 writer,
3368 )
3369 }
3370
3371 fn volume_label(&self) -> Option<&str> {
3372 if self.label.is_empty() {
3373 None
3374 } else {
3375 Some(&self.label)
3376 }
3377 }
3378
3379 fn fs_type(&self) -> &str {
3380 if self.vh.is_hfsx() {
3381 "HFSX"
3382 } else {
3383 "HFS+"
3384 }
3385 }
3386
3387 fn validate_name(&self, name: &str) -> Result<(), FilesystemError> {
3388 validate_hfsplus_create_name(name)
3389 }
3390
3391 fn uses_colon_paths(&self) -> bool {
3392 true
3393 }
3394
3395 fn total_size(&self) -> u64 {
3396 self.vh.total_blocks as u64 * self.vh.block_size as u64
3397 }
3398
3399 fn used_size(&self) -> u64 {
3400 (self.vh.total_blocks - self.vh.free_blocks) as u64 * self.vh.block_size as u64
3401 }
3402
3403 fn allocation_unit(&self) -> Option<u64> {
3409 if self.vh.block_size == 0 {
3410 None
3411 } else {
3412 Some(self.vh.block_size as u64)
3413 }
3414 }
3415
3416 fn last_data_byte(&mut self) -> Result<u64, FilesystemError> {
3417 let bitmap = self.read_allocation_bitmap()?;
3418 let search_up_to = alt_vh_blocks(self.vh.total_blocks, self.vh.block_size).start;
3420 let last = find_last_set_bit(&bitmap, search_up_to);
3421 let trailing: u64 = if self.vh.block_size <= 512 { 2 } else { 1 };
3424 match last {
3425 Some(block) => {
3426 let byte = (block as u64 + 1 + trailing) * self.vh.block_size as u64;
3427 Ok(byte.min(self.total_size()))
3428 }
3429 None => Ok(self.total_size()),
3430 }
3431 }
3432
3433 fn defragmented_minimum_size(&mut self) -> Result<u64, FilesystemError> {
3434 let block_size = self.vh.block_size as u64;
3455 let used_blocks = self.vh.total_blocks.saturating_sub(self.vh.free_blocks) as u64;
3456 let used_bytes = used_blocks.saturating_mul(block_size);
3457 let catalog_pad = self.vh.catalog_file.logical_size.saturating_mul(3);
3462 let total = used_bytes.saturating_add(catalog_pad).saturating_add(1024);
3463 Ok(total.div_ceil(block_size) * block_size)
3464 }
3465
3466 fn write_resource_fork_to(
3467 &mut self,
3468 entry: &FileEntry,
3469 writer: &mut dyn std::io::Write,
3470 ) -> Result<u64, FilesystemError> {
3471 let file_id = entry.location as u32;
3472 let (_data_fork, rsrc_fork) = self.find_file_by_id(file_id).ok_or_else(|| {
3473 FilesystemError::NotFound(format!("file id {file_id} not found in catalog"))
3474 })?;
3475 if rsrc_fork.logical_size == 0 {
3476 return Ok(0);
3477 }
3478 let data = read_fork(
3479 &mut self.reader,
3480 self.partition_offset,
3481 self.vh.block_size,
3482 &rsrc_fork,
3483 )?;
3484 writer.write_all(&data)?;
3485 Ok(data.len() as u64)
3486 }
3487
3488 fn resource_fork_size(&mut self, entry: &FileEntry) -> u64 {
3489 let file_id = entry.location as u32;
3490 self.find_file_by_id(file_id)
3491 .map(|(_d, r)| r.logical_size)
3492 .unwrap_or(0)
3493 }
3494
3495 fn fragmentation_stats(
3496 &mut self,
3497 ) -> Option<Result<disk_core::filesystem::FragmentationStats, FilesystemError>> {
3498 Some(Ok(hfsplus_fragmentation_stats(
3499 self.catalog_data(),
3500 self.catalog_first_leaf(),
3501 self.catalog_node_size(),
3502 )))
3503 }
3504
3505 fn blessed_system_folder(&mut self) -> Option<(u64, String)> {
3506 let cnid = self.vh.finder_info[0];
3508 if cnid == 0 {
3509 let cnid_x = self.vh.finder_info[5];
3511 if cnid_x == 0 {
3512 return None;
3513 }
3514 return self.lookup_folder_name(cnid_x);
3515 }
3516 self.lookup_folder_name(cnid)
3517 }
3518
3519 fn fsck(&mut self) -> Option<Result<disk_core::fsck::FsckResult, FilesystemError>> {
3520 Some(crate::hfsplus_fsck::check(self))
3521 }
3522
3523 fn journal_detail(&mut self) -> Result<Option<Box<dyn std::any::Any + Send>>, FilesystemError> {
3524 Ok(HfsPlusFilesystem::journal_detail(self)?
3526 .map(|d| Box::new(d) as Box<dyn std::any::Any + Send>))
3527 }
3528}
3529
3530impl<R: Read + Seek + Send> HfsPlusFilesystem<R> {
3531 fn lookup_folder_name(&self, cnid: u32) -> Option<(u64, String)> {
3532 if let Some((_node, _rec, offset)) = self.find_catalog_record_by_cnid(cnid) {
3534 let node_size = self.catalog_header.node_size as usize;
3535 let key_len = BigEndian::read_u16(&self.catalog_data[offset..offset + 2]) as usize;
3536 let mut rec_data_start = offset + 2 + key_len;
3537 if !rec_data_start.is_multiple_of(2) {
3538 rec_data_start += 1;
3539 }
3540 if rec_data_start + 10 <= self.catalog_data.len() {
3542 let name_len = BigEndian::read_u16(
3543 &self.catalog_data[rec_data_start + 8..rec_data_start + 10],
3544 ) as usize;
3545 let name_end = rec_data_start + 10 + name_len * 2;
3546 if name_end <= self.catalog_data.len() {
3547 let name = decode_utf16be(&self.catalog_data[rec_data_start + 10..name_end]);
3548 return Some((cnid as u64, name));
3549 }
3550 }
3551 let _ = node_size;
3552 }
3553 Some((cnid as u64, format!("CNID {}", cnid)))
3555 }
3556}
3557
3558fn write_fork_to<R: Read + Seek>(
3560 reader: &mut R,
3561 partition_offset: u64,
3562 block_size: u32,
3563 fork: &ForkData,
3564 writer: &mut dyn std::io::Write,
3565) -> Result<u64, FilesystemError> {
3566 let size = fork.logical_size;
3567 let mut written: u64 = 0;
3568 let mut buf = vec![0u8; 64 * 1024];
3569 for ext in &fork.extents {
3570 if ext.is_empty() || written >= size {
3571 break;
3572 }
3573 let offset = partition_offset + ext.start_block as u64 * block_size as u64;
3574 let extent_len = ext.block_count as u64 * block_size as u64;
3575 let to_emit = extent_len.min(size - written);
3576 reader.seek(SeekFrom::Start(offset))?;
3577 let mut left = to_emit;
3578 while left > 0 {
3579 let n = (buf.len() as u64).min(left) as usize;
3580 reader.read_exact(&mut buf[..n])?;
3581 writer.write_all(&buf[..n])?;
3582 left -= n as u64;
3583 }
3584 written += to_emit;
3585 }
3586 Ok(written)
3587}
3588
3589fn read_fork<R: Read + Seek>(
3591 reader: &mut R,
3592 partition_offset: u64,
3593 block_size: u32,
3594 fork: &ForkData,
3595) -> Result<Vec<u8>, FilesystemError> {
3596 let size = fork.logical_size as usize;
3597 let mut data = Vec::with_capacity(size);
3598
3599 let extent_count = fork.extents.iter().filter(|e| !e.is_empty()).count();
3600 let t_total = std::time::Instant::now();
3601 log::debug!(
3602 "[HFS+ read_fork] start: logical_size={} bytes, inline_extents={}",
3603 size,
3604 extent_count
3605 );
3606
3607 for (idx, ext) in fork.extents.iter().enumerate() {
3608 if ext.is_empty() {
3609 break;
3610 }
3611 let offset = partition_offset + ext.start_block as u64 * block_size as u64;
3612 let len = ext.block_count as u64 * block_size as u64;
3613 let read_len = len.min((size - data.len()) as u64) as usize;
3614 let t_ext = std::time::Instant::now();
3615 reader.seek(SeekFrom::Start(offset))?;
3616 let mut buf = vec![0u8; read_len];
3617 reader.read_exact(&mut buf)?;
3618 let elapsed_ms = t_ext.elapsed().as_secs_f64() * 1000.0;
3619 let mb = read_len as f64 / (1024.0 * 1024.0);
3620 let mbps = if elapsed_ms > 0.0 {
3621 mb / (elapsed_ms / 1000.0)
3622 } else {
3623 f64::INFINITY
3624 };
3625 log::debug!(
3626 "[HFS+ read_fork] ext[{}]: offset={} bytes={} time={:.1}ms throughput={:.2} MB/s",
3627 idx,
3628 offset,
3629 read_len,
3630 elapsed_ms,
3631 mbps
3632 );
3633 data.extend_from_slice(&buf);
3634 if data.len() >= size {
3635 break;
3636 }
3637 }
3638
3639 data.truncate(size);
3640 let total_ms = t_total.elapsed().as_secs_f64() * 1000.0;
3641 let total_mb = size as f64 / (1024.0 * 1024.0);
3642 let total_mbps = if total_ms > 0.0 {
3643 total_mb / (total_ms / 1000.0)
3644 } else {
3645 f64::INFINITY
3646 };
3647 log::debug!(
3648 "[HFS+ read_fork] done: {} bytes in {:.1}ms ({:.2} MB/s)",
3649 size,
3650 total_ms,
3651 total_mbps
3652 );
3653 Ok(data)
3654}
3655
3656fn collect_hfsplus_overflow_extents(
3668 extents_data: &[u8],
3669 file_id: u32,
3670 fork_type: u8,
3671 min_start_block: u32,
3672) -> Vec<ExtentDescriptor> {
3673 use crate::hfs_common::walk_leaf_records;
3674
3675 if extents_data.len() < 14 + 30 {
3676 return Vec::new();
3677 }
3678 let header = BTreeHeaderRecord::parse(&extents_data[14..14 + 30.min(extents_data.len() - 14)]);
3679 let node_size = header.node_size as usize;
3680 if node_size == 0 {
3681 return Vec::new();
3682 }
3683
3684 let mut out: Vec<(u32, ExtentDescriptor)> = Vec::new();
3685 walk_leaf_records::<(), _>(
3686 extents_data,
3687 header.first_leaf_node,
3688 node_size,
3689 |_node, _rec_idx, _abs_off, rec| {
3690 if rec.len() < 12 {
3691 return None;
3692 }
3693 let key_len = BigEndian::read_u16(&rec[0..2]) as usize;
3694 if key_len < 10 || 2 + key_len > rec.len() {
3696 return None;
3697 }
3698 let rec_fork = rec[2];
3699 let rec_file = BigEndian::read_u32(&rec[4..8]);
3700 let rec_start_block = BigEndian::read_u32(&rec[8..12]);
3701 if rec_fork != fork_type || rec_file != file_id {
3702 return None;
3703 }
3704 if rec_start_block < min_start_block {
3705 return None;
3706 }
3707 let data_off = 2 + key_len;
3710 if data_off + 64 > rec.len() {
3711 return None;
3712 }
3713 for j in 0..8 {
3714 let ext = ExtentDescriptor::parse(&rec[data_off + j * 8..data_off + j * 8 + 8]);
3715 if ext.block_count > 0 {
3716 out.push((rec_start_block, ext));
3717 }
3718 }
3719 None
3720 },
3721 );
3722
3723 out.sort_by_key(|(sb, _)| *sb);
3724 out.into_iter().map(|(_, e)| e).collect()
3725}
3726
3727fn read_fork_with_overflow<R: Read + Seek>(
3735 reader: &mut R,
3736 partition_offset: u64,
3737 block_size: u32,
3738 fork: &ForkData,
3739 file_id: u32,
3740 fork_type: u8,
3741 extents_overflow_data: Option<&[u8]>,
3742) -> Result<Vec<u8>, FilesystemError> {
3743 let mut data = read_fork(reader, partition_offset, block_size, fork)?;
3744 let target = fork.logical_size as usize;
3745 if data.len() >= target {
3746 return Ok(data);
3747 }
3748 let Some(ext_data) = extents_overflow_data else {
3749 return Err(FilesystemError::InvalidData(format!(
3750 "file {file_id} fork {fork_type:#x}: {} bytes requested but only \
3751 {} bytes in inline extents and no extents-overflow B-tree available",
3752 fork.logical_size,
3753 data.len()
3754 )));
3755 };
3756 let inline_blocks: u32 = fork.extents.iter().map(|e| e.block_count).sum();
3757 let overflow = collect_hfsplus_overflow_extents(ext_data, file_id, fork_type, inline_blocks);
3758 for ext in overflow {
3759 if data.len() >= target {
3760 break;
3761 }
3762 if ext.block_count == 0 {
3763 continue;
3764 }
3765 let offset = partition_offset + ext.start_block as u64 * block_size as u64;
3766 let extent_len = ext.block_count as u64 * block_size as u64;
3767 let to_read = extent_len.min((target - data.len()) as u64) as usize;
3768 reader.seek(SeekFrom::Start(offset))?;
3769 let mut buf = vec![0u8; to_read];
3770 reader.read_exact(&mut buf)?;
3771 data.extend_from_slice(&buf);
3772 }
3773 if data.len() < target {
3774 return Err(FilesystemError::InvalidData(format!(
3775 "file {file_id} fork {fork_type:#x}: extents (inline + overflow) \
3776 cover {} of {} bytes",
3777 data.len(),
3778 fork.logical_size
3779 )));
3780 }
3781 Ok(data)
3782}
3783
3784fn translate_fork_offset(
3790 fork: &ForkData,
3791 block_size: u32,
3792 partition_offset: u64,
3793 fork_offset: u64,
3794) -> Option<(u64, u64)> {
3795 let bs = block_size as u64;
3796 let mut cursor: u64 = 0;
3797 for ext in &fork.extents {
3798 if ext.block_count == 0 {
3799 continue;
3800 }
3801 let len = ext.block_count as u64 * bs;
3802 if fork_offset < cursor + len {
3803 let local = fork_offset - cursor;
3804 let dev = partition_offset + ext.start_block as u64 * bs + local;
3805 return Some((dev, len - local));
3806 }
3807 cursor += len;
3808 }
3809 None
3810}
3811
3812fn read_node_from_fork<R: Read + Seek>(
3814 reader: &mut R,
3815 partition_offset: u64,
3816 block_size: u32,
3817 fork: &ForkData,
3818 node_idx: u32,
3819 node_size: usize,
3820) -> Option<Vec<u8>> {
3821 let fork_offset = (node_idx as u64).checked_mul(node_size as u64)?;
3822 let (dev_off, contig) = translate_fork_offset(fork, block_size, partition_offset, fork_offset)?;
3823 if (contig as usize) < node_size {
3824 return None;
3828 }
3829 reader.seek(SeekFrom::Start(dev_off)).ok()?;
3830 let mut buf = vec![0u8; node_size];
3831 reader.read_exact(&mut buf).ok()?;
3832 Some(buf)
3833}
3834
3835pub fn probe_hfsplus_volume_label<R: Read + Seek>(
3852 reader: &mut R,
3853 partition_offset: u64,
3854) -> Option<String> {
3855 let (fs_type, hfsplus_offset) = resolve_apple_hfs(reader, partition_offset);
3862 if fs_type != "hfsplus" {
3863 return None;
3864 }
3865 reader.seek(SeekFrom::Start(hfsplus_offset + 1024)).ok()?;
3866 let mut vh_buf = [0u8; 512];
3867 reader.read_exact(&mut vh_buf).ok()?;
3868 let vh = HfsPlusVolumeHeader::parse(&vh_buf).ok()?;
3869 if vh.block_size == 0 {
3870 return None;
3871 }
3872
3873 let probe_size = (vh.block_size as usize).clamp(512, 32 * 1024);
3878 let node0 = read_node_from_fork(
3879 reader,
3880 hfsplus_offset,
3881 vh.block_size,
3882 &vh.catalog_file,
3883 0,
3884 probe_size,
3885 )?;
3886 if node0.len() < 14 + 30 {
3887 return None;
3888 }
3889 let header = BTreeHeaderRecord::parse(&node0[14..14 + 30]);
3890 let node_size = header.node_size as usize;
3891 if node_size == 0 || node_size > 32 * 1024 || header.first_leaf_node == 0 {
3892 return None;
3893 }
3894
3895 let leaf = read_node_from_fork(
3898 reader,
3899 hfsplus_offset,
3900 vh.block_size,
3901 &vh.catalog_file,
3902 header.first_leaf_node,
3903 node_size,
3904 )?;
3905 if leaf.len() < node_size || leaf[8] as i8 != -1 {
3906 return None;
3907 }
3908 let num_records = BigEndian::read_u16(&leaf[10..12]) as usize;
3909 if num_records == 0 {
3910 return None;
3911 }
3912 let (rec_start, rec_end) = crate::hfs_common::btree_record_range(&leaf, node_size, 0);
3913 if rec_start >= rec_end || rec_end > node_size {
3914 return None;
3915 }
3916 let rec = &leaf[rec_start..rec_end];
3917 if rec.len() < 8 {
3918 return None;
3919 }
3920 let key_len = BigEndian::read_u16(&rec[0..2]) as usize;
3921 if key_len < 6 || 2 + key_len > rec.len() {
3922 return None;
3923 }
3924 let parent_id = BigEndian::read_u32(&rec[2..6]);
3925 if parent_id != 1 {
3926 return None;
3927 }
3928 let name_length = BigEndian::read_u16(&rec[6..8]) as usize;
3929 if name_length == 0 {
3930 return None;
3931 }
3932 let body_start = 8;
3933 let body_end = body_start + name_length * 2;
3934 if body_end > rec.len() || body_end > 2 + key_len {
3935 return None;
3936 }
3937 let label = decode_utf16be(&rec[body_start..body_end]);
3938 if label.is_empty() {
3939 None
3940 } else {
3941 Some(label)
3942 }
3943}
3944
3945fn write_fork_data<R: Write + Seek>(
3947 writer: &mut R,
3948 partition_offset: u64,
3949 block_size: u32,
3950 fork: &ForkData,
3951 data: &[u8],
3952) -> Result<(), FilesystemError> {
3953 let mut written = 0usize;
3954 for ext in &fork.extents {
3955 if ext.is_empty() || written >= data.len() {
3956 break;
3957 }
3958 let offset = partition_offset + ext.start_block as u64 * block_size as u64;
3959 let extent_len = ext.block_count as u64 * block_size as u64;
3960 let to_write = extent_len.min((data.len() - written) as u64) as usize;
3961 writer.seek(SeekFrom::Start(offset))?;
3962 writer.write_all(&data[written..written + to_write])?;
3963 written += to_write;
3964 }
3965 Ok(())
3966}
3967
3968#[allow(clippy::too_many_arguments)]
3975fn write_fork_data_with_overflow<R: Write + Seek>(
3976 writer: &mut R,
3977 partition_offset: u64,
3978 block_size: u32,
3979 fork: &ForkData,
3980 file_id: u32,
3981 fork_type: u8,
3982 extents_overflow_data: Option<&[u8]>,
3983 data: &[u8],
3984) -> Result<(), FilesystemError> {
3985 let mut written = 0usize;
3986 let write_extent = |writer: &mut R, ext: &ExtentDescriptor, written: &mut usize| {
3987 if ext.block_count == 0 || *written >= data.len() {
3988 return Ok(false);
3989 }
3990 let offset = partition_offset + ext.start_block as u64 * block_size as u64;
3991 let extent_len = ext.block_count as u64 * block_size as u64;
3992 let to_write = extent_len.min((data.len() - *written) as u64) as usize;
3993 writer.seek(SeekFrom::Start(offset))?;
3994 writer.write_all(&data[*written..*written + to_write])?;
3995 *written += to_write;
3996 Ok::<bool, FilesystemError>(true)
3997 };
3998
3999 for ext in &fork.extents {
4000 if ext.is_empty() {
4001 break;
4002 }
4003 if !write_extent(writer, ext, &mut written)? {
4004 return Ok(());
4005 }
4006 }
4007 if written >= data.len() {
4008 return Ok(());
4009 }
4010 let Some(ext_data) = extents_overflow_data else {
4012 return Err(FilesystemError::InvalidData(format!(
4013 "special file {file_id} fork {fork_type:#x}: {} bytes to write but only \
4014 inline extents and no extents-overflow B-tree",
4015 data.len()
4016 )));
4017 };
4018 let inline_blocks: u32 = fork.extents.iter().map(|e| e.block_count).sum();
4019 for ext in collect_hfsplus_overflow_extents(ext_data, file_id, fork_type, inline_blocks) {
4020 if !write_extent(writer, &ext, &mut written)? {
4021 break;
4022 }
4023 }
4024 if written < data.len() {
4025 return Err(FilesystemError::InvalidData(format!(
4026 "special file {file_id} fork {fork_type:#x}: extents cover {written} of {} bytes",
4027 data.len()
4028 )));
4029 }
4030 Ok(())
4031}
4032
4033fn decode_utf16be(data: &[u8]) -> String {
4035 let chars: Vec<u16> = data.chunks_exact(2).map(BigEndian::read_u16).collect();
4036 String::from_utf16_lossy(&chars)
4037}
4038
4039fn hfsplus_fragmentation_stats(
4047 catalog_data: &[u8],
4048 first_leaf: u32,
4049 node_size: usize,
4050) -> disk_core::filesystem::FragmentationStats {
4051 use crate::hfs_common::walk_leaf_records;
4052 use byteorder::{BigEndian, ByteOrder};
4053
4054 let mut stats = disk_core::filesystem::FragmentationStats::default();
4055 if node_size == 0 || catalog_data.is_empty() {
4056 return stats;
4057 }
4058 walk_leaf_records::<(), _>(
4059 catalog_data,
4060 first_leaf,
4061 node_size,
4062 |_node_idx, _rec_idx, _abs_off, rec| {
4063 if rec.len() < 4 {
4068 return None;
4069 }
4070 let key_len = BigEndian::read_u16(&rec[0..2]) as usize;
4071 let mut body_start = 2 + key_len;
4072 if !body_start.is_multiple_of(2) {
4073 body_start += 1;
4074 }
4075 if body_start + 88 + 80 > rec.len() {
4076 return None;
4077 }
4078 let body = &rec[body_start..];
4079 let rec_type = BigEndian::read_u16(&body[0..2]);
4080 if rec_type != 0x0002 {
4081 return None;
4082 }
4083 let data_fork = &body[88..88 + 80];
4084 let total_blocks = BigEndian::read_u32(&data_fork[12..16]);
4085 if total_blocks == 0 {
4086 return None;
4087 }
4088 let mut inline_extents: u32 = 0;
4089 let mut inline_blocks: u32 = 0;
4090 for i in 0..8 {
4091 let off = 16 + i * 8;
4092 let count = BigEndian::read_u32(&data_fork[off + 4..off + 8]);
4093 if count > 0 {
4094 inline_extents += 1;
4095 inline_blocks = inline_blocks.saturating_add(count);
4096 }
4097 }
4098 stats.files_with_data += 1;
4099 if inline_extents > 1 || total_blocks > inline_blocks {
4102 stats.fragmented_files += 1;
4103 }
4104 None
4105 },
4106 );
4107 stats
4108}
4109
4110fn find_volume_label(catalog_data: &[u8], header: &BTreeHeaderRecord) -> String {
4111 use crate::hfs_common::walk_leaf_records;
4112
4113 let node_size = header.node_size as usize;
4114 if node_size == 0 {
4115 return String::new();
4116 }
4117
4118 walk_leaf_records::<String, _>(
4119 catalog_data,
4120 header.first_leaf_node,
4121 node_size,
4122 |_node_idx, _rec_idx, _abs_off, rec| {
4123 if rec.len() < 8 {
4124 return None;
4125 }
4126 let key_len = BigEndian::read_u16(&rec[0..2]) as usize;
4127 if key_len < 6 || 2 + key_len > rec.len() {
4128 return None;
4129 }
4130 let parent_id = BigEndian::read_u32(&rec[2..6]);
4132 if parent_id != 2 {
4133 return None;
4134 }
4135 let name_length = BigEndian::read_u16(&rec[6..8]) as usize;
4136 if name_length != 0 {
4137 return None;
4138 }
4139
4140 let mut data_start = 2 + key_len;
4144 if !data_start.is_multiple_of(2) {
4145 data_start += 1;
4146 }
4147 if data_start + 10 > rec.len() {
4148 return None;
4149 }
4150 let record_type = BigEndian::read_i16(&rec[data_start..data_start + 2]);
4151 if record_type != 3 {
4153 return None;
4154 }
4155 let thread_name_len =
4156 BigEndian::read_u16(&rec[data_start + 8..data_start + 10]) as usize;
4157 let body_start = data_start + 10;
4158 let body_end = body_start + thread_name_len * 2;
4159 if body_end > rec.len() {
4160 return None;
4161 }
4162 Some(decode_utf16be(&rec[body_start..body_end]))
4163 },
4164 )
4165 .unwrap_or_default()
4166}
4167
4168fn find_last_set_bit(bitmap: &[u8], max_bits: u32) -> Option<u32> {
4174 let max = max_bits as usize;
4175 let scan_bytes = max.div_ceil(8).min(bitmap.len());
4176 for byte_idx in (0..scan_bytes).rev() {
4177 let byte = bitmap[byte_idx];
4178 if byte == 0 {
4179 continue;
4180 }
4181 for bit_pos in 0..8u32 {
4185 let global = (byte_idx as u32) * 8 + (7 - bit_pos);
4186 if global >= max_bits {
4187 continue;
4188 }
4189 if (byte >> bit_pos) & 1 == 1 {
4190 return Some(global);
4191 }
4192 }
4193 }
4194 None
4195}
4196
4197impl<R: Read + Write + Seek + Send> EditableFilesystem for HfsPlusFilesystem<R> {
4200 fn as_filesystem(&self) -> &dyn disk_core::filesystem::Filesystem {
4201 self
4202 }
4203 fn as_filesystem_mut(&mut self) -> &mut dyn disk_core::filesystem::Filesystem {
4204 self
4205 }
4206 fn create_file(
4207 &mut self,
4208 parent: &FileEntry,
4209 name: &str,
4210 data: &mut dyn std::io::Read,
4211 data_len: u64,
4212 options: &CreateFileOptions,
4213 ) -> Result<FileEntry, FilesystemError> {
4214 let snap = self.snapshot();
4215 let result = self.create_file_inner(parent, name, data, data_len, options);
4216 if result.is_err() {
4217 self.restore_snapshot(snap);
4218 }
4219 result
4220 }
4221
4222 fn create_directory(
4223 &mut self,
4224 parent: &FileEntry,
4225 name: &str,
4226 options: &CreateDirectoryOptions,
4227 ) -> Result<FileEntry, FilesystemError> {
4228 let snap = self.snapshot();
4229 let result = self.create_directory_inner(parent, name, options);
4230 if result.is_err() {
4231 self.restore_snapshot(snap);
4232 }
4233 result
4234 }
4235
4236 fn delete_entry(
4237 &mut self,
4238 parent: &FileEntry,
4239 entry: &FileEntry,
4240 ) -> Result<(), FilesystemError> {
4241 let snap = self.snapshot();
4242 let result = self.delete_entry_inner(parent, entry);
4243 if result.is_err() {
4244 self.restore_snapshot(snap);
4245 }
4246 result
4247 }
4248
4249 fn rename(
4250 &mut self,
4251 parent: &FileEntry,
4252 entry: &FileEntry,
4253 new_name: &str,
4254 ) -> Result<(), FilesystemError> {
4255 if new_name == entry.name {
4256 return Ok(());
4257 }
4258 validate_hfsplus_create_name(new_name)?;
4259 let snap = self.snapshot();
4260 let result = self.rename_inner(parent, entry, new_name);
4261 if result.is_err() {
4262 self.restore_snapshot(snap);
4263 }
4264 result
4265 }
4266
4267 fn set_type_creator(
4268 &mut self,
4269 entry: &FileEntry,
4270 type_code: &str,
4271 creator_code: &str,
4272 ) -> Result<(), FilesystemError> {
4273 let snap = self.snapshot();
4274 let result = self.set_type_creator_inner(entry, type_code, creator_code);
4275 if result.is_err() {
4276 self.restore_snapshot(snap);
4277 }
4278 result
4279 }
4280
4281 fn write_resource_fork(
4282 &mut self,
4283 entry: &FileEntry,
4284 data: &mut dyn std::io::Read,
4285 len: u64,
4286 ) -> Result<(), FilesystemError> {
4287 let snap = self.snapshot();
4288 let result = self.write_resource_fork_inner(entry, data, len);
4289 if result.is_err() {
4290 self.restore_snapshot(snap);
4291 }
4292 result
4293 }
4294
4295 fn set_permissions(&mut self, entry: &FileEntry, mode: u32) -> Result<(), FilesystemError> {
4296 let frec = self.record_body_offset(entry.location as u32)?;
4297 let base = Self::bsd_type_bits(&self.catalog_data, frec, entry.is_directory());
4298 let new = disk_core::unix_common::inode::with_permission_bits(base, mode) as u16;
4299 let at = frec + HfsPlusBsdInfo::OFFSET + 10;
4300 BigEndian::write_u16(&mut self.catalog_data[at..at + 2], new);
4301 Ok(())
4302 }
4303
4304 fn set_owner(&mut self, entry: &FileEntry, uid: u32, gid: u32) -> Result<(), FilesystemError> {
4305 let frec = self.record_body_offset(entry.location as u32)?;
4307 let at = frec + HfsPlusBsdInfo::OFFSET;
4308 BigEndian::write_u32(&mut self.catalog_data[at..at + 4], uid);
4309 BigEndian::write_u32(&mut self.catalog_data[at + 4..at + 8], gid);
4310 let mode_at = frec + HfsPlusBsdInfo::OFFSET + 10;
4315 if BigEndian::read_u16(&self.catalog_data[mode_at..mode_at + 2]) == 0 {
4316 let default_mode = if entry.is_directory() {
4317 0o040_755
4318 } else {
4319 0o100_644
4320 };
4321 BigEndian::write_u16(
4322 &mut self.catalog_data[mode_at..mode_at + 2],
4323 default_mode as u16,
4324 );
4325 }
4326 Ok(())
4327 }
4328
4329 fn sync_metadata(&mut self) -> Result<(), FilesystemError> {
4330 self.do_sync_metadata()
4331 }
4332
4333 fn free_space(&mut self) -> Result<u64, FilesystemError> {
4334 Ok(self.vh.free_blocks as u64 * self.vh.block_size as u64)
4335 }
4336
4337 fn set_blessed_folder(&mut self, entry: &FileEntry) -> Result<(), FilesystemError> {
4338 let snap = self.snapshot();
4339 let result = self.set_blessed_folder_inner(entry);
4340 if result.is_err() {
4341 self.restore_snapshot(snap);
4342 }
4343 result
4344 }
4345
4346 fn write_boot_blocks(&mut self, blocks: &[u8; 1024]) -> Result<(), FilesystemError> {
4347 self.set_boot_blocks(blocks);
4348 Ok(())
4349 }
4350}
4351
4352impl<R: Read + Write + Seek + Send> HfsPlusFilesystem<R> {
4353 pub fn set_xattr(
4364 &mut self,
4365 cnid: u32,
4366 name: &str,
4367 value: &[u8],
4368 ) -> Result<(), FilesystemError> {
4369 let snap = self.snapshot();
4370 let result = (|| -> Result<(), FilesystemError> {
4371 self.remove_xattr_records(cnid, Some(name))?;
4372 let rec = Self::build_inline_attr_record(cnid, name, value);
4373 self.insert_xattr_record(&rec)?;
4374 if let Some(set) = self.xattr_cnids.as_mut() {
4375 set.insert(cnid);
4376 }
4377 Ok(())
4378 })();
4379 if result.is_err() {
4380 self.restore_snapshot(snap);
4381 }
4382 result
4383 }
4384
4385 pub fn remove_xattr(&mut self, cnid: u32, name: &str) -> Result<(), FilesystemError> {
4388 let snap = self.snapshot();
4389 let result = (|| -> Result<(), FilesystemError> {
4390 let removed = self.remove_xattr_records(cnid, Some(name))?;
4391 if removed == 0 {
4392 return Err(FilesystemError::NotFound(format!(
4393 "extended attribute '{name}' on cnid {cnid}"
4394 )));
4395 }
4396 if self.list_xattrs(cnid)?.is_empty() {
4398 if let Some(set) = self.xattr_cnids.as_mut() {
4399 set.remove(&cnid);
4400 }
4401 }
4402 Ok(())
4403 })();
4404 if result.is_err() {
4405 self.restore_snapshot(snap);
4406 }
4407 result
4408 }
4409}
4410
4411#[derive(Debug, Clone, Default)]
4421pub(crate) struct RecordMetadata {
4422 pub create_date: u32,
4423 pub content_mod_date: u32,
4424 pub attribute_mod_date: u32,
4425 pub access_date: u32,
4426 pub backup_date: u32,
4427 pub bsd_info: [u8; 16],
4428 pub finder_info: [u8; 16],
4429 pub extended_finder_info: [u8; 16],
4430 pub text_encoding: u32,
4431}
4432
4433impl<R: Read + Write + Seek + Send> HfsPlusFilesystem<R> {
4434 pub(crate) fn record_body_offset(&mut self, cnid: u32) -> Result<usize, FilesystemError> {
4451 let (_, _, t_offset) = self.find_catalog_record_by_cnid(cnid).ok_or_else(|| {
4452 FilesystemError::NotFound(format!("thread record for CNID {cnid} not found"))
4453 })?;
4454 let key_len = BigEndian::read_u16(&self.catalog_data[t_offset..t_offset + 2]) as usize;
4455 let mut rec_data_start = t_offset + 2 + key_len;
4456 if !rec_data_start.is_multiple_of(2) {
4457 rec_data_start += 1;
4458 }
4459 let thread_parent =
4460 BigEndian::read_u32(&self.catalog_data[rec_data_start + 4..rec_data_start + 8]);
4461 let thread_name_len =
4462 BigEndian::read_u16(&self.catalog_data[rec_data_start + 8..rec_data_start + 10])
4463 as usize;
4464 let thread_name = decode_utf16be(
4465 &self.catalog_data[rec_data_start + 10..rec_data_start + 10 + thread_name_len * 2],
4466 );
4467
4468 let (_, _, f_offset) = self
4469 .find_catalog_record(thread_parent, &thread_name)
4470 .ok_or_else(|| {
4471 FilesystemError::NotFound(format!(
4472 "catalog record for CNID {cnid} ('{thread_name}') not found"
4473 ))
4474 })?;
4475 let fkey_len = BigEndian::read_u16(&self.catalog_data[f_offset..f_offset + 2]) as usize;
4476 let mut frec = f_offset + 2 + fkey_len;
4477 if !frec.is_multiple_of(2) {
4478 frec += 1;
4479 }
4480 if frec + 84 > self.catalog_data.len() {
4481 return Err(FilesystemError::InvalidData(format!(
4482 "catalog record for CNID {cnid} truncated"
4483 )));
4484 }
4485 Ok(frec)
4486 }
4487
4488 fn bsd_type_bits(catalog: &[u8], frec: usize, is_dir: bool) -> u32 {
4497 let at = frec + HfsPlusBsdInfo::OFFSET + 10;
4498 let cur = BigEndian::read_u16(&catalog[at..at + 2]) as u32;
4499 if cur & 0o170_000 != 0 {
4500 cur
4501 } else if is_dir {
4502 0o040_000
4503 } else {
4504 0o100_000
4505 }
4506 }
4507
4508 pub(crate) fn set_record_metadata(
4509 &mut self,
4510 cnid: u32,
4511 meta: &RecordMetadata,
4512 ) -> Result<(), FilesystemError> {
4513 let frec = self.record_body_offset(cnid)?;
4514
4515 BigEndian::write_u32(
4516 &mut self.catalog_data[frec + 12..frec + 16],
4517 meta.create_date,
4518 );
4519 BigEndian::write_u32(
4520 &mut self.catalog_data[frec + 16..frec + 20],
4521 meta.content_mod_date,
4522 );
4523 BigEndian::write_u32(
4524 &mut self.catalog_data[frec + 20..frec + 24],
4525 meta.attribute_mod_date,
4526 );
4527 BigEndian::write_u32(
4528 &mut self.catalog_data[frec + 24..frec + 28],
4529 meta.access_date,
4530 );
4531 BigEndian::write_u32(
4532 &mut self.catalog_data[frec + 28..frec + 32],
4533 meta.backup_date,
4534 );
4535 self.catalog_data[frec + 32..frec + 48].copy_from_slice(&meta.bsd_info);
4536 self.catalog_data[frec + 48..frec + 64].copy_from_slice(&meta.finder_info);
4537 self.catalog_data[frec + 64..frec + 80].copy_from_slice(&meta.extended_finder_info);
4538 BigEndian::write_u32(
4539 &mut self.catalog_data[frec + 80..frec + 84],
4540 meta.text_encoding,
4541 );
4542 Ok(())
4543 }
4544
4545 pub(crate) fn set_volume_dates(
4553 &mut self,
4554 create_date: u32,
4555 modify_date: u32,
4556 backup_date: u32,
4557 checked_date: u32,
4558 ) {
4559 self.vh.create_date = create_date;
4560 self.vh.modify_date = modify_date;
4561 self.vh.backup_date = backup_date;
4562 self.vh.checked_date = checked_date;
4563 }
4564
4565 pub(crate) fn set_volume_finder_info(&mut self, finder_info: [u32; 8]) {
4569 self.vh.finder_info = finder_info;
4570 }
4571
4572 pub(crate) fn set_boot_blocks(&mut self, blocks: &[u8; 1024]) {
4577 self.pending_boot_blocks = Some(Box::new(*blocks));
4578 }
4579}
4580
4581impl<R: Read + Write + Seek + Send> HfsPlusFilesystem<R> {
4582 pub fn ensure_private_dir(&mut self) -> Result<FileEntry, FilesystemError> {
4588 let private_name = hfsplus_private_dir_name();
4589 let root = self.root()?;
4590 for entry in self.list_directory(&root)? {
4591 if entry.is_directory() && entry.name == private_name {
4592 return Ok(entry);
4593 }
4594 }
4595 self.create_directory(&root, &private_name, &CreateDirectoryOptions::default())
4596 }
4597
4598 pub fn ensure_dir_private_dir(&mut self) -> Result<FileEntry, FilesystemError> {
4602 let dir_private_name = hfsplus_dir_private_dir_name();
4603 let root = self.root()?;
4604 for entry in self.list_directory(&root)? {
4605 if entry.is_directory() && entry.name == dir_private_name {
4606 return Ok(entry);
4607 }
4608 }
4609 self.create_directory(&root, &dir_private_name, &CreateDirectoryOptions::default())
4610 }
4611
4612 pub fn create_hardlink_inode(
4622 &mut self,
4623 inode_num: u32,
4624 data: &mut dyn std::io::Read,
4625 data_len: u64,
4626 options: &CreateFileOptions,
4627 ) -> Result<FileEntry, FilesystemError> {
4628 let snap = self.snapshot();
4629 let result = (|| -> Result<FileEntry, FilesystemError> {
4630 let private = self.ensure_private_dir()?;
4631 let inode_name = format!("iNode{inode_num}");
4632 self.create_file(&private, &inode_name, data, data_len, options)
4633 })();
4634 if result.is_err() {
4635 self.restore_snapshot(snap);
4636 }
4637 result
4638 }
4639
4640 pub fn create_hardlink(
4650 &mut self,
4651 parent: &FileEntry,
4652 name: &str,
4653 inode_num: u32,
4654 ) -> Result<FileEntry, FilesystemError> {
4655 let snap = self.snapshot();
4656 let result = (|| -> Result<FileEntry, FilesystemError> {
4657 let private_cnid = self.find_private_dir_cnid()?.ok_or_else(|| {
4658 FilesystemError::NotFound(
4659 "HFS+ Private Data directory missing — call create_hardlink_inode first".into(),
4660 )
4661 })?;
4662 let inode_name = format!("iNode{inode_num}");
4664 let inode_cnid = self
4665 .list_children(private_cnid)?
4666 .into_iter()
4667 .find_map(|c| match c {
4668 CatalogEntry::File {
4669 file_id, name: n, ..
4670 } if n == inode_name => Some(file_id),
4671 _ => None,
4672 })
4673 .ok_or_else(|| {
4674 FilesystemError::NotFound(format!(
4675 "hardlink inode '{inode_name}' missing — create it before linking"
4676 ))
4677 })?;
4678
4679 let mut empty: &[u8] = &[];
4681 let stub = self.create_file(
4682 parent,
4683 name,
4684 &mut empty,
4685 0,
4686 &CreateFileOptions {
4687 os_type: Some(*b"hlnk"),
4688 os_creator: Some(*b"hfs+"),
4689 ..Default::default()
4690 },
4691 )?;
4692
4693 let parent_cnid = parent.location as u32;
4695 let (_, _, abs) = self.find_catalog_record(parent_cnid, name).ok_or_else(|| {
4696 FilesystemError::NotFound(format!(
4697 "freshly-created stub '{name}' missing from catalog"
4698 ))
4699 })?;
4700 let key_len = BigEndian::read_u16(&self.catalog_data[abs..abs + 2]) as usize;
4701 let mut body = abs + 2 + key_len;
4702 if !body.is_multiple_of(2) {
4703 body += 1;
4704 }
4705 BigEndian::write_u32(&mut self.catalog_data[body + 44..body + 48], inode_num);
4706
4707 self.adjust_inode_link_count(private_cnid, inode_num, 1)?;
4709
4710 self.hardlink_inode_map = None;
4713
4714 let mut fe = stub;
4717 fe.link_target_cnid = Some(inode_cnid as u64);
4718 Ok(fe)
4719 })();
4720 if result.is_err() {
4721 self.restore_snapshot(snap);
4722 }
4723 result
4724 }
4725
4726 pub fn create_dir_hardlink_inode(
4732 &mut self,
4733 inode_num: u32,
4734 options: &CreateDirectoryOptions,
4735 ) -> Result<FileEntry, FilesystemError> {
4736 let snap = self.snapshot();
4737 let result = (|| -> Result<FileEntry, FilesystemError> {
4738 let private = self.ensure_dir_private_dir()?;
4739 let inode_name = format!("dir_{inode_num}");
4740 self.create_directory(&private, &inode_name, options)
4741 })();
4742 if result.is_err() {
4743 self.restore_snapshot(snap);
4744 }
4745 result
4746 }
4747
4748 pub fn create_dir_hardlink(
4755 &mut self,
4756 parent: &FileEntry,
4757 name: &str,
4758 inode_num: u32,
4759 ) -> Result<FileEntry, FilesystemError> {
4760 let snap = self.snapshot();
4761 let result = (|| -> Result<FileEntry, FilesystemError> {
4762 let dir_private_cnid = self.find_dir_private_dir_cnid()?.ok_or_else(|| {
4763 FilesystemError::NotFound(
4764 ".HFS+ Private Directory Data missing — call create_dir_hardlink_inode first"
4765 .into(),
4766 )
4767 })?;
4768 let inode_name = format!("dir_{inode_num}");
4769 let inode_cnid = self
4770 .list_children(dir_private_cnid)?
4771 .into_iter()
4772 .find_map(|c| match c {
4773 CatalogEntry::Folder {
4774 folder_id, name: n, ..
4775 } if n == inode_name => Some(folder_id),
4776 _ => None,
4777 })
4778 .ok_or_else(|| {
4779 FilesystemError::NotFound(format!(
4780 "directory inode '{inode_name}' missing — create it before linking"
4781 ))
4782 })?;
4783
4784 let mut empty: &[u8] = &[];
4785 let stub = self.create_file(
4786 parent,
4787 name,
4788 &mut empty,
4789 0,
4790 &CreateFileOptions {
4791 os_type: Some(*b"fdrp"),
4792 os_creator: Some(*b"MACS"),
4793 ..Default::default()
4794 },
4795 )?;
4796
4797 let parent_cnid = parent.location as u32;
4799 let (_, _, abs) = self.find_catalog_record(parent_cnid, name).ok_or_else(|| {
4800 FilesystemError::NotFound(format!(
4801 "freshly-created stub '{name}' missing from catalog"
4802 ))
4803 })?;
4804 let key_len = BigEndian::read_u16(&self.catalog_data[abs..abs + 2]) as usize;
4805 let mut body = abs + 2 + key_len;
4806 if !body.is_multiple_of(2) {
4807 body += 1;
4808 }
4809 BigEndian::write_u32(&mut self.catalog_data[body + 44..body + 48], inode_num);
4810
4811 self.adjust_dir_inode_link_count(dir_private_cnid, inode_num, 1)?;
4814
4815 self.dir_hardlink_inode_map = None;
4817
4818 let mut fe = stub;
4821 fe.entry_type = disk_core::entry::EntryType::Directory;
4822 fe.size = 0;
4823 fe.link_target_cnid = Some(inode_cnid as u64);
4824 Ok(fe)
4825 })();
4826 if result.is_err() {
4827 self.restore_snapshot(snap);
4828 }
4829 result
4830 }
4831
4832 fn adjust_dir_inode_link_count(
4836 &mut self,
4837 dir_private_cnid: u32,
4838 inode_num: u32,
4839 delta: i64,
4840 ) -> Result<u32, FilesystemError> {
4841 let inode_name = format!("dir_{inode_num}");
4842 let (_, _, abs_off) = self
4843 .find_catalog_record(dir_private_cnid, &inode_name)
4844 .ok_or_else(|| {
4845 FilesystemError::InvalidData(format!(
4846 "directory inode '{inode_name}' missing from .HFS+ Private Directory Data"
4847 ))
4848 })?;
4849 let key_len = BigEndian::read_u16(&self.catalog_data[abs_off..abs_off + 2]) as usize;
4850 let mut body_start = abs_off + 2 + key_len;
4851 if !body_start.is_multiple_of(2) {
4852 body_start += 1;
4853 }
4854 let cur = BigEndian::read_u32(&self.catalog_data[body_start + 44..body_start + 48]) as i64;
4855 let new = (cur + delta).max(0) as u32;
4856 BigEndian::write_u32(
4857 &mut self.catalog_data[body_start + 44..body_start + 48],
4858 new,
4859 );
4860 Ok(new)
4861 }
4862
4863 fn create_file_inner(
4864 &mut self,
4865 parent: &FileEntry,
4866 name: &str,
4867 data: &mut dyn std::io::Read,
4868 data_len: u64,
4869 options: &CreateFileOptions,
4870 ) -> Result<FileEntry, FilesystemError> {
4871 let parent_cnid = parent.location as u32;
4872
4873 validate_hfsplus_create_name(name)?;
4874
4875 if self.find_catalog_record(parent_cnid, name).is_some() {
4877 return Err(FilesystemError::AlreadyExists(name.into()));
4878 }
4879
4880 let file_id = self.vh.next_catalog_id;
4882 self.vh.next_catalog_id += 1;
4883
4884 let (type_code, creator_code) = hfs_common::resolve_create_type_creator(
4888 name,
4889 options.os_type,
4890 options.os_creator,
4891 options.type_code.as_deref(),
4892 options.creator_code.as_deref(),
4893 );
4894
4895 let data_fork = self.write_data_to_blocks(data, data_len, file_id, HFSPLUS_FORK_DATA)?;
4897
4898 let rsrc_fork = if let Some(ref rsrc_src) = options.resource_fork {
4900 match rsrc_src {
4901 disk_core::filesystem::ResourceForkSource::Data(rsrc_data) => {
4902 let mut cursor = std::io::Cursor::new(rsrc_data);
4903 self.write_data_to_blocks(
4904 &mut cursor,
4905 rsrc_data.len() as u64,
4906 file_id,
4907 HFSPLUS_FORK_RESOURCE,
4908 )?
4909 }
4910 disk_core::filesystem::ResourceForkSource::File(path) => {
4911 let mut f = std::fs::File::open(path)?;
4912 let len = f.metadata()?.len();
4913 self.write_data_to_blocks(&mut f, len, file_id, HFSPLUS_FORK_RESOURCE)?
4914 }
4915 }
4916 } else {
4917 ForkData::empty()
4918 };
4919
4920 let mtime = options.unix_times.map(|t| t.mtime_or_now());
4924 let file_rec = Self::build_file_record_with_dates(
4925 file_id,
4926 &data_fork,
4927 &rsrc_fork,
4928 &type_code,
4929 &creator_code,
4930 mtime,
4931 options.finder_flags.unwrap_or(0),
4932 );
4933
4934 let key = Self::build_catalog_key(parent_cnid, name);
4936 let mut key_record = key.clone();
4937 if key_record.len() % 2 != 0 {
4939 key_record.push(0);
4940 }
4941 key_record.extend_from_slice(&file_rec);
4942
4943 self.insert_catalog_record(&key_record)?;
4945
4946 let (thread_key, thread_data) =
4948 Self::build_thread_record(CATALOG_FILE_THREAD, parent_cnid, name);
4949 let mut thread_record = thread_key;
4950 if thread_record.len() % 2 != 0 {
4951 thread_record.push(0);
4952 }
4953 thread_record.extend_from_slice(&thread_data);
4954 let actual_thread_key = Self::build_catalog_key(file_id, "");
4957 let mut actual_thread_record = actual_thread_key;
4958 if actual_thread_record.len() % 2 != 0 {
4959 actual_thread_record.push(0);
4960 }
4961 actual_thread_record.extend_from_slice(&thread_data);
4962 self.insert_catalog_record(&actual_thread_record)?;
4963
4964 self.update_parent_valence(parent_cnid, 1)?;
4966
4967 self.vh.file_count += 1;
4969
4970 let path = if parent.path == "/" {
4971 format!("/{name}")
4972 } else {
4973 format!("{}/{name}", parent.path)
4974 };
4975 let mut fe = FileEntry::new_file(name.to_string(), path, data_len, file_id as u64);
4976 if type_code != [0; 4] {
4977 fe.type_code = Some(type_code);
4978 fe.creator_code = Some(creator_code);
4979 }
4980 if rsrc_fork.logical_size > 0 {
4981 fe.resource_fork_size = Some(rsrc_fork.logical_size);
4982 }
4983 let stamped = mtime.unwrap_or_else(disk_core::times::now);
4984 fe.modified_unix = Some(stamped);
4985 Ok(fe)
4986 }
4987
4988 fn create_directory_inner(
4989 &mut self,
4990 parent: &FileEntry,
4991 name: &str,
4992 options: &CreateDirectoryOptions,
4993 ) -> Result<FileEntry, FilesystemError> {
4994 let parent_cnid = parent.location as u32;
4995
4996 validate_hfsplus_create_name(name)?;
4997
4998 if parent_cnid == 2
5010 && (name == hfsplus_private_dir_name() || name == hfsplus_dir_private_dir_name())
5011 {
5012 let already_present = self
5013 .find_private_dir_cnid()?
5014 .filter(|_| name == hfsplus_private_dir_name())
5015 .is_some()
5016 || self
5017 .find_dir_private_dir_cnid()?
5018 .filter(|_| name == hfsplus_dir_private_dir_name())
5019 .is_some();
5020 if already_present {
5021 let bt = std::backtrace::Backtrace::force_capture();
5022 log::error!(
5023 "[HFS+] refused create_directory for magic name (already \
5024 present). parent_cnid={}, leading NULs={}\nbacktrace:\n{bt}",
5025 parent_cnid,
5026 name.chars().take_while(|c| *c == '\0').count()
5027 );
5028 return Err(FilesystemError::AlreadyExists(name.into()));
5029 }
5030 }
5031
5032 if self.find_catalog_record(parent_cnid, name).is_some() {
5034 return Err(FilesystemError::AlreadyExists(name.into()));
5035 }
5036
5037 let folder_id = self.vh.next_catalog_id;
5039 self.vh.next_catalog_id += 1;
5040
5041 let mtime = options.unix_times.map(|t| t.mtime_or_now());
5044 let folder_rec = Self::build_folder_record_with_dates(folder_id, mtime);
5045
5046 let key = Self::build_catalog_key(parent_cnid, name);
5048 let mut key_record = key.clone();
5049 if key_record.len() % 2 != 0 {
5050 key_record.push(0);
5051 }
5052 key_record.extend_from_slice(&folder_rec);
5053
5054 self.insert_catalog_record(&key_record)?;
5055
5056 let (_thread_key, thread_data) =
5058 Self::build_thread_record(CATALOG_FOLDER_THREAD, parent_cnid, name);
5059 let actual_thread_key = Self::build_catalog_key(folder_id, "");
5060 let mut actual_thread_record = actual_thread_key;
5061 if actual_thread_record.len() % 2 != 0 {
5062 actual_thread_record.push(0);
5063 }
5064 actual_thread_record.extend_from_slice(&thread_data);
5065 self.insert_catalog_record(&actual_thread_record)?;
5066
5067 self.update_parent_valence(parent_cnid, 1)?;
5069
5070 self.vh.folder_count += 1;
5072
5073 let path = if parent.path == "/" {
5074 format!("/{name}")
5075 } else {
5076 format!("{}/{name}", parent.path)
5077 };
5078 let mut fe = FileEntry::new_directory(name.to_string(), path, folder_id as u64);
5079 let stamped = mtime.unwrap_or_else(disk_core::times::now);
5080 fe.modified_unix = Some(stamped);
5081 Ok(fe)
5082 }
5083
5084 fn delete_entry_inner(
5085 &mut self,
5086 parent: &FileEntry,
5087 entry: &FileEntry,
5088 ) -> Result<(), FilesystemError> {
5089 let parent_cnid = parent.location as u32;
5090 let cnid = entry.location as u32;
5091
5092 let xattrs_removed = self.remove_xattr_records(cnid, None)?;
5097 if xattrs_removed > 0 {
5098 if let Some(set) = self.xattr_cnids.as_mut() {
5099 set.remove(&cnid);
5100 }
5101 }
5102
5103 if entry.is_directory() {
5105 let children = self.list_children(cnid)?;
5106 if !children.is_empty() {
5107 return Err(FilesystemError::InvalidData(
5108 "cannot delete non-empty directory".into(),
5109 ));
5110 }
5111 }
5112
5113 let link_inode_num = if entry.is_directory() {
5118 None
5119 } else {
5120 self.read_file_record_link_inode_num(parent_cnid, &entry.name)?
5121 };
5122 if let Some(inode_num) = link_inode_num {
5123 self.delete_hardlink_inode_ref(inode_num)?;
5124 }
5125
5126 let (node_idx, rec_idx, _offset) = self
5128 .find_catalog_record(parent_cnid, &entry.name)
5129 .ok_or_else(|| {
5130 FilesystemError::NotFound(format!("entry '{}' not found in catalog", entry.name))
5131 })?;
5132
5133 if !entry.is_directory() {
5138 if let Some((data_fork, rsrc_fork)) = self.find_file_by_id(cnid) {
5139 self.free_fork_overflow_extents(cnid, HFSPLUS_FORK_DATA, &data_fork);
5140 self.free_fork_overflow_extents(cnid, HFSPLUS_FORK_RESOURCE, &rsrc_fork);
5141 self.remove_extents_overflow_records_for(cnid, HFSPLUS_FORK_DATA)?;
5142 self.remove_extents_overflow_records_for(cnid, HFSPLUS_FORK_RESOURCE)?;
5143 self.free_fork_blocks(&data_fork);
5144 self.free_fork_blocks(&rsrc_fork);
5145 }
5146 }
5147
5148 self.remove_catalog_record(node_idx, rec_idx)?;
5149
5150 if let Some((t_node, t_rec, _)) = self.find_catalog_record_by_cnid(cnid) {
5152 self.remove_catalog_record(t_node, t_rec)?;
5153 }
5154
5155 self.update_parent_valence(parent_cnid, -1)?;
5157
5158 if entry.is_directory() {
5160 self.vh.folder_count = self.vh.folder_count.saturating_sub(1);
5161 } else {
5162 self.vh.file_count = self.vh.file_count.saturating_sub(1);
5163 }
5164
5165 Ok(())
5166 }
5167
5168 fn rename_inner(
5169 &mut self,
5170 parent: &FileEntry,
5171 entry: &FileEntry,
5172 new_name: &str,
5173 ) -> Result<(), FilesystemError> {
5174 let parent_cnid = parent.location as u32;
5175 let cnid = entry.location as u32;
5176
5177 let (node_idx, rec_idx, _abs) = self
5179 .find_catalog_record(parent_cnid, &entry.name)
5180 .ok_or_else(|| {
5181 FilesystemError::NotFound(format!("entry '{}' not found in catalog", entry.name))
5182 })?;
5183
5184 if let Some((n2, r2, _)) = self.find_catalog_record(parent_cnid, new_name) {
5187 if (n2, r2) != (node_idx, rec_idx) {
5188 return Err(FilesystemError::AlreadyExists(new_name.to_string()));
5189 }
5190 }
5191
5192 let node_size = self.catalog_node_size();
5195 let node_off = node_idx as usize * node_size;
5196 let node = &self.catalog_data[node_off..node_off + node_size];
5197 let (rec_start, rec_end) = crate::hfs_common::btree_record_range(node, node_size, rec_idx);
5198 let key_len = BigEndian::read_u16(&node[rec_start..rec_start + 2]) as usize;
5199 let mut data_rel = 2 + key_len;
5200 if !data_rel.is_multiple_of(2) {
5201 data_rel += 1;
5202 }
5203 let data_bytes = node[rec_start + data_rel..rec_end].to_vec();
5204
5205 let mut new_record = Self::build_catalog_key(parent_cnid, new_name);
5206 if new_record.len() % 2 != 0 {
5207 new_record.push(0);
5208 }
5209 new_record.extend_from_slice(&data_bytes);
5210
5211 self.remove_catalog_record(node_idx, rec_idx)?;
5212 self.insert_catalog_record(&new_record)?;
5213
5214 if let Some((t_node, t_rec, _)) = self.find_catalog_record_by_cnid(cnid) {
5219 self.remove_catalog_record(t_node, t_rec)?;
5220 let thread_type = if entry.is_directory() {
5221 CATALOG_FOLDER_THREAD
5222 } else {
5223 CATALOG_FILE_THREAD
5224 };
5225 let (_k, thread_data) = Self::build_thread_record(thread_type, parent_cnid, new_name);
5226 let mut thread_record = Self::build_catalog_key(cnid, "");
5227 if thread_record.len() % 2 != 0 {
5228 thread_record.push(0);
5229 }
5230 thread_record.extend_from_slice(&thread_data);
5231 self.insert_catalog_record(&thread_record)?;
5232 }
5233 Ok(())
5235 }
5236
5237 fn adjust_inode_link_count(
5243 &mut self,
5244 private_cnid: u32,
5245 inode_num: u32,
5246 delta: i64,
5247 ) -> Result<u32, FilesystemError> {
5248 let inode_name = format!("iNode{inode_num}");
5249 let (_, _, abs_off) = self
5250 .find_catalog_record(private_cnid, &inode_name)
5251 .ok_or_else(|| {
5252 FilesystemError::InvalidData(format!(
5253 "hardlink inode '{inode_name}' missing from HFS+ Private Data"
5254 ))
5255 })?;
5256 let key_len = BigEndian::read_u16(&self.catalog_data[abs_off..abs_off + 2]) as usize;
5257 let mut body_start = abs_off + 2 + key_len;
5258 if !body_start.is_multiple_of(2) {
5259 body_start += 1;
5260 }
5261 let cur = BigEndian::read_u32(&self.catalog_data[body_start + 44..body_start + 48]) as i64;
5262 let new = (cur + delta).max(0) as u32;
5263 BigEndian::write_u32(
5264 &mut self.catalog_data[body_start + 44..body_start + 48],
5265 new,
5266 );
5267 self.stamp_record_dates(body_start, false);
5268 Ok(new)
5269 }
5270
5271 fn delete_hardlink_inode_ref(&mut self, inode_num: u32) -> Result<(), FilesystemError> {
5277 let private_cnid = self.find_private_dir_cnid()?.ok_or_else(|| {
5278 FilesystemError::InvalidData(
5279 "hardlink stub on volume with no HFS+ Private Data directory".into(),
5280 )
5281 })?;
5282 let inode_cnid = self.resolve_hardlink_inode(inode_num)?.ok_or_else(|| {
5283 FilesystemError::InvalidData(format!(
5284 "hardlink target iNode{inode_num} missing from HFS+ Private Data"
5285 ))
5286 })?;
5287
5288 let new_count = self.adjust_inode_link_count(private_cnid, inode_num, -1)?;
5289 if new_count > 0 {
5290 return Ok(());
5291 }
5292
5293 if let Some((data_fork, rsrc_fork)) = self.find_file_by_id(inode_cnid) {
5295 self.free_fork_overflow_extents(inode_cnid, HFSPLUS_FORK_DATA, &data_fork);
5296 self.free_fork_overflow_extents(inode_cnid, HFSPLUS_FORK_RESOURCE, &rsrc_fork);
5297 self.remove_extents_overflow_records_for(inode_cnid, HFSPLUS_FORK_DATA)?;
5298 self.remove_extents_overflow_records_for(inode_cnid, HFSPLUS_FORK_RESOURCE)?;
5299 self.free_fork_blocks(&data_fork);
5300 self.free_fork_blocks(&rsrc_fork);
5301 }
5302
5303 let xattrs_removed = self.remove_xattr_records(inode_cnid, None)?;
5304 if xattrs_removed > 0 {
5305 if let Some(set) = self.xattr_cnids.as_mut() {
5306 set.remove(&inode_cnid);
5307 }
5308 }
5309
5310 let inode_name = format!("iNode{inode_num}");
5311 if let Some((n, r, _)) = self.find_catalog_record(private_cnid, &inode_name) {
5312 self.remove_catalog_record(n, r)?;
5313 }
5314 if let Some((tn, tr, _)) = self.find_catalog_record_by_cnid(inode_cnid) {
5315 self.remove_catalog_record(tn, tr)?;
5316 }
5317 self.update_parent_valence(private_cnid, -1)?;
5318 self.vh.file_count = self.vh.file_count.saturating_sub(1);
5319
5320 if let Some(map) = self.hardlink_inode_map.as_mut() {
5321 map.remove(&inode_num);
5322 }
5323 Ok(())
5324 }
5325
5326 fn set_type_creator_inner(
5327 &mut self,
5328 entry: &FileEntry,
5329 type_code: &str,
5330 creator_code: &str,
5331 ) -> Result<(), FilesystemError> {
5332 let cnid = entry.location as u32;
5333
5334 let (_t_node, _t_rec, t_offset) =
5336 self.find_catalog_record_by_cnid(cnid).ok_or_else(|| {
5337 FilesystemError::NotFound(format!("thread for CNID {cnid} not found"))
5338 })?;
5339
5340 let node_size = self.catalog_header.node_size as usize;
5342 let key_len = BigEndian::read_u16(&self.catalog_data[t_offset..t_offset + 2]) as usize;
5343 let mut rec_data_start = t_offset + 2 + key_len;
5344 if !rec_data_start.is_multiple_of(2) {
5345 rec_data_start += 1;
5346 }
5347 let thread_parent =
5348 BigEndian::read_u32(&self.catalog_data[rec_data_start + 4..rec_data_start + 8]);
5349 let thread_name_len =
5350 BigEndian::read_u16(&self.catalog_data[rec_data_start + 8..rec_data_start + 10])
5351 as usize;
5352 let thread_name = decode_utf16be(
5353 &self.catalog_data[rec_data_start + 10..rec_data_start + 10 + thread_name_len * 2],
5354 );
5355
5356 let (f_node, f_rec, f_offset) = self
5358 .find_catalog_record(thread_parent, &thread_name)
5359 .ok_or_else(|| {
5360 FilesystemError::NotFound(format!("file record for '{}' not found", thread_name))
5361 })?;
5362
5363 let fkey_len = BigEndian::read_u16(&self.catalog_data[f_offset..f_offset + 2]) as usize;
5365 let mut frec_start = f_offset + 2 + fkey_len;
5366 if !frec_start.is_multiple_of(2) {
5367 frec_start += 1;
5368 }
5369
5370 let tc = hfs_common::encode_fourcc(type_code);
5372 let cc = hfs_common::encode_fourcc(creator_code);
5373 self.catalog_data[frec_start + 48..frec_start + 52].copy_from_slice(&tc);
5374 self.catalog_data[frec_start + 52..frec_start + 56].copy_from_slice(&cc);
5375
5376 self.stamp_record_dates(frec_start, false);
5378
5379 let _ = (f_node, f_rec, node_size); Ok(())
5381 }
5382
5383 fn write_resource_fork_inner(
5384 &mut self,
5385 entry: &FileEntry,
5386 data: &mut dyn std::io::Read,
5387 len: u64,
5388 ) -> Result<(), FilesystemError> {
5389 let cnid = entry.location as u32;
5390
5391 if let Some((_data_fork, rsrc_fork)) = self.find_file_by_id(cnid) {
5393 self.free_fork_overflow_extents(cnid, HFSPLUS_FORK_RESOURCE, &rsrc_fork);
5394 self.remove_extents_overflow_records_for(cnid, HFSPLUS_FORK_RESOURCE)?;
5395 self.free_fork_blocks(&rsrc_fork);
5396 }
5397
5398 let new_rsrc = self.write_data_to_blocks(data, len, cnid, HFSPLUS_FORK_RESOURCE)?;
5400
5401 let (t_node, _t_rec, t_offset) =
5403 self.find_catalog_record_by_cnid(cnid).ok_or_else(|| {
5404 FilesystemError::NotFound(format!("thread for CNID {cnid} not found"))
5405 })?;
5406
5407 let key_len = BigEndian::read_u16(&self.catalog_data[t_offset..t_offset + 2]) as usize;
5408 let mut rec_data_start = t_offset + 2 + key_len;
5409 if !rec_data_start.is_multiple_of(2) {
5410 rec_data_start += 1;
5411 }
5412 let thread_parent =
5413 BigEndian::read_u32(&self.catalog_data[rec_data_start + 4..rec_data_start + 8]);
5414 let thread_name_len =
5415 BigEndian::read_u16(&self.catalog_data[rec_data_start + 8..rec_data_start + 10])
5416 as usize;
5417 let thread_name = decode_utf16be(
5418 &self.catalog_data[rec_data_start + 10..rec_data_start + 10 + thread_name_len * 2],
5419 );
5420
5421 let (_f_node, _f_rec, f_offset) = self
5422 .find_catalog_record(thread_parent, &thread_name)
5423 .ok_or_else(|| {
5424 FilesystemError::NotFound(format!("file record for '{}' not found", thread_name))
5425 })?;
5426
5427 let fkey_len = BigEndian::read_u16(&self.catalog_data[f_offset..f_offset + 2]) as usize;
5428 let mut frec_start = f_offset + 2 + fkey_len;
5429 if !frec_start.is_multiple_of(2) {
5430 frec_start += 1;
5431 }
5432
5433 let mut rsrc_bytes = [0u8; 80];
5435 new_rsrc.serialize(&mut rsrc_bytes);
5436 self.catalog_data[frec_start + 168..frec_start + 248].copy_from_slice(&rsrc_bytes);
5437
5438 self.stamp_record_dates(frec_start, true);
5440
5441 let _ = t_node;
5442 Ok(())
5443 }
5444
5445 fn set_blessed_folder_inner(&mut self, entry: &FileEntry) -> Result<(), FilesystemError> {
5446 if !entry.is_directory() {
5447 return Err(FilesystemError::InvalidData(
5448 "can only bless a directory".into(),
5449 ));
5450 }
5451 self.vh.finder_info[0] = entry.location as u32;
5452 Ok(())
5453 }
5454}
5455
5456pub struct ForkStreamReader<'a, R: Read + Seek> {
5471 reader: &'a mut R,
5472 extents: Vec<(u64, u64)>,
5474 logical_size: u64,
5476 pos: u64,
5478 cur_extent: usize,
5479 cur_extent_consumed: u64,
5481}
5482
5483impl<'a, R: Read + Seek> Read for ForkStreamReader<'a, R> {
5484 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
5485 if self.pos >= self.logical_size || buf.is_empty() {
5486 return Ok(0);
5487 }
5488 while self.cur_extent < self.extents.len() {
5490 let (_, len) = self.extents[self.cur_extent];
5491 if self.cur_extent_consumed < len {
5492 break;
5493 }
5494 self.cur_extent += 1;
5495 self.cur_extent_consumed = 0;
5496 }
5497 if self.cur_extent >= self.extents.len() {
5498 return Ok(0);
5499 }
5500 let (off, len) = self.extents[self.cur_extent];
5501 let remaining_in_extent = len - self.cur_extent_consumed;
5502 let remaining_logical = self.logical_size - self.pos;
5503 let max_chunk = remaining_in_extent.min(remaining_logical);
5504 let to_read = (buf.len() as u64).min(max_chunk) as usize;
5505 self.reader
5506 .seek(SeekFrom::Start(off + self.cur_extent_consumed))?;
5507 let n = self.reader.read(&mut buf[..to_read])?;
5508 if n == 0 {
5509 return Ok(0);
5510 }
5511 self.cur_extent_consumed += n as u64;
5512 self.pos += n as u64;
5513 Ok(n)
5514 }
5515}
5516
5517pub struct CompactHfsPlusReader<R: Read + Seek> {
5518 reader: R,
5519 partition_offset: u64,
5520 block_size: u32,
5521 total_blocks: u32,
5522 bitmap: Vec<u8>,
5523 current_block: u32,
5525 block_pos: u64,
5527 #[allow(dead_code)]
5528 original_size: u64,
5529}
5530
5531impl<R: Read + Seek> CompactHfsPlusReader<R> {
5532 pub fn new(
5533 mut reader: R,
5534 partition_offset: u64,
5535 ) -> Result<(Self, CompactResult), FilesystemError> {
5536 log::debug!(
5538 "[HFS+ compact] seeking to VH at offset {}",
5539 partition_offset + 1024
5540 );
5541 reader.seek(SeekFrom::Start(partition_offset + 1024))?;
5542 let mut vh_buf = [0u8; 512];
5543 reader.read_exact(&mut vh_buf)?;
5544 let vh = HfsPlusVolumeHeader::parse(&vh_buf)?;
5545 log::debug!(
5546 "[HFS+ compact] VH ok: block_size={}, total_blocks={}, alloc_file extents[0]=(start={}, count={})",
5547 vh.block_size,
5548 vh.total_blocks,
5549 vh.allocation_file.extents[0].start_block,
5550 vh.allocation_file.extents[0].block_count,
5551 );
5552
5553 let extents_overflow_data = if vh.extents_file.logical_size > 0 {
5556 Some(read_fork(
5557 &mut reader,
5558 partition_offset,
5559 vh.block_size,
5560 &vh.extents_file,
5561 )?)
5562 } else {
5563 None
5564 };
5565
5566 let alloc_data = read_fork_with_overflow(
5568 &mut reader,
5569 partition_offset,
5570 vh.block_size,
5571 &vh.allocation_file,
5572 HFSPLUS_ALLOCATION_FILE_ID,
5573 HFSPLUS_FORK_DATA,
5574 extents_overflow_data.as_deref(),
5575 )
5576 .map_err(|e| {
5577 log::debug!("[HFS+ compact] read_fork(alloc_file) failed: {e}");
5578 e
5579 })?;
5580 log::debug!(
5581 "[HFS+ compact] allocation bitmap read: {} bytes",
5582 alloc_data.len()
5583 );
5584
5585 let mut allocated = 0u32;
5587 for bit in 0..vh.total_blocks {
5588 let byte_idx = bit as usize / 8;
5589 let bit_idx = 7 - (bit % 8);
5590 if byte_idx < alloc_data.len() && (alloc_data[byte_idx] >> bit_idx) & 1 == 1 {
5591 allocated += 1;
5592 }
5593 }
5594 log::debug!(
5595 "[HFS+ compact] allocated={} / {} total blocks ({} free)",
5596 allocated,
5597 vh.total_blocks,
5598 vh.total_blocks - allocated,
5599 );
5600
5601 let original_size = vh.total_blocks as u64 * vh.block_size as u64;
5602 let compacted_size = original_size;
5605 let data_size = allocated as u64 * vh.block_size as u64;
5607 log::debug!(
5608 "[HFS+ compact] compacted_size={} data_size={} original_size={} (layout-preserving; free blocks -> zeros)",
5609 compacted_size, data_size, original_size
5610 );
5611
5612 let result = CompactResult {
5613 original_size,
5614 compacted_size,
5615 data_size,
5616 clusters_used: allocated,
5617 };
5618
5619 Ok((
5620 CompactHfsPlusReader {
5621 reader,
5622 partition_offset,
5623 block_size: vh.block_size,
5624 total_blocks: vh.total_blocks,
5625 bitmap: alloc_data,
5626 current_block: 0,
5627 block_pos: 0,
5628 original_size,
5629 },
5630 result,
5631 ))
5632 }
5633
5634 fn is_block_allocated(&self, block: u32) -> bool {
5635 let byte_idx = block as usize / 8;
5636 let bit_idx = 7 - (block % 8);
5637 byte_idx < self.bitmap.len() && (self.bitmap[byte_idx] >> bit_idx) & 1 == 1
5638 }
5639}
5640
5641impl<R: Read + Seek> Read for CompactHfsPlusReader<R> {
5642 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
5643 if self.current_block >= self.total_blocks {
5644 return Ok(0);
5645 }
5646
5647 let block_size = self.block_size as u64;
5648
5649 let allocated = self.is_block_allocated(self.current_block);
5656 let mut run_end = self.current_block + 1;
5657 while run_end < self.total_blocks && self.is_block_allocated(run_end) == allocated {
5658 run_end += 1;
5659 }
5660
5661 let bytes_in_run = (run_end - self.current_block) as u64 * block_size - self.block_pos;
5662 let to_read = (bytes_in_run as usize).min(buf.len());
5663
5664 let n = if allocated {
5665 let offset =
5666 self.partition_offset + self.current_block as u64 * block_size + self.block_pos;
5667 self.reader.seek(SeekFrom::Start(offset))?;
5668 self.reader.read(&mut buf[..to_read])?
5669 } else {
5670 buf[..to_read].fill(0);
5672 to_read
5673 };
5674
5675 let mut remaining_in_n = n as u64;
5677 if self.block_pos > 0 {
5679 let want = block_size - self.block_pos;
5680 let take = remaining_in_n.min(want);
5681 self.block_pos += take;
5682 remaining_in_n -= take;
5683 if self.block_pos >= block_size {
5684 self.block_pos = 0;
5685 self.current_block += 1;
5686 }
5687 }
5688 let whole = (remaining_in_n / block_size) as u32;
5690 self.current_block += whole;
5691 remaining_in_n -= whole as u64 * block_size;
5692 if remaining_in_n > 0 {
5694 self.block_pos = remaining_in_n;
5695 }
5696
5697 Ok(n)
5698 }
5699}
5700
5701pub fn create_blank_hfsplus(
5730 size_bytes: u64,
5731 block_size: u32,
5732 name: &str,
5733 case_sensitive: bool,
5734) -> Vec<u8> {
5735 create_blank_hfsplus_sized(size_bytes, block_size, name, case_sensitive, 0, 0)
5736}
5737
5738pub fn default_hfsplus_catalog_bytes(volume_bytes: u64) -> u64 {
5745 volume_bytes / 200
5746}
5747
5748pub fn create_blank_hfsplus_sized(
5755 size_bytes: u64,
5756 block_size: u32,
5757 name: &str,
5758 case_sensitive: bool,
5759 min_catalog_bytes: u32,
5760 min_extents_bytes: u32,
5761) -> Vec<u8> {
5762 let (front, vh_bytes, image_size) = build_blank_hfsplus_front(
5763 size_bytes,
5764 block_size,
5765 name,
5766 case_sensitive,
5767 min_catalog_bytes,
5768 min_extents_bytes,
5769 );
5770 let mut img = vec![0u8; image_size];
5771 img[..front.len()].copy_from_slice(&front);
5772 let alt = image_size - 1024;
5773 img[alt..alt + 512].copy_from_slice(&vh_bytes);
5774 img
5775}
5776
5777fn build_blank_hfsplus_front(
5782 size_bytes: u64,
5783 block_size: u32,
5784 name: &str,
5785 case_sensitive: bool,
5786 min_catalog_bytes: u32,
5787 min_extents_bytes: u32,
5788) -> (Vec<u8>, [u8; 512], usize) {
5789 assert!(
5790 block_size.is_power_of_two() && (512..=4096).contains(&block_size),
5791 "block_size must be a power of 2 in [512, 4096], got {block_size}"
5792 );
5793 let bs = block_size as u64;
5794 let total_blocks = (size_bytes / bs) as u32;
5795 assert!(total_blocks >= 64, "image too small ({size_bytes} bytes)");
5796
5797 let node_size: usize = 4096;
5800 let nodes_per_block: u32 = (node_size as u32 / block_size).max(1);
5801 let blocks_per_node: u32 = (block_size / node_size as u32).max(1);
5802 let _ = nodes_per_block;
5803
5804 let max_btree_nodes: u32 = ((node_size - 256) * 8) as u32;
5822 let to_nodes = |bytes: u64| -> u32 {
5823 bytes
5824 .div_ceil(node_size as u64)
5825 .clamp(4, max_btree_nodes as u64) as u32
5826 };
5827 let catalog_node_count =
5828 to_nodes(default_hfsplus_catalog_bytes(size_bytes).max(min_catalog_bytes as u64));
5829 let extents_node_count = to_nodes(min_extents_bytes as u64);
5830
5831 let catalog_btree_blocks: u32 = catalog_node_count * blocks_per_node;
5832 let extents_btree_blocks: u32 = extents_node_count * blocks_per_node;
5833
5834 let bitmap_bytes = total_blocks.div_ceil(8) as u64;
5835 let bitmap_blocks = bitmap_bytes.div_ceil(bs) as u32;
5836
5837 let bitmap_start: u32 = 1;
5838 let extents_start: u32 = bitmap_start + bitmap_blocks;
5839 let catalog_start: u32 = extents_start + extents_btree_blocks;
5840 let reserved_blocks: u32 = catalog_start + catalog_btree_blocks;
5841 assert!(
5842 reserved_blocks + 1 < total_blocks,
5843 "image too small for reserved region: need {} blocks, have {total_blocks}",
5844 reserved_blocks + 1
5845 );
5846
5847 let image_size = total_blocks as usize * block_size as usize;
5848 let front_size = reserved_blocks as usize * block_size as usize;
5853 let mut front = vec![0u8; front_size];
5854
5855 let alt_vh_start_block: u32 = ((image_size as u64 - 1024) / bs) as u32;
5861 let trailing_blocks: u32 = total_blocks - alt_vh_start_block;
5862
5863 {
5865 let bitmap_off = bitmap_start as usize * block_size as usize;
5866 let bitmap = &mut front[bitmap_off..bitmap_off + (bitmap_bytes as usize).max(1)];
5867 let mut mark = |blk: u32| {
5868 let byte_idx = (blk / 8) as usize;
5870 let bit_pos = 7 - (blk % 8);
5871 bitmap[byte_idx] |= 1 << bit_pos;
5872 };
5873 for blk in 0..reserved_blocks {
5874 mark(blk);
5875 }
5876 for blk in alt_vh_start_block..total_blocks {
5877 mark(blk);
5878 }
5879 }
5880
5881 {
5886 let off = extents_start as usize * block_size as usize;
5887 write_blank_btree_header_node(
5888 &mut front[off..off + node_size],
5889 node_size,
5890 0,
5891 extents_node_count,
5892 extents_node_count - 1, 10,
5894 0, );
5896 }
5897
5898 let name_utf16: Vec<u16> = fs_mac_unicode::hfs_unicode::decompose_str(name);
5900 {
5901 let off = catalog_start as usize * block_size as usize;
5902 let key_compare = if case_sensitive {
5903 KEY_COMPARE_BINARY
5904 } else {
5905 KEY_COMPARE_CASE_FOLDING
5906 };
5907 write_blank_btree_header_node(
5908 &mut front[off..off + node_size],
5909 node_size,
5910 2,
5911 catalog_node_count,
5912 catalog_node_count - 2,
5913 516, key_compare,
5915 );
5916 let leaf_off = off + node_size;
5917 write_root_catalog_leaf(&mut front[leaf_off..leaf_off + node_size], &name_utf16);
5918 }
5919
5920 let signature = if case_sensitive {
5922 HFSX_SIGNATURE
5923 } else {
5924 HFS_PLUS_SIGNATURE
5925 };
5926 let now = hfs_common::hfs_now();
5927 let vh = HfsPlusVolumeHeader {
5928 signature,
5929 version: if case_sensitive { 5 } else { 4 },
5930 attributes: HFSPLUS_VOLUME_UNMOUNTED_BIT,
5931 last_mounted_version: 0,
5932 journal_info_block: 0,
5933 create_date: now,
5934 modify_date: now,
5935 backup_date: 0,
5936 checked_date: now,
5937 file_count: 0,
5938 folder_count: 0,
5944 block_size,
5945 total_blocks,
5946 free_blocks: total_blocks - reserved_blocks - trailing_blocks,
5947 next_allocation: reserved_blocks,
5948 rsrc_clump_size: block_size,
5949 data_clump_size: block_size,
5950 next_catalog_id: 16,
5951 write_count: 0,
5952 encodings_bitmap: 1, finder_info: [0u32; 8],
5954 allocation_file: ForkData {
5955 logical_size: bitmap_blocks as u64 * bs,
5956 clump_size: 0,
5957 total_blocks: bitmap_blocks,
5958 extents: extent_array(bitmap_start, bitmap_blocks),
5959 },
5960 extents_file: ForkData {
5961 logical_size: extents_btree_blocks as u64 * bs,
5962 clump_size: 0,
5963 total_blocks: extents_btree_blocks,
5964 extents: extent_array(extents_start, extents_btree_blocks),
5965 },
5966 catalog_file: ForkData {
5967 logical_size: catalog_btree_blocks as u64 * bs,
5968 clump_size: 0,
5969 total_blocks: catalog_btree_blocks,
5970 extents: extent_array(catalog_start, catalog_btree_blocks),
5971 },
5972 attributes_file: ForkData::empty(),
5973 startup_file: ForkData::empty(),
5974 };
5975 let vh_bytes = vh.serialize();
5976
5977 front[1024..1536].copy_from_slice(&vh_bytes);
5979
5980 (front, vh_bytes, image_size)
5981}
5982
5983#[allow(clippy::too_many_arguments)]
5985pub fn write_blank_hfsplus_sized_into<W: Write + Seek>(
5986 target: &mut W,
5987 at_offset: u64,
5988 size_bytes: u64,
5989 block_size: u32,
5990 name: &str,
5991 case_sensitive: bool,
5992 min_catalog_bytes: u32,
5993 min_extents_bytes: u32,
5994) -> std::io::Result<u64> {
5995 let (front, vh_bytes, image_size) = build_blank_hfsplus_front(
5996 size_bytes,
5997 block_size,
5998 name,
5999 case_sensitive,
6000 min_catalog_bytes,
6001 min_extents_bytes,
6002 );
6003 let image_size = image_size as u64;
6004 target.seek(SeekFrom::Start(at_offset))?;
6005 target.write_all(&front)?;
6006 target.seek(SeekFrom::Start(at_offset + image_size - 1024))?;
6007 target.write_all(&vh_bytes)?;
6008 target.write_all(&[0u8; 512])?;
6009 Ok(image_size)
6010}
6011
6012pub fn write_blank_hfsplus_into<W: Write + Seek>(
6024 target: &mut W,
6025 at_offset: u64,
6026 size_bytes: u64,
6027 block_size: u32,
6028 name: &str,
6029 case_sensitive: bool,
6030) -> std::io::Result<()> {
6031 let (front, vh_bytes, _image_size) =
6032 build_blank_hfsplus_front(size_bytes, block_size, name, case_sensitive, 0, 0);
6033 target.seek(SeekFrom::Start(at_offset))?;
6034 target.write_all(&front)?;
6035 target.seek(SeekFrom::Start(at_offset + size_bytes - 1024))?;
6036 target.write_all(&vh_bytes)?;
6037 target.write_all(&[0u8; 512])?;
6038 Ok(())
6039}
6040
6041fn extent_array(start: u32, count: u32) -> [ExtentDescriptor; 8] {
6043 let mut e = [ExtentDescriptor {
6044 start_block: 0,
6045 block_count: 0,
6046 }; 8];
6047 e[0] = ExtentDescriptor {
6048 start_block: start,
6049 block_count: count,
6050 };
6051 e
6052}
6053
6054pub(crate) fn write_blank_btree_header_node(
6059 node: &mut [u8],
6060 node_size: usize,
6061 leaf_records: u32,
6062 total_nodes: u32,
6063 free_nodes: u32,
6064 max_key_len: u16,
6065 key_compare_type: u8,
6066) {
6067 node.fill(0);
6068 node[8] = 1; BigEndian::write_u16(&mut node[10..12], 3); let empty = leaf_records == 0;
6083 let (depth, root_first_last): (u16, u32) = if empty { (0, 0) } else { (1, 1) };
6084 let hr = 14usize;
6085 BigEndian::write_u16(&mut node[hr..hr + 2], depth);
6086 BigEndian::write_u32(&mut node[hr + 2..hr + 6], root_first_last); BigEndian::write_u32(&mut node[hr + 6..hr + 10], leaf_records);
6088 BigEndian::write_u32(&mut node[hr + 10..hr + 14], root_first_last); BigEndian::write_u32(&mut node[hr + 14..hr + 18], root_first_last); BigEndian::write_u16(&mut node[hr + 18..hr + 20], node_size as u16);
6091 BigEndian::write_u16(&mut node[hr + 20..hr + 22], max_key_len);
6092 BigEndian::write_u32(&mut node[hr + 22..hr + 26], total_nodes);
6093 BigEndian::write_u32(&mut node[hr + 26..hr + 30], free_nodes);
6094 node[hr + 36] = 0; node[hr + 37] = key_compare_type;
6097 let attributes = if max_key_len == 10 {
6100 hfs_common::KBT_BIG_KEYS_MASK
6101 } else {
6102 hfs_common::KBT_BIG_KEYS_MASK | hfs_common::KBT_VARIABLE_INDEX_KEYS_MASK
6103 };
6104 BigEndian::write_u32(&mut node[hr + 38..hr + 42], attributes);
6105
6106 let user_off: u16 = 120;
6116 let bitmap_off: u16 = 248;
6117 let free_off: u16 = (node_size as u16).saturating_sub(8); let ot_end = node_size;
6120 BigEndian::write_u16(&mut node[ot_end - 2..ot_end], 14); BigEndian::write_u16(&mut node[ot_end - 4..ot_end - 2], user_off);
6122 BigEndian::write_u16(&mut node[ot_end - 6..ot_end - 4], bitmap_off);
6123 BigEndian::write_u16(&mut node[ot_end - 8..ot_end - 6], free_off);
6124
6125 node[bitmap_off as usize] = if empty { 0b10000000 } else { 0b11000000 };
6129}
6130
6131#[cfg(test)]
6135pub(crate) fn write_empty_leaf_node(node: &mut [u8]) {
6136 node.fill(0);
6137 node[8] = 0xFF; node[9] = 1; BigEndian::write_u16(&mut node[10..12], 0); let n = node.len();
6141 BigEndian::write_u16(&mut node[n - 2..n], 14); }
6143
6144fn write_root_catalog_leaf(node: &mut [u8], name_utf16: &[u16]) {
6148 node.fill(0);
6149 let n = node.len();
6150 node[8] = 0xFF; node[9] = 1; BigEndian::write_u16(&mut node[10..12], 2); let now = hfs_common::hfs_now();
6155
6156 let r0_off = 14usize;
6159 let key_body = 4 + 2 + name_utf16.len() * 2;
6160 BigEndian::write_u16(&mut node[r0_off..r0_off + 2], key_body as u16);
6161 BigEndian::write_u32(&mut node[r0_off + 2..r0_off + 6], 1); BigEndian::write_u16(&mut node[r0_off + 6..r0_off + 8], name_utf16.len() as u16);
6163 for (i, &u) in name_utf16.iter().enumerate() {
6164 BigEndian::write_u16(&mut node[r0_off + 8 + i * 2..r0_off + 10 + i * 2], u);
6165 }
6166 let mut r0_data = r0_off + 2 + key_body;
6167 if !r0_data.is_multiple_of(2) {
6168 r0_data += 1;
6169 }
6170 BigEndian::write_i16(&mut node[r0_data..r0_data + 2], CATALOG_FOLDER);
6171 BigEndian::write_u32(&mut node[r0_data + 8..r0_data + 12], 2);
6173 BigEndian::write_u32(&mut node[r0_data + 12..r0_data + 16], now);
6174 BigEndian::write_u32(&mut node[r0_data + 16..r0_data + 20], now);
6175 BigEndian::write_u32(&mut node[r0_data + 20..r0_data + 24], now);
6176 BigEndian::write_u32(&mut node[r0_data + 24..r0_data + 28], now);
6177 let r0_end = r0_data + 88; let r1_off = r0_end;
6182 BigEndian::write_u16(&mut node[r1_off..r1_off + 2], 6); BigEndian::write_u32(&mut node[r1_off + 2..r1_off + 6], 2); BigEndian::write_u16(&mut node[r1_off + 6..r1_off + 8], 0); let mut r1_data = r1_off + 2 + 6;
6186 if !r1_data.is_multiple_of(2) {
6187 r1_data += 1;
6188 }
6189 BigEndian::write_i16(&mut node[r1_data..r1_data + 2], 3); BigEndian::write_u32(&mut node[r1_data + 4..r1_data + 8], 1);
6193 BigEndian::write_u16(
6194 &mut node[r1_data + 8..r1_data + 10],
6195 name_utf16.len() as u16,
6196 );
6197 for (i, &u) in name_utf16.iter().enumerate() {
6198 BigEndian::write_u16(&mut node[r1_data + 10 + i * 2..r1_data + 12 + i * 2], u);
6199 }
6200 let r1_end = r1_data + 10 + name_utf16.len() * 2;
6201
6202 BigEndian::write_u16(&mut node[n - 2..n], 14); BigEndian::write_u16(&mut node[n - 4..n - 2], r1_off as u16);
6205 BigEndian::write_u16(&mut node[n - 6..n - 4], r1_end as u16); }
6207
6208pub fn resize_hfsplus_in_place(
6212 device: &mut (impl Read + Write + Seek),
6213 partition_offset: u64,
6214 new_size_bytes: u64,
6215 log: &mut impl FnMut(&str),
6216) -> anyhow::Result<()> {
6217 device.seek(SeekFrom::Start(partition_offset + 1024))?;
6219 let mut vh_buf = [0u8; 512];
6220 device.read_exact(&mut vh_buf)?;
6221
6222 let sig = BigEndian::read_u16(&vh_buf[0..2]);
6223 if sig != HFS_PLUS_SIGNATURE && sig != HFSX_SIGNATURE {
6224 log("HFS+ resize: not an HFS+ volume, skipping");
6225 return Ok(());
6226 }
6227 let mut vh = HfsPlusVolumeHeader::parse(&vh_buf)?;
6228 let bs = vh.block_size;
6229 let old_total = vh.total_blocks;
6230 let new_total = (new_size_bytes / bs as u64) as u32;
6231 if new_total < 8 {
6232 anyhow::bail!("HFS+ resize: {new_size_bytes} bytes is too small for a volume");
6233 }
6234
6235 let af = &vh.allocation_file;
6237 if af.extents[0].block_count == 0 || af.extents[1].block_count != 0 {
6238 anyhow::bail!("HFS+ resize: the allocation file is not a single extent; refusing");
6239 }
6240 let (mut af_start, mut af_blocks) = (af.extents[0].start_block, af.extents[0].block_count);
6241 let mut bitmap = vec![0u8; af_blocks as usize * bs as usize];
6242 device.seek(SeekFrom::Start(
6243 partition_offset + af_start as u64 * bs as u64,
6244 ))?;
6245 device.read_exact(&mut bitmap)?;
6246
6247 let old_tail = alt_vh_blocks(old_total, bs);
6248 let new_tail = alt_vh_blocks(new_total, bs);
6249 if new_total < old_total {
6250 let highest = find_last_set_bit(&bitmap, old_tail.start).map_or(0, |b| b + 1);
6252 if highest > new_tail.start {
6253 anyhow::bail!(
6254 "HFS+ resize: new size {} blocks ends before the last allocated block ({})",
6255 new_total,
6256 highest
6257 );
6258 }
6259 }
6260
6261 let cap_bits = (bitmap.len() as u64 * 8).min(u32::MAX as u64) as u32;
6264 for b in old_tail.start..old_total.min(cap_bits) {
6265 bitmap_clear_bit_be(&mut bitmap, b);
6266 }
6267 for b in old_total..new_total.min(cap_bits) {
6268 bitmap_clear_bit_be(&mut bitmap, b);
6269 }
6270
6271 let needed_blocks = (new_total as u64).div_ceil(8).div_ceil(bs as u64) as u32;
6274 if needed_blocks > af_blocks {
6275 let mut grown = bitmap.clone();
6276 grown.resize(needed_blocks as usize * bs as usize, 0);
6277 let usable = new_tail.start;
6278 let extend_ok = (af_start + af_blocks..af_start + needed_blocks)
6279 .all(|b| b < usable && !bitmap_test_bit_be(&grown, b));
6280 let new_start = if extend_ok {
6281 af_start
6282 } else {
6283 hfs_common::bitmap_find_clear_run_be(&grown, usable, needed_blocks).ok_or_else(
6284 || {
6285 anyhow::anyhow!(
6286 "HFS+ resize: no free run of {needed_blocks} blocks for the allocation file"
6287 )
6288 },
6289 )?
6290 };
6291 if new_start != af_start {
6292 for b in af_start..af_start + af_blocks {
6293 bitmap_clear_bit_be(&mut grown, b);
6294 }
6295 }
6296 for b in new_start..new_start + needed_blocks {
6297 bitmap_set_bit_be(&mut grown, b);
6298 }
6299 log(&format!(
6300 "HFS+ resize: allocation file grows from {af_blocks} to {needed_blocks} blocks at \
6301 block {new_start}"
6302 ));
6303 bitmap = grown;
6304 af_start = new_start;
6305 af_blocks = needed_blocks;
6306 vh.allocation_file.extents[0] = ExtentDescriptor {
6307 start_block: af_start,
6308 block_count: af_blocks,
6309 };
6310 vh.allocation_file.total_blocks = af_blocks;
6311 vh.allocation_file.logical_size = af_blocks as u64 * bs as u64;
6312 }
6313 for b in new_tail.clone() {
6314 bitmap_set_bit_be(&mut bitmap, b);
6315 }
6316 let cap_bits = (bitmap.len() as u64 * 8).min(u32::MAX as u64) as u32;
6317 for b in new_total..cap_bits {
6318 bitmap_clear_bit_be(&mut bitmap, b);
6319 }
6320
6321 let used = count_used_blocks(&bitmap, new_total);
6322 vh.total_blocks = new_total;
6323 vh.free_blocks = new_total - used;
6324 if vh.next_allocation >= new_tail.start {
6325 vh.next_allocation = 0;
6326 }
6327 log(&format!(
6328 "HFS+ resize: {} -> {} blocks ({} free)",
6329 old_total, new_total, vh.free_blocks
6330 ));
6331
6332 device.seek(SeekFrom::Start(
6333 partition_offset + af_start as u64 * bs as u64,
6334 ))?;
6335 device.write_all(&bitmap)?;
6336 let out = vh.serialize();
6337 device.seek(SeekFrom::Start(partition_offset + 1024))?;
6338 device.write_all(&out)?;
6339 if new_size_bytes > 1024 {
6340 device.seek(SeekFrom::Start(partition_offset + new_size_bytes - 1024))?;
6341 device.write_all(&out)?;
6342 }
6343 device.flush()?;
6344 Ok(())
6345}
6346
6347pub fn mirror_alternate_volume_header(
6351 device: &mut (impl Read + Write + Seek),
6352 partition_offset: u64,
6353 partition_len: u64,
6354 log: &mut impl FnMut(&str),
6355) -> anyhow::Result<bool> {
6356 device.seek(SeekFrom::Start(partition_offset + 1024))?;
6357 let mut vh_buf = [0u8; 512];
6358 device.read_exact(&mut vh_buf)?;
6359 let sig = BigEndian::read_u16(&vh_buf[0..2]);
6360 if sig != HFS_PLUS_SIGNATURE && sig != HFSX_SIGNATURE {
6361 return Ok(false);
6362 }
6363 let block_size = BigEndian::read_u32(&vh_buf[40..44]) as u64;
6364 let total_blocks = BigEndian::read_u32(&vh_buf[44..48]) as u64;
6365 let volume_len = total_blocks * block_size;
6366 if partition_len < 2048 || volume_len >= partition_len {
6367 return Ok(false);
6368 }
6369 device.seek(SeekFrom::Start(partition_offset + partition_len - 1024))?;
6370 device.write_all(&vh_buf)?;
6371 device.flush()?;
6372 log(&format!(
6373 "HFS+ volume of {volume_len} bytes in a {partition_len}-byte partition: alternate \
6374 volume header mirrored at the partition end"
6375 ));
6376 Ok(true)
6377}
6378
6379fn alt_vh_blocks(total_blocks: u32, block_size: u32) -> std::ops::Range<u32> {
6382 if total_blocks == 0 {
6383 return 0..0;
6384 }
6385 let bytes = total_blocks as u64 * block_size as u64;
6386 let start = (bytes.saturating_sub(1024) / block_size as u64) as u32;
6387 start..total_blocks
6388}
6389
6390fn count_used_blocks(bitmap: &[u8], total_blocks: u32) -> u32 {
6392 let full = (total_blocks / 8) as usize;
6393 let mut used: u32 = bitmap[..full.min(bitmap.len())]
6394 .iter()
6395 .map(|b| b.count_ones())
6396 .sum();
6397 let keep = total_blocks % 8;
6398 if keep > 0 {
6399 if let Some(&last) = bitmap.get(full) {
6400 let mask = 0xFFu8 << (8 - keep);
6401 used += (last & mask).count_ones();
6402 }
6403 }
6404 used
6405}
6406
6407pub fn validate_hfsplus_integrity(
6409 device: &mut (impl Read + Seek),
6410 partition_offset: u64,
6411 log: &mut impl FnMut(&str),
6412) -> anyhow::Result<()> {
6413 device.seek(SeekFrom::Start(partition_offset + 1024))?;
6414 let mut vh_buf = [0u8; 512];
6415 device.read_exact(&mut vh_buf)?;
6416
6417 let sig = BigEndian::read_u16(&vh_buf[0..2]);
6418 if sig != HFS_PLUS_SIGNATURE && sig != HFSX_SIGNATURE {
6419 log("HFS+ validate: not an HFS+ volume, skipping");
6420 return Ok(());
6421 }
6422
6423 let block_size = BigEndian::read_u32(&vh_buf[40..44]);
6424 let total_blocks = BigEndian::read_u32(&vh_buf[44..48]);
6425
6426 if !block_size.is_power_of_two() || block_size < 512 {
6427 anyhow::bail!("HFS+ validate: invalid block size {block_size}");
6428 }
6429 if total_blocks == 0 {
6430 anyhow::bail!("HFS+ validate: zero total blocks");
6431 }
6432
6433 log(&format!(
6434 "HFS+ validate: OK ({total_blocks} blocks, {block_size} block size)"
6435 ));
6436 Ok(())
6437}
6438
6439pub fn resolve_apple_hfs<R: Read + Seek>(
6451 reader: &mut R,
6452 partition_offset: u64,
6453) -> (&'static str, u64) {
6454 if reader
6459 .seek(SeekFrom::Start(partition_offset + 1024))
6460 .is_err()
6461 {
6462 return ("unknown", partition_offset);
6463 }
6464 let mut buf = [0u8; 512];
6465 if reader.read_exact(&mut buf).is_err() {
6466 return ("unknown", partition_offset);
6467 }
6468 let sig = u16::from_be_bytes([buf[0], buf[1]]);
6469 match sig {
6470 0x4244 => {
6471 let embedded_sig = u16::from_be_bytes([buf[124], buf[125]]);
6473 if embedded_sig == 0x482B {
6474 let block_size = u32::from_be_bytes([buf[20], buf[21], buf[22], buf[23]]) as u64;
6479 let first_alloc_block = u16::from_be_bytes([buf[28], buf[29]]) as u64;
6480 let embedded_start = u16::from_be_bytes([buf[126], buf[127]]) as u64;
6481 let hfsplus_offset =
6483 partition_offset + first_alloc_block * 512 + embedded_start * block_size;
6484 ("hfsplus", hfsplus_offset)
6485 } else {
6486 ("hfs", partition_offset)
6487 }
6488 }
6489 0x482B | 0x4858 => ("hfsplus", partition_offset),
6491 _ => ("unknown", partition_offset),
6492 }
6493}
6494
6495#[cfg(test)]
6496#[allow(clippy::identity_op)] mod tests {
6498 use super::*;
6499
6500 #[test]
6503 fn thousand_file_churn_keeps_the_catalog_clean() {
6504 use std::io::Cursor;
6505 let img = create_blank_hfsplus(64 * 1024 * 1024, 4096, "Churn", false);
6506 let mut fs = HfsPlusFilesystem::open(Cursor::new(img), 0).unwrap();
6507 fs.prepare_for_edit().unwrap();
6508 let root = fs.root().unwrap();
6509 let clean = |fs: &mut HfsPlusFilesystem<Cursor<Vec<u8>>>, stage: &str| {
6510 fs.sync_metadata().unwrap();
6511 let r = fs.fsck().expect("hfsplus has fsck").unwrap();
6512 assert!(r.errors.is_empty(), "{stage}: {:?}", r.errors);
6513 };
6514 let put = |fs: &mut HfsPlusFilesystem<Cursor<Vec<u8>>>, name: &str, data: &[u8]| {
6515 fs.create_file(
6516 &root,
6517 name,
6518 &mut Cursor::new(data),
6519 data.len() as u64,
6520 &CreateFileOptions::default(),
6521 )
6522 .unwrap_or_else(|e| panic!("creating {name}: {e}"))
6523 };
6524 let free0 = fs.vh.free_blocks;
6525 let trees0 = fs.vh.catalog_file.total_blocks + fs.vh.extents_file.total_blocks;
6526 for i in 0..1000 {
6527 put(
6528 &mut fs,
6529 &format!("f{i:04}.txt"),
6530 format!("file {i:04}").as_bytes(),
6531 );
6532 }
6533 clean(&mut fs, "after 1000 creates");
6534 let extra = put(&mut fs, "extra.bin", &[7u8; 5000]);
6535 clean(&mut fs, "after one more");
6536 fs.delete_entry(&root, &extra).unwrap();
6537 clean(&mut fs, "after deleting it");
6538 for e in fs.list_directory(&root).unwrap() {
6539 fs.delete_entry(&root, &e).unwrap();
6540 }
6541 let trees = fs.vh.catalog_file.total_blocks + fs.vh.extents_file.total_blocks;
6543 assert_eq!(
6544 fs.vh.free_blocks + (trees - trees0),
6545 free0,
6546 "every block came back"
6547 );
6548 clean(&mut fs, "after deleting the 1000");
6549 put(&mut fs, "last.bin", &[9u8; 100]);
6550 clean(&mut fs, "after the last create");
6551 let names: Vec<String> = fs
6552 .list_directory(&root)
6553 .unwrap()
6554 .into_iter()
6555 .map(|e| e.name)
6556 .collect();
6557 assert_eq!(names, ["last.bin"]);
6558 }
6559
6560 #[test]
6563 fn blank_btree_headers_carry_apples_attribute_bits() {
6564 let img = create_blank_hfsplus(16 * 1024 * 1024, 4096, "Attrs", false);
6565 let vh = &img[1024..1536];
6566 let block_size = BigEndian::read_u32(&vh[0x28..0x2c]) as usize;
6567 let attrs = |fork: usize| {
6571 let start = BigEndian::read_u32(&vh[fork + 0x10..fork + 0x14]) as usize;
6572 BigEndian::read_u32(&img[start * block_size + 52..start * block_size + 56])
6573 };
6574 assert_eq!(attrs(0xC0), hfs_common::KBT_BIG_KEYS_MASK, "extents");
6575 let variable = hfs_common::KBT_BIG_KEYS_MASK | hfs_common::KBT_VARIABLE_INDEX_KEYS_MASK;
6576 assert_eq!(attrs(0x110), variable, "catalog");
6577 }
6578
6579 #[test]
6580 fn test_volume_header_parse() {
6581 let mut data = [0u8; 512];
6582 BigEndian::write_u16(&mut data[0..2], HFS_PLUS_SIGNATURE);
6583 BigEndian::write_u16(&mut data[2..4], 4); BigEndian::write_u32(&mut data[40..44], 4096); BigEndian::write_u32(&mut data[44..48], 100000); BigEndian::write_u32(&mut data[48..52], 30000); let vh = HfsPlusVolumeHeader::parse(&data).unwrap();
6589 assert_eq!(vh.signature, HFS_PLUS_SIGNATURE);
6590 assert_eq!(vh.block_size, 4096);
6591 assert_eq!(vh.total_blocks, 100000);
6592 assert_eq!(vh.free_blocks, 30000);
6593 assert!(!vh.is_hfsx());
6594 }
6595
6596 #[test]
6597 fn test_hfsx_signature() {
6598 let mut data = [0u8; 512];
6599 BigEndian::write_u16(&mut data[0..2], HFSX_SIGNATURE);
6600 BigEndian::write_u32(&mut data[40..44], 4096);
6601 BigEndian::write_u32(&mut data[44..48], 100000);
6602 BigEndian::write_u32(&mut data[48..52], 30000);
6603
6604 let vh = HfsPlusVolumeHeader::parse(&data).unwrap();
6605 assert!(vh.is_hfsx());
6606 }
6607
6608 #[test]
6609 fn test_decode_utf16be() {
6610 let data = [0x00, 0x48, 0x00, 0x65, 0x00, 0x6C, 0x00, 0x6C, 0x00, 0x6F];
6612 assert_eq!(decode_utf16be(&data), "Hello");
6613 }
6614
6615 #[test]
6616 fn test_decode_utf16be_with_non_ascii() {
6617 let data = [0x00, 0xC4];
6619 assert_eq!(decode_utf16be(&data), "Ä");
6620 }
6621
6622 #[test]
6623 fn test_extent_descriptor() {
6624 let mut data = [0u8; 8];
6625 BigEndian::write_u32(&mut data[0..4], 100);
6626 BigEndian::write_u32(&mut data[4..8], 50);
6627
6628 let ext = ExtentDescriptor::parse(&data);
6629 assert_eq!(ext.start_block, 100);
6630 assert_eq!(ext.block_count, 50);
6631 assert!(!ext.is_empty());
6632 }
6633
6634 #[test]
6635 fn test_find_last_set_bit() {
6636 let bitmap = [0b10100000, 0b00001000]; assert_eq!(find_last_set_bit(&bitmap, 16), Some(12));
6638
6639 let empty = [0u8; 2];
6640 assert_eq!(find_last_set_bit(&empty, 16), None);
6641 }
6642
6643 #[test]
6644 fn test_find_last_set_bit_dense_byte() {
6645 assert_eq!(find_last_set_bit(&[0b11111100], 8), Some(5));
6650 assert_eq!(find_last_set_bit(&[0b11111111], 8), Some(7));
6651 assert_eq!(find_last_set_bit(&[0b00000001], 8), Some(7));
6652 assert_eq!(find_last_set_bit(&[0b10000000], 8), Some(0));
6653 assert_eq!(find_last_set_bit(&[0b11111100, 0b00000000], 16), Some(5));
6655 assert_eq!(find_last_set_bit(&[0b11111111], 4), Some(3));
6658 }
6659
6660 #[test]
6661 fn test_volume_header_serialize_roundtrip() {
6662 let mut data = [0u8; 512];
6663 BigEndian::write_u16(&mut data[0..2], HFS_PLUS_SIGNATURE);
6664 BigEndian::write_u16(&mut data[2..4], 4);
6665 BigEndian::write_u32(&mut data[40..44], 4096);
6666 BigEndian::write_u32(&mut data[44..48], 100);
6667 BigEndian::write_u32(&mut data[48..52], 50);
6668 BigEndian::write_u32(&mut data[64..68], 10); BigEndian::write_u32(&mut data[80..84], 42); let vh = HfsPlusVolumeHeader::parse(&data).unwrap();
6672 assert_eq!(vh.next_catalog_id, 10);
6673 assert_eq!(vh.finder_info[0], 42);
6674
6675 let serialized = vh.serialize();
6676 let vh2 = HfsPlusVolumeHeader::parse(&serialized).unwrap();
6677 assert_eq!(vh2.block_size, 4096);
6678 assert_eq!(vh2.total_blocks, 100);
6679 assert_eq!(vh2.free_blocks, 50);
6680 assert_eq!(vh2.next_catalog_id, 10);
6681 assert_eq!(vh2.finder_info[0], 42);
6682 }
6683
6684 #[test]
6685 fn test_fork_data_serialize_roundtrip() {
6686 let fork = ForkData {
6687 logical_size: 12345,
6688 clump_size: 0,
6689 total_blocks: 3,
6690 extents: [
6691 ExtentDescriptor {
6692 start_block: 10,
6693 block_count: 3,
6694 },
6695 ExtentDescriptor {
6696 start_block: 0,
6697 block_count: 0,
6698 },
6699 ExtentDescriptor {
6700 start_block: 0,
6701 block_count: 0,
6702 },
6703 ExtentDescriptor {
6704 start_block: 0,
6705 block_count: 0,
6706 },
6707 ExtentDescriptor {
6708 start_block: 0,
6709 block_count: 0,
6710 },
6711 ExtentDescriptor {
6712 start_block: 0,
6713 block_count: 0,
6714 },
6715 ExtentDescriptor {
6716 start_block: 0,
6717 block_count: 0,
6718 },
6719 ExtentDescriptor {
6720 start_block: 0,
6721 block_count: 0,
6722 },
6723 ],
6724 };
6725 let mut buf = [0u8; 80];
6726 fork.serialize(&mut buf);
6727 let fork2 = ForkData::parse(&buf);
6728 assert_eq!(fork2.logical_size, 12345);
6729 assert_eq!(fork2.total_blocks, 3);
6730 assert_eq!(fork2.extents[0].start_block, 10);
6731 assert_eq!(fork2.extents[0].block_count, 3);
6732 }
6733
6734 fn make_editable_hfsplus_image() -> Vec<u8> {
6744 let block_size = 4096u32;
6745 let total_blocks = 256u32;
6746 let image_size = total_blocks as usize * block_size as usize; let mut img = vec![0u8; image_size];
6748
6749 let bitmap_block = 1u32;
6751 let catalog_start_block = 2u32;
6752 let catalog_blocks = 4u32;
6753 let alloc_blocks = 1 + 1 + catalog_blocks; let bitmap_data = &mut img[bitmap_block as usize * block_size as usize
6755 ..(bitmap_block + 1) as usize * block_size as usize];
6756 bitmap_data[0] = 0b11111100;
6758
6759 let node_size = 4096usize;
6761 let catalog_offset = catalog_start_block as usize * block_size as usize;
6762 let catalog_size = catalog_blocks as usize * block_size as usize;
6763
6764 let hdr_off = catalog_offset;
6766 img[hdr_off + 8] = 1; BigEndian::write_u16(&mut img[hdr_off + 10..hdr_off + 12], 3); let hr = hdr_off + 14;
6772 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], 516); BigEndian::write_u32(&mut img[hr + 22..hr + 26], 4); BigEndian::write_u32(&mut img[hr + 26..hr + 30], 2); let ot = hdr_off + node_size; BigEndian::write_u16(&mut img[ot - 2..ot], 14); BigEndian::write_u16(&mut img[ot - 4..ot - 2], 142); BigEndian::write_u16(&mut img[ot - 6..ot - 4], 270); BigEndian::write_u16(&mut img[ot - 8..ot - 6], 526); img[hdr_off + 270] = 0b11000000;
6795
6796 let leaf_off = catalog_offset + node_size;
6798 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;
6806 BigEndian::write_u16(&mut img[r0_off..r0_off + 2], 6); BigEndian::write_u32(&mut img[r0_off + 2..r0_off + 6], 1); BigEndian::write_u16(&mut img[r0_off + 6..r0_off + 8], 0); let r0_data = r0_off + 8;
6811 BigEndian::write_i16(&mut img[r0_data..r0_data + 2], CATALOG_FOLDER); BigEndian::write_u32(&mut img[r0_data + 8..r0_data + 12], 2); let r1_off = r0_data + 88; BigEndian::write_u16(&mut img[r1_off..r1_off + 2], 6); BigEndian::write_u32(&mut img[r1_off + 2..r1_off + 6], 2); BigEndian::write_u16(&mut img[r1_off + 6..r1_off + 8], 0); let r1_data = r1_off + 8;
6823 BigEndian::write_i16(&mut img[r1_data..r1_data + 2], 3); BigEndian::write_u32(&mut img[r1_data + 4..r1_data + 8], 1); BigEndian::write_u16(&mut img[r1_data + 8..r1_data + 10], 0); let lot = leaf_off + node_size;
6830 BigEndian::write_u16(&mut img[lot - 2..lot], 14); let r1_rel = (r1_off - leaf_off) as u16;
6832 BigEndian::write_u16(&mut img[lot - 4..lot - 2], r1_rel); let free_rel = (r1_data + 10 - leaf_off) as u16;
6834 BigEndian::write_u16(&mut img[lot - 6..lot - 4], free_rel); let vh = HfsPlusVolumeHeader {
6838 signature: HFS_PLUS_SIGNATURE,
6839 version: 4,
6840 attributes: 0,
6841 last_mounted_version: 0,
6842 journal_info_block: 0,
6843 create_date: hfs_common::hfs_now(),
6844 modify_date: hfs_common::hfs_now(),
6845 backup_date: 0,
6846 checked_date: 0,
6847 file_count: 0,
6848 folder_count: 0, block_size,
6850 total_blocks,
6851 free_blocks: total_blocks - alloc_blocks,
6852 next_allocation: alloc_blocks,
6853 rsrc_clump_size: 0,
6854 data_clump_size: 0,
6855 next_catalog_id: 16, write_count: 0,
6857 encodings_bitmap: 0,
6858 finder_info: [0u32; 8],
6859 allocation_file: ForkData {
6860 logical_size: block_size as u64,
6861 clump_size: 0,
6862 total_blocks: 1,
6863 extents: {
6864 let mut e = [ExtentDescriptor {
6865 start_block: 0,
6866 block_count: 0,
6867 }; 8];
6868 e[0] = ExtentDescriptor {
6869 start_block: bitmap_block,
6870 block_count: 1,
6871 };
6872 e
6873 },
6874 },
6875 extents_file: ForkData::empty(),
6876 catalog_file: ForkData {
6877 logical_size: catalog_size as u64,
6878 clump_size: 0,
6879 total_blocks: catalog_blocks,
6880 extents: {
6881 let mut e = [ExtentDescriptor {
6882 start_block: 0,
6883 block_count: 0,
6884 }; 8];
6885 e[0] = ExtentDescriptor {
6886 start_block: catalog_start_block,
6887 block_count: catalog_blocks,
6888 };
6889 e
6890 },
6891 },
6892 attributes_file: ForkData::empty(),
6893 startup_file: ForkData::empty(),
6894 };
6895
6896 let vh_bytes = vh.serialize();
6897 img[1024..1024 + 512].copy_from_slice(&vh_bytes);
6898
6899 let backup_pos = image_size - 1024;
6901 img[backup_pos..backup_pos + 512].copy_from_slice(&vh_bytes);
6902
6903 img
6904 }
6905
6906 fn make_editable_hfsplus_image_with_extents_btree() -> Vec<u8> {
6918 let mut img = make_editable_hfsplus_image();
6921 let block_size = 4096u32;
6922 let node_size = 4096usize;
6923 let extents_start_block = 6u32;
6924 let extents_blocks = 4u32;
6925
6926 let bitmap_off = 1usize * block_size as usize;
6930 img[bitmap_off] = 0xFF; img[bitmap_off + 1] = 0b11000000; let hdr_off = extents_start_block as usize * block_size as usize;
6935 img[hdr_off + 8] = 1; BigEndian::write_u16(&mut img[hdr_off + 10..hdr_off + 12], 3); let hr = hdr_off + 14;
6940 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], 0); 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);
6946 BigEndian::write_u16(&mut img[hr + 20..hr + 22], 10); BigEndian::write_u32(&mut img[hr + 22..hr + 26], 4); BigEndian::write_u32(&mut img[hr + 26..hr + 30], 2); let ot = hdr_off + node_size;
6953 BigEndian::write_u16(&mut img[ot - 2..ot], 14);
6954 BigEndian::write_u16(&mut img[ot - 4..ot - 2], 142);
6955 BigEndian::write_u16(&mut img[ot - 6..ot - 4], 270);
6956 BigEndian::write_u16(&mut img[ot - 8..ot - 6], 526);
6957
6958 img[hdr_off + 270] = 0b11000000;
6960
6961 let leaf_off = hdr_off + node_size;
6963 img[leaf_off + 8] = 0xFF; img[leaf_off + 9] = 1; BigEndian::write_u16(&mut img[leaf_off + 10..leaf_off + 12], 0); let lot = leaf_off + node_size;
6970 BigEndian::write_u16(&mut img[lot - 2..lot], 14);
6971
6972 let mut vh = HfsPlusVolumeHeader::parse(&img[1024..1536]).expect("parse seed VH");
6978 vh.extents_file = ForkData {
6979 logical_size: (extents_blocks as u64) * (block_size as u64),
6980 clump_size: 0,
6981 total_blocks: extents_blocks,
6982 extents: {
6983 let mut e = [ExtentDescriptor {
6984 start_block: 0,
6985 block_count: 0,
6986 }; 8];
6987 e[0] = ExtentDescriptor {
6988 start_block: extents_start_block,
6989 block_count: extents_blocks,
6990 };
6991 e
6992 },
6993 };
6994 vh.free_blocks -= extents_blocks;
6995 vh.next_allocation = 10;
6996 let vh_bytes = vh.serialize();
6997 img[1024..1536].copy_from_slice(&vh_bytes);
6998 let alt = img.len() - 1024;
6999 img[alt..alt + 512].copy_from_slice(&vh_bytes);
7000 img
7001 }
7002
7003 fn make_editable_hfsplus_image_with_inline_xattr(cnid: u32, value: &[u8]) -> Vec<u8> {
7014 let mut img = make_editable_hfsplus_image();
7015 let block_size = 4096u32;
7016 let node_size = 4096usize;
7017 let attr_start_block = 6u32;
7018 let attr_blocks = 2u32;
7019
7020 let bitmap_off = 1usize * block_size as usize;
7023 img[bitmap_off] = 0xFF; let hdr_off = attr_start_block as usize * block_size as usize;
7027 img[hdr_off + 8] = 1; BigEndian::write_u16(&mut img[hdr_off + 10..hdr_off + 12], 3);
7029
7030 let hr = hdr_off + 14;
7031 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], 1); 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);
7037 BigEndian::write_u16(&mut img[hr + 20..hr + 22], 512); BigEndian::write_u32(&mut img[hr + 22..hr + 26], attr_blocks); let ot = hdr_off + node_size;
7043 BigEndian::write_u16(&mut img[ot - 2..ot], 14);
7044 BigEndian::write_u16(&mut img[ot - 4..ot - 2], 142);
7045 BigEndian::write_u16(&mut img[ot - 6..ot - 4], 270);
7046 BigEndian::write_u16(&mut img[ot - 8..ot - 6], 526);
7047
7048 img[hdr_off + 270] = 0b11000000;
7050
7051 let leaf_off = hdr_off + node_size;
7053 img[leaf_off + 8] = 0xFF; img[leaf_off + 9] = 1; BigEndian::write_u16(&mut img[leaf_off + 10..leaf_off + 12], 1); let name = "com.apple.FinderInfo";
7060 let name_utf16: Vec<u16> = name.encode_utf16().collect();
7061 let name_byte_len = name_utf16.len() * 2;
7062 let key_body_len = 12 + name_byte_len;
7063 let data_section_len = 4 + 8 + 4 + value.len();
7065 let rec_total = 2 + key_body_len + data_section_len;
7066
7067 let r0 = leaf_off + 14;
7068 BigEndian::write_u16(&mut img[r0..r0 + 2], key_body_len as u16);
7069 BigEndian::write_u32(&mut img[r0 + 4..r0 + 8], cnid);
7071 BigEndian::write_u32(&mut img[r0 + 8..r0 + 12], 0); BigEndian::write_u16(&mut img[r0 + 12..r0 + 14], name_utf16.len() as u16);
7073 for (i, ch) in name_utf16.iter().enumerate() {
7074 BigEndian::write_u16(&mut img[r0 + 14 + i * 2..r0 + 14 + i * 2 + 2], *ch);
7075 }
7076 let data_off = r0 + 2 + key_body_len;
7077 BigEndian::write_u32(&mut img[data_off..data_off + 4], HFSPLUS_ATTR_INLINE_DATA);
7078 BigEndian::write_u32(&mut img[data_off + 12..data_off + 16], value.len() as u32);
7080 img[data_off + 16..data_off + 16 + value.len()].copy_from_slice(value);
7081
7082 let lot = leaf_off + node_size;
7084 BigEndian::write_u16(&mut img[lot - 2..lot], 14);
7085 BigEndian::write_u16(&mut img[lot - 4..lot - 2], (14 + rec_total) as u16);
7086
7087 let mut vh = HfsPlusVolumeHeader::parse(&img[1024..1536]).expect("parse seed VH");
7090 vh.attributes_file = ForkData {
7091 logical_size: (attr_blocks as u64) * (block_size as u64),
7092 clump_size: 0,
7093 total_blocks: attr_blocks,
7094 extents: {
7095 let mut e = [ExtentDescriptor {
7096 start_block: 0,
7097 block_count: 0,
7098 }; 8];
7099 e[0] = ExtentDescriptor {
7100 start_block: attr_start_block,
7101 block_count: attr_blocks,
7102 };
7103 e
7104 },
7105 };
7106 vh.free_blocks -= attr_blocks;
7107 vh.next_allocation = 8;
7108 let vh_bytes = vh.serialize();
7109 img[1024..1536].copy_from_slice(&vh_bytes);
7110 let alt = img.len() - 1024;
7111 img[alt..alt + 512].copy_from_slice(&vh_bytes);
7112 img
7113 }
7114
7115 fn make_hfsplus_image_with_label(volume_name: &str, num_children: usize) -> Vec<u8> {
7122 let block_size = 4096u32;
7123 let total_blocks = 256u32;
7124 let image_size = total_blocks as usize * block_size as usize;
7125 let mut img = vec![0u8; image_size];
7126
7127 let bitmap_block = 1u32;
7129 let catalog_start_block = 2u32;
7130 let catalog_blocks = 4u32;
7131 img[bitmap_block as usize * block_size as usize] = 0b11111100;
7133
7134 let node_size = 4096usize;
7135 let catalog_offset = catalog_start_block as usize * block_size as usize;
7136
7137 let hdr = catalog_offset;
7139 img[hdr + 8] = 1; BigEndian::write_u16(&mut img[hdr + 10..hdr + 12], 3);
7141 let hr = hdr + 14;
7142 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], (1 + num_children + 1) as u32);
7145 BigEndian::write_u32(&mut img[hr + 10..hr + 14], 1); BigEndian::write_u32(&mut img[hr + 14..hr + 18], 2);
7148 BigEndian::write_u16(&mut img[hr + 18..hr + 20], node_size as u16);
7149 BigEndian::write_u16(&mut img[hr + 20..hr + 22], 516);
7150 BigEndian::write_u32(&mut img[hr + 22..hr + 26], 4);
7151 BigEndian::write_u32(&mut img[hr + 26..hr + 30], 0);
7152 let ot = hdr + node_size;
7154 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); let leaf = catalog_offset + node_size;
7163 img[leaf + 8] = 0xFF; img[leaf + 9] = 1; let total_recs = 1 + num_children;
7166 BigEndian::write_u16(&mut img[leaf + 10..leaf + 12], total_recs as u16);
7167
7168 let name_units: Vec<u16> = volume_name.encode_utf16().collect();
7171 let name_bytes = name_units.len() * 2;
7172 let r0 = leaf + 14;
7173 let key_len = 6 + name_bytes; BigEndian::write_u16(&mut img[r0..r0 + 2], key_len as u16);
7175 BigEndian::write_u32(&mut img[r0 + 2..r0 + 6], 1); BigEndian::write_u16(&mut img[r0 + 6..r0 + 8], name_units.len() as u16);
7177 for (i, u) in name_units.iter().enumerate() {
7178 BigEndian::write_u16(&mut img[r0 + 8 + i * 2..r0 + 10 + i * 2], *u);
7179 }
7180 let r0_data = r0 + 2 + key_len + ((2 + key_len) % 2); BigEndian::write_i16(&mut img[r0_data..r0_data + 2], CATALOG_FOLDER);
7182 BigEndian::write_u32(&mut img[r0_data + 8..r0_data + 12], 2); let mut rec_offsets = vec![14u16];
7189 let mut cursor = r0_data + 88;
7190 for i in 0..num_children {
7191 let child_name = format!("c{i}");
7192 let cn_units: Vec<u16> = child_name.encode_utf16().collect();
7193 let key_len = 6 + cn_units.len() * 2;
7194 let off = cursor;
7195 BigEndian::write_u16(&mut img[off..off + 2], key_len as u16);
7196 BigEndian::write_u32(&mut img[off + 2..off + 6], 2);
7197 BigEndian::write_u16(&mut img[off + 6..off + 8], cn_units.len() as u16);
7198 for (j, u) in cn_units.iter().enumerate() {
7199 BigEndian::write_u16(&mut img[off + 8 + j * 2..off + 10 + j * 2], *u);
7200 }
7201 let data = off + 2 + key_len + ((2 + key_len) % 2);
7204 BigEndian::write_i16(&mut img[data..data + 2], CATALOG_FOLDER);
7205 rec_offsets.push((off - leaf) as u16);
7206 cursor = data + 88;
7207 }
7208
7209 let lot = leaf + node_size;
7211 for (i, off) in rec_offsets.iter().enumerate() {
7212 BigEndian::write_u16(&mut img[lot - 2 - i * 2..lot - i * 2], *off);
7213 }
7214 BigEndian::write_u16(
7215 &mut img[lot - 2 - rec_offsets.len() * 2..lot - rec_offsets.len() * 2],
7216 (cursor - leaf) as u16,
7217 );
7218
7219 let vh = HfsPlusVolumeHeader {
7221 signature: HFS_PLUS_SIGNATURE,
7222 version: 4,
7223 attributes: 0,
7224 last_mounted_version: 0,
7225 journal_info_block: 0,
7226 create_date: 0,
7227 modify_date: 0,
7228 backup_date: 0,
7229 checked_date: 0,
7230 file_count: 0,
7231 folder_count: num_children as u32,
7232 block_size,
7233 total_blocks,
7234 free_blocks: total_blocks - 6,
7235 next_allocation: 6,
7236 rsrc_clump_size: 0,
7237 data_clump_size: 0,
7238 next_catalog_id: (16 + num_children) as u32,
7239 write_count: 0,
7240 encodings_bitmap: 0,
7241 finder_info: [0; 8],
7242 allocation_file: ForkData {
7243 logical_size: block_size as u64,
7244 clump_size: 0,
7245 total_blocks: 1,
7246 extents: {
7247 let mut e = [ExtentDescriptor {
7248 start_block: 0,
7249 block_count: 0,
7250 }; 8];
7251 e[0] = ExtentDescriptor {
7252 start_block: bitmap_block,
7253 block_count: 1,
7254 };
7255 e
7256 },
7257 },
7258 extents_file: ForkData::empty(),
7259 catalog_file: ForkData {
7260 logical_size: (catalog_blocks as u64) * (block_size as u64),
7261 clump_size: 0,
7262 total_blocks: catalog_blocks,
7263 extents: {
7264 let mut e = [ExtentDescriptor {
7265 start_block: 0,
7266 block_count: 0,
7267 }; 8];
7268 e[0] = ExtentDescriptor {
7269 start_block: catalog_start_block,
7270 block_count: catalog_blocks,
7271 };
7272 e
7273 },
7274 },
7275 attributes_file: ForkData::empty(),
7276 startup_file: ForkData::empty(),
7277 };
7278 let vh_bytes = vh.serialize();
7279 img[1024..1024 + 512].copy_from_slice(&vh_bytes);
7280 let alt = image_size - 1024;
7281 img[alt..alt + 512].copy_from_slice(&vh_bytes);
7282 img
7283 }
7284
7285 #[test]
7286 fn test_probe_hfsplus_volume_label_reads_from_root_key() {
7287 let img = make_hfsplus_image_with_label("Ariel-backup", 20);
7292 let mut cursor = std::io::Cursor::new(img);
7293 let label = probe_hfsplus_volume_label(&mut cursor, 0);
7294 assert_eq!(label.as_deref(), Some("Ariel-backup"));
7295 }
7296
7297 #[test]
7298 fn test_probe_hfsplus_volume_label_short_name() {
7299 let img = make_hfsplus_image_with_label("X", 0);
7300 let mut cursor = std::io::Cursor::new(img);
7301 assert_eq!(
7302 probe_hfsplus_volume_label(&mut cursor, 0).as_deref(),
7303 Some("X")
7304 );
7305 }
7306
7307 #[test]
7308 fn test_prepare_for_edit_refuses_journaled_volume_without_valid_journal() {
7309 let mut img = make_editable_hfsplus_image();
7315 let attr_off = 1024 + 4; let mut attrs = BigEndian::read_u32(&img[attr_off..attr_off + 4]);
7317 attrs |= HFSPLUS_VOLUME_JOURNALED_BIT;
7318 BigEndian::write_u32(&mut img[attr_off..attr_off + 4], attrs);
7319
7320 let cursor = std::io::Cursor::new(img);
7321 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("read-only open should succeed");
7322 let err = fs
7323 .prepare_for_edit()
7324 .expect_err("journaled volume with no valid journal must refuse edit prep");
7325 match err {
7326 FilesystemError::Unsupported(msg) => assert!(msg.contains("journal")),
7327 other => panic!("expected Unsupported(journal...), got {other:?}"),
7328 }
7329 }
7330
7331 #[test]
7332 fn test_sync_metadata_stamps_clean_unmount_attributes() {
7333 let mut img = make_editable_hfsplus_image();
7339 let attr_off = 1024 + 4;
7340 let dirty = 0x80004000u32 | HFSPLUS_BOOT_VOLUME_INCONSISTENT_BIT;
7341 BigEndian::write_u32(&mut img[attr_off..attr_off + 4], dirty);
7342
7343 let cursor = std::io::Cursor::new(img);
7344 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("open");
7345 fs.prepare_for_edit().expect("prep");
7346 fs.sync_metadata().expect("sync");
7347
7348 let img = fs.reader.into_inner();
7349 let after = BigEndian::read_u32(&img[attr_off..attr_off + 4]);
7350 assert!(
7351 after & HFSPLUS_VOLUME_UNMOUNTED_BIT != 0,
7352 "unmounted bit must be set after sync, got 0x{after:08x}"
7353 );
7354 assert!(
7355 after & HFSPLUS_VOLUME_INCONSISTENT_BIT == 0,
7356 "inconsistent bit must be cleared, got 0x{after:08x}"
7357 );
7358 assert!(
7359 after & HFSPLUS_BOOT_VOLUME_INCONSISTENT_BIT == 0,
7360 "boot-inconsistent bit must be cleared, got 0x{after:08x}"
7361 );
7362 }
7363
7364 #[test]
7365 fn test_prepare_for_edit_clean_volume_succeeds() {
7366 let img = make_editable_hfsplus_image();
7367 let cursor = std::io::Cursor::new(img);
7368 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
7369 fs.prepare_for_edit()
7370 .expect("clean volume should accept edit prep");
7371 assert!(fs.xattr_cnids.as_ref().unwrap().is_empty());
7373 }
7374
7375 #[test]
7378 fn deleting_a_leafs_first_record_refreshes_the_separators() {
7379 use crate::hfs_common::{
7382 btree_record_range, btree_stale_index_keys, BTreeHeader, BTreeKeyFormat,
7383 };
7384 type Fs = HfsPlusFilesystem<std::io::Cursor<Vec<u8>>>;
7385 let img = create_blank_hfsplus_sized(64 * 1024 * 1024, 4096, "IKey", false, 16 << 20, 0);
7386 let mut fs = Fs::open(std::io::Cursor::new(img), 0).unwrap();
7387 fs.prepare_for_edit().unwrap();
7388 let root = fs.root().unwrap();
7389 let options = CreateFileOptions::default();
7390 for i in 0..1200 {
7391 let mut empty = std::io::Cursor::new(Vec::new());
7392 fs.create_file(&root, &format!("f{i:04}.txt"), &mut empty, 0, &options)
7393 .unwrap();
7394 }
7395 let node_size = fs.catalog_node_size();
7396 let header = BTreeHeader::read(fs.catalog_data());
7397 assert!(
7398 header.depth >= 2,
7399 "need an index level, depth {}",
7400 header.depth
7401 );
7402
7403 let first = header.first_leaf_node as usize;
7406 let second =
7407 BigEndian::read_u32(&fs.catalog_data()[first * node_size..first * node_size + 4])
7408 as usize;
7409 let name = {
7410 let node = &fs.catalog_data()[second * node_size..(second + 1) * node_size];
7411 let (s, e) = btree_record_range(node, node_size, 0);
7412 let rec = &node[s..e];
7413 let name_len = BigEndian::read_u16(&rec[6..8]) as usize;
7414 (0..name_len)
7415 .map(|i| BigEndian::read_u16(&rec[8 + 2 * i..10 + 2 * i]) as u8 as char)
7416 .collect::<String>()
7417 };
7418 assert!(
7419 name.starts_with('f'),
7420 "expected a file record first, got {name:?}"
7421 );
7422 let victim = fs
7423 .list_directory(&root)
7424 .unwrap()
7425 .into_iter()
7426 .find(|e| e.name == name)
7427 .expect("victim listed");
7428 fs.delete_entry(&root, &victim).unwrap();
7429
7430 let cmp = |a: &[u8], b: &[u8]| Fs::catalog_compare(a, b, false);
7431 let stale = btree_stale_index_keys(
7432 fs.catalog_data(),
7433 node_size,
7434 &BTreeKeyFormat::HFSPLUS_CATALOG,
7435 &cmp,
7436 );
7437 assert!(stale.is_empty(), "separators still stale: {stale:?}");
7438 let report = fs.fsck().unwrap().unwrap();
7439 assert!(
7440 report.errors.is_empty(),
7441 "{:?}",
7442 report
7443 .errors
7444 .iter()
7445 .map(|e| &e.message)
7446 .take(4)
7447 .collect::<Vec<_>>()
7448 );
7449 }
7450
7451 #[test]
7452 fn emptied_leaves_leave_the_index_too() {
7453 use crate::hfs_common::{btree_bitmap_test, btree_record_range, BTREE_INDEX_NODE};
7454 let img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "Leaves", false);
7455 let mut fs = HfsPlusFilesystem::open(std::io::Cursor::new(img), 0).unwrap();
7456 fs.prepare_for_edit().unwrap();
7457 let root = fs.root().unwrap();
7458 let names: Vec<String> = (0..220).map(|i| format!("leaf-file-{i:04}.txt")).collect();
7459 let fill = |fs: &mut HfsPlusFilesystem<std::io::Cursor<Vec<u8>>>| {
7460 for n in &names {
7461 let mut src = std::io::Cursor::new(n.as_bytes().to_vec());
7462 fs.create_file(
7463 &root,
7464 n,
7465 &mut src,
7466 n.len() as u64,
7467 &CreateFileOptions::default(),
7468 )
7469 .unwrap_or_else(|e| panic!("create {n}: {e:?}"));
7470 }
7471 };
7472 fill(&mut fs);
7473 assert!(
7474 fs.catalog_header.depth > 1,
7475 "the test needs a split catalog"
7476 );
7477
7478 for e in fs.list_directory(&root).unwrap() {
7479 fs.delete_entry(&root, &e).unwrap();
7480 }
7481 let node_size = fs.catalog_header.node_size as usize;
7483 let data = &fs.catalog_data;
7484 for idx in 1..(data.len() / node_size) as u32 {
7485 if !btree_bitmap_test(data, node_size, idx) {
7486 continue;
7487 }
7488 let off = idx as usize * node_size;
7489 let node = &data[off..off + node_size];
7490 if node[8] as i8 != BTREE_INDEX_NODE {
7491 continue;
7492 }
7493 let n = BigEndian::read_u16(&node[10..12]) as usize;
7494 for r in 0..n {
7495 let (_, end) = btree_record_range(node, node_size, r);
7496 let child = BigEndian::read_u32(&node[end - 4..end]);
7497 assert!(
7498 btree_bitmap_test(data, node_size, child),
7499 "index node {idx} points at free node {child}"
7500 );
7501 }
7502 }
7503
7504 fill(&mut fs);
7505 let listed = fs.list_directory(&root).unwrap();
7506 assert_eq!(listed.len(), names.len());
7507 for e in &listed {
7508 assert_eq!(fs.read_file(e, usize::MAX).unwrap(), e.name.as_bytes());
7509 }
7510 fs.sync_metadata().unwrap();
7511 let report = fs.fsck().unwrap().unwrap();
7512 assert!(report.errors.is_empty(), "{:?}", report.errors);
7513 }
7514
7515 #[test]
7516 fn test_create_blank_hfsplus_32mib() {
7517 let img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "MyVol", false);
7518 assert_eq!(img.len(), 32 * 1024 * 1024);
7519
7520 let cursor = std::io::Cursor::new(img);
7521 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("blank should open");
7522 assert_eq!(fs.fs_type(), "HFS+");
7523 assert_eq!(fs.label, "MyVol");
7524
7525 let root = fs.root().unwrap();
7526 let entries = fs.list_directory(&root).unwrap();
7527 assert!(entries.is_empty(), "freshly built blank must list empty");
7528
7529 assert_eq!(fs.vh.total_blocks, 8192);
7536 assert_eq!(fs.vh.free_blocks, 8192 - 47 - 1);
7537 fs.prepare_for_edit().expect("blank must accept edit prep");
7539 }
7540
7541 #[test]
7542 fn test_create_blank_hfsx_32mib_signature() {
7543 let img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "CSVol", true);
7544 let cursor = std::io::Cursor::new(img);
7545 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("HFSX blank should open");
7546 assert_eq!(fs.fs_type(), "HFSX");
7547 assert!(fs.vh.is_hfsx());
7548 assert_eq!(fs.label, "CSVol");
7549 let root = fs.root().unwrap();
7550 assert!(fs.list_directory(&root).unwrap().is_empty());
7551 }
7552
7553 #[test]
7554 fn test_create_blank_hfsplus_supports_create_file() {
7555 let img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "Work", false);
7557 let cursor = std::io::Cursor::new(img);
7558 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
7559 fs.prepare_for_edit().unwrap();
7560 let root = fs.root().unwrap();
7561 let payload = b"hello world".to_vec();
7562 let mut data = std::io::Cursor::new(payload.clone());
7563 fs.create_file(
7564 &root,
7565 "hello.txt",
7566 &mut data,
7567 payload.len() as u64,
7568 &CreateFileOptions::default(),
7569 )
7570 .expect("create_file on blank");
7571 fs.sync_metadata().unwrap();
7572
7573 let img2 = fs.reader.into_inner();
7574 let mut cursor2 = std::io::Cursor::new(img2);
7575 let mut fs2 = HfsPlusFilesystem::open(&mut cursor2, 0).unwrap();
7576 let root2 = fs2.root().unwrap();
7577 let entries = fs2.list_directory(&root2).unwrap();
7578 assert_eq!(entries.len(), 1);
7579 assert_eq!(entries[0].name, "hello.txt");
7580 let got = fs2.read_file(&entries[0], 1024).unwrap();
7581 assert_eq!(got, payload);
7582 }
7583
7584 #[test]
7585 fn test_hfsplus_rename_round_trips() {
7586 let img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "Work", false);
7587 let mut fs = HfsPlusFilesystem::open(std::io::Cursor::new(img), 0).unwrap();
7588 fs.prepare_for_edit().unwrap();
7589 let root = fs.root().unwrap();
7590
7591 let payload = b"keep my bytes across a rename".to_vec();
7592 let mut data = std::io::Cursor::new(payload.clone());
7593 let fe = fs
7594 .create_file(
7595 &root,
7596 "before.txt",
7597 &mut data,
7598 payload.len() as u64,
7599 &CreateFileOptions::default(),
7600 )
7601 .unwrap();
7602 let cnid = fe.location;
7603 fs.create_directory(&root, "olddir", &CreateDirectoryOptions::default())
7604 .unwrap();
7605
7606 fs.rename(&root, &fe, "a much longer renamed file.txt")
7609 .unwrap();
7610 let old_dir = fs
7612 .list_directory(&root)
7613 .unwrap()
7614 .into_iter()
7615 .find(|e| e.name == "olddir")
7616 .unwrap();
7617 fs.rename(&root, &old_dir, "newdir").unwrap();
7618
7619 let mut t = std::io::Cursor::new(b"x".as_slice());
7621 fs.create_file(&root, "taken.txt", &mut t, 1, &CreateFileOptions::default())
7622 .unwrap();
7623 let renamed_now = fs
7624 .list_directory(&root)
7625 .unwrap()
7626 .into_iter()
7627 .find(|e| e.name == "a much longer renamed file.txt")
7628 .unwrap();
7629 assert!(matches!(
7630 fs.rename(&root, &renamed_now, "taken.txt"),
7631 Err(FilesystemError::AlreadyExists(_))
7632 ));
7633
7634 fs.sync_metadata().unwrap();
7636 let img2 = fs.reader.into_inner();
7637 let mut c2 = std::io::Cursor::new(img2);
7638 let mut fs2 = HfsPlusFilesystem::open(&mut c2, 0).unwrap();
7639 let root2 = fs2.root().unwrap();
7640 let entries = fs2.list_directory(&root2).unwrap();
7641 assert!(
7642 entries
7643 .iter()
7644 .any(|e| e.name == "a much longer renamed file.txt"),
7645 "{entries:?}"
7646 );
7647 assert!(
7648 !entries.iter().any(|e| e.name == "before.txt"),
7649 "{entries:?}"
7650 );
7651 let renamed = entries
7652 .iter()
7653 .find(|e| e.name == "a much longer renamed file.txt")
7654 .unwrap();
7655 assert_eq!(renamed.location, cnid);
7657 assert_eq!(fs2.read_file(renamed, 4096).unwrap(), payload);
7658 let newdir = entries.iter().find(|e| e.name == "newdir").unwrap();
7660 assert!(fs2.list_directory(newdir).is_ok());
7661 }
7662
7663 #[test]
7664 fn test_create_file_stamps_dates() {
7665 let img = make_editable_hfsplus_image();
7669 let cursor = std::io::Cursor::new(img);
7670 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
7671 fs.prepare_for_edit().unwrap();
7672
7673 let now_before = hfs_common::hfs_now();
7674 let root = fs.root().unwrap();
7675 let mut data = std::io::Cursor::new(b"hi".as_slice());
7676 let entry = fs
7677 .create_file(
7678 &root,
7679 "dated.txt",
7680 &mut data,
7681 2,
7682 &CreateFileOptions::default(),
7683 )
7684 .unwrap();
7685 let now_after = hfs_common::hfs_now();
7686
7687 let cnid = entry.location as u32;
7689 let (_n, _r, f_off) = fs.find_catalog_record(2, "dated.txt").unwrap();
7690 let fkey_len = BigEndian::read_u16(&fs.catalog_data[f_off..f_off + 2]) as usize;
7691 let mut frec = f_off + 2 + fkey_len;
7692 if !frec.is_multiple_of(2) {
7693 frec += 1;
7694 }
7695 let create_date = BigEndian::read_u32(&fs.catalog_data[frec + 12..frec + 16]);
7696 let content_mod = BigEndian::read_u32(&fs.catalog_data[frec + 16..frec + 20]);
7697 let attr_mod = BigEndian::read_u32(&fs.catalog_data[frec + 20..frec + 24]);
7698 let access = BigEndian::read_u32(&fs.catalog_data[frec + 24..frec + 28]);
7699
7700 for (label, value) in [
7701 ("createDate", create_date),
7702 ("contentModDate", content_mod),
7703 ("attributeModDate", attr_mod),
7704 ("accessDate", access),
7705 ] {
7706 assert!(
7707 value >= now_before && value <= now_after + 1,
7708 "{label} {value} not in [{now_before}, {now_after}+1]"
7709 );
7710 }
7711
7712 assert_eq!(fs.vh.file_count, 1);
7714 let _ = cnid;
7715 }
7716
7717 #[test]
7718 fn test_set_type_creator_bumps_attribute_mod_date() {
7719 let img = make_editable_hfsplus_image();
7723 let cursor = std::io::Cursor::new(img);
7724 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
7725 fs.prepare_for_edit().unwrap();
7726
7727 let root = fs.root().unwrap();
7728 let mut data = std::io::Cursor::new(b"x".as_slice());
7729 let entry = fs
7730 .create_file(&root, "a.txt", &mut data, 1, &CreateFileOptions::default())
7731 .unwrap();
7732
7733 let (_n, _r, f_off) = fs.find_catalog_record(2, "a.txt").unwrap();
7734 let fkey_len = BigEndian::read_u16(&fs.catalog_data[f_off..f_off + 2]) as usize;
7735 let mut frec = f_off + 2 + fkey_len;
7736 if !frec.is_multiple_of(2) {
7737 frec += 1;
7738 }
7739 let content_before = BigEndian::read_u32(&fs.catalog_data[frec + 16..frec + 20]);
7740
7741 BigEndian::write_u32(
7745 &mut fs.catalog_data[frec + 16..frec + 20],
7746 content_before - 60,
7747 );
7748 BigEndian::write_u32(
7749 &mut fs.catalog_data[frec + 20..frec + 24],
7750 content_before - 60,
7751 );
7752
7753 let now_before = hfs_common::hfs_now();
7754 fs.set_type_creator(&entry, "TEXT", "ttxt").unwrap();
7755 let now_after = hfs_common::hfs_now();
7756
7757 let content_after = BigEndian::read_u32(&fs.catalog_data[frec + 16..frec + 20]);
7758 let attr_after = BigEndian::read_u32(&fs.catalog_data[frec + 20..frec + 24]);
7759 assert_eq!(
7760 content_after,
7761 content_before - 60,
7762 "contentModDate must not move on attribute-only change"
7763 );
7764 assert!(
7765 attr_after >= now_before && attr_after <= now_after + 1,
7766 "attributeModDate {attr_after} not in [{now_before}, {now_after}+1]"
7767 );
7768 }
7769
7770 #[test]
7771 fn test_extents_overflow_round_trip_12_extents() {
7772 let img = make_editable_hfsplus_image_with_extents_btree();
7777 let cursor = std::io::Cursor::new(img);
7778 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
7779 fs.prepare_for_edit().unwrap();
7780
7781 fs.ensure_bitmap().unwrap();
7785 let bitmap = fs.bitmap.as_mut().unwrap();
7786 for byte in bitmap.iter_mut().take(32) {
7787 *byte = 0xFF;
7788 }
7789 let free_positions: Vec<u32> = (0..12).map(|i| 11 + 2 * i as u32).collect();
7790 for &blk in &free_positions {
7791 bitmap_clear_bit_be(bitmap, blk);
7792 }
7793 fs.vh.free_blocks = 12;
7794
7795 let block_size = fs.vh.block_size as usize;
7796 let payload: Vec<u8> = (0..(12 * block_size)).map(|i| (i % 251) as u8).collect();
7797 let mut reader = std::io::Cursor::new(payload.clone());
7798 let root = fs.root().unwrap();
7799 let entry = fs
7800 .create_file(
7801 &root,
7802 "frag12.bin",
7803 &mut reader,
7804 payload.len() as u64,
7805 &CreateFileOptions::default(),
7806 )
7807 .expect("12-extent allocation should succeed via overflow B-tree");
7808
7809 let cnid = entry.location as u32;
7810 let (data_fork, _rsrc) = fs.find_file_by_id(cnid).unwrap();
7811 for slot in &data_fork.extents {
7813 assert_eq!(slot.block_count, 1);
7814 }
7815 assert_eq!(data_fork.total_blocks, 12);
7816
7817 fs.sync_metadata().unwrap();
7819
7820 let img_after = fs.reader.into_inner();
7823 let mut cursor2 = std::io::Cursor::new(img_after);
7824 let mut fs2 = HfsPlusFilesystem::open(&mut cursor2, 0).unwrap();
7825 let root2 = fs2.root().unwrap();
7826 let entries = fs2.list_directory(&root2).unwrap();
7827 let reopened = entries.iter().find(|e| e.name == "frag12.bin").unwrap();
7828 let got = fs2.read_file(reopened, payload.len() + 1).unwrap();
7829 assert_eq!(got.len(), payload.len());
7830 assert_eq!(got, payload);
7831 }
7832
7833 #[test]
7834 fn test_allocate_extents_fragmented_bitmap() {
7835 let img = make_editable_hfsplus_image();
7839 let cursor = std::io::Cursor::new(img);
7840 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
7841 fs.prepare_for_edit().unwrap();
7842
7843 fs.ensure_bitmap().unwrap();
7847 let bitmap = fs.bitmap.as_mut().unwrap();
7848 for byte in bitmap.iter_mut().take(32) {
7849 *byte = 0xFF;
7850 }
7851 for blk in [7u32, 9, 11, 13] {
7853 bitmap_clear_bit_be(bitmap, blk);
7854 }
7855 fs.vh.free_blocks = 4;
7856
7857 let block_size = fs.vh.block_size as usize;
7858 let payload: Vec<u8> = (0..(4 * block_size)).map(|i| (i % 251) as u8).collect();
7859 let mut reader = std::io::Cursor::new(payload.clone());
7860 let root = fs.root().unwrap();
7861 let entry = fs
7862 .create_file(
7863 &root,
7864 "frag.bin",
7865 &mut reader,
7866 payload.len() as u64,
7867 &CreateFileOptions::default(),
7868 )
7869 .expect("fragmented allocation should succeed");
7870
7871 let cnid = entry.location as u32;
7872 let (data_fork, _rsrc) = fs.find_file_by_id(cnid).expect("file record present");
7873
7874 for (i, expected_start) in [7u32, 9, 11, 13].iter().enumerate() {
7876 assert_eq!(data_fork.extents[i].block_count, 1, "slot {i}");
7877 assert_eq!(
7878 data_fork.extents[i].start_block, *expected_start,
7879 "slot {i}"
7880 );
7881 }
7882 for slot in &data_fork.extents[4..] {
7883 assert!(slot.is_empty(), "slot past 4 should be empty");
7884 }
7885 assert_eq!(data_fork.total_blocks, 4);
7886
7887 let mut got = Vec::new();
7889 fs.write_file_to(&entry, &mut got).unwrap();
7890 assert_eq!(got, payload);
7891 assert_eq!(fs.vh.free_blocks, 0);
7892 }
7893
7894 #[test]
7895 fn test_snapshot_rollback_on_disk_full_create_file() {
7896 let img = make_editable_hfsplus_image();
7902 let cursor = std::io::Cursor::new(img);
7903 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
7904 fs.prepare_for_edit().unwrap();
7905
7906 let next_cnid_before = fs.vh.next_catalog_id;
7907 let free_blocks_before = fs.vh.free_blocks;
7908 let file_count_before = fs.vh.file_count;
7909 let catalog_before = fs.catalog_data.clone();
7910
7911 let root = fs.root().unwrap();
7912 let big_len: u64 = 251 * 4096 + 1; let big_buf = vec![0u8; big_len as usize];
7914 let mut reader = std::io::Cursor::new(big_buf);
7915 let err = fs
7916 .create_file(
7917 &root,
7918 "too-big.bin",
7919 &mut reader,
7920 big_len,
7921 &CreateFileOptions::default(),
7922 )
7923 .expect_err("create_file should fail with DiskFull");
7924 assert!(matches!(err, FilesystemError::DiskFull(_)), "got {err:?}");
7925
7926 assert_eq!(fs.vh.next_catalog_id, next_cnid_before);
7927 assert_eq!(fs.vh.free_blocks, free_blocks_before);
7928 assert_eq!(fs.vh.file_count, file_count_before);
7929 assert_eq!(fs.catalog_data, catalog_before);
7930 }
7931
7932 #[test]
7933 fn test_set_xattr_round_trip_and_delete_cleanup() {
7934 let sentinel_cnid = 99u32;
7943 let img = make_editable_hfsplus_image_with_inline_xattr(sentinel_cnid, &[0xAA; 8]);
7944 let cursor = std::io::Cursor::new(img);
7945 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
7946 fs.prepare_for_edit().unwrap();
7947
7948 let root = fs.root().unwrap();
7949 let mut data = std::io::Cursor::new(b"x".as_slice());
7950 let entry = fs
7951 .create_file(
7952 &root,
7953 "with-xattr.txt",
7954 &mut data,
7955 1,
7956 &CreateFileOptions::default(),
7957 )
7958 .expect("create_file");
7959 let cnid = entry.location as u32;
7960
7961 let pairs: [(&str, &[u8]); 3] = [
7962 ("com.apple.FinderInfo", &[1u8; 32]),
7963 ("com.apple.metadata:_kMDItemUserTags", b"\x00bplist00 tag"),
7964 ("user.short", b"hi"),
7965 ];
7966 for (name, value) in pairs.iter() {
7967 fs.set_xattr(cnid, name, value).expect("set_xattr");
7968 }
7969 fs.sync_metadata().unwrap();
7970
7971 let img2 = fs.reader.into_inner();
7973 let mut cursor2 = std::io::Cursor::new(img2);
7974 let mut fs2 = HfsPlusFilesystem::open(&mut cursor2, 0).unwrap();
7975 let mut got = fs2.list_xattrs(cnid).expect("list_xattrs");
7976 got.sort_by(|a, b| a.name.cmp(&b.name));
7977 let mut want: Vec<(String, Vec<u8>)> = pairs
7978 .iter()
7979 .map(|(n, v)| (n.to_string(), v.to_vec()))
7980 .collect();
7981 want.sort_by(|a, b| a.0.cmp(&b.0));
7982 assert_eq!(got.len(), want.len());
7983 for (rec, (wname, wval)) in got.iter().zip(want.iter()) {
7984 assert_eq!(rec.name, *wname);
7985 match &rec.kind {
7986 XattrKind::Inline(bytes) => assert_eq!(bytes, wval),
7987 other => panic!("expected Inline xattr, got {other:?}"),
7988 }
7989 }
7990
7991 let sentinel = fs2
7993 .list_xattrs(sentinel_cnid)
7994 .expect("sentinel list_xattrs");
7995 assert_eq!(sentinel.len(), 1, "sentinel record should survive");
7996
7997 fs2.prepare_for_edit().unwrap();
7999 let root2 = fs2.root().unwrap();
8000 let listing = fs2.list_directory(&root2).unwrap();
8001 let target = listing
8002 .iter()
8003 .find(|e| e.name == "with-xattr.txt")
8004 .cloned()
8005 .expect("entry on reopened volume");
8006 fs2.delete_entry(&root2, &target).expect("delete");
8007 let after = fs2.list_xattrs(cnid).expect("post-delete list_xattrs");
8008 assert!(
8009 after.is_empty(),
8010 "expected 0 records after delete, got {}",
8011 after.len()
8012 );
8013 let sentinel_after = fs2
8014 .list_xattrs(sentinel_cnid)
8015 .expect("sentinel post-delete");
8016 assert_eq!(sentinel_after.len(), 1, "sentinel must survive delete");
8017 }
8018
8019 #[test]
8020 fn test_remove_xattr_not_found_and_cleanup() {
8021 let cnid = 17u32;
8024 let img = make_editable_hfsplus_image_with_inline_xattr(cnid, &[0u8; 16]);
8025 let cursor = std::io::Cursor::new(img);
8026 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
8027 fs.prepare_for_edit().unwrap();
8028 assert!(fs.xattr_cnids.as_ref().unwrap().contains(&cnid));
8029
8030 let err = fs
8031 .remove_xattr(cnid, "com.apple.does-not-exist")
8032 .expect_err("missing xattr must error");
8033 assert!(matches!(err, FilesystemError::NotFound(_)));
8034
8035 fs.remove_xattr(cnid, "com.apple.FinderInfo")
8036 .expect("remove existing xattr");
8037 assert!(
8038 !fs.xattr_cnids.as_ref().unwrap().contains(&cnid),
8039 "cnid should be dropped from cache once its last xattr is gone"
8040 );
8041 }
8042
8043 #[test]
8044 fn test_list_xattrs_round_trips_inline_finder_info() {
8045 let cnid = 17u32;
8049 let finder_info: Vec<u8> = (0..32u8).collect();
8050 let img = make_editable_hfsplus_image_with_inline_xattr(cnid, &finder_info);
8051 let cursor = std::io::Cursor::new(img);
8052 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
8053
8054 let recs = fs.list_xattrs(cnid).expect("list_xattrs cnid=17");
8055 assert_eq!(recs.len(), 1, "expected exactly one xattr record");
8056 let rec = &recs[0];
8057 assert_eq!(rec.name, "com.apple.FinderInfo");
8058 assert_eq!(rec.start_block, 0);
8059 match &rec.kind {
8060 XattrKind::Inline(bytes) => assert_eq!(bytes, &finder_info),
8061 other => panic!("expected Inline xattr, got {other:?}"),
8062 }
8063
8064 let none = fs.list_xattrs(99).expect("list_xattrs cnid=99");
8066 assert!(none.is_empty(), "expected no xattrs for cnid=99");
8067
8068 let bare = make_editable_hfsplus_image();
8070 let mut bare_fs = HfsPlusFilesystem::open(std::io::Cursor::new(bare), 0).unwrap();
8071 let empty = bare_fs.list_xattrs(cnid).expect("list_xattrs no attr file");
8072 assert!(empty.is_empty(), "no attributes file → no records");
8073 }
8074
8075 #[test]
8076 fn test_hardlink_resolution_two_links_one_inode() {
8077 const PAYLOAD: &[u8] = b"INODE_PAYLOAD_42\n";
8082 const I_NODE_NUM: u32 = 99;
8083
8084 let mut img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "Links", false);
8085
8086 let inode_cnid: u32;
8092 {
8093 let cursor = std::io::Cursor::new(&mut img);
8094 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("open blank");
8095 fs.prepare_for_edit().expect("prepare_for_edit");
8096 let root = fs.root().expect("root");
8097
8098 let private_dir = fs
8099 .create_directory(
8100 &root,
8101 &hfsplus_private_dir_name(),
8102 &CreateDirectoryOptions::default(),
8103 )
8104 .expect("create private dir");
8105
8106 let mut inode_data = std::io::Cursor::new(PAYLOAD);
8107 let inode_entry = fs
8108 .create_file(
8109 &private_dir,
8110 &format!("iNode{I_NODE_NUM}"),
8111 &mut inode_data,
8112 PAYLOAD.len() as u64,
8113 &CreateFileOptions::default(),
8114 )
8115 .expect("create inode file");
8116 inode_cnid = inode_entry.location as u32;
8117
8118 for link_name in ["linkA", "linkB"] {
8119 let mut empty = std::io::Cursor::new(&[][..]);
8120 fs.create_file(
8121 &root,
8122 link_name,
8123 &mut empty,
8124 0,
8125 &CreateFileOptions {
8126 os_type: Some(*b"hlnk"),
8127 os_creator: Some(*b"hfs+"),
8128 ..Default::default()
8129 },
8130 )
8131 .expect("create link stub");
8132
8133 let (_node, _rec, abs) = fs
8136 .find_catalog_record(2, link_name)
8137 .expect("link stub in catalog");
8138 let key_len = BigEndian::read_u16(&fs.catalog_data[abs..abs + 2]) as usize;
8139 let mut body_start = abs + 2 + key_len;
8140 if !body_start.is_multiple_of(2) {
8141 body_start += 1;
8142 }
8143 BigEndian::write_u32(
8144 &mut fs.catalog_data[body_start + 44..body_start + 48],
8145 I_NODE_NUM,
8146 );
8147 }
8148
8149 fs.sync_metadata().expect("sync");
8150 }
8151
8152 let cursor = std::io::Cursor::new(&mut img);
8155 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("re-open");
8156 let root = fs.root().expect("root");
8157 let entries = fs.list_directory(&root).expect("list root");
8158 let link_a = entries
8159 .iter()
8160 .find(|e| e.name == "linkA")
8161 .cloned()
8162 .expect("linkA");
8163 let link_b = entries
8164 .iter()
8165 .find(|e| e.name == "linkB")
8166 .cloned()
8167 .expect("linkB");
8168
8169 assert_eq!(
8170 link_a.link_target_cnid,
8171 Some(inode_cnid as u64),
8172 "linkA must resolve to inode CNID"
8173 );
8174 assert_eq!(
8175 link_b.link_target_cnid,
8176 Some(inode_cnid as u64),
8177 "linkB must resolve to inode CNID"
8178 );
8179 assert_eq!(link_a.size, PAYLOAD.len() as u64);
8182 assert_eq!(link_b.size, PAYLOAD.len() as u64);
8183
8184 let bytes_a = fs.read_file(&link_a, usize::MAX).expect("read linkA");
8185 let bytes_b = fs.read_file(&link_b, usize::MAX).expect("read linkB");
8186 assert_eq!(bytes_a, PAYLOAD);
8187 assert_eq!(bytes_b, PAYLOAD);
8188
8189 let private = entries
8193 .iter()
8194 .find(|e| e.name == hfsplus_private_dir_name())
8195 .expect("private dir at root");
8196 assert!(private.link_target_cnid.is_none());
8197 }
8198
8199 #[test]
8200 fn test_hardlink_delete_decrements_then_frees_inode() {
8201 const PAYLOAD: &[u8] = b"INODE_DATA_FOR_DELETE\n";
8205 const I_NODE_NUM: u32 = 7;
8206
8207 let mut img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "DelLinks", false);
8208
8209 let inode_cnid: u32;
8212 let private_cnid: u32;
8213 let payload_blocks_used: u32;
8214 let free_after_build: u32;
8215 let free_with_inode: u32;
8216 {
8217 let cursor = std::io::Cursor::new(&mut img);
8218 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("open blank");
8219 fs.prepare_for_edit().expect("prepare_for_edit");
8220 free_after_build = fs.vh.free_blocks;
8221 let root = fs.root().expect("root");
8222 let private_dir = fs
8223 .create_directory(
8224 &root,
8225 &hfsplus_private_dir_name(),
8226 &CreateDirectoryOptions::default(),
8227 )
8228 .expect("create private dir");
8229 private_cnid = private_dir.location as u32;
8230
8231 let mut inode_data = std::io::Cursor::new(PAYLOAD);
8232 let inode_entry = fs
8233 .create_file(
8234 &private_dir,
8235 &format!("iNode{I_NODE_NUM}"),
8236 &mut inode_data,
8237 PAYLOAD.len() as u64,
8238 &CreateFileOptions::default(),
8239 )
8240 .expect("create inode file");
8241 inode_cnid = inode_entry.location as u32;
8242 free_with_inode = fs.vh.free_blocks;
8243 payload_blocks_used = free_after_build - free_with_inode;
8244 assert!(
8245 payload_blocks_used >= 1,
8246 "inode payload must consume at least one block"
8247 );
8248
8249 {
8251 let (_, _, abs) = fs
8252 .find_catalog_record(private_cnid, &format!("iNode{I_NODE_NUM}"))
8253 .expect("inode catalog record");
8254 let key_len = BigEndian::read_u16(&fs.catalog_data[abs..abs + 2]) as usize;
8255 let mut body = abs + 2 + key_len;
8256 if !body.is_multiple_of(2) {
8257 body += 1;
8258 }
8259 BigEndian::write_u32(&mut fs.catalog_data[body + 44..body + 48], 2);
8260 }
8261
8262 for link_name in ["linkA", "linkB"] {
8263 let mut empty = std::io::Cursor::new(&[][..]);
8264 fs.create_file(
8265 &root,
8266 link_name,
8267 &mut empty,
8268 0,
8269 &CreateFileOptions {
8270 os_type: Some(*b"hlnk"),
8271 os_creator: Some(*b"hfs+"),
8272 ..Default::default()
8273 },
8274 )
8275 .expect("create stub");
8276 let (_, _, abs) = fs.find_catalog_record(2, link_name).expect("stub");
8277 let key_len = BigEndian::read_u16(&fs.catalog_data[abs..abs + 2]) as usize;
8278 let mut body = abs + 2 + key_len;
8279 if !body.is_multiple_of(2) {
8280 body += 1;
8281 }
8282 BigEndian::write_u32(&mut fs.catalog_data[body + 44..body + 48], I_NODE_NUM);
8283 }
8284 fs.sync_metadata().expect("sync");
8285 }
8286
8287 let cursor = std::io::Cursor::new(&mut img);
8290 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("re-open");
8291 fs.prepare_for_edit().expect("prepare_for_edit");
8292 let root = fs.root().expect("root");
8293 let entries = fs.list_directory(&root).expect("list");
8294 let link_a = entries
8295 .iter()
8296 .find(|e| e.name == "linkA")
8297 .cloned()
8298 .expect("linkA");
8299 let link_b = entries
8300 .iter()
8301 .find(|e| e.name == "linkB")
8302 .cloned()
8303 .expect("linkB");
8304 let free_before_delete = fs.vh.free_blocks;
8305
8306 fs.delete_entry(&root, &link_a).expect("delete linkA");
8307 let cnt_after_first = {
8309 let (_, _, abs) = fs
8310 .find_catalog_record(private_cnid, &format!("iNode{I_NODE_NUM}"))
8311 .expect("inode still present");
8312 let key_len = BigEndian::read_u16(&fs.catalog_data[abs..abs + 2]) as usize;
8313 let mut body = abs + 2 + key_len;
8314 if !body.is_multiple_of(2) {
8315 body += 1;
8316 }
8317 BigEndian::read_u32(&fs.catalog_data[body + 44..body + 48])
8318 };
8319 assert_eq!(cnt_after_first, 1, "linkCount should decrement to 1");
8320 assert_eq!(
8321 fs.vh.free_blocks, free_before_delete,
8322 "no blocks freed while inode still has refs"
8323 );
8324 let bytes_b = fs.read_file(&link_b, usize::MAX).expect("read linkB");
8326 assert_eq!(bytes_b, PAYLOAD);
8327
8328 fs.delete_entry(&root, &link_b).expect("delete linkB");
8329 let private_children = fs.list_children(private_cnid).expect("list private dir");
8331 assert!(
8332 private_children.is_empty(),
8333 "iNode entry should be removed once linkCount hits 0; got {} children",
8334 private_children.len()
8335 );
8336 assert!(
8337 fs.find_file_by_id(inode_cnid).is_none(),
8338 "inode catalog row should be gone"
8339 );
8340 assert_eq!(
8341 fs.vh.free_blocks,
8342 free_before_delete + payload_blocks_used,
8343 "inode payload blocks should be reclaimed"
8344 );
8345 }
8346
8347 #[test]
8348 fn test_hardlink_capture_and_replay_dedupes_inode() {
8349 const PAYLOAD: &[u8] = b"shared inode contents (3 names)\n";
8356 const I_NODE_NUM: u32 = 17;
8357
8358 let mut source_img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "Src", false);
8360 let source_inode_cnid: u32;
8361 {
8362 let cursor = std::io::Cursor::new(&mut source_img);
8363 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("open src blank");
8364 fs.prepare_for_edit().expect("prepare_for_edit");
8365 let mut data = std::io::Cursor::new(PAYLOAD);
8366 let inode = fs
8367 .create_hardlink_inode(
8368 I_NODE_NUM,
8369 &mut data,
8370 PAYLOAD.len() as u64,
8371 &CreateFileOptions::default(),
8372 )
8373 .expect("create source inode");
8374 source_inode_cnid = inode.location as u32;
8375 let private_cnid = fs.find_private_dir_cnid().unwrap().expect("private dir");
8378 let _ = fs
8379 .adjust_inode_link_count(private_cnid, I_NODE_NUM, 0)
8380 .expect("init linkCount");
8381 let root = fs.root().expect("root");
8382 for name in ["a.txt", "b.txt", "c.txt"] {
8383 fs.create_hardlink(&root, name, I_NODE_NUM)
8384 .expect("create stub");
8385 }
8386 fs.sync_metadata().expect("sync src");
8387 }
8388
8389 let cursor = std::io::Cursor::new(&mut source_img);
8391 let mut src_fs = HfsPlusFilesystem::open(cursor, 0).expect("re-open src");
8392 let snap =
8393 crate::hfsplus_clone::SourceCatalogSnapshot::capture(&mut src_fs).expect("capture");
8394
8395 let inodes: Vec<_> = snap.files.iter().filter(|f| f.is_inode).collect();
8397 assert_eq!(inodes.len(), 1, "expected exactly one inode in snapshot");
8398 let inode = inodes[0];
8399 assert_eq!(inode.cnid, source_inode_cnid);
8400 assert_eq!(inode.inode_num, Some(I_NODE_NUM));
8401 assert_eq!(inode.data_fork_size, PAYLOAD.len() as u64);
8402 let inode_payload = {
8404 use std::io::Read;
8405 let mut buf = Vec::new();
8406 src_fs
8407 .fork_stream_reader(inode.cnid, 0x00, &inode.data_fork_raw)
8408 .expect("fork reader")
8409 .read_to_end(&mut buf)
8410 .expect("read fork");
8411 buf
8412 };
8413 assert_eq!(inode_payload, PAYLOAD);
8414
8415 let user_files: Vec<_> = snap.files.iter().filter(|f| !f.is_inode).collect();
8416 assert!(
8417 user_files.is_empty(),
8418 "user-tree files should be empty (the three stubs are in `hardlinks`); \
8419 got {:?}",
8420 user_files.iter().map(|f| &f.name).collect::<Vec<_>>()
8421 );
8422
8423 assert_eq!(snap.hardlinks.len(), 3, "expected 3 hardlink stubs");
8424 for h in &snap.hardlinks {
8425 assert_eq!(h.inode_num, I_NODE_NUM);
8426 assert_eq!(h.inode_source_cnid, source_inode_cnid);
8427 assert_eq!(h.parent_cnid, 2, "stubs live at root");
8428 }
8429
8430 let mut target_img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "Tgt", false);
8432 let target_inode_cnid: u32;
8433 {
8434 let cursor = std::io::Cursor::new(&mut target_img);
8435 let mut tgt = HfsPlusFilesystem::open(cursor, 0).expect("open tgt blank");
8436 tgt.prepare_for_edit().expect("prepare_for_edit");
8437
8438 let mut data = std::io::Cursor::new(&inode_payload);
8439 let target_inode = tgt
8440 .create_hardlink_inode(
8441 inode.inode_num.unwrap(),
8442 &mut data,
8443 inode.data_fork_size,
8444 &CreateFileOptions::default(),
8445 )
8446 .expect("replay inode");
8447 target_inode_cnid = target_inode.location as u32;
8448
8449 let root = tgt.root().expect("tgt root");
8450 for h in &snap.hardlinks {
8451 tgt.create_hardlink(&root, &h.name, h.inode_num)
8452 .expect("replay stub");
8453 }
8454 tgt.sync_metadata().expect("sync tgt");
8455 }
8456
8457 let cursor = std::io::Cursor::new(&mut target_img);
8459 let mut tgt_fs = HfsPlusFilesystem::open(cursor, 0).expect("re-open tgt");
8460 let root = tgt_fs.root().expect("tgt root");
8461 let entries = tgt_fs.list_directory(&root).expect("list tgt root");
8462
8463 let stubs: Vec<&FileEntry> = entries
8466 .iter()
8467 .filter(|e| ["a.txt", "b.txt", "c.txt"].contains(&e.name.as_str()))
8468 .collect();
8469 assert_eq!(stubs.len(), 3);
8470 for stub in &stubs {
8471 assert_eq!(
8472 stub.link_target_cnid,
8473 Some(target_inode_cnid as u64),
8474 "stub {} must resolve to the single target inode",
8475 stub.name
8476 );
8477 assert_eq!(stub.size, PAYLOAD.len() as u64);
8478 }
8479 for stub in &stubs {
8480 let bytes = tgt_fs.read_file(stub, usize::MAX).expect("read tgt stub");
8481 assert_eq!(bytes, PAYLOAD);
8482 }
8483
8484 let private_cnid = tgt_fs
8486 .find_private_dir_cnid()
8487 .unwrap()
8488 .expect("private dir");
8489 let inode_name = format!("iNode{I_NODE_NUM}");
8490 let (_, _, abs) = tgt_fs
8491 .find_catalog_record(private_cnid, &inode_name)
8492 .expect("inode in private dir");
8493 let key_len = BigEndian::read_u16(&tgt_fs.catalog_data[abs..abs + 2]) as usize;
8494 let mut body = abs + 2 + key_len;
8495 if !body.is_multiple_of(2) {
8496 body += 1;
8497 }
8498 let link_count = BigEndian::read_u32(&tgt_fs.catalog_data[body + 44..body + 48]);
8499 assert_eq!(link_count, 3, "inode linkCount must equal stub count");
8500 }
8501
8502 #[test]
8503 fn test_dir_hardlink_capture_and_replay_resolves_to_inode() {
8504 const PAYLOAD: &[u8] = b"file inside dir-hardlinked folder\n";
8511 const D_INODE_NUM: u32 = 13;
8512
8513 let mut source_img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "DhSrc", false);
8515 let source_inode_cnid: u32;
8516 {
8517 let cursor = std::io::Cursor::new(&mut source_img);
8518 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("open src");
8519 fs.prepare_for_edit().expect("prepare_for_edit");
8520 let inode_dir = fs
8521 .create_dir_hardlink_inode(D_INODE_NUM, &CreateDirectoryOptions::default())
8522 .expect("create dir inode");
8523 source_inode_cnid = inode_dir.location as u32;
8524 let mut data = std::io::Cursor::new(PAYLOAD);
8527 fs.create_file(
8528 &inode_dir,
8529 "inside.txt",
8530 &mut data,
8531 PAYLOAD.len() as u64,
8532 &CreateFileOptions::default(),
8533 )
8534 .expect("create file in dir inode");
8535 let dir_priv = fs
8537 .find_dir_private_dir_cnid()
8538 .unwrap()
8539 .expect("dir private");
8540 let _ = fs
8541 .adjust_dir_inode_link_count(dir_priv, D_INODE_NUM, 0)
8542 .expect("init dir linkCount");
8543 let root = fs.root().expect("root");
8544 for name in ["alpha", "beta"] {
8545 fs.create_dir_hardlink(&root, name, D_INODE_NUM)
8546 .expect("create dir hardlink stub");
8547 }
8548 fs.sync_metadata().expect("sync src");
8549 }
8550
8551 let cursor = std::io::Cursor::new(&mut source_img);
8553 let mut src_fs = HfsPlusFilesystem::open(cursor, 0).expect("re-open src");
8554 let snap =
8555 crate::hfsplus_clone::SourceCatalogSnapshot::capture(&mut src_fs).expect("capture");
8556
8557 let dir_inodes: Vec<_> = snap.dirs.iter().filter(|d| d.is_dir_inode).collect();
8559 assert_eq!(
8560 dir_inodes.len(),
8561 1,
8562 "expected exactly one dir inode in snapshot; got {}",
8563 dir_inodes.len()
8564 );
8565 let dir_inode = dir_inodes[0];
8566 assert_eq!(dir_inode.cnid, source_inode_cnid);
8567 assert_eq!(dir_inode.inode_num, Some(D_INODE_NUM));
8568
8569 assert_eq!(snap.dir_hardlinks.len(), 2, "expected 2 dir-hardlink stubs");
8570 for h in &snap.dir_hardlinks {
8571 assert_eq!(h.inode_num, D_INODE_NUM);
8572 assert_eq!(h.inode_dir_source_cnid, source_inode_cnid);
8573 assert_eq!(h.parent_cnid, 2, "stubs live at root");
8574 }
8575
8576 let mut target_img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "DhTgt", false);
8578 let target_inode_cnid: u32;
8579 {
8580 let cursor = std::io::Cursor::new(&mut target_img);
8581 let mut tgt = HfsPlusFilesystem::open(cursor, 0).expect("open tgt");
8582 tgt.prepare_for_edit().expect("prepare_for_edit");
8583
8584 let inode = tgt
8586 .create_dir_hardlink_inode(
8587 dir_inode.inode_num.unwrap(),
8588 &CreateDirectoryOptions::default(),
8589 )
8590 .expect("replay dir inode");
8591 target_inode_cnid = inode.location as u32;
8592
8593 let mut data = std::io::Cursor::new(PAYLOAD);
8596 tgt.create_file(
8597 &inode,
8598 "inside.txt",
8599 &mut data,
8600 PAYLOAD.len() as u64,
8601 &CreateFileOptions::default(),
8602 )
8603 .expect("replay child file");
8604
8605 let root = tgt.root().expect("tgt root");
8607 for h in &snap.dir_hardlinks {
8608 tgt.create_dir_hardlink(&root, &h.name, h.inode_num)
8609 .expect("replay dir stub");
8610 }
8611 tgt.sync_metadata().expect("sync tgt");
8612 }
8613
8614 let cursor = std::io::Cursor::new(&mut target_img);
8616 let mut tgt_fs = HfsPlusFilesystem::open(cursor, 0).expect("re-open tgt");
8617 let root = tgt_fs.root().expect("tgt root");
8618 let entries = tgt_fs.list_directory(&root).expect("list tgt root");
8619
8620 for stub_name in ["alpha", "beta"] {
8623 let stub = entries
8624 .iter()
8625 .find(|e| e.name == stub_name)
8626 .cloned()
8627 .unwrap_or_else(|| panic!("stub {stub_name} missing from target root"));
8628 assert!(
8629 stub.is_directory(),
8630 "stub {stub_name} must surface as a directory"
8631 );
8632 assert_eq!(
8633 stub.link_target_cnid,
8634 Some(target_inode_cnid as u64),
8635 "stub {stub_name} must resolve to the single target inode"
8636 );
8637
8638 let children = tgt_fs.list_directory(&stub).expect("list stub");
8639 let inside = children
8640 .iter()
8641 .find(|c| c.name == "inside.txt")
8642 .unwrap_or_else(|| panic!("inside.txt missing under {stub_name}"));
8643 let bytes = tgt_fs
8644 .read_file(inside, usize::MAX)
8645 .expect("read inside.txt via stub");
8646 assert_eq!(bytes, PAYLOAD);
8647 }
8648
8649 let dir_priv = tgt_fs
8651 .find_dir_private_dir_cnid()
8652 .unwrap()
8653 .expect("tgt dir private");
8654 let inode_name = format!("dir_{D_INODE_NUM}");
8655 let (_, _, abs) = tgt_fs
8656 .find_catalog_record(dir_priv, &inode_name)
8657 .expect("dir inode in private dir");
8658 let key_len = BigEndian::read_u16(&tgt_fs.catalog_data[abs..abs + 2]) as usize;
8659 let mut body = abs + 2 + key_len;
8660 if !body.is_multiple_of(2) {
8661 body += 1;
8662 }
8663 let link_count = BigEndian::read_u32(&tgt_fs.catalog_data[body + 44..body + 48]);
8664 assert_eq!(link_count, 2, "dir inode linkCount must equal stub count");
8665 }
8666
8667 #[test]
8668 fn test_hfsx_case_sensitive_allows_distinct_case_siblings() {
8669 const PAYLOAD: &[u8] = b"hi\n";
8675
8676 {
8678 let mut img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "HFSX", true);
8679 let cursor = std::io::Cursor::new(&mut img);
8680 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("open hfsx");
8681 assert!(
8682 fs.case_sensitive(),
8683 "fresh HFSX blank must be case-sensitive"
8684 );
8685 fs.prepare_for_edit().expect("prepare");
8686 let root = fs.root().expect("root");
8687
8688 let mut data = std::io::Cursor::new(PAYLOAD);
8689 fs.create_file(
8690 &root,
8691 "Foo",
8692 &mut data,
8693 PAYLOAD.len() as u64,
8694 &CreateFileOptions::default(),
8695 )
8696 .expect("create Foo");
8697
8698 let mut data = std::io::Cursor::new(PAYLOAD);
8699 fs.create_file(
8700 &root,
8701 "foo",
8702 &mut data,
8703 PAYLOAD.len() as u64,
8704 &CreateFileOptions::default(),
8705 )
8706 .expect("create foo on HFSX must succeed");
8707
8708 fs.sync_metadata().expect("sync");
8709 let entries = fs.list_directory(&root).expect("list");
8711 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
8712 assert!(names.contains(&"Foo"), "Foo missing: {names:?}");
8713 assert!(names.contains(&"foo"), "foo missing: {names:?}");
8714 }
8715
8716 {
8718 let mut img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "HFSPLUS", false);
8719 let cursor = std::io::Cursor::new(&mut img);
8720 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("open hfs+");
8721 assert!(
8722 !fs.case_sensitive(),
8723 "plain HFS+ blank must be case-folding"
8724 );
8725 fs.prepare_for_edit().expect("prepare");
8726 let root = fs.root().expect("root");
8727
8728 let mut data = std::io::Cursor::new(PAYLOAD);
8729 fs.create_file(
8730 &root,
8731 "Foo",
8732 &mut data,
8733 PAYLOAD.len() as u64,
8734 &CreateFileOptions::default(),
8735 )
8736 .expect("create Foo");
8737
8738 let mut data = std::io::Cursor::new(PAYLOAD);
8739 let err = fs
8740 .create_file(
8741 &root,
8742 "foo",
8743 &mut data,
8744 PAYLOAD.len() as u64,
8745 &CreateFileOptions::default(),
8746 )
8747 .expect_err("create foo must fail on case-folding HFS+");
8748 match err {
8749 FilesystemError::AlreadyExists(_) => {}
8750 other => panic!("expected AlreadyExists, got {other:?}"),
8751 }
8752 }
8753 }
8754
8755 #[test]
8756 fn test_clone_capture_carries_xattrs_byte_equal() {
8757 let cnid = 99u32;
8767 let seed_value: Vec<u8> = (0..32u8).collect();
8768 let extra_value: Vec<u8> = b"\x00bplist00 demo".to_vec();
8769 let img = make_editable_hfsplus_image_with_inline_xattr(cnid, &seed_value);
8770
8771 let cursor = std::io::Cursor::new(img);
8774 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
8775 fs.prepare_for_edit().unwrap();
8776 fs.set_xattr(cnid, "com.apple.metadata:demo", &extra_value)
8777 .expect("set extra xattr on sentinel cnid");
8778 fs.set_xattr(2u32, "com.apple.FinderInfo", &seed_value)
8779 .expect("set xattr on root dir");
8780 let root = fs.root().expect("root");
8785 let mut data = std::io::Cursor::new(b"hi".as_slice());
8786 fs.create_file(
8787 &root,
8788 "carrier.txt",
8789 &mut data,
8790 2,
8791 &CreateFileOptions::default(),
8792 )
8793 .expect("create_file");
8794 fs.sync_metadata().unwrap();
8795 let img2 = fs.reader.into_inner();
8796
8797 let mut cursor2 = std::io::Cursor::new(img2);
8798 let mut fs2 = HfsPlusFilesystem::open(&mut cursor2, 0).unwrap();
8799 let snap = crate::hfsplus_clone::SourceCatalogSnapshot::capture(&mut fs2).expect("capture");
8800
8801 let carrier = snap
8804 .files
8805 .iter()
8806 .find(|f| f.name == "carrier.txt")
8807 .expect("carrier file in snapshot");
8808 assert!(
8809 carrier.xattrs.is_empty(),
8810 "fresh file should have no xattrs, got {}",
8811 carrier.xattrs.len()
8812 );
8813
8814 let root_dir = snap
8816 .dirs
8817 .iter()
8818 .find(|d| d.cnid == 2)
8819 .expect("root dir in snapshot");
8820 assert_eq!(root_dir.xattrs.len(), 1, "expected 1 xattr on root dir");
8821 assert_eq!(root_dir.xattrs[0].name, "com.apple.FinderInfo");
8822 match &root_dir.xattrs[0].kind {
8823 XattrKind::Inline(bytes) => assert_eq!(bytes, &seed_value),
8824 other => panic!("expected inline FinderInfo on root, got {other:?}"),
8825 }
8826
8827 let mut sentinel = fs2.list_xattrs(cnid).expect("list_xattrs sentinel");
8831 sentinel.sort_by(|a, b| a.name.cmp(&b.name));
8832 assert_eq!(sentinel.len(), 2, "sentinel should carry 2 xattrs");
8833 let mut want: Vec<(String, Vec<u8>)> = vec![
8834 ("com.apple.FinderInfo".into(), seed_value.clone()),
8835 ("com.apple.metadata:demo".into(), extra_value.clone()),
8836 ];
8837 want.sort_by(|a, b| a.0.cmp(&b.0));
8838 for (rec, (wn, wv)) in sentinel.iter().zip(want.iter()) {
8839 assert_eq!(rec.name, *wn);
8840 match &rec.kind {
8841 XattrKind::Inline(bytes) => assert_eq!(bytes, wv),
8842 other => panic!("expected inline, got {other:?}"),
8843 }
8844 }
8845 }
8846
8847 #[test]
8848 fn test_hfsplus_editable_open_and_free_space() {
8849 let img = make_editable_hfsplus_image();
8850 let cursor = std::io::Cursor::new(img);
8851 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
8852 assert_eq!(fs.fs_type(), "HFS+");
8853 let free = fs.free_space().unwrap();
8854 assert!(free > 0);
8855 assert_eq!(free, 250 * 4096);
8857 }
8858
8859 #[test]
8860 fn test_hfsplus_defragmented_minimum_size() {
8861 let img = make_editable_hfsplus_image();
8864 let cursor = std::io::Cursor::new(img);
8865 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
8866
8867 let total = fs.total_size();
8868 let in_place = fs.last_data_byte().unwrap();
8869 let defrag = fs.defragmented_minimum_size().unwrap();
8870
8871 assert_eq!(total, 256 * 4096);
8872 assert_eq!(in_place, 7 * 4096);
8875 assert_eq!(defrag, 19 * 4096);
8879 assert!(defrag <= total);
8880 }
8881
8882 #[test]
8884 fn last_data_byte_counts_the_block_before_the_alternate_header() {
8885 let mut img = make_editable_hfsplus_image();
8886 let block_size = 4096usize;
8887 let total = 256u32;
8888 let b = total - 2;
8890 img[block_size + (b / 8) as usize] |= 0x80 >> (b % 8);
8891 let mut fs = HfsPlusFilesystem::open(std::io::Cursor::new(img), 0).unwrap();
8892 assert_eq!(fs.last_data_byte().unwrap(), 256 * 4096);
8893 }
8894
8895 #[test]
8896 fn test_hfsplus_defragmented_min_smaller_on_fragmented() {
8897 let mut img = make_editable_hfsplus_image();
8902 let block_size = 4096usize;
8903 let bitmap_byte = block_size + 25; img[bitmap_byte] |= 0b1000_0000;
8907 let free_off = 1024 + 48;
8910 let free = BigEndian::read_u32(&img[free_off..free_off + 4]);
8911 BigEndian::write_u32(&mut img[free_off..free_off + 4], free - 1);
8912 let image_size = img.len();
8914 let alt_free_off = image_size - 1024 + 48;
8915 BigEndian::write_u32(&mut img[alt_free_off..alt_free_off + 4], free - 1);
8916
8917 let cursor = std::io::Cursor::new(img);
8918 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
8919
8920 let in_place = fs.last_data_byte().unwrap();
8921 let defrag = fs.defragmented_minimum_size().unwrap();
8922
8923 assert_eq!(in_place, 202 * 4096);
8926 assert_eq!(defrag, 20 * 4096);
8929 assert!(defrag < in_place);
8930 }
8931
8932 #[test]
8933 fn test_hfsplus_editable_create_file_and_read() {
8934 let img = make_editable_hfsplus_image();
8935 let cursor = std::io::Cursor::new(img);
8936 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
8937
8938 let root = fs.root().unwrap();
8939 let test_data = b"Hello, HFS+ World!";
8940 let mut data_reader = std::io::Cursor::new(test_data.as_slice());
8941
8942 let options = CreateFileOptions::default();
8943 let fe = fs
8944 .create_file(
8945 &root,
8946 "test.txt",
8947 &mut data_reader,
8948 test_data.len() as u64,
8949 &options,
8950 )
8951 .unwrap();
8952
8953 assert_eq!(fe.name, "test.txt");
8954 assert_eq!(fe.size, test_data.len() as u64);
8955
8956 let entries = fs.list_directory(&root).unwrap();
8958 assert!(entries.iter().any(|e| e.name == "test.txt"));
8959
8960 let read_back = fs.read_file(&fe, 1024).unwrap();
8962 assert_eq!(&read_back, test_data);
8963 }
8964
8965 #[test]
8966 fn test_hfsplus_editable_create_directory() {
8967 let img = make_editable_hfsplus_image();
8968 let cursor = std::io::Cursor::new(img);
8969 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
8970
8971 let root = fs.root().unwrap();
8972 let options = CreateDirectoryOptions::default();
8973 let dir = fs.create_directory(&root, "TestDir", &options).unwrap();
8974
8975 assert_eq!(dir.name, "TestDir");
8976 assert!(dir.is_directory());
8977
8978 let entries = fs.list_directory(&root).unwrap();
8979 assert!(entries
8980 .iter()
8981 .any(|e| e.name == "TestDir" && e.is_directory()));
8982 }
8983
8984 #[test]
8985 fn test_hfsplus_editable_delete_file() {
8986 let img = make_editable_hfsplus_image();
8987 let cursor = std::io::Cursor::new(img);
8988 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
8989
8990 let root = fs.root().unwrap();
8991 let test_data = b"delete me";
8992 let mut data_reader = std::io::Cursor::new(test_data.as_slice());
8993 let options = CreateFileOptions::default();
8994 let fe = fs
8995 .create_file(
8996 &root,
8997 "gone.txt",
8998 &mut data_reader,
8999 test_data.len() as u64,
9000 &options,
9001 )
9002 .unwrap();
9003
9004 let free_before = fs.free_space().unwrap();
9005
9006 fs.delete_entry(&root, &fe).unwrap();
9008
9009 let entries = fs.list_directory(&root).unwrap();
9011 assert!(!entries.iter().any(|e| e.name == "gone.txt"));
9012
9013 let free_after = fs.free_space().unwrap();
9015 assert!(free_after > free_before);
9016 }
9017
9018 #[test]
9019 fn test_hfsplus_editable_duplicate_name_rejected() {
9020 let img = make_editable_hfsplus_image();
9021 let cursor = std::io::Cursor::new(img);
9022 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
9023
9024 let root = fs.root().unwrap();
9025 let test_data = b"first";
9026 let mut r1 = std::io::Cursor::new(test_data.as_slice());
9027 let options = CreateFileOptions::default();
9028 fs.create_file(&root, "dup.txt", &mut r1, 5, &options)
9029 .unwrap();
9030
9031 let mut r2 = std::io::Cursor::new(test_data.as_slice());
9033 let result = fs.create_file(&root, "dup.txt", &mut r2, 5, &options);
9034 assert!(matches!(result, Err(FilesystemError::AlreadyExists(_))));
9035 }
9036
9037 #[test]
9038 fn test_hfsplus_editable_blessed_folder() {
9039 let img = make_editable_hfsplus_image();
9040 let cursor = std::io::Cursor::new(img);
9041 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
9042
9043 assert!(fs.blessed_system_folder().is_none());
9045
9046 let root = fs.root().unwrap();
9048 let options = CreateDirectoryOptions::default();
9049 let sys_dir = fs
9050 .create_directory(&root, "System Folder", &options)
9051 .unwrap();
9052
9053 fs.set_blessed_folder(&sys_dir).unwrap();
9055
9056 let blessed = fs.blessed_system_folder();
9058 assert!(blessed.is_some());
9059 let (cnid, name) = blessed.unwrap();
9060 assert_eq!(cnid, sys_dir.location);
9061 assert_eq!(name, "System Folder");
9062 }
9063
9064 #[test]
9065 fn test_hfsplus_editable_type_creator_auto_detect() {
9066 let img = make_editable_hfsplus_image();
9067 let cursor = std::io::Cursor::new(img);
9068 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
9069
9070 let root = fs.root().unwrap();
9071 let test_data = b"text content";
9072 let mut data_reader = std::io::Cursor::new(test_data.as_slice());
9073 let options = CreateFileOptions::default();
9074 let fe = fs
9075 .create_file(
9076 &root,
9077 "hello.txt",
9078 &mut data_reader,
9079 test_data.len() as u64,
9080 &options,
9081 )
9082 .unwrap();
9083
9084 assert!(fe.type_code.is_some());
9086 assert_eq!(fe.type_code, Some(*b"TEXT"));
9087 }
9088
9089 #[test]
9090 fn test_hfsplus_catalog_variable_index_keys_grow_multilevel() {
9091 use crate::hfs_common::{
9108 btree_find_record, btree_insert_full, BTreeHeader, BTreeKeyFormat,
9109 };
9110 use byteorder::{BigEndian, ByteOrder};
9111 type Fs = HfsPlusFilesystem<std::io::Cursor<Vec<u8>>>;
9112
9113 let node_size = 512usize;
9114 let total_nodes = 1800u32;
9117 let mut buf = vec![0u8; total_nodes as usize * node_size];
9118 write_blank_btree_header_node(
9119 &mut buf[0..node_size],
9120 node_size,
9121 0,
9122 total_nodes,
9123 total_nodes - 2,
9124 516,
9125 KEY_COMPARE_CASE_FOLDING,
9126 );
9127 write_empty_leaf_node(&mut buf[node_size..2 * node_size]);
9128
9129 let kf = BTreeKeyFormat::HFSPLUS_CATALOG;
9130 let cmp = |a: &[u8], b: &[u8]| Fs::catalog_compare(a, b, false);
9131
9132 const DIRS: u32 = 50;
9137 const N: u32 = 2500;
9138 let mut inserted: Vec<(u32, String)> = Vec::with_capacity(N as usize);
9139 for i in 0..N {
9140 let hash = i.wrapping_mul(2_654_435_761);
9141 let parent = 16 + (hash % DIRS); let name = format!("f{hash:08x}");
9143 let mut rec = Fs::build_catalog_key(parent, &name);
9144 rec.extend_from_slice(&[0u8; 40]);
9147 btree_insert_full(&mut buf, &rec, &kf, &cmp)
9148 .unwrap_or_else(|e| panic!("insert #{i} (parent {parent}, {name}): {e}"));
9149 inserted.push((parent, name));
9150 }
9151
9152 let header = BTreeHeader::read(&buf);
9154 assert!(
9155 header.depth >= 3,
9156 "catalog did not reach depth >= 3 (got {}); test needs a deeper tree to \
9157 exercise index-node splits",
9158 header.depth
9159 );
9160 assert_eq!(
9161 header.leaf_records, N,
9162 "leaf_records header count drifted from inserts"
9163 );
9164
9165 let mut keys: Vec<Vec<u8>> = Vec::with_capacity(N as usize);
9169 crate::hfs_common::walk_leaf_records::<(), _>(
9170 &buf,
9171 header.first_leaf_node,
9172 node_size,
9173 |_n, _r, _o, rec| {
9174 let kl = BigEndian::read_u16(&rec[0..2]) as usize;
9175 keys.push(rec[..(2 + kl).min(rec.len())].to_vec());
9176 None
9177 },
9178 );
9179 assert_eq!(
9180 keys.len(),
9181 N as usize,
9182 "leaf walk recovered {} of {N} records — chain is broken or has cycles",
9183 keys.len()
9184 );
9185 for w in keys.windows(2) {
9186 assert_eq!(
9187 cmp(&w[0], &w[1]),
9188 std::cmp::Ordering::Less,
9189 "catalog leaves are not strictly ascending — separator/descent corruption"
9190 );
9191 }
9192
9193 fn key_extract(rec: &[u8]) -> &[u8] {
9198 let kl = BigEndian::read_u16(&rec[0..2]) as usize;
9199 &rec[..(2 + kl).min(rec.len())]
9200 }
9201 for (parent, name) in &inserted {
9202 let search = Fs::build_catalog_key(*parent, name);
9203 let (found, _chain) = btree_find_record(&buf, &header, &search, &key_extract, &cmp);
9204 assert!(
9205 found.is_some(),
9206 "record (parent {parent}, {name}) not findable by descent — misrouted index"
9207 );
9208 }
9209 }
9210
9211 #[test]
9212 fn test_hfsplus_blank_sized_catalog_20k_inserts_fsck_clean() {
9213 use crate::hfs_common::BTreeHeader;
9221 type Fs = HfsPlusFilesystem<std::io::Cursor<Vec<u8>>>;
9222
9223 let img =
9226 create_blank_hfsplus_sized(64 * 1024 * 1024, 4096, "PutShuffle", false, 16 << 20, 0);
9227 let mut fs = Fs::open(std::io::Cursor::new(img), 0).unwrap();
9228 fs.prepare_for_edit().unwrap();
9229
9230 const DIRS: u32 = 50;
9232 let root = fs.root().unwrap();
9233 let mut dir_ids = Vec::with_capacity(DIRS as usize);
9234 for d in 0..DIRS {
9235 let de = fs
9236 .create_directory(&root, &format!("dir{d:03}"), &Default::default())
9237 .unwrap();
9238 dir_ids.push(de.location as u32);
9239 }
9240 let base_cnid = fs.vh.next_catalog_id;
9241
9242 let n: u32 = 20_000;
9250 let mut per_dir = vec![0i32; DIRS as usize];
9251 let empty_fork = ForkData::empty();
9252 for i in 0..n {
9253 let hash = i.wrapping_mul(2_654_435_761);
9254 let d = (hash % DIRS) as usize;
9255 let name = format!("f{hash:08x}");
9256 let mut kr = Fs::build_catalog_key(dir_ids[d], &name);
9257 if !kr.len().is_multiple_of(2) {
9258 kr.push(0);
9259 }
9260 kr.extend_from_slice(&Fs::build_file_record_with_dates(
9261 base_cnid + i,
9262 &empty_fork,
9263 &empty_fork,
9264 &[0u8; 4],
9265 &[0u8; 4],
9266 None,
9267 0,
9268 ));
9269 fs.insert_catalog_record(&kr)
9270 .unwrap_or_else(|e| panic!("insert #{i} into dir{d}: {e}"));
9271 per_dir[d] += 1;
9272 }
9273 fs.vh.file_count += n;
9276 fs.vh.next_catalog_id = base_cnid + n;
9277 for (d, &cnt) in per_dir.iter().enumerate() {
9278 fs.update_parent_valence(dir_ids[d], cnt).unwrap();
9279 }
9280
9281 let h = BTreeHeader::read(fs.catalog_data());
9283 assert!(
9284 h.depth >= 3,
9285 "catalog depth {} < 3 after 20k inserts",
9286 h.depth
9287 );
9288 let result = fs.fsck().unwrap().unwrap();
9292 assert!(
9293 result.errors.is_empty(),
9294 "fsck found {} errors after 20k inserts: {:?}",
9295 result.errors.len(),
9296 result
9297 .errors
9298 .iter()
9299 .take(8)
9300 .map(|e| &e.message)
9301 .collect::<Vec<_>>()
9302 );
9303 }
9304
9305 #[test]
9306 fn test_hfsplus_catalog_grows_map_nodes_past_cap_phase_c() {
9307 use crate::hfs_common::{btree_bitmap_segments, BTreeHeader};
9315 type Fs = HfsPlusFilesystem<std::io::Cursor<Vec<u8>>>;
9316 let node_size = 4096usize;
9317 let header_cap = ((node_size - 256) * 8) as u32; let min_catalog = (header_cap - 16) * node_size as u32;
9321 let img =
9322 create_blank_hfsplus_sized(180 * 1024 * 1024, 4096, "MapCap", false, min_catalog, 0);
9323 let mut fs = Fs::open(std::io::Cursor::new(img), 0).unwrap();
9324 fs.prepare_for_edit().unwrap();
9325
9326 let start = BTreeHeader::read(fs.catalog_data()).total_nodes;
9327 assert!(
9328 start <= header_cap && start > header_cap - 64,
9329 "blank catalog {start} not near cap"
9330 );
9331 assert_eq!(
9333 btree_bitmap_segments(fs.catalog_data(), node_size).len(),
9334 1,
9335 "fresh near-cap catalog should have no map nodes"
9336 );
9337
9338 let mut guard = 0;
9340 while BTreeHeader::read(fs.catalog_data()).total_nodes <= header_cap {
9341 fs.grow_btree_fork(BTreeFork::Catalog).unwrap();
9342 guard += 1;
9343 assert!(guard < 64, "too many grows to cross the cap");
9344 }
9345 let h = BTreeHeader::read(fs.catalog_data());
9346 assert!(
9347 h.total_nodes > header_cap,
9348 "did not cross the cap: {}",
9349 h.total_nodes
9350 );
9351 let segs = btree_bitmap_segments(fs.catalog_data(), node_size);
9353 assert!(
9354 segs.len() >= 2,
9355 "expected a map-node segment after crossing the cap, got {} segment(s)",
9356 segs.len()
9357 );
9358
9359 fs.sync_metadata().unwrap();
9361 let result = fs.fsck().unwrap().unwrap();
9362 assert!(
9363 result.errors.is_empty(),
9364 "fsck after map-node grow: {:?}",
9365 result
9366 .errors
9367 .iter()
9368 .take(8)
9369 .map(|e| &e.message)
9370 .collect::<Vec<_>>()
9371 );
9372
9373 let img = fs.reader.get_ref().clone();
9375 if let Ok(path) = std::env::var("RB_EMIT_MAPC_IMAGE") {
9376 std::fs::write(&path, &img).unwrap();
9377 }
9378 let fs2 = Fs::open(std::io::Cursor::new(img), 0).unwrap();
9379 assert_eq!(
9380 BTreeHeader::read(fs2.catalog_data()).total_nodes,
9381 h.total_nodes,
9382 "reopen lost grown nodes"
9383 );
9384 assert!(
9385 btree_bitmap_segments(fs2.catalog_data(), node_size).len() >= 2,
9386 "reopen lost the map node"
9387 );
9388 }
9389
9390 #[test]
9391 fn test_hfsplus_catalog_grow_spills_to_overflow_phase_b() {
9392 type Fs = HfsPlusFilesystem<std::io::Cursor<Vec<u8>>>;
9400 let img = create_blank_hfsplus_sized(16 * 1024 * 1024, 4096, "SpillVol", false, 0, 0);
9401 let mut fs = Fs::open(std::io::Cursor::new(img), 0).unwrap();
9402 fs.prepare_for_edit().unwrap();
9403 let root = fs.root().unwrap();
9404 let mut dirs = Vec::new();
9405 for d in 0..20 {
9406 dirs.push(
9407 fs.create_directory(&root, &format!("d{d}"), &Default::default())
9408 .unwrap(),
9409 );
9410 }
9411 const N: u32 = 1200;
9412 for i in 0..N {
9413 let d = &dirs[(i % 20) as usize];
9414 let mut data = std::io::Cursor::new(vec![b'x']);
9415 fs.create_file(
9416 d,
9417 &format!("f{i:05}.txt"),
9418 &mut data,
9419 1,
9420 &Default::default(),
9421 )
9422 .unwrap_or_else(|e| panic!("create_file #{i} (grow+spill should cover it): {e}"));
9423 }
9424
9425 let inline: u32 = fs
9428 .vh
9429 .catalog_file
9430 .extents
9431 .iter()
9432 .map(|e| e.block_count)
9433 .sum();
9434 let inline_count = fs
9435 .vh
9436 .catalog_file
9437 .extents
9438 .iter()
9439 .filter(|e| e.block_count != 0)
9440 .count();
9441 assert_eq!(
9442 inline_count, 8,
9443 "catalog should have filled all 8 inline extents"
9444 );
9445 assert!(
9446 fs.vh.catalog_file.total_blocks > inline,
9447 "catalog total_blocks {} should exceed inline-extent blocks {inline} (overflow used)",
9448 fs.vh.catalog_file.total_blocks
9449 );
9450
9451 fs.sync_metadata().unwrap();
9452 let result = fs.fsck().unwrap().unwrap();
9453 assert!(
9454 result.errors.is_empty(),
9455 "fsck after overflow spill: {:?}",
9456 result
9457 .errors
9458 .iter()
9459 .take(8)
9460 .map(|e| &e.message)
9461 .collect::<Vec<_>>()
9462 );
9463
9464 let img = fs.reader.get_ref().clone();
9467 if let Ok(path) = std::env::var("RB_EMIT_SPILL_IMAGE") {
9468 std::fs::write(&path, &img).unwrap();
9469 }
9470 let mut fs2 = Fs::open(std::io::Cursor::new(img), 0).unwrap();
9471 let r2 = fs2.root().unwrap();
9472 let mut total = 0;
9473 for de in fs2.list_directory(&r2).unwrap() {
9474 if de.is_directory() {
9475 total += fs2.list_directory(&de).unwrap().len();
9476 }
9477 }
9478 assert_eq!(total as u32, N, "reopen lost files: {total} != {N}");
9479 }
9480
9481 #[test]
9486 fn the_alternate_volume_header_follows_the_partition_end_when_known() {
9487 use disk_core::filesystem::{CreateFileOptions, EditableFilesystem, Filesystem};
9488 const VOLUME: u64 = 8 * 1024 * 1024;
9489 const PARTITION: u64 = VOLUME + 6 * 1024;
9490 let mut img = create_blank_hfsplus(VOLUME, 4096, "Slack", false);
9491 img.resize(PARTITION as usize, 0);
9492 {
9493 let mut fs =
9494 HfsPlusFilesystem::open_sized(std::io::Cursor::new(&mut img), 0, Some(PARTITION))
9495 .unwrap();
9496 fs.prepare_for_edit().unwrap();
9497 let root = fs.root().unwrap();
9498 fs.create_file(&root, "a", &mut &b"x"[..], 1, &CreateFileOptions::default())
9499 .unwrap();
9500 fs.sync_metadata().unwrap();
9501 }
9502 let primary = &img[1024..1536];
9503 let alt = &img[PARTITION as usize - 1024..PARTITION as usize - 512];
9504 assert_eq!(
9505 &alt[0..2],
9506 b"H+",
9507 "alternate volume header missing at the partition end"
9508 );
9509 assert_eq!(primary, alt);
9510 }
9511
9512 #[test]
9514 fn create_file_writes_the_finder_flags_it_is_given() {
9515 use disk_core::filesystem::{CreateFileOptions, EditableFilesystem, Filesystem};
9516 let mut img = create_blank_hfsplus(8 * 1024 * 1024, 4096, "Flags", false);
9517 let mut fs = HfsPlusFilesystem::open(std::io::Cursor::new(&mut img), 0).unwrap();
9518 fs.prepare_for_edit().unwrap();
9519 let root = fs.root().unwrap();
9520 let opts = CreateFileOptions {
9521 os_type: Some(*b"APPL"),
9522 os_creator: Some(*b"ttxt"),
9523 finder_flags: Some(0x2000),
9524 ..Default::default()
9525 };
9526 fs.create_file(&root, "app", &mut &b"x"[..], 1, &opts)
9527 .unwrap();
9528 fs.sync_metadata().unwrap();
9529 let entry = fs
9530 .list_directory(&root)
9531 .unwrap()
9532 .into_iter()
9533 .find(|e| e.name == "app")
9534 .unwrap();
9535 assert_eq!(entry.finder_flags, Some(0x2000));
9536 }
9537
9538 #[test]
9539 fn a_grow_allocates_the_blocks_the_new_nodes_occupy() {
9540 assert_eq!(btree_grow_blocks(10, 8, 4096, 8192), 4);
9542 assert_eq!(btree_grow_blocks(4, 8, 1024, 4096), 2);
9544 assert_eq!(btree_grow_blocks(11, 8, 4096, 8192), 4);
9546 assert_eq!(btree_grow_blocks(11, 1, 4096, 8192), 0);
9547 assert_eq!(btree_grow_blocks(10, 8, 4096, 512), 64);
9549 assert_eq!(btree_grow_blocks(10, 8, 4096, 4096), 8);
9550 }
9551
9552 #[test]
9553 fn test_hfsplus_catalog_grows_fork_on_full_phase_a() {
9554 use crate::hfs_common::BTreeHeader;
9560 type Fs = HfsPlusFilesystem<std::io::Cursor<Vec<u8>>>;
9561
9562 let img = create_blank_hfsplus_sized(16 * 1024 * 1024, 4096, "GrowVol", false, 0, 0);
9565 let mut fs = Fs::open(std::io::Cursor::new(img), 0).unwrap();
9566 fs.prepare_for_edit().unwrap();
9567
9568 let start_nodes = BTreeHeader::read(fs.catalog_data()).total_nodes;
9569
9570 const DIRS: u32 = 20;
9571 let root = fs.root().unwrap();
9572 let mut dir_ids = Vec::with_capacity(DIRS as usize);
9573 for d in 0..DIRS {
9574 let de = fs
9575 .create_directory(&root, &format!("dir{d:03}"), &Default::default())
9576 .unwrap();
9577 dir_ids.push(de.location as u32);
9578 }
9579 let base_cnid = fs.vh.next_catalog_id;
9580
9581 let n: u32 = 6_000;
9582 let mut per_dir = vec![0i32; DIRS as usize];
9583 let empty_fork = ForkData::empty();
9584 for i in 0..n {
9585 let hash = i.wrapping_mul(2_654_435_761);
9586 let d = (hash % DIRS) as usize;
9587 let name = format!("f{hash:08x}");
9588 let mut kr = Fs::build_catalog_key(dir_ids[d], &name);
9589 if !kr.len().is_multiple_of(2) {
9590 kr.push(0);
9591 }
9592 kr.extend_from_slice(&Fs::build_file_record_with_dates(
9593 base_cnid + i,
9594 &empty_fork,
9595 &empty_fork,
9596 &[0u8; 4],
9597 &[0u8; 4],
9598 None,
9599 0,
9600 ));
9601 fs.insert_catalog_record(&kr)
9602 .unwrap_or_else(|e| panic!("insert #{i} into dir{d} (grow should cover it): {e}"));
9603 per_dir[d] += 1;
9604 }
9605 fs.vh.file_count += n;
9606 fs.vh.next_catalog_id = base_cnid + n;
9607 for (d, &cnt) in per_dir.iter().enumerate() {
9608 fs.update_parent_valence(dir_ids[d], cnt).unwrap();
9609 }
9610
9611 let end_nodes = BTreeHeader::read(fs.catalog_data()).total_nodes;
9613 assert!(
9614 end_nodes >= start_nodes + 16,
9615 "catalog total_nodes only went {start_nodes} -> {end_nodes}; expected >= 2 grows"
9616 );
9617 let cat_extents = fs
9619 .vh
9620 .catalog_file
9621 .extents
9622 .iter()
9623 .filter(|e| e.block_count != 0)
9624 .count();
9625 assert_eq!(
9626 cat_extents, 1,
9627 "contiguous tail growth should keep the catalog fork at 1 extent, got {cat_extents}"
9628 );
9629
9630 fs.sync_metadata().unwrap();
9632
9633 let result = fs.fsck().unwrap().unwrap();
9635 assert!(
9636 result.errors.is_empty(),
9637 "fsck found {} errors after grow: {:?}",
9638 result.errors.len(),
9639 result
9640 .errors
9641 .iter()
9642 .take(8)
9643 .map(|e| &e.message)
9644 .collect::<Vec<_>>()
9645 );
9646
9647 let img = fs.reader.get_ref().clone();
9650 if let Ok(path) = std::env::var("RB_EMIT_GROW_IMAGE") {
9653 std::fs::write(&path, &img).unwrap();
9654 }
9655 let mut fs2 = Fs::open(std::io::Cursor::new(img), 0).unwrap();
9656 let reopened_nodes = BTreeHeader::read(fs2.catalog_data()).total_nodes;
9657 assert_eq!(reopened_nodes, end_nodes, "reopen lost grown nodes");
9658 let r2 = fs2.root().unwrap();
9659 let dirs = fs2.list_directory(&r2).unwrap();
9660 assert_eq!(dirs.len(), DIRS as usize, "reopen lost directories");
9661 }
9662
9663 #[test]
9664 fn test_hfsplus_extents_overflow_grows_multilevel() {
9665 use crate::hfs_common::{
9672 btree_find_record, btree_insert_full, BTreeHeader, BTreeKeyFormat,
9673 };
9674 type Fs = HfsPlusFilesystem<std::io::Cursor<Vec<u8>>>;
9675
9676 let node_size = 512usize;
9677 let total_nodes = 1800u32;
9678 let mut buf = vec![0u8; total_nodes as usize * node_size];
9679 write_blank_btree_header_node(
9680 &mut buf[0..node_size],
9681 node_size,
9682 0,
9683 total_nodes,
9684 total_nodes - 2,
9685 10,
9686 0,
9687 );
9688 write_empty_leaf_node(&mut buf[node_size..2 * node_size]);
9689
9690 let kf = BTreeKeyFormat::HFSPLUS_EXTENTS;
9691 const N: u32 = 600;
9695 let mut ids: Vec<u32> = Vec::with_capacity(N as usize);
9696 for i in 0..N {
9697 let file_id = i.wrapping_mul(2_654_435_761);
9698 let rec = Fs::build_extents_overflow_record(file_id, 0, 8, &[]);
9699 btree_insert_full(&mut buf, &rec, &kf, &Fs::extents_compare)
9700 .unwrap_or_else(|e| panic!("extents insert #{i} (file {file_id}): {e}"));
9701 ids.push(file_id);
9702 }
9703
9704 let header = BTreeHeader::read(&buf);
9705 assert!(
9706 header.depth >= 2,
9707 "extents-overflow tree stayed depth {} — split past a leaf level failed",
9708 header.depth
9709 );
9710 assert_eq!(header.leaf_records, N);
9711
9712 let mut recs: Vec<Vec<u8>> = Vec::with_capacity(N as usize);
9713 crate::hfs_common::walk_leaf_records::<(), _>(
9714 &buf,
9715 header.first_leaf_node,
9716 node_size,
9717 |_n, _r, _o, r| {
9718 recs.push(r.to_vec());
9719 None
9720 },
9721 );
9722 assert_eq!(recs.len(), N as usize, "leaf walk lost records");
9723 for w in recs.windows(2) {
9724 assert_eq!(
9725 Fs::extents_compare(&w[0], &w[1]),
9726 std::cmp::Ordering::Less,
9727 "extents-overflow leaves out of order — bad separator"
9728 );
9729 }
9730
9731 fn ext_key(rec: &[u8]) -> &[u8] {
9733 &rec[..12.min(rec.len())]
9734 }
9735 let ext_cmp = |a: &[u8], b: &[u8]| Fs::extents_compare(a, b);
9736 for &file_id in &ids {
9737 let search = Fs::build_extents_overflow_record(file_id, 0, 8, &[]);
9738 let (found, _) = btree_find_record(&buf, &header, &search, &ext_key, &ext_cmp);
9739 assert!(
9740 found.is_some(),
9741 "extents record for file {file_id} not findable"
9742 );
9743 }
9744 }
9745
9746 #[test]
9747 fn test_hfsplus_attributes_grows_multilevel() {
9748 use crate::hfs_common::{
9754 btree_find_record, btree_insert_full, BTreeHeader, BTreeKeyFormat,
9755 };
9756 use byteorder::{BigEndian, ByteOrder};
9757 type Fs = HfsPlusFilesystem<std::io::Cursor<Vec<u8>>>;
9758
9759 let node_size = 512usize;
9760 let total_nodes = 1800u32;
9761 let mut buf = vec![0u8; total_nodes as usize * node_size];
9762 write_blank_btree_header_node(
9763 &mut buf[0..node_size],
9764 node_size,
9765 0,
9766 total_nodes,
9767 total_nodes - 2,
9768 264,
9769 0,
9770 );
9771 write_empty_leaf_node(&mut buf[node_size..2 * node_size]);
9772
9773 let kf = BTreeKeyFormat::HFSPLUS_ATTRIBUTES;
9774 const N: u32 = 600;
9775 let value = [0xABu8; 8];
9776 let mut cnids: Vec<u32> = Vec::with_capacity(N as usize);
9777 for i in 0..N {
9778 let cnid = i.wrapping_mul(2_654_435_761);
9779 let rec = Fs::build_inline_attr_record(cnid, "xattr", &value);
9780 btree_insert_full(&mut buf, &rec, &kf, &Fs::attr_compare)
9781 .unwrap_or_else(|e| panic!("attr insert #{i} (cnid {cnid}): {e}"));
9782 cnids.push(cnid);
9783 }
9784
9785 let header = BTreeHeader::read(&buf);
9786 assert!(
9787 header.depth >= 2,
9788 "attributes tree stayed depth {} — split past a leaf level failed",
9789 header.depth
9790 );
9791 assert_eq!(header.leaf_records, N);
9792
9793 let mut recs: Vec<Vec<u8>> = Vec::with_capacity(N as usize);
9794 crate::hfs_common::walk_leaf_records::<(), _>(
9795 &buf,
9796 header.first_leaf_node,
9797 node_size,
9798 |_n, _r, _o, r| {
9799 recs.push(r.to_vec());
9800 None
9801 },
9802 );
9803 assert_eq!(recs.len(), N as usize, "leaf walk lost records");
9804 for w in recs.windows(2) {
9805 assert_eq!(
9806 Fs::attr_compare(&w[0], &w[1]),
9807 std::cmp::Ordering::Less,
9808 "attributes leaves out of order — bad separator"
9809 );
9810 }
9811
9812 fn attr_key(rec: &[u8]) -> &[u8] {
9813 let kl = BigEndian::read_u16(&rec[0..2]) as usize;
9814 &rec[..(2 + kl).min(rec.len())]
9815 }
9816 let attr_cmp = |a: &[u8], b: &[u8]| Fs::attr_compare(a, b);
9817 for &cnid in &cnids {
9818 let search = Fs::build_inline_attr_record(cnid, "xattr", &value);
9819 let (found, _) = btree_find_record(&buf, &header, &search, &attr_key, &attr_cmp);
9820 assert!(found.is_some(), "attr record for cnid {cnid} not findable");
9821 }
9822 }
9823
9824 #[test]
9825 fn test_hfsplus_fragmented_file_splits_extents_overflow_btree_real_path() {
9826 type Fs = HfsPlusFilesystem<std::io::Cursor<Vec<u8>>>;
9834 let img = create_blank_hfsplus_sized(32 << 20, 4096, "Frag", false, 0, 256 << 10);
9837 let mut fs = Fs::open(std::io::Cursor::new(img), 0).unwrap();
9838 fs.prepare_for_edit().unwrap();
9839
9840 fs.ensure_bitmap().unwrap();
9846 let block_size = fs.vh.block_size as usize;
9847 let n_blocks: u32 = 520;
9848 let first_free = fs.vh.next_allocation;
9849 {
9850 let bitmap = fs.bitmap.as_mut().unwrap();
9851 for b in bitmap.iter_mut() {
9852 *b = 0xFF;
9853 }
9854 let mut blk = first_free + 1;
9855 for _ in 0..n_blocks {
9856 bitmap_clear_bit_be(bitmap, blk);
9857 blk += 2; }
9859 }
9860 fs.vh.free_blocks = n_blocks;
9861
9862 let payload: Vec<u8> = (0..(n_blocks as usize * block_size))
9863 .map(|i| (i % 251) as u8)
9864 .collect();
9865 let mut reader = std::io::Cursor::new(payload.clone());
9866 let root = fs.root().unwrap();
9867 let entry = fs
9868 .create_file(
9869 &root,
9870 "frag.bin",
9871 &mut reader,
9872 payload.len() as u64,
9873 &CreateFileOptions::default(),
9874 )
9875 .expect("fragmented file create should succeed via a splitting overflow B-tree");
9876
9877 let depth =
9879 crate::hfs_common::BTreeHeader::read(fs.extents_overflow_data.as_ref().unwrap()).depth;
9880 assert!(
9881 depth >= 2,
9882 "extents-overflow tree depth {depth} < 2 — 64 overflow records did not split it"
9883 );
9884
9885 let cnid = entry.location as u32;
9887 let (data_fork, _rsrc) = fs.find_file_by_id(cnid).unwrap();
9888 assert_eq!(data_fork.total_blocks, n_blocks);
9889
9890 let got = fs.read_file(&entry, payload.len() + 1).unwrap();
9892 assert_eq!(got.len(), payload.len());
9893 assert_eq!(
9894 got, payload,
9895 "fragmented read-back mismatch through split overflow tree"
9896 );
9897 }
9898
9899 #[test]
9900 fn deleting_the_last_overflow_record_retires_the_extents_tree() {
9901 use crate::hfs_common::{btree_lonely_empty_leaves, BTreeHeader};
9904 type Fs = HfsPlusFilesystem<std::io::Cursor<Vec<u8>>>;
9905 let img = create_blank_hfsplus_sized(32 << 20, 4096, "Retire", false, 0, 256 << 10);
9906 let mut fs = Fs::open(std::io::Cursor::new(img), 0).unwrap();
9907 fs.prepare_for_edit().unwrap();
9908
9909 fs.ensure_bitmap().unwrap();
9911 let block_size = fs.vh.block_size as usize;
9912 let n_blocks: u32 = 520;
9913 let first_free = fs.vh.next_allocation;
9914 {
9915 let bitmap = fs.bitmap.as_mut().unwrap();
9916 for b in bitmap.iter_mut() {
9917 *b = 0xFF;
9918 }
9919 let mut blk = first_free + 1;
9920 for _ in 0..n_blocks {
9921 bitmap_clear_bit_be(bitmap, blk);
9922 blk += 2;
9923 }
9924 }
9925 fs.vh.free_blocks = n_blocks;
9926 let payload: Vec<u8> = (0..(n_blocks as usize * block_size))
9927 .map(|i| (i % 251) as u8)
9928 .collect();
9929 let root = fs.root().unwrap();
9930 let entry = fs
9931 .create_file(
9932 &root,
9933 "frag.bin",
9934 &mut std::io::Cursor::new(payload.clone()),
9935 payload.len() as u64,
9936 &CreateFileOptions::default(),
9937 )
9938 .unwrap();
9939 let before = BTreeHeader::read(fs.extents_overflow_data.as_ref().unwrap());
9940 assert!(
9941 before.depth >= 2 && before.leaf_records > 0,
9942 "depth {} leaf_records {}",
9943 before.depth,
9944 before.leaf_records
9945 );
9946
9947 fs.delete_entry(&root, &entry).unwrap();
9948
9949 let tree = fs.extents_overflow_data.as_ref().unwrap();
9950 let h = BTreeHeader::read(tree);
9951 assert_eq!((h.leaf_records, h.root_node, h.depth), (0, 0, 0));
9952 assert_eq!(
9953 h.free_nodes,
9954 h.total_nodes - 1,
9955 "only the header node stays"
9956 );
9957 assert!(btree_lonely_empty_leaves(tree, h.node_size as usize).is_empty());
9960 }
9961
9962 #[test]
9963 fn a_grow_moves_the_alternate_header_blocks_and_the_bitmap() {
9964 type Fs = HfsPlusFilesystem<std::io::Cursor<Vec<u8>>>;
9968 for (bs, old_mib, new_mib) in [(4096u32, 24u64, 30u64), (2048u32, 32u64, 64u64)] {
9969 let mut img = create_blank_hfsplus(old_mib << 20, bs, "Grow", false);
9970 img.resize((new_mib << 20) as usize, 0);
9971 let mut cur = std::io::Cursor::new(img);
9972 let mut log = |_: &str| {};
9973 resize_hfsplus_in_place(&mut cur, 0, new_mib << 20, &mut log).unwrap();
9974
9975 let mut fs = Fs::open_sized(cur, 0, Some(new_mib << 20)).unwrap();
9976 let total = fs.vh.total_blocks;
9977 assert_eq!(total as u64, (new_mib << 20) / bs as u64, "bs {bs}");
9978 let bitmap = fs.read_allocation_bitmap_for_fsck().unwrap();
9979 let old_total = ((old_mib << 20) / bs as u64) as u32;
9980 for b in alt_vh_blocks(old_total, bs) {
9981 assert!(
9982 !bitmap_test_bit_be(&bitmap, b),
9983 "bs {bs}: old tail block {b} still marked"
9984 );
9985 }
9986 for b in alt_vh_blocks(total, bs) {
9987 assert!(
9988 bitmap_test_bit_be(&bitmap, b),
9989 "bs {bs}: new tail block {b} is free"
9990 );
9991 }
9992 let used = (0..total)
9993 .filter(|&b| bitmap_test_bit_be(&bitmap, b))
9994 .count() as u32;
9995 assert_eq!(fs.vh.free_blocks, total - used, "bs {bs}");
9996 assert!(
9997 fs.vh.allocation_file.total_blocks as u64 * bs as u64 * 8 >= total as u64,
9998 "bs {bs}: allocation file too small for {total} blocks"
9999 );
10000 let report = fs.fsck().unwrap().unwrap();
10001 assert!(
10002 report.errors.is_empty(),
10003 "bs {bs}: {:?}",
10004 report
10005 .errors
10006 .iter()
10007 .map(|e| &e.message)
10008 .take(4)
10009 .collect::<Vec<_>>()
10010 );
10011 }
10012 }
10013
10014 #[test]
10015 fn test_hfsplus_catalog_shuffled_inserts_pack_densely() {
10016 use crate::hfs_common::{btree_insert_full, BTreeHeader, BTreeKeyFormat};
10023 use byteorder::{BigEndian, ByteOrder};
10024 type Fs = HfsPlusFilesystem<std::io::Cursor<Vec<u8>>>;
10025
10026 let node_size = 512usize;
10027 let total_nodes = 1800u32;
10028 let mut buf = vec![0u8; total_nodes as usize * node_size];
10029 write_blank_btree_header_node(
10030 &mut buf[0..node_size],
10031 node_size,
10032 0,
10033 total_nodes,
10034 total_nodes - 2,
10035 516,
10036 KEY_COMPARE_CASE_FOLDING,
10037 );
10038 write_empty_leaf_node(&mut buf[node_size..2 * node_size]);
10039
10040 let kf = BTreeKeyFormat::HFSPLUS_CATALOG;
10041 let cmp = |a: &[u8], b: &[u8]| Fs::catalog_compare(a, b, false);
10042 const DIRS: u32 = 40;
10043 const N: u32 = 3000;
10044 for i in 0..N {
10045 let hash = i.wrapping_mul(2_654_435_761);
10046 let parent = 16 + (hash % DIRS);
10047 let name = format!("f{hash:08x}");
10048 let mut rec = Fs::build_catalog_key(parent, &name);
10049 rec.extend_from_slice(&[0u8; 40]);
10050 btree_insert_full(&mut buf, &rec, &kf, &cmp)
10051 .unwrap_or_else(|e| panic!("insert #{i}: {e}"));
10052 }
10053
10054 let header = BTreeHeader::read(&buf);
10056 assert!(
10057 header.depth >= 3,
10058 "need a multi-level tree to judge packing"
10059 );
10060 let mut used_total = 0usize;
10061 let mut leaves = 0usize;
10062 let mut node = header.first_leaf_node;
10063 let mut seen = std::collections::HashSet::new();
10064 while node != 0 && seen.insert(node) {
10065 let off = node as usize * node_size;
10066 let num = BigEndian::read_u16(&buf[off + 10..off + 12]) as usize;
10067 let free_off_pos = off + node_size - 2 * (num + 1);
10069 let free_off = BigEndian::read_u16(&buf[free_off_pos..free_off_pos + 2]) as usize;
10070 used_total += free_off + 2 * (num + 1);
10071 leaves += 1;
10072 node = BigEndian::read_u32(&buf[off..off + 4]); }
10074 let occupancy = used_total as f64 / (leaves * node_size) as f64;
10076 assert!(
10077 occupancy >= 0.80,
10078 "shuffled multi-dir leaf occupancy {occupancy:.3} (<0.80) over {leaves} leaves — \
10079 the rotation is not packing densely"
10080 );
10081 }
10082
10083 #[test]
10084 fn sized_streaming_matches_in_memory() {
10085 for _ in 0..5 {
10087 let t0 = hfs_common::hfs_now();
10088 let mem = create_blank_hfsplus_sized(20 << 20, 4096, "Vol", true, 8192, 0);
10089 let mut cur = std::io::Cursor::new(vec![0u8; 20 << 20]);
10090 let len =
10091 write_blank_hfsplus_sized_into(&mut cur, 0, 20 << 20, 4096, "Vol", true, 8192, 0)
10092 .unwrap();
10093 if hfs_common::hfs_now() != t0 {
10094 continue;
10095 }
10096 assert_eq!(len as usize, mem.len());
10097 assert!(cur.get_ref()[..mem.len()] == mem[..]);
10098 return;
10099 }
10100 panic!("the clock ticked during every attempt");
10101 }
10102}