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 = self.vh.total_blocks.saturating_sub(2);
3427 let last = find_last_set_bit(&bitmap, search_up_to);
3428 let trailing: u64 = if self.vh.block_size <= 512 { 2 } else { 1 };
3431 match last {
3432 Some(block) => {
3433 let byte = (block as u64 + 1 + trailing) * self.vh.block_size as u64;
3434 Ok(byte.min(self.total_size()))
3435 }
3436 None => Ok(self.total_size()),
3437 }
3438 }
3439
3440 fn defragmented_minimum_size(&mut self) -> Result<u64, FilesystemError> {
3441 let block_size = self.vh.block_size as u64;
3462 let used_blocks = self.vh.total_blocks.saturating_sub(self.vh.free_blocks) as u64;
3463 let used_bytes = used_blocks.saturating_mul(block_size);
3464 let catalog_pad = self.vh.catalog_file.logical_size.saturating_mul(3);
3469 let total = used_bytes.saturating_add(catalog_pad).saturating_add(1024);
3470 Ok(total.div_ceil(block_size) * block_size)
3471 }
3472
3473 fn write_resource_fork_to(
3474 &mut self,
3475 entry: &FileEntry,
3476 writer: &mut dyn std::io::Write,
3477 ) -> Result<u64, FilesystemError> {
3478 let file_id = entry.location as u32;
3479 let (_data_fork, rsrc_fork) = self.find_file_by_id(file_id).ok_or_else(|| {
3480 FilesystemError::NotFound(format!("file id {file_id} not found in catalog"))
3481 })?;
3482 if rsrc_fork.logical_size == 0 {
3483 return Ok(0);
3484 }
3485 let data = read_fork(
3486 &mut self.reader,
3487 self.partition_offset,
3488 self.vh.block_size,
3489 &rsrc_fork,
3490 )?;
3491 writer.write_all(&data)?;
3492 Ok(data.len() as u64)
3493 }
3494
3495 fn resource_fork_size(&mut self, entry: &FileEntry) -> u64 {
3496 let file_id = entry.location as u32;
3497 self.find_file_by_id(file_id)
3498 .map(|(_d, r)| r.logical_size)
3499 .unwrap_or(0)
3500 }
3501
3502 fn fragmentation_stats(
3503 &mut self,
3504 ) -> Option<Result<disk_core::filesystem::FragmentationStats, FilesystemError>> {
3505 Some(Ok(hfsplus_fragmentation_stats(
3506 self.catalog_data(),
3507 self.catalog_first_leaf(),
3508 self.catalog_node_size(),
3509 )))
3510 }
3511
3512 fn blessed_system_folder(&mut self) -> Option<(u64, String)> {
3513 let cnid = self.vh.finder_info[0];
3515 if cnid == 0 {
3516 let cnid_x = self.vh.finder_info[5];
3518 if cnid_x == 0 {
3519 return None;
3520 }
3521 return self.lookup_folder_name(cnid_x);
3522 }
3523 self.lookup_folder_name(cnid)
3524 }
3525
3526 fn fsck(&mut self) -> Option<Result<disk_core::fsck::FsckResult, FilesystemError>> {
3527 Some(crate::hfsplus_fsck::check(self))
3528 }
3529
3530 fn journal_detail(&mut self) -> Result<Option<Box<dyn std::any::Any + Send>>, FilesystemError> {
3531 Ok(HfsPlusFilesystem::journal_detail(self)?
3533 .map(|d| Box::new(d) as Box<dyn std::any::Any + Send>))
3534 }
3535}
3536
3537impl<R: Read + Seek + Send> HfsPlusFilesystem<R> {
3538 fn lookup_folder_name(&self, cnid: u32) -> Option<(u64, String)> {
3539 if let Some((_node, _rec, offset)) = self.find_catalog_record_by_cnid(cnid) {
3541 let node_size = self.catalog_header.node_size as usize;
3542 let key_len = BigEndian::read_u16(&self.catalog_data[offset..offset + 2]) as usize;
3543 let mut rec_data_start = offset + 2 + key_len;
3544 if !rec_data_start.is_multiple_of(2) {
3545 rec_data_start += 1;
3546 }
3547 if rec_data_start + 10 <= self.catalog_data.len() {
3549 let name_len = BigEndian::read_u16(
3550 &self.catalog_data[rec_data_start + 8..rec_data_start + 10],
3551 ) as usize;
3552 let name_end = rec_data_start + 10 + name_len * 2;
3553 if name_end <= self.catalog_data.len() {
3554 let name = decode_utf16be(&self.catalog_data[rec_data_start + 10..name_end]);
3555 return Some((cnid as u64, name));
3556 }
3557 }
3558 let _ = node_size;
3559 }
3560 Some((cnid as u64, format!("CNID {}", cnid)))
3562 }
3563}
3564
3565fn write_fork_to<R: Read + Seek>(
3567 reader: &mut R,
3568 partition_offset: u64,
3569 block_size: u32,
3570 fork: &ForkData,
3571 writer: &mut dyn std::io::Write,
3572) -> Result<u64, FilesystemError> {
3573 let size = fork.logical_size;
3574 let mut written: u64 = 0;
3575 let mut buf = vec![0u8; 64 * 1024];
3576 for ext in &fork.extents {
3577 if ext.is_empty() || written >= size {
3578 break;
3579 }
3580 let offset = partition_offset + ext.start_block as u64 * block_size as u64;
3581 let extent_len = ext.block_count as u64 * block_size as u64;
3582 let to_emit = extent_len.min(size - written);
3583 reader.seek(SeekFrom::Start(offset))?;
3584 let mut left = to_emit;
3585 while left > 0 {
3586 let n = (buf.len() as u64).min(left) as usize;
3587 reader.read_exact(&mut buf[..n])?;
3588 writer.write_all(&buf[..n])?;
3589 left -= n as u64;
3590 }
3591 written += to_emit;
3592 }
3593 Ok(written)
3594}
3595
3596fn read_fork<R: Read + Seek>(
3598 reader: &mut R,
3599 partition_offset: u64,
3600 block_size: u32,
3601 fork: &ForkData,
3602) -> Result<Vec<u8>, FilesystemError> {
3603 let size = fork.logical_size as usize;
3604 let mut data = Vec::with_capacity(size);
3605
3606 let extent_count = fork.extents.iter().filter(|e| !e.is_empty()).count();
3607 let t_total = std::time::Instant::now();
3608 log::debug!(
3609 "[HFS+ read_fork] start: logical_size={} bytes, inline_extents={}",
3610 size,
3611 extent_count
3612 );
3613
3614 for (idx, ext) in fork.extents.iter().enumerate() {
3615 if ext.is_empty() {
3616 break;
3617 }
3618 let offset = partition_offset + ext.start_block as u64 * block_size as u64;
3619 let len = ext.block_count as u64 * block_size as u64;
3620 let read_len = len.min((size - data.len()) as u64) as usize;
3621 let t_ext = std::time::Instant::now();
3622 reader.seek(SeekFrom::Start(offset))?;
3623 let mut buf = vec![0u8; read_len];
3624 reader.read_exact(&mut buf)?;
3625 let elapsed_ms = t_ext.elapsed().as_secs_f64() * 1000.0;
3626 let mb = read_len as f64 / (1024.0 * 1024.0);
3627 let mbps = if elapsed_ms > 0.0 {
3628 mb / (elapsed_ms / 1000.0)
3629 } else {
3630 f64::INFINITY
3631 };
3632 log::debug!(
3633 "[HFS+ read_fork] ext[{}]: offset={} bytes={} time={:.1}ms throughput={:.2} MB/s",
3634 idx,
3635 offset,
3636 read_len,
3637 elapsed_ms,
3638 mbps
3639 );
3640 data.extend_from_slice(&buf);
3641 if data.len() >= size {
3642 break;
3643 }
3644 }
3645
3646 data.truncate(size);
3647 let total_ms = t_total.elapsed().as_secs_f64() * 1000.0;
3648 let total_mb = size as f64 / (1024.0 * 1024.0);
3649 let total_mbps = if total_ms > 0.0 {
3650 total_mb / (total_ms / 1000.0)
3651 } else {
3652 f64::INFINITY
3653 };
3654 log::debug!(
3655 "[HFS+ read_fork] done: {} bytes in {:.1}ms ({:.2} MB/s)",
3656 size,
3657 total_ms,
3658 total_mbps
3659 );
3660 Ok(data)
3661}
3662
3663fn collect_hfsplus_overflow_extents(
3675 extents_data: &[u8],
3676 file_id: u32,
3677 fork_type: u8,
3678 min_start_block: u32,
3679) -> Vec<ExtentDescriptor> {
3680 use crate::hfs_common::walk_leaf_records;
3681
3682 if extents_data.len() < 14 + 30 {
3683 return Vec::new();
3684 }
3685 let header = BTreeHeaderRecord::parse(&extents_data[14..14 + 30.min(extents_data.len() - 14)]);
3686 let node_size = header.node_size as usize;
3687 if node_size == 0 {
3688 return Vec::new();
3689 }
3690
3691 let mut out: Vec<(u32, ExtentDescriptor)> = Vec::new();
3692 walk_leaf_records::<(), _>(
3693 extents_data,
3694 header.first_leaf_node,
3695 node_size,
3696 |_node, _rec_idx, _abs_off, rec| {
3697 if rec.len() < 12 {
3698 return None;
3699 }
3700 let key_len = BigEndian::read_u16(&rec[0..2]) as usize;
3701 if key_len < 10 || 2 + key_len > rec.len() {
3703 return None;
3704 }
3705 let rec_fork = rec[2];
3706 let rec_file = BigEndian::read_u32(&rec[4..8]);
3707 let rec_start_block = BigEndian::read_u32(&rec[8..12]);
3708 if rec_fork != fork_type || rec_file != file_id {
3709 return None;
3710 }
3711 if rec_start_block < min_start_block {
3712 return None;
3713 }
3714 let data_off = 2 + key_len;
3717 if data_off + 64 > rec.len() {
3718 return None;
3719 }
3720 for j in 0..8 {
3721 let ext = ExtentDescriptor::parse(&rec[data_off + j * 8..data_off + j * 8 + 8]);
3722 if ext.block_count > 0 {
3723 out.push((rec_start_block, ext));
3724 }
3725 }
3726 None
3727 },
3728 );
3729
3730 out.sort_by_key(|(sb, _)| *sb);
3731 out.into_iter().map(|(_, e)| e).collect()
3732}
3733
3734fn read_fork_with_overflow<R: Read + Seek>(
3742 reader: &mut R,
3743 partition_offset: u64,
3744 block_size: u32,
3745 fork: &ForkData,
3746 file_id: u32,
3747 fork_type: u8,
3748 extents_overflow_data: Option<&[u8]>,
3749) -> Result<Vec<u8>, FilesystemError> {
3750 let mut data = read_fork(reader, partition_offset, block_size, fork)?;
3751 let target = fork.logical_size as usize;
3752 if data.len() >= target {
3753 return Ok(data);
3754 }
3755 let Some(ext_data) = extents_overflow_data else {
3756 return Err(FilesystemError::InvalidData(format!(
3757 "file {file_id} fork {fork_type:#x}: {} bytes requested but only \
3758 {} bytes in inline extents and no extents-overflow B-tree available",
3759 fork.logical_size,
3760 data.len()
3761 )));
3762 };
3763 let inline_blocks: u32 = fork.extents.iter().map(|e| e.block_count).sum();
3764 let overflow = collect_hfsplus_overflow_extents(ext_data, file_id, fork_type, inline_blocks);
3765 for ext in overflow {
3766 if data.len() >= target {
3767 break;
3768 }
3769 if ext.block_count == 0 {
3770 continue;
3771 }
3772 let offset = partition_offset + ext.start_block as u64 * block_size as u64;
3773 let extent_len = ext.block_count as u64 * block_size as u64;
3774 let to_read = extent_len.min((target - data.len()) as u64) as usize;
3775 reader.seek(SeekFrom::Start(offset))?;
3776 let mut buf = vec![0u8; to_read];
3777 reader.read_exact(&mut buf)?;
3778 data.extend_from_slice(&buf);
3779 }
3780 if data.len() < target {
3781 return Err(FilesystemError::InvalidData(format!(
3782 "file {file_id} fork {fork_type:#x}: extents (inline + overflow) \
3783 cover {} of {} bytes",
3784 data.len(),
3785 fork.logical_size
3786 )));
3787 }
3788 Ok(data)
3789}
3790
3791fn translate_fork_offset(
3797 fork: &ForkData,
3798 block_size: u32,
3799 partition_offset: u64,
3800 fork_offset: u64,
3801) -> Option<(u64, u64)> {
3802 let bs = block_size as u64;
3803 let mut cursor: u64 = 0;
3804 for ext in &fork.extents {
3805 if ext.block_count == 0 {
3806 continue;
3807 }
3808 let len = ext.block_count as u64 * bs;
3809 if fork_offset < cursor + len {
3810 let local = fork_offset - cursor;
3811 let dev = partition_offset + ext.start_block as u64 * bs + local;
3812 return Some((dev, len - local));
3813 }
3814 cursor += len;
3815 }
3816 None
3817}
3818
3819fn read_node_from_fork<R: Read + Seek>(
3821 reader: &mut R,
3822 partition_offset: u64,
3823 block_size: u32,
3824 fork: &ForkData,
3825 node_idx: u32,
3826 node_size: usize,
3827) -> Option<Vec<u8>> {
3828 let fork_offset = (node_idx as u64).checked_mul(node_size as u64)?;
3829 let (dev_off, contig) = translate_fork_offset(fork, block_size, partition_offset, fork_offset)?;
3830 if (contig as usize) < node_size {
3831 return None;
3835 }
3836 reader.seek(SeekFrom::Start(dev_off)).ok()?;
3837 let mut buf = vec![0u8; node_size];
3838 reader.read_exact(&mut buf).ok()?;
3839 Some(buf)
3840}
3841
3842pub fn probe_hfsplus_volume_label<R: Read + Seek>(
3859 reader: &mut R,
3860 partition_offset: u64,
3861) -> Option<String> {
3862 let (fs_type, hfsplus_offset) = resolve_apple_hfs(reader, partition_offset);
3869 if fs_type != "hfsplus" {
3870 return None;
3871 }
3872 reader.seek(SeekFrom::Start(hfsplus_offset + 1024)).ok()?;
3873 let mut vh_buf = [0u8; 512];
3874 reader.read_exact(&mut vh_buf).ok()?;
3875 let vh = HfsPlusVolumeHeader::parse(&vh_buf).ok()?;
3876 if vh.block_size == 0 {
3877 return None;
3878 }
3879
3880 let probe_size = (vh.block_size as usize).clamp(512, 32 * 1024);
3885 let node0 = read_node_from_fork(
3886 reader,
3887 hfsplus_offset,
3888 vh.block_size,
3889 &vh.catalog_file,
3890 0,
3891 probe_size,
3892 )?;
3893 if node0.len() < 14 + 30 {
3894 return None;
3895 }
3896 let header = BTreeHeaderRecord::parse(&node0[14..14 + 30]);
3897 let node_size = header.node_size as usize;
3898 if node_size == 0 || node_size > 32 * 1024 || header.first_leaf_node == 0 {
3899 return None;
3900 }
3901
3902 let leaf = read_node_from_fork(
3905 reader,
3906 hfsplus_offset,
3907 vh.block_size,
3908 &vh.catalog_file,
3909 header.first_leaf_node,
3910 node_size,
3911 )?;
3912 if leaf.len() < node_size || leaf[8] as i8 != -1 {
3913 return None;
3914 }
3915 let num_records = BigEndian::read_u16(&leaf[10..12]) as usize;
3916 if num_records == 0 {
3917 return None;
3918 }
3919 let (rec_start, rec_end) = crate::hfs_common::btree_record_range(&leaf, node_size, 0);
3920 if rec_start >= rec_end || rec_end > node_size {
3921 return None;
3922 }
3923 let rec = &leaf[rec_start..rec_end];
3924 if rec.len() < 8 {
3925 return None;
3926 }
3927 let key_len = BigEndian::read_u16(&rec[0..2]) as usize;
3928 if key_len < 6 || 2 + key_len > rec.len() {
3929 return None;
3930 }
3931 let parent_id = BigEndian::read_u32(&rec[2..6]);
3932 if parent_id != 1 {
3933 return None;
3934 }
3935 let name_length = BigEndian::read_u16(&rec[6..8]) as usize;
3936 if name_length == 0 {
3937 return None;
3938 }
3939 let body_start = 8;
3940 let body_end = body_start + name_length * 2;
3941 if body_end > rec.len() || body_end > 2 + key_len {
3942 return None;
3943 }
3944 let label = decode_utf16be(&rec[body_start..body_end]);
3945 if label.is_empty() {
3946 None
3947 } else {
3948 Some(label)
3949 }
3950}
3951
3952fn write_fork_data<R: Write + Seek>(
3954 writer: &mut R,
3955 partition_offset: u64,
3956 block_size: u32,
3957 fork: &ForkData,
3958 data: &[u8],
3959) -> Result<(), FilesystemError> {
3960 let mut written = 0usize;
3961 for ext in &fork.extents {
3962 if ext.is_empty() || written >= data.len() {
3963 break;
3964 }
3965 let offset = partition_offset + ext.start_block as u64 * block_size as u64;
3966 let extent_len = ext.block_count as u64 * block_size as u64;
3967 let to_write = extent_len.min((data.len() - written) as u64) as usize;
3968 writer.seek(SeekFrom::Start(offset))?;
3969 writer.write_all(&data[written..written + to_write])?;
3970 written += to_write;
3971 }
3972 Ok(())
3973}
3974
3975#[allow(clippy::too_many_arguments)]
3982fn write_fork_data_with_overflow<R: Write + Seek>(
3983 writer: &mut R,
3984 partition_offset: u64,
3985 block_size: u32,
3986 fork: &ForkData,
3987 file_id: u32,
3988 fork_type: u8,
3989 extents_overflow_data: Option<&[u8]>,
3990 data: &[u8],
3991) -> Result<(), FilesystemError> {
3992 let mut written = 0usize;
3993 let write_extent = |writer: &mut R, ext: &ExtentDescriptor, written: &mut usize| {
3994 if ext.block_count == 0 || *written >= data.len() {
3995 return Ok(false);
3996 }
3997 let offset = partition_offset + ext.start_block as u64 * block_size as u64;
3998 let extent_len = ext.block_count as u64 * block_size as u64;
3999 let to_write = extent_len.min((data.len() - *written) as u64) as usize;
4000 writer.seek(SeekFrom::Start(offset))?;
4001 writer.write_all(&data[*written..*written + to_write])?;
4002 *written += to_write;
4003 Ok::<bool, FilesystemError>(true)
4004 };
4005
4006 for ext in &fork.extents {
4007 if ext.is_empty() {
4008 break;
4009 }
4010 if !write_extent(writer, ext, &mut written)? {
4011 return Ok(());
4012 }
4013 }
4014 if written >= data.len() {
4015 return Ok(());
4016 }
4017 let Some(ext_data) = extents_overflow_data else {
4019 return Err(FilesystemError::InvalidData(format!(
4020 "special file {file_id} fork {fork_type:#x}: {} bytes to write but only \
4021 inline extents and no extents-overflow B-tree",
4022 data.len()
4023 )));
4024 };
4025 let inline_blocks: u32 = fork.extents.iter().map(|e| e.block_count).sum();
4026 for ext in collect_hfsplus_overflow_extents(ext_data, file_id, fork_type, inline_blocks) {
4027 if !write_extent(writer, &ext, &mut written)? {
4028 break;
4029 }
4030 }
4031 if written < data.len() {
4032 return Err(FilesystemError::InvalidData(format!(
4033 "special file {file_id} fork {fork_type:#x}: extents cover {written} of {} bytes",
4034 data.len()
4035 )));
4036 }
4037 Ok(())
4038}
4039
4040fn decode_utf16be(data: &[u8]) -> String {
4042 let chars: Vec<u16> = data.chunks_exact(2).map(BigEndian::read_u16).collect();
4043 String::from_utf16_lossy(&chars)
4044}
4045
4046fn hfsplus_fragmentation_stats(
4054 catalog_data: &[u8],
4055 first_leaf: u32,
4056 node_size: usize,
4057) -> disk_core::filesystem::FragmentationStats {
4058 use crate::hfs_common::walk_leaf_records;
4059 use byteorder::{BigEndian, ByteOrder};
4060
4061 let mut stats = disk_core::filesystem::FragmentationStats::default();
4062 if node_size == 0 || catalog_data.is_empty() {
4063 return stats;
4064 }
4065 walk_leaf_records::<(), _>(
4066 catalog_data,
4067 first_leaf,
4068 node_size,
4069 |_node_idx, _rec_idx, _abs_off, rec| {
4070 if rec.len() < 4 {
4075 return None;
4076 }
4077 let key_len = BigEndian::read_u16(&rec[0..2]) as usize;
4078 let mut body_start = 2 + key_len;
4079 if !body_start.is_multiple_of(2) {
4080 body_start += 1;
4081 }
4082 if body_start + 88 + 80 > rec.len() {
4083 return None;
4084 }
4085 let body = &rec[body_start..];
4086 let rec_type = BigEndian::read_u16(&body[0..2]);
4087 if rec_type != 0x0002 {
4088 return None;
4089 }
4090 let data_fork = &body[88..88 + 80];
4091 let total_blocks = BigEndian::read_u32(&data_fork[12..16]);
4092 if total_blocks == 0 {
4093 return None;
4094 }
4095 let mut inline_extents: u32 = 0;
4096 let mut inline_blocks: u32 = 0;
4097 for i in 0..8 {
4098 let off = 16 + i * 8;
4099 let count = BigEndian::read_u32(&data_fork[off + 4..off + 8]);
4100 if count > 0 {
4101 inline_extents += 1;
4102 inline_blocks = inline_blocks.saturating_add(count);
4103 }
4104 }
4105 stats.files_with_data += 1;
4106 if inline_extents > 1 || total_blocks > inline_blocks {
4109 stats.fragmented_files += 1;
4110 }
4111 None
4112 },
4113 );
4114 stats
4115}
4116
4117fn find_volume_label(catalog_data: &[u8], header: &BTreeHeaderRecord) -> String {
4118 use crate::hfs_common::walk_leaf_records;
4119
4120 let node_size = header.node_size as usize;
4121 if node_size == 0 {
4122 return String::new();
4123 }
4124
4125 walk_leaf_records::<String, _>(
4126 catalog_data,
4127 header.first_leaf_node,
4128 node_size,
4129 |_node_idx, _rec_idx, _abs_off, rec| {
4130 if rec.len() < 8 {
4131 return None;
4132 }
4133 let key_len = BigEndian::read_u16(&rec[0..2]) as usize;
4134 if key_len < 6 || 2 + key_len > rec.len() {
4135 return None;
4136 }
4137 let parent_id = BigEndian::read_u32(&rec[2..6]);
4139 if parent_id != 2 {
4140 return None;
4141 }
4142 let name_length = BigEndian::read_u16(&rec[6..8]) as usize;
4143 if name_length != 0 {
4144 return None;
4145 }
4146
4147 let mut data_start = 2 + key_len;
4151 if !data_start.is_multiple_of(2) {
4152 data_start += 1;
4153 }
4154 if data_start + 10 > rec.len() {
4155 return None;
4156 }
4157 let record_type = BigEndian::read_i16(&rec[data_start..data_start + 2]);
4158 if record_type != 3 {
4160 return None;
4161 }
4162 let thread_name_len =
4163 BigEndian::read_u16(&rec[data_start + 8..data_start + 10]) as usize;
4164 let body_start = data_start + 10;
4165 let body_end = body_start + thread_name_len * 2;
4166 if body_end > rec.len() {
4167 return None;
4168 }
4169 Some(decode_utf16be(&rec[body_start..body_end]))
4170 },
4171 )
4172 .unwrap_or_default()
4173}
4174
4175fn find_last_set_bit(bitmap: &[u8], max_bits: u32) -> Option<u32> {
4181 let max = max_bits as usize;
4182 let scan_bytes = max.div_ceil(8).min(bitmap.len());
4183 for byte_idx in (0..scan_bytes).rev() {
4184 let byte = bitmap[byte_idx];
4185 if byte == 0 {
4186 continue;
4187 }
4188 for bit_pos in 0..8u32 {
4192 let global = (byte_idx as u32) * 8 + (7 - bit_pos);
4193 if global >= max_bits {
4194 continue;
4195 }
4196 if (byte >> bit_pos) & 1 == 1 {
4197 return Some(global);
4198 }
4199 }
4200 }
4201 None
4202}
4203
4204impl<R: Read + Write + Seek + Send> EditableFilesystem for HfsPlusFilesystem<R> {
4207 fn as_filesystem(&self) -> &dyn disk_core::filesystem::Filesystem {
4208 self
4209 }
4210 fn as_filesystem_mut(&mut self) -> &mut dyn disk_core::filesystem::Filesystem {
4211 self
4212 }
4213 fn create_file(
4214 &mut self,
4215 parent: &FileEntry,
4216 name: &str,
4217 data: &mut dyn std::io::Read,
4218 data_len: u64,
4219 options: &CreateFileOptions,
4220 ) -> Result<FileEntry, FilesystemError> {
4221 let snap = self.snapshot();
4222 let result = self.create_file_inner(parent, name, data, data_len, options);
4223 if result.is_err() {
4224 self.restore_snapshot(snap);
4225 }
4226 result
4227 }
4228
4229 fn create_directory(
4230 &mut self,
4231 parent: &FileEntry,
4232 name: &str,
4233 options: &CreateDirectoryOptions,
4234 ) -> Result<FileEntry, FilesystemError> {
4235 let snap = self.snapshot();
4236 let result = self.create_directory_inner(parent, name, options);
4237 if result.is_err() {
4238 self.restore_snapshot(snap);
4239 }
4240 result
4241 }
4242
4243 fn delete_entry(
4244 &mut self,
4245 parent: &FileEntry,
4246 entry: &FileEntry,
4247 ) -> Result<(), FilesystemError> {
4248 let snap = self.snapshot();
4249 let result = self.delete_entry_inner(parent, entry);
4250 if result.is_err() {
4251 self.restore_snapshot(snap);
4252 }
4253 result
4254 }
4255
4256 fn rename(
4257 &mut self,
4258 parent: &FileEntry,
4259 entry: &FileEntry,
4260 new_name: &str,
4261 ) -> Result<(), FilesystemError> {
4262 if new_name == entry.name {
4263 return Ok(());
4264 }
4265 validate_hfsplus_create_name(new_name)?;
4266 let snap = self.snapshot();
4267 let result = self.rename_inner(parent, entry, new_name);
4268 if result.is_err() {
4269 self.restore_snapshot(snap);
4270 }
4271 result
4272 }
4273
4274 fn set_type_creator(
4275 &mut self,
4276 entry: &FileEntry,
4277 type_code: &str,
4278 creator_code: &str,
4279 ) -> Result<(), FilesystemError> {
4280 let snap = self.snapshot();
4281 let result = self.set_type_creator_inner(entry, type_code, creator_code);
4282 if result.is_err() {
4283 self.restore_snapshot(snap);
4284 }
4285 result
4286 }
4287
4288 fn write_resource_fork(
4289 &mut self,
4290 entry: &FileEntry,
4291 data: &mut dyn std::io::Read,
4292 len: u64,
4293 ) -> Result<(), FilesystemError> {
4294 let snap = self.snapshot();
4295 let result = self.write_resource_fork_inner(entry, data, len);
4296 if result.is_err() {
4297 self.restore_snapshot(snap);
4298 }
4299 result
4300 }
4301
4302 fn set_permissions(&mut self, entry: &FileEntry, mode: u32) -> Result<(), FilesystemError> {
4303 let frec = self.record_body_offset(entry.location as u32)?;
4304 let base = Self::bsd_type_bits(&self.catalog_data, frec, entry.is_directory());
4305 let new = disk_core::unix_common::inode::with_permission_bits(base, mode) as u16;
4306 let at = frec + HfsPlusBsdInfo::OFFSET + 10;
4307 BigEndian::write_u16(&mut self.catalog_data[at..at + 2], new);
4308 Ok(())
4309 }
4310
4311 fn set_owner(&mut self, entry: &FileEntry, uid: u32, gid: u32) -> Result<(), FilesystemError> {
4312 let frec = self.record_body_offset(entry.location as u32)?;
4314 let at = frec + HfsPlusBsdInfo::OFFSET;
4315 BigEndian::write_u32(&mut self.catalog_data[at..at + 4], uid);
4316 BigEndian::write_u32(&mut self.catalog_data[at + 4..at + 8], gid);
4317 let mode_at = frec + HfsPlusBsdInfo::OFFSET + 10;
4322 if BigEndian::read_u16(&self.catalog_data[mode_at..mode_at + 2]) == 0 {
4323 let default_mode = if entry.is_directory() {
4324 0o040_755
4325 } else {
4326 0o100_644
4327 };
4328 BigEndian::write_u16(
4329 &mut self.catalog_data[mode_at..mode_at + 2],
4330 default_mode as u16,
4331 );
4332 }
4333 Ok(())
4334 }
4335
4336 fn sync_metadata(&mut self) -> Result<(), FilesystemError> {
4337 self.do_sync_metadata()
4338 }
4339
4340 fn free_space(&mut self) -> Result<u64, FilesystemError> {
4341 Ok(self.vh.free_blocks as u64 * self.vh.block_size as u64)
4342 }
4343
4344 fn set_blessed_folder(&mut self, entry: &FileEntry) -> Result<(), FilesystemError> {
4345 let snap = self.snapshot();
4346 let result = self.set_blessed_folder_inner(entry);
4347 if result.is_err() {
4348 self.restore_snapshot(snap);
4349 }
4350 result
4351 }
4352
4353 fn write_boot_blocks(&mut self, blocks: &[u8; 1024]) -> Result<(), FilesystemError> {
4354 self.set_boot_blocks(blocks);
4355 Ok(())
4356 }
4357}
4358
4359impl<R: Read + Write + Seek + Send> HfsPlusFilesystem<R> {
4360 pub fn set_xattr(
4371 &mut self,
4372 cnid: u32,
4373 name: &str,
4374 value: &[u8],
4375 ) -> Result<(), FilesystemError> {
4376 let snap = self.snapshot();
4377 let result = (|| -> Result<(), FilesystemError> {
4378 self.remove_xattr_records(cnid, Some(name))?;
4379 let rec = Self::build_inline_attr_record(cnid, name, value);
4380 self.insert_xattr_record(&rec)?;
4381 if let Some(set) = self.xattr_cnids.as_mut() {
4382 set.insert(cnid);
4383 }
4384 Ok(())
4385 })();
4386 if result.is_err() {
4387 self.restore_snapshot(snap);
4388 }
4389 result
4390 }
4391
4392 pub fn remove_xattr(&mut self, cnid: u32, name: &str) -> Result<(), FilesystemError> {
4395 let snap = self.snapshot();
4396 let result = (|| -> Result<(), FilesystemError> {
4397 let removed = self.remove_xattr_records(cnid, Some(name))?;
4398 if removed == 0 {
4399 return Err(FilesystemError::NotFound(format!(
4400 "extended attribute '{name}' on cnid {cnid}"
4401 )));
4402 }
4403 if self.list_xattrs(cnid)?.is_empty() {
4405 if let Some(set) = self.xattr_cnids.as_mut() {
4406 set.remove(&cnid);
4407 }
4408 }
4409 Ok(())
4410 })();
4411 if result.is_err() {
4412 self.restore_snapshot(snap);
4413 }
4414 result
4415 }
4416}
4417
4418#[derive(Debug, Clone, Default)]
4428pub(crate) struct RecordMetadata {
4429 pub create_date: u32,
4430 pub content_mod_date: u32,
4431 pub attribute_mod_date: u32,
4432 pub access_date: u32,
4433 pub backup_date: u32,
4434 pub bsd_info: [u8; 16],
4435 pub finder_info: [u8; 16],
4436 pub extended_finder_info: [u8; 16],
4437 pub text_encoding: u32,
4438}
4439
4440impl<R: Read + Write + Seek + Send> HfsPlusFilesystem<R> {
4441 pub(crate) fn record_body_offset(&mut self, cnid: u32) -> Result<usize, FilesystemError> {
4458 let (_, _, t_offset) = self.find_catalog_record_by_cnid(cnid).ok_or_else(|| {
4459 FilesystemError::NotFound(format!("thread record for CNID {cnid} not found"))
4460 })?;
4461 let key_len = BigEndian::read_u16(&self.catalog_data[t_offset..t_offset + 2]) as usize;
4462 let mut rec_data_start = t_offset + 2 + key_len;
4463 if !rec_data_start.is_multiple_of(2) {
4464 rec_data_start += 1;
4465 }
4466 let thread_parent =
4467 BigEndian::read_u32(&self.catalog_data[rec_data_start + 4..rec_data_start + 8]);
4468 let thread_name_len =
4469 BigEndian::read_u16(&self.catalog_data[rec_data_start + 8..rec_data_start + 10])
4470 as usize;
4471 let thread_name = decode_utf16be(
4472 &self.catalog_data[rec_data_start + 10..rec_data_start + 10 + thread_name_len * 2],
4473 );
4474
4475 let (_, _, f_offset) = self
4476 .find_catalog_record(thread_parent, &thread_name)
4477 .ok_or_else(|| {
4478 FilesystemError::NotFound(format!(
4479 "catalog record for CNID {cnid} ('{thread_name}') not found"
4480 ))
4481 })?;
4482 let fkey_len = BigEndian::read_u16(&self.catalog_data[f_offset..f_offset + 2]) as usize;
4483 let mut frec = f_offset + 2 + fkey_len;
4484 if !frec.is_multiple_of(2) {
4485 frec += 1;
4486 }
4487 if frec + 84 > self.catalog_data.len() {
4488 return Err(FilesystemError::InvalidData(format!(
4489 "catalog record for CNID {cnid} truncated"
4490 )));
4491 }
4492 Ok(frec)
4493 }
4494
4495 fn bsd_type_bits(catalog: &[u8], frec: usize, is_dir: bool) -> u32 {
4504 let at = frec + HfsPlusBsdInfo::OFFSET + 10;
4505 let cur = BigEndian::read_u16(&catalog[at..at + 2]) as u32;
4506 if cur & 0o170_000 != 0 {
4507 cur
4508 } else if is_dir {
4509 0o040_000
4510 } else {
4511 0o100_000
4512 }
4513 }
4514
4515 pub(crate) fn set_record_metadata(
4516 &mut self,
4517 cnid: u32,
4518 meta: &RecordMetadata,
4519 ) -> Result<(), FilesystemError> {
4520 let frec = self.record_body_offset(cnid)?;
4521
4522 BigEndian::write_u32(
4523 &mut self.catalog_data[frec + 12..frec + 16],
4524 meta.create_date,
4525 );
4526 BigEndian::write_u32(
4527 &mut self.catalog_data[frec + 16..frec + 20],
4528 meta.content_mod_date,
4529 );
4530 BigEndian::write_u32(
4531 &mut self.catalog_data[frec + 20..frec + 24],
4532 meta.attribute_mod_date,
4533 );
4534 BigEndian::write_u32(
4535 &mut self.catalog_data[frec + 24..frec + 28],
4536 meta.access_date,
4537 );
4538 BigEndian::write_u32(
4539 &mut self.catalog_data[frec + 28..frec + 32],
4540 meta.backup_date,
4541 );
4542 self.catalog_data[frec + 32..frec + 48].copy_from_slice(&meta.bsd_info);
4543 self.catalog_data[frec + 48..frec + 64].copy_from_slice(&meta.finder_info);
4544 self.catalog_data[frec + 64..frec + 80].copy_from_slice(&meta.extended_finder_info);
4545 BigEndian::write_u32(
4546 &mut self.catalog_data[frec + 80..frec + 84],
4547 meta.text_encoding,
4548 );
4549 Ok(())
4550 }
4551
4552 pub(crate) fn set_volume_dates(
4560 &mut self,
4561 create_date: u32,
4562 modify_date: u32,
4563 backup_date: u32,
4564 checked_date: u32,
4565 ) {
4566 self.vh.create_date = create_date;
4567 self.vh.modify_date = modify_date;
4568 self.vh.backup_date = backup_date;
4569 self.vh.checked_date = checked_date;
4570 }
4571
4572 pub(crate) fn set_volume_finder_info(&mut self, finder_info: [u32; 8]) {
4576 self.vh.finder_info = finder_info;
4577 }
4578
4579 pub(crate) fn set_boot_blocks(&mut self, blocks: &[u8; 1024]) {
4584 self.pending_boot_blocks = Some(Box::new(*blocks));
4585 }
4586}
4587
4588impl<R: Read + Write + Seek + Send> HfsPlusFilesystem<R> {
4589 pub fn ensure_private_dir(&mut self) -> Result<FileEntry, FilesystemError> {
4595 let private_name = hfsplus_private_dir_name();
4596 let root = self.root()?;
4597 for entry in self.list_directory(&root)? {
4598 if entry.is_directory() && entry.name == private_name {
4599 return Ok(entry);
4600 }
4601 }
4602 self.create_directory(&root, &private_name, &CreateDirectoryOptions::default())
4603 }
4604
4605 pub fn ensure_dir_private_dir(&mut self) -> Result<FileEntry, FilesystemError> {
4609 let dir_private_name = hfsplus_dir_private_dir_name();
4610 let root = self.root()?;
4611 for entry in self.list_directory(&root)? {
4612 if entry.is_directory() && entry.name == dir_private_name {
4613 return Ok(entry);
4614 }
4615 }
4616 self.create_directory(&root, &dir_private_name, &CreateDirectoryOptions::default())
4617 }
4618
4619 pub fn create_hardlink_inode(
4629 &mut self,
4630 inode_num: u32,
4631 data: &mut dyn std::io::Read,
4632 data_len: u64,
4633 options: &CreateFileOptions,
4634 ) -> Result<FileEntry, FilesystemError> {
4635 let snap = self.snapshot();
4636 let result = (|| -> Result<FileEntry, FilesystemError> {
4637 let private = self.ensure_private_dir()?;
4638 let inode_name = format!("iNode{inode_num}");
4639 self.create_file(&private, &inode_name, data, data_len, options)
4640 })();
4641 if result.is_err() {
4642 self.restore_snapshot(snap);
4643 }
4644 result
4645 }
4646
4647 pub fn create_hardlink(
4657 &mut self,
4658 parent: &FileEntry,
4659 name: &str,
4660 inode_num: u32,
4661 ) -> Result<FileEntry, FilesystemError> {
4662 let snap = self.snapshot();
4663 let result = (|| -> Result<FileEntry, FilesystemError> {
4664 let private_cnid = self.find_private_dir_cnid()?.ok_or_else(|| {
4665 FilesystemError::NotFound(
4666 "HFS+ Private Data directory missing — call create_hardlink_inode first".into(),
4667 )
4668 })?;
4669 let inode_name = format!("iNode{inode_num}");
4671 let inode_cnid = self
4672 .list_children(private_cnid)?
4673 .into_iter()
4674 .find_map(|c| match c {
4675 CatalogEntry::File {
4676 file_id, name: n, ..
4677 } if n == inode_name => Some(file_id),
4678 _ => None,
4679 })
4680 .ok_or_else(|| {
4681 FilesystemError::NotFound(format!(
4682 "hardlink inode '{inode_name}' missing — create it before linking"
4683 ))
4684 })?;
4685
4686 let mut empty: &[u8] = &[];
4688 let stub = self.create_file(
4689 parent,
4690 name,
4691 &mut empty,
4692 0,
4693 &CreateFileOptions {
4694 os_type: Some(*b"hlnk"),
4695 os_creator: Some(*b"hfs+"),
4696 ..Default::default()
4697 },
4698 )?;
4699
4700 let parent_cnid = parent.location as u32;
4702 let (_, _, abs) = self.find_catalog_record(parent_cnid, name).ok_or_else(|| {
4703 FilesystemError::NotFound(format!(
4704 "freshly-created stub '{name}' missing from catalog"
4705 ))
4706 })?;
4707 let key_len = BigEndian::read_u16(&self.catalog_data[abs..abs + 2]) as usize;
4708 let mut body = abs + 2 + key_len;
4709 if !body.is_multiple_of(2) {
4710 body += 1;
4711 }
4712 BigEndian::write_u32(&mut self.catalog_data[body + 44..body + 48], inode_num);
4713
4714 self.adjust_inode_link_count(private_cnid, inode_num, 1)?;
4716
4717 self.hardlink_inode_map = None;
4720
4721 let mut fe = stub;
4724 fe.link_target_cnid = Some(inode_cnid as u64);
4725 Ok(fe)
4726 })();
4727 if result.is_err() {
4728 self.restore_snapshot(snap);
4729 }
4730 result
4731 }
4732
4733 pub fn create_dir_hardlink_inode(
4739 &mut self,
4740 inode_num: u32,
4741 options: &CreateDirectoryOptions,
4742 ) -> Result<FileEntry, FilesystemError> {
4743 let snap = self.snapshot();
4744 let result = (|| -> Result<FileEntry, FilesystemError> {
4745 let private = self.ensure_dir_private_dir()?;
4746 let inode_name = format!("dir_{inode_num}");
4747 self.create_directory(&private, &inode_name, options)
4748 })();
4749 if result.is_err() {
4750 self.restore_snapshot(snap);
4751 }
4752 result
4753 }
4754
4755 pub fn create_dir_hardlink(
4762 &mut self,
4763 parent: &FileEntry,
4764 name: &str,
4765 inode_num: u32,
4766 ) -> Result<FileEntry, FilesystemError> {
4767 let snap = self.snapshot();
4768 let result = (|| -> Result<FileEntry, FilesystemError> {
4769 let dir_private_cnid = self.find_dir_private_dir_cnid()?.ok_or_else(|| {
4770 FilesystemError::NotFound(
4771 ".HFS+ Private Directory Data missing — call create_dir_hardlink_inode first"
4772 .into(),
4773 )
4774 })?;
4775 let inode_name = format!("dir_{inode_num}");
4776 let inode_cnid = self
4777 .list_children(dir_private_cnid)?
4778 .into_iter()
4779 .find_map(|c| match c {
4780 CatalogEntry::Folder {
4781 folder_id, name: n, ..
4782 } if n == inode_name => Some(folder_id),
4783 _ => None,
4784 })
4785 .ok_or_else(|| {
4786 FilesystemError::NotFound(format!(
4787 "directory inode '{inode_name}' missing — create it before linking"
4788 ))
4789 })?;
4790
4791 let mut empty: &[u8] = &[];
4792 let stub = self.create_file(
4793 parent,
4794 name,
4795 &mut empty,
4796 0,
4797 &CreateFileOptions {
4798 os_type: Some(*b"fdrp"),
4799 os_creator: Some(*b"MACS"),
4800 ..Default::default()
4801 },
4802 )?;
4803
4804 let parent_cnid = parent.location as u32;
4806 let (_, _, abs) = self.find_catalog_record(parent_cnid, name).ok_or_else(|| {
4807 FilesystemError::NotFound(format!(
4808 "freshly-created stub '{name}' missing from catalog"
4809 ))
4810 })?;
4811 let key_len = BigEndian::read_u16(&self.catalog_data[abs..abs + 2]) as usize;
4812 let mut body = abs + 2 + key_len;
4813 if !body.is_multiple_of(2) {
4814 body += 1;
4815 }
4816 BigEndian::write_u32(&mut self.catalog_data[body + 44..body + 48], inode_num);
4817
4818 self.adjust_dir_inode_link_count(dir_private_cnid, inode_num, 1)?;
4821
4822 self.dir_hardlink_inode_map = None;
4824
4825 let mut fe = stub;
4828 fe.entry_type = disk_core::entry::EntryType::Directory;
4829 fe.size = 0;
4830 fe.link_target_cnid = Some(inode_cnid as u64);
4831 Ok(fe)
4832 })();
4833 if result.is_err() {
4834 self.restore_snapshot(snap);
4835 }
4836 result
4837 }
4838
4839 fn adjust_dir_inode_link_count(
4843 &mut self,
4844 dir_private_cnid: u32,
4845 inode_num: u32,
4846 delta: i64,
4847 ) -> Result<u32, FilesystemError> {
4848 let inode_name = format!("dir_{inode_num}");
4849 let (_, _, abs_off) = self
4850 .find_catalog_record(dir_private_cnid, &inode_name)
4851 .ok_or_else(|| {
4852 FilesystemError::InvalidData(format!(
4853 "directory inode '{inode_name}' missing from .HFS+ Private Directory Data"
4854 ))
4855 })?;
4856 let key_len = BigEndian::read_u16(&self.catalog_data[abs_off..abs_off + 2]) as usize;
4857 let mut body_start = abs_off + 2 + key_len;
4858 if !body_start.is_multiple_of(2) {
4859 body_start += 1;
4860 }
4861 let cur = BigEndian::read_u32(&self.catalog_data[body_start + 44..body_start + 48]) as i64;
4862 let new = (cur + delta).max(0) as u32;
4863 BigEndian::write_u32(
4864 &mut self.catalog_data[body_start + 44..body_start + 48],
4865 new,
4866 );
4867 Ok(new)
4868 }
4869
4870 fn create_file_inner(
4871 &mut self,
4872 parent: &FileEntry,
4873 name: &str,
4874 data: &mut dyn std::io::Read,
4875 data_len: u64,
4876 options: &CreateFileOptions,
4877 ) -> Result<FileEntry, FilesystemError> {
4878 let parent_cnid = parent.location as u32;
4879
4880 validate_hfsplus_create_name(name)?;
4881
4882 if self.find_catalog_record(parent_cnid, name).is_some() {
4884 return Err(FilesystemError::AlreadyExists(name.into()));
4885 }
4886
4887 let file_id = self.vh.next_catalog_id;
4889 self.vh.next_catalog_id += 1;
4890
4891 let (type_code, creator_code) = hfs_common::resolve_create_type_creator(
4895 name,
4896 options.os_type,
4897 options.os_creator,
4898 options.type_code.as_deref(),
4899 options.creator_code.as_deref(),
4900 );
4901
4902 let data_fork = self.write_data_to_blocks(data, data_len, file_id, HFSPLUS_FORK_DATA)?;
4904
4905 let rsrc_fork = if let Some(ref rsrc_src) = options.resource_fork {
4907 match rsrc_src {
4908 disk_core::filesystem::ResourceForkSource::Data(rsrc_data) => {
4909 let mut cursor = std::io::Cursor::new(rsrc_data);
4910 self.write_data_to_blocks(
4911 &mut cursor,
4912 rsrc_data.len() as u64,
4913 file_id,
4914 HFSPLUS_FORK_RESOURCE,
4915 )?
4916 }
4917 disk_core::filesystem::ResourceForkSource::File(path) => {
4918 let mut f = std::fs::File::open(path)?;
4919 let len = f.metadata()?.len();
4920 self.write_data_to_blocks(&mut f, len, file_id, HFSPLUS_FORK_RESOURCE)?
4921 }
4922 }
4923 } else {
4924 ForkData::empty()
4925 };
4926
4927 let mtime = options.unix_times.map(|t| t.mtime_or_now());
4931 let file_rec = Self::build_file_record_with_dates(
4932 file_id,
4933 &data_fork,
4934 &rsrc_fork,
4935 &type_code,
4936 &creator_code,
4937 mtime,
4938 options.finder_flags.unwrap_or(0),
4939 );
4940
4941 let key = Self::build_catalog_key(parent_cnid, name);
4943 let mut key_record = key.clone();
4944 if key_record.len() % 2 != 0 {
4946 key_record.push(0);
4947 }
4948 key_record.extend_from_slice(&file_rec);
4949
4950 self.insert_catalog_record(&key_record)?;
4952
4953 let (thread_key, thread_data) =
4955 Self::build_thread_record(CATALOG_FILE_THREAD, parent_cnid, name);
4956 let mut thread_record = thread_key;
4957 if thread_record.len() % 2 != 0 {
4958 thread_record.push(0);
4959 }
4960 thread_record.extend_from_slice(&thread_data);
4961 let actual_thread_key = Self::build_catalog_key(file_id, "");
4964 let mut actual_thread_record = actual_thread_key;
4965 if actual_thread_record.len() % 2 != 0 {
4966 actual_thread_record.push(0);
4967 }
4968 actual_thread_record.extend_from_slice(&thread_data);
4969 self.insert_catalog_record(&actual_thread_record)?;
4970
4971 self.update_parent_valence(parent_cnid, 1)?;
4973
4974 self.vh.file_count += 1;
4976
4977 let path = if parent.path == "/" {
4978 format!("/{name}")
4979 } else {
4980 format!("{}/{name}", parent.path)
4981 };
4982 let mut fe = FileEntry::new_file(name.to_string(), path, data_len, file_id as u64);
4983 if type_code != [0; 4] {
4984 fe.type_code = Some(type_code);
4985 fe.creator_code = Some(creator_code);
4986 }
4987 if rsrc_fork.logical_size > 0 {
4988 fe.resource_fork_size = Some(rsrc_fork.logical_size);
4989 }
4990 let stamped = mtime.unwrap_or_else(disk_core::times::now);
4991 fe.modified_unix = Some(stamped);
4992 Ok(fe)
4993 }
4994
4995 fn create_directory_inner(
4996 &mut self,
4997 parent: &FileEntry,
4998 name: &str,
4999 options: &CreateDirectoryOptions,
5000 ) -> Result<FileEntry, FilesystemError> {
5001 let parent_cnid = parent.location as u32;
5002
5003 validate_hfsplus_create_name(name)?;
5004
5005 if parent_cnid == 2
5017 && (name == hfsplus_private_dir_name() || name == hfsplus_dir_private_dir_name())
5018 {
5019 let already_present = self
5020 .find_private_dir_cnid()?
5021 .filter(|_| name == hfsplus_private_dir_name())
5022 .is_some()
5023 || self
5024 .find_dir_private_dir_cnid()?
5025 .filter(|_| name == hfsplus_dir_private_dir_name())
5026 .is_some();
5027 if already_present {
5028 let bt = std::backtrace::Backtrace::force_capture();
5029 log::error!(
5030 "[HFS+] refused create_directory for magic name (already \
5031 present). parent_cnid={}, leading NULs={}\nbacktrace:\n{bt}",
5032 parent_cnid,
5033 name.chars().take_while(|c| *c == '\0').count()
5034 );
5035 return Err(FilesystemError::AlreadyExists(name.into()));
5036 }
5037 }
5038
5039 if self.find_catalog_record(parent_cnid, name).is_some() {
5041 return Err(FilesystemError::AlreadyExists(name.into()));
5042 }
5043
5044 let folder_id = self.vh.next_catalog_id;
5046 self.vh.next_catalog_id += 1;
5047
5048 let mtime = options.unix_times.map(|t| t.mtime_or_now());
5051 let folder_rec = Self::build_folder_record_with_dates(folder_id, mtime);
5052
5053 let key = Self::build_catalog_key(parent_cnid, name);
5055 let mut key_record = key.clone();
5056 if key_record.len() % 2 != 0 {
5057 key_record.push(0);
5058 }
5059 key_record.extend_from_slice(&folder_rec);
5060
5061 self.insert_catalog_record(&key_record)?;
5062
5063 let (_thread_key, thread_data) =
5065 Self::build_thread_record(CATALOG_FOLDER_THREAD, parent_cnid, name);
5066 let actual_thread_key = Self::build_catalog_key(folder_id, "");
5067 let mut actual_thread_record = actual_thread_key;
5068 if actual_thread_record.len() % 2 != 0 {
5069 actual_thread_record.push(0);
5070 }
5071 actual_thread_record.extend_from_slice(&thread_data);
5072 self.insert_catalog_record(&actual_thread_record)?;
5073
5074 self.update_parent_valence(parent_cnid, 1)?;
5076
5077 self.vh.folder_count += 1;
5079
5080 let path = if parent.path == "/" {
5081 format!("/{name}")
5082 } else {
5083 format!("{}/{name}", parent.path)
5084 };
5085 let mut fe = FileEntry::new_directory(name.to_string(), path, folder_id as u64);
5086 let stamped = mtime.unwrap_or_else(disk_core::times::now);
5087 fe.modified_unix = Some(stamped);
5088 Ok(fe)
5089 }
5090
5091 fn delete_entry_inner(
5092 &mut self,
5093 parent: &FileEntry,
5094 entry: &FileEntry,
5095 ) -> Result<(), FilesystemError> {
5096 let parent_cnid = parent.location as u32;
5097 let cnid = entry.location as u32;
5098
5099 let xattrs_removed = self.remove_xattr_records(cnid, None)?;
5104 if xattrs_removed > 0 {
5105 if let Some(set) = self.xattr_cnids.as_mut() {
5106 set.remove(&cnid);
5107 }
5108 }
5109
5110 if entry.is_directory() {
5112 let children = self.list_children(cnid)?;
5113 if !children.is_empty() {
5114 return Err(FilesystemError::InvalidData(
5115 "cannot delete non-empty directory".into(),
5116 ));
5117 }
5118 }
5119
5120 let link_inode_num = if entry.is_directory() {
5125 None
5126 } else {
5127 self.read_file_record_link_inode_num(parent_cnid, &entry.name)?
5128 };
5129 if let Some(inode_num) = link_inode_num {
5130 self.delete_hardlink_inode_ref(inode_num)?;
5131 }
5132
5133 let (node_idx, rec_idx, _offset) = self
5135 .find_catalog_record(parent_cnid, &entry.name)
5136 .ok_or_else(|| {
5137 FilesystemError::NotFound(format!("entry '{}' not found in catalog", entry.name))
5138 })?;
5139
5140 if !entry.is_directory() {
5145 if let Some((data_fork, rsrc_fork)) = self.find_file_by_id(cnid) {
5146 self.free_fork_overflow_extents(cnid, HFSPLUS_FORK_DATA, &data_fork);
5147 self.free_fork_overflow_extents(cnid, HFSPLUS_FORK_RESOURCE, &rsrc_fork);
5148 self.remove_extents_overflow_records_for(cnid, HFSPLUS_FORK_DATA)?;
5149 self.remove_extents_overflow_records_for(cnid, HFSPLUS_FORK_RESOURCE)?;
5150 self.free_fork_blocks(&data_fork);
5151 self.free_fork_blocks(&rsrc_fork);
5152 }
5153 }
5154
5155 self.remove_catalog_record(node_idx, rec_idx)?;
5156
5157 if let Some((t_node, t_rec, _)) = self.find_catalog_record_by_cnid(cnid) {
5159 self.remove_catalog_record(t_node, t_rec)?;
5160 }
5161
5162 self.update_parent_valence(parent_cnid, -1)?;
5164
5165 if entry.is_directory() {
5167 self.vh.folder_count = self.vh.folder_count.saturating_sub(1);
5168 } else {
5169 self.vh.file_count = self.vh.file_count.saturating_sub(1);
5170 }
5171
5172 Ok(())
5173 }
5174
5175 fn rename_inner(
5176 &mut self,
5177 parent: &FileEntry,
5178 entry: &FileEntry,
5179 new_name: &str,
5180 ) -> Result<(), FilesystemError> {
5181 let parent_cnid = parent.location as u32;
5182 let cnid = entry.location as u32;
5183
5184 let (node_idx, rec_idx, _abs) = self
5186 .find_catalog_record(parent_cnid, &entry.name)
5187 .ok_or_else(|| {
5188 FilesystemError::NotFound(format!("entry '{}' not found in catalog", entry.name))
5189 })?;
5190
5191 if let Some((n2, r2, _)) = self.find_catalog_record(parent_cnid, new_name) {
5194 if (n2, r2) != (node_idx, rec_idx) {
5195 return Err(FilesystemError::AlreadyExists(new_name.to_string()));
5196 }
5197 }
5198
5199 let node_size = self.catalog_node_size();
5202 let node_off = node_idx as usize * node_size;
5203 let node = &self.catalog_data[node_off..node_off + node_size];
5204 let (rec_start, rec_end) = crate::hfs_common::btree_record_range(node, node_size, rec_idx);
5205 let key_len = BigEndian::read_u16(&node[rec_start..rec_start + 2]) as usize;
5206 let mut data_rel = 2 + key_len;
5207 if !data_rel.is_multiple_of(2) {
5208 data_rel += 1;
5209 }
5210 let data_bytes = node[rec_start + data_rel..rec_end].to_vec();
5211
5212 let mut new_record = Self::build_catalog_key(parent_cnid, new_name);
5213 if new_record.len() % 2 != 0 {
5214 new_record.push(0);
5215 }
5216 new_record.extend_from_slice(&data_bytes);
5217
5218 self.remove_catalog_record(node_idx, rec_idx)?;
5219 self.insert_catalog_record(&new_record)?;
5220
5221 if let Some((t_node, t_rec, _)) = self.find_catalog_record_by_cnid(cnid) {
5226 self.remove_catalog_record(t_node, t_rec)?;
5227 let thread_type = if entry.is_directory() {
5228 CATALOG_FOLDER_THREAD
5229 } else {
5230 CATALOG_FILE_THREAD
5231 };
5232 let (_k, thread_data) = Self::build_thread_record(thread_type, parent_cnid, new_name);
5233 let mut thread_record = Self::build_catalog_key(cnid, "");
5234 if thread_record.len() % 2 != 0 {
5235 thread_record.push(0);
5236 }
5237 thread_record.extend_from_slice(&thread_data);
5238 self.insert_catalog_record(&thread_record)?;
5239 }
5240 Ok(())
5242 }
5243
5244 fn adjust_inode_link_count(
5250 &mut self,
5251 private_cnid: u32,
5252 inode_num: u32,
5253 delta: i64,
5254 ) -> Result<u32, FilesystemError> {
5255 let inode_name = format!("iNode{inode_num}");
5256 let (_, _, abs_off) = self
5257 .find_catalog_record(private_cnid, &inode_name)
5258 .ok_or_else(|| {
5259 FilesystemError::InvalidData(format!(
5260 "hardlink inode '{inode_name}' missing from HFS+ Private Data"
5261 ))
5262 })?;
5263 let key_len = BigEndian::read_u16(&self.catalog_data[abs_off..abs_off + 2]) as usize;
5264 let mut body_start = abs_off + 2 + key_len;
5265 if !body_start.is_multiple_of(2) {
5266 body_start += 1;
5267 }
5268 let cur = BigEndian::read_u32(&self.catalog_data[body_start + 44..body_start + 48]) as i64;
5269 let new = (cur + delta).max(0) as u32;
5270 BigEndian::write_u32(
5271 &mut self.catalog_data[body_start + 44..body_start + 48],
5272 new,
5273 );
5274 self.stamp_record_dates(body_start, false);
5275 Ok(new)
5276 }
5277
5278 fn delete_hardlink_inode_ref(&mut self, inode_num: u32) -> Result<(), FilesystemError> {
5284 let private_cnid = self.find_private_dir_cnid()?.ok_or_else(|| {
5285 FilesystemError::InvalidData(
5286 "hardlink stub on volume with no HFS+ Private Data directory".into(),
5287 )
5288 })?;
5289 let inode_cnid = self.resolve_hardlink_inode(inode_num)?.ok_or_else(|| {
5290 FilesystemError::InvalidData(format!(
5291 "hardlink target iNode{inode_num} missing from HFS+ Private Data"
5292 ))
5293 })?;
5294
5295 let new_count = self.adjust_inode_link_count(private_cnid, inode_num, -1)?;
5296 if new_count > 0 {
5297 return Ok(());
5298 }
5299
5300 if let Some((data_fork, rsrc_fork)) = self.find_file_by_id(inode_cnid) {
5302 self.free_fork_overflow_extents(inode_cnid, HFSPLUS_FORK_DATA, &data_fork);
5303 self.free_fork_overflow_extents(inode_cnid, HFSPLUS_FORK_RESOURCE, &rsrc_fork);
5304 self.remove_extents_overflow_records_for(inode_cnid, HFSPLUS_FORK_DATA)?;
5305 self.remove_extents_overflow_records_for(inode_cnid, HFSPLUS_FORK_RESOURCE)?;
5306 self.free_fork_blocks(&data_fork);
5307 self.free_fork_blocks(&rsrc_fork);
5308 }
5309
5310 let xattrs_removed = self.remove_xattr_records(inode_cnid, None)?;
5311 if xattrs_removed > 0 {
5312 if let Some(set) = self.xattr_cnids.as_mut() {
5313 set.remove(&inode_cnid);
5314 }
5315 }
5316
5317 let inode_name = format!("iNode{inode_num}");
5318 if let Some((n, r, _)) = self.find_catalog_record(private_cnid, &inode_name) {
5319 self.remove_catalog_record(n, r)?;
5320 }
5321 if let Some((tn, tr, _)) = self.find_catalog_record_by_cnid(inode_cnid) {
5322 self.remove_catalog_record(tn, tr)?;
5323 }
5324 self.update_parent_valence(private_cnid, -1)?;
5325 self.vh.file_count = self.vh.file_count.saturating_sub(1);
5326
5327 if let Some(map) = self.hardlink_inode_map.as_mut() {
5328 map.remove(&inode_num);
5329 }
5330 Ok(())
5331 }
5332
5333 fn set_type_creator_inner(
5334 &mut self,
5335 entry: &FileEntry,
5336 type_code: &str,
5337 creator_code: &str,
5338 ) -> Result<(), FilesystemError> {
5339 let cnid = entry.location as u32;
5340
5341 let (_t_node, _t_rec, t_offset) =
5343 self.find_catalog_record_by_cnid(cnid).ok_or_else(|| {
5344 FilesystemError::NotFound(format!("thread for CNID {cnid} not found"))
5345 })?;
5346
5347 let node_size = self.catalog_header.node_size as usize;
5349 let key_len = BigEndian::read_u16(&self.catalog_data[t_offset..t_offset + 2]) as usize;
5350 let mut rec_data_start = t_offset + 2 + key_len;
5351 if !rec_data_start.is_multiple_of(2) {
5352 rec_data_start += 1;
5353 }
5354 let thread_parent =
5355 BigEndian::read_u32(&self.catalog_data[rec_data_start + 4..rec_data_start + 8]);
5356 let thread_name_len =
5357 BigEndian::read_u16(&self.catalog_data[rec_data_start + 8..rec_data_start + 10])
5358 as usize;
5359 let thread_name = decode_utf16be(
5360 &self.catalog_data[rec_data_start + 10..rec_data_start + 10 + thread_name_len * 2],
5361 );
5362
5363 let (f_node, f_rec, f_offset) = self
5365 .find_catalog_record(thread_parent, &thread_name)
5366 .ok_or_else(|| {
5367 FilesystemError::NotFound(format!("file record for '{}' not found", thread_name))
5368 })?;
5369
5370 let fkey_len = BigEndian::read_u16(&self.catalog_data[f_offset..f_offset + 2]) as usize;
5372 let mut frec_start = f_offset + 2 + fkey_len;
5373 if !frec_start.is_multiple_of(2) {
5374 frec_start += 1;
5375 }
5376
5377 let tc = hfs_common::encode_fourcc(type_code);
5379 let cc = hfs_common::encode_fourcc(creator_code);
5380 self.catalog_data[frec_start + 48..frec_start + 52].copy_from_slice(&tc);
5381 self.catalog_data[frec_start + 52..frec_start + 56].copy_from_slice(&cc);
5382
5383 self.stamp_record_dates(frec_start, false);
5385
5386 let _ = (f_node, f_rec, node_size); Ok(())
5388 }
5389
5390 fn write_resource_fork_inner(
5391 &mut self,
5392 entry: &FileEntry,
5393 data: &mut dyn std::io::Read,
5394 len: u64,
5395 ) -> Result<(), FilesystemError> {
5396 let cnid = entry.location as u32;
5397
5398 if let Some((_data_fork, rsrc_fork)) = self.find_file_by_id(cnid) {
5400 self.free_fork_overflow_extents(cnid, HFSPLUS_FORK_RESOURCE, &rsrc_fork);
5401 self.remove_extents_overflow_records_for(cnid, HFSPLUS_FORK_RESOURCE)?;
5402 self.free_fork_blocks(&rsrc_fork);
5403 }
5404
5405 let new_rsrc = self.write_data_to_blocks(data, len, cnid, HFSPLUS_FORK_RESOURCE)?;
5407
5408 let (t_node, _t_rec, t_offset) =
5410 self.find_catalog_record_by_cnid(cnid).ok_or_else(|| {
5411 FilesystemError::NotFound(format!("thread for CNID {cnid} not found"))
5412 })?;
5413
5414 let key_len = BigEndian::read_u16(&self.catalog_data[t_offset..t_offset + 2]) as usize;
5415 let mut rec_data_start = t_offset + 2 + key_len;
5416 if !rec_data_start.is_multiple_of(2) {
5417 rec_data_start += 1;
5418 }
5419 let thread_parent =
5420 BigEndian::read_u32(&self.catalog_data[rec_data_start + 4..rec_data_start + 8]);
5421 let thread_name_len =
5422 BigEndian::read_u16(&self.catalog_data[rec_data_start + 8..rec_data_start + 10])
5423 as usize;
5424 let thread_name = decode_utf16be(
5425 &self.catalog_data[rec_data_start + 10..rec_data_start + 10 + thread_name_len * 2],
5426 );
5427
5428 let (_f_node, _f_rec, f_offset) = self
5429 .find_catalog_record(thread_parent, &thread_name)
5430 .ok_or_else(|| {
5431 FilesystemError::NotFound(format!("file record for '{}' not found", thread_name))
5432 })?;
5433
5434 let fkey_len = BigEndian::read_u16(&self.catalog_data[f_offset..f_offset + 2]) as usize;
5435 let mut frec_start = f_offset + 2 + fkey_len;
5436 if !frec_start.is_multiple_of(2) {
5437 frec_start += 1;
5438 }
5439
5440 let mut rsrc_bytes = [0u8; 80];
5442 new_rsrc.serialize(&mut rsrc_bytes);
5443 self.catalog_data[frec_start + 168..frec_start + 248].copy_from_slice(&rsrc_bytes);
5444
5445 self.stamp_record_dates(frec_start, true);
5447
5448 let _ = t_node;
5449 Ok(())
5450 }
5451
5452 fn set_blessed_folder_inner(&mut self, entry: &FileEntry) -> Result<(), FilesystemError> {
5453 if !entry.is_directory() {
5454 return Err(FilesystemError::InvalidData(
5455 "can only bless a directory".into(),
5456 ));
5457 }
5458 self.vh.finder_info[0] = entry.location as u32;
5459 Ok(())
5460 }
5461}
5462
5463pub struct ForkStreamReader<'a, R: Read + Seek> {
5478 reader: &'a mut R,
5479 extents: Vec<(u64, u64)>,
5481 logical_size: u64,
5483 pos: u64,
5485 cur_extent: usize,
5486 cur_extent_consumed: u64,
5488}
5489
5490impl<'a, R: Read + Seek> Read for ForkStreamReader<'a, R> {
5491 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
5492 if self.pos >= self.logical_size || buf.is_empty() {
5493 return Ok(0);
5494 }
5495 while self.cur_extent < self.extents.len() {
5497 let (_, len) = self.extents[self.cur_extent];
5498 if self.cur_extent_consumed < len {
5499 break;
5500 }
5501 self.cur_extent += 1;
5502 self.cur_extent_consumed = 0;
5503 }
5504 if self.cur_extent >= self.extents.len() {
5505 return Ok(0);
5506 }
5507 let (off, len) = self.extents[self.cur_extent];
5508 let remaining_in_extent = len - self.cur_extent_consumed;
5509 let remaining_logical = self.logical_size - self.pos;
5510 let max_chunk = remaining_in_extent.min(remaining_logical);
5511 let to_read = (buf.len() as u64).min(max_chunk) as usize;
5512 self.reader
5513 .seek(SeekFrom::Start(off + self.cur_extent_consumed))?;
5514 let n = self.reader.read(&mut buf[..to_read])?;
5515 if n == 0 {
5516 return Ok(0);
5517 }
5518 self.cur_extent_consumed += n as u64;
5519 self.pos += n as u64;
5520 Ok(n)
5521 }
5522}
5523
5524pub struct CompactHfsPlusReader<R: Read + Seek> {
5525 reader: R,
5526 partition_offset: u64,
5527 block_size: u32,
5528 total_blocks: u32,
5529 bitmap: Vec<u8>,
5530 current_block: u32,
5532 block_pos: u64,
5534 #[allow(dead_code)]
5535 original_size: u64,
5536}
5537
5538impl<R: Read + Seek> CompactHfsPlusReader<R> {
5539 pub fn new(
5540 mut reader: R,
5541 partition_offset: u64,
5542 ) -> Result<(Self, CompactResult), FilesystemError> {
5543 log::debug!(
5545 "[HFS+ compact] seeking to VH at offset {}",
5546 partition_offset + 1024
5547 );
5548 reader.seek(SeekFrom::Start(partition_offset + 1024))?;
5549 let mut vh_buf = [0u8; 512];
5550 reader.read_exact(&mut vh_buf)?;
5551 let vh = HfsPlusVolumeHeader::parse(&vh_buf)?;
5552 log::debug!(
5553 "[HFS+ compact] VH ok: block_size={}, total_blocks={}, alloc_file extents[0]=(start={}, count={})",
5554 vh.block_size,
5555 vh.total_blocks,
5556 vh.allocation_file.extents[0].start_block,
5557 vh.allocation_file.extents[0].block_count,
5558 );
5559
5560 let extents_overflow_data = if vh.extents_file.logical_size > 0 {
5563 Some(read_fork(
5564 &mut reader,
5565 partition_offset,
5566 vh.block_size,
5567 &vh.extents_file,
5568 )?)
5569 } else {
5570 None
5571 };
5572
5573 let alloc_data = read_fork_with_overflow(
5575 &mut reader,
5576 partition_offset,
5577 vh.block_size,
5578 &vh.allocation_file,
5579 HFSPLUS_ALLOCATION_FILE_ID,
5580 HFSPLUS_FORK_DATA,
5581 extents_overflow_data.as_deref(),
5582 )
5583 .map_err(|e| {
5584 log::debug!("[HFS+ compact] read_fork(alloc_file) failed: {e}");
5585 e
5586 })?;
5587 log::debug!(
5588 "[HFS+ compact] allocation bitmap read: {} bytes",
5589 alloc_data.len()
5590 );
5591
5592 let mut allocated = 0u32;
5594 for bit in 0..vh.total_blocks {
5595 let byte_idx = bit as usize / 8;
5596 let bit_idx = 7 - (bit % 8);
5597 if byte_idx < alloc_data.len() && (alloc_data[byte_idx] >> bit_idx) & 1 == 1 {
5598 allocated += 1;
5599 }
5600 }
5601 log::debug!(
5602 "[HFS+ compact] allocated={} / {} total blocks ({} free)",
5603 allocated,
5604 vh.total_blocks,
5605 vh.total_blocks - allocated,
5606 );
5607
5608 let original_size = vh.total_blocks as u64 * vh.block_size as u64;
5609 let compacted_size = original_size;
5612 let data_size = allocated as u64 * vh.block_size as u64;
5614 log::debug!(
5615 "[HFS+ compact] compacted_size={} data_size={} original_size={} (layout-preserving; free blocks -> zeros)",
5616 compacted_size, data_size, original_size
5617 );
5618
5619 let result = CompactResult {
5620 original_size,
5621 compacted_size,
5622 data_size,
5623 clusters_used: allocated,
5624 };
5625
5626 Ok((
5627 CompactHfsPlusReader {
5628 reader,
5629 partition_offset,
5630 block_size: vh.block_size,
5631 total_blocks: vh.total_blocks,
5632 bitmap: alloc_data,
5633 current_block: 0,
5634 block_pos: 0,
5635 original_size,
5636 },
5637 result,
5638 ))
5639 }
5640
5641 fn is_block_allocated(&self, block: u32) -> bool {
5642 let byte_idx = block as usize / 8;
5643 let bit_idx = 7 - (block % 8);
5644 byte_idx < self.bitmap.len() && (self.bitmap[byte_idx] >> bit_idx) & 1 == 1
5645 }
5646}
5647
5648impl<R: Read + Seek> Read for CompactHfsPlusReader<R> {
5649 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
5650 if self.current_block >= self.total_blocks {
5651 return Ok(0);
5652 }
5653
5654 let block_size = self.block_size as u64;
5655
5656 let allocated = self.is_block_allocated(self.current_block);
5663 let mut run_end = self.current_block + 1;
5664 while run_end < self.total_blocks && self.is_block_allocated(run_end) == allocated {
5665 run_end += 1;
5666 }
5667
5668 let bytes_in_run = (run_end - self.current_block) as u64 * block_size - self.block_pos;
5669 let to_read = (bytes_in_run as usize).min(buf.len());
5670
5671 let n = if allocated {
5672 let offset =
5673 self.partition_offset + self.current_block as u64 * block_size + self.block_pos;
5674 self.reader.seek(SeekFrom::Start(offset))?;
5675 self.reader.read(&mut buf[..to_read])?
5676 } else {
5677 buf[..to_read].fill(0);
5679 to_read
5680 };
5681
5682 let mut remaining_in_n = n as u64;
5684 if self.block_pos > 0 {
5686 let want = block_size - self.block_pos;
5687 let take = remaining_in_n.min(want);
5688 self.block_pos += take;
5689 remaining_in_n -= take;
5690 if self.block_pos >= block_size {
5691 self.block_pos = 0;
5692 self.current_block += 1;
5693 }
5694 }
5695 let whole = (remaining_in_n / block_size) as u32;
5697 self.current_block += whole;
5698 remaining_in_n -= whole as u64 * block_size;
5699 if remaining_in_n > 0 {
5701 self.block_pos = remaining_in_n;
5702 }
5703
5704 Ok(n)
5705 }
5706}
5707
5708pub fn create_blank_hfsplus(
5737 size_bytes: u64,
5738 block_size: u32,
5739 name: &str,
5740 case_sensitive: bool,
5741) -> Vec<u8> {
5742 create_blank_hfsplus_sized(size_bytes, block_size, name, case_sensitive, 0, 0)
5743}
5744
5745pub fn default_hfsplus_catalog_bytes(volume_bytes: u64) -> u64 {
5752 volume_bytes / 200
5753}
5754
5755pub fn create_blank_hfsplus_sized(
5762 size_bytes: u64,
5763 block_size: u32,
5764 name: &str,
5765 case_sensitive: bool,
5766 min_catalog_bytes: u32,
5767 min_extents_bytes: u32,
5768) -> Vec<u8> {
5769 let (front, vh_bytes, image_size) = build_blank_hfsplus_front(
5770 size_bytes,
5771 block_size,
5772 name,
5773 case_sensitive,
5774 min_catalog_bytes,
5775 min_extents_bytes,
5776 );
5777 let mut img = vec![0u8; image_size];
5778 img[..front.len()].copy_from_slice(&front);
5779 let alt = image_size - 1024;
5780 img[alt..alt + 512].copy_from_slice(&vh_bytes);
5781 img
5782}
5783
5784fn build_blank_hfsplus_front(
5789 size_bytes: u64,
5790 block_size: u32,
5791 name: &str,
5792 case_sensitive: bool,
5793 min_catalog_bytes: u32,
5794 min_extents_bytes: u32,
5795) -> (Vec<u8>, [u8; 512], usize) {
5796 assert!(
5797 block_size.is_power_of_two() && (512..=4096).contains(&block_size),
5798 "block_size must be a power of 2 in [512, 4096], got {block_size}"
5799 );
5800 let bs = block_size as u64;
5801 let total_blocks = (size_bytes / bs) as u32;
5802 assert!(total_blocks >= 64, "image too small ({size_bytes} bytes)");
5803
5804 let node_size: usize = 4096;
5807 let nodes_per_block: u32 = (node_size as u32 / block_size).max(1);
5808 let blocks_per_node: u32 = (block_size / node_size as u32).max(1);
5809 let _ = nodes_per_block;
5810
5811 let max_btree_nodes: u32 = ((node_size - 256) * 8) as u32;
5829 let to_nodes = |bytes: u64| -> u32 {
5830 bytes
5831 .div_ceil(node_size as u64)
5832 .clamp(4, max_btree_nodes as u64) as u32
5833 };
5834 let catalog_node_count =
5835 to_nodes(default_hfsplus_catalog_bytes(size_bytes).max(min_catalog_bytes as u64));
5836 let extents_node_count = to_nodes(min_extents_bytes as u64);
5837
5838 let catalog_btree_blocks: u32 = catalog_node_count * blocks_per_node;
5839 let extents_btree_blocks: u32 = extents_node_count * blocks_per_node;
5840
5841 let bitmap_bytes = total_blocks.div_ceil(8) as u64;
5842 let bitmap_blocks = bitmap_bytes.div_ceil(bs) as u32;
5843
5844 let bitmap_start: u32 = 1;
5845 let extents_start: u32 = bitmap_start + bitmap_blocks;
5846 let catalog_start: u32 = extents_start + extents_btree_blocks;
5847 let reserved_blocks: u32 = catalog_start + catalog_btree_blocks;
5848 assert!(
5849 reserved_blocks + 1 < total_blocks,
5850 "image too small for reserved region: need {} blocks, have {total_blocks}",
5851 reserved_blocks + 1
5852 );
5853
5854 let image_size = total_blocks as usize * block_size as usize;
5855 let front_size = reserved_blocks as usize * block_size as usize;
5860 let mut front = vec![0u8; front_size];
5861
5862 let alt_vh_start_block: u32 = ((image_size as u64 - 1024) / bs) as u32;
5868 let trailing_blocks: u32 = total_blocks - alt_vh_start_block;
5869
5870 {
5872 let bitmap_off = bitmap_start as usize * block_size as usize;
5873 let bitmap = &mut front[bitmap_off..bitmap_off + (bitmap_bytes as usize).max(1)];
5874 let mut mark = |blk: u32| {
5875 let byte_idx = (blk / 8) as usize;
5877 let bit_pos = 7 - (blk % 8);
5878 bitmap[byte_idx] |= 1 << bit_pos;
5879 };
5880 for blk in 0..reserved_blocks {
5881 mark(blk);
5882 }
5883 for blk in alt_vh_start_block..total_blocks {
5884 mark(blk);
5885 }
5886 }
5887
5888 {
5893 let off = extents_start as usize * block_size as usize;
5894 write_blank_btree_header_node(
5895 &mut front[off..off + node_size],
5896 node_size,
5897 0,
5898 extents_node_count,
5899 extents_node_count - 1, 10,
5901 0, );
5903 }
5904
5905 let name_utf16: Vec<u16> = fs_mac_unicode::hfs_unicode::decompose_str(name);
5907 {
5908 let off = catalog_start as usize * block_size as usize;
5909 let key_compare = if case_sensitive {
5910 KEY_COMPARE_BINARY
5911 } else {
5912 KEY_COMPARE_CASE_FOLDING
5913 };
5914 write_blank_btree_header_node(
5915 &mut front[off..off + node_size],
5916 node_size,
5917 2,
5918 catalog_node_count,
5919 catalog_node_count - 2,
5920 516, key_compare,
5922 );
5923 let leaf_off = off + node_size;
5924 write_root_catalog_leaf(&mut front[leaf_off..leaf_off + node_size], &name_utf16);
5925 }
5926
5927 let signature = if case_sensitive {
5929 HFSX_SIGNATURE
5930 } else {
5931 HFS_PLUS_SIGNATURE
5932 };
5933 let now = hfs_common::hfs_now();
5934 let vh = HfsPlusVolumeHeader {
5935 signature,
5936 version: if case_sensitive { 5 } else { 4 },
5937 attributes: HFSPLUS_VOLUME_UNMOUNTED_BIT,
5938 last_mounted_version: 0,
5939 journal_info_block: 0,
5940 create_date: now,
5941 modify_date: now,
5942 backup_date: 0,
5943 checked_date: now,
5944 file_count: 0,
5945 folder_count: 0,
5951 block_size,
5952 total_blocks,
5953 free_blocks: total_blocks - reserved_blocks - trailing_blocks,
5954 next_allocation: reserved_blocks,
5955 rsrc_clump_size: block_size,
5956 data_clump_size: block_size,
5957 next_catalog_id: 16,
5958 write_count: 0,
5959 encodings_bitmap: 1, finder_info: [0u32; 8],
5961 allocation_file: ForkData {
5962 logical_size: bitmap_blocks as u64 * bs,
5963 clump_size: 0,
5964 total_blocks: bitmap_blocks,
5965 extents: extent_array(bitmap_start, bitmap_blocks),
5966 },
5967 extents_file: ForkData {
5968 logical_size: extents_btree_blocks as u64 * bs,
5969 clump_size: 0,
5970 total_blocks: extents_btree_blocks,
5971 extents: extent_array(extents_start, extents_btree_blocks),
5972 },
5973 catalog_file: ForkData {
5974 logical_size: catalog_btree_blocks as u64 * bs,
5975 clump_size: 0,
5976 total_blocks: catalog_btree_blocks,
5977 extents: extent_array(catalog_start, catalog_btree_blocks),
5978 },
5979 attributes_file: ForkData::empty(),
5980 startup_file: ForkData::empty(),
5981 };
5982 let vh_bytes = vh.serialize();
5983
5984 front[1024..1536].copy_from_slice(&vh_bytes);
5986
5987 (front, vh_bytes, image_size)
5988}
5989
5990pub fn write_blank_hfsplus_into<W: Write + Seek>(
6002 target: &mut W,
6003 at_offset: u64,
6004 size_bytes: u64,
6005 block_size: u32,
6006 name: &str,
6007 case_sensitive: bool,
6008) -> std::io::Result<()> {
6009 let (front, vh_bytes, _image_size) =
6010 build_blank_hfsplus_front(size_bytes, block_size, name, case_sensitive, 0, 0);
6011 target.seek(SeekFrom::Start(at_offset))?;
6012 target.write_all(&front)?;
6013 target.seek(SeekFrom::Start(at_offset + size_bytes - 1024))?;
6014 target.write_all(&vh_bytes)?;
6015 target.write_all(&[0u8; 512])?;
6016 Ok(())
6017}
6018
6019fn extent_array(start: u32, count: u32) -> [ExtentDescriptor; 8] {
6021 let mut e = [ExtentDescriptor {
6022 start_block: 0,
6023 block_count: 0,
6024 }; 8];
6025 e[0] = ExtentDescriptor {
6026 start_block: start,
6027 block_count: count,
6028 };
6029 e
6030}
6031
6032pub(crate) fn write_blank_btree_header_node(
6037 node: &mut [u8],
6038 node_size: usize,
6039 leaf_records: u32,
6040 total_nodes: u32,
6041 free_nodes: u32,
6042 max_key_len: u16,
6043 key_compare_type: u8,
6044) {
6045 node.fill(0);
6046 node[8] = 1; BigEndian::write_u16(&mut node[10..12], 3); let empty = leaf_records == 0;
6061 let (depth, root_first_last): (u16, u32) = if empty { (0, 0) } else { (1, 1) };
6062 let hr = 14usize;
6063 BigEndian::write_u16(&mut node[hr..hr + 2], depth);
6064 BigEndian::write_u32(&mut node[hr + 2..hr + 6], root_first_last); BigEndian::write_u32(&mut node[hr + 6..hr + 10], leaf_records);
6066 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);
6069 BigEndian::write_u16(&mut node[hr + 20..hr + 22], max_key_len);
6070 BigEndian::write_u32(&mut node[hr + 22..hr + 26], total_nodes);
6071 BigEndian::write_u32(&mut node[hr + 26..hr + 30], free_nodes);
6072 node[hr + 36] = 0; node[hr + 37] = key_compare_type;
6075 let attributes = if max_key_len == 10 {
6078 hfs_common::KBT_BIG_KEYS_MASK
6079 } else {
6080 hfs_common::KBT_BIG_KEYS_MASK | hfs_common::KBT_VARIABLE_INDEX_KEYS_MASK
6081 };
6082 BigEndian::write_u32(&mut node[hr + 38..hr + 42], attributes);
6083
6084 let user_off: u16 = 120;
6094 let bitmap_off: u16 = 248;
6095 let free_off: u16 = (node_size as u16).saturating_sub(8); let ot_end = node_size;
6098 BigEndian::write_u16(&mut node[ot_end - 2..ot_end], 14); BigEndian::write_u16(&mut node[ot_end - 4..ot_end - 2], user_off);
6100 BigEndian::write_u16(&mut node[ot_end - 6..ot_end - 4], bitmap_off);
6101 BigEndian::write_u16(&mut node[ot_end - 8..ot_end - 6], free_off);
6102
6103 node[bitmap_off as usize] = if empty { 0b10000000 } else { 0b11000000 };
6107}
6108
6109#[cfg(test)]
6113pub(crate) fn write_empty_leaf_node(node: &mut [u8]) {
6114 node.fill(0);
6115 node[8] = 0xFF; node[9] = 1; BigEndian::write_u16(&mut node[10..12], 0); let n = node.len();
6119 BigEndian::write_u16(&mut node[n - 2..n], 14); }
6121
6122fn write_root_catalog_leaf(node: &mut [u8], name_utf16: &[u16]) {
6126 node.fill(0);
6127 let n = node.len();
6128 node[8] = 0xFF; node[9] = 1; BigEndian::write_u16(&mut node[10..12], 2); let now = hfs_common::hfs_now();
6133
6134 let r0_off = 14usize;
6137 let key_body = 4 + 2 + name_utf16.len() * 2;
6138 BigEndian::write_u16(&mut node[r0_off..r0_off + 2], key_body as u16);
6139 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);
6141 for (i, &u) in name_utf16.iter().enumerate() {
6142 BigEndian::write_u16(&mut node[r0_off + 8 + i * 2..r0_off + 10 + i * 2], u);
6143 }
6144 let mut r0_data = r0_off + 2 + key_body;
6145 if !r0_data.is_multiple_of(2) {
6146 r0_data += 1;
6147 }
6148 BigEndian::write_i16(&mut node[r0_data..r0_data + 2], CATALOG_FOLDER);
6149 BigEndian::write_u32(&mut node[r0_data + 8..r0_data + 12], 2);
6151 BigEndian::write_u32(&mut node[r0_data + 12..r0_data + 16], now);
6152 BigEndian::write_u32(&mut node[r0_data + 16..r0_data + 20], now);
6153 BigEndian::write_u32(&mut node[r0_data + 20..r0_data + 24], now);
6154 BigEndian::write_u32(&mut node[r0_data + 24..r0_data + 28], now);
6155 let r0_end = r0_data + 88; let r1_off = r0_end;
6160 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;
6164 if !r1_data.is_multiple_of(2) {
6165 r1_data += 1;
6166 }
6167 BigEndian::write_i16(&mut node[r1_data..r1_data + 2], 3); BigEndian::write_u32(&mut node[r1_data + 4..r1_data + 8], 1);
6171 BigEndian::write_u16(
6172 &mut node[r1_data + 8..r1_data + 10],
6173 name_utf16.len() as u16,
6174 );
6175 for (i, &u) in name_utf16.iter().enumerate() {
6176 BigEndian::write_u16(&mut node[r1_data + 10 + i * 2..r1_data + 12 + i * 2], u);
6177 }
6178 let r1_end = r1_data + 10 + name_utf16.len() * 2;
6179
6180 BigEndian::write_u16(&mut node[n - 2..n], 14); BigEndian::write_u16(&mut node[n - 4..n - 2], r1_off as u16);
6183 BigEndian::write_u16(&mut node[n - 6..n - 4], r1_end as u16); }
6185
6186pub fn resize_hfsplus_in_place(
6190 device: &mut (impl Read + Write + Seek),
6191 partition_offset: u64,
6192 new_size_bytes: u64,
6193 log: &mut impl FnMut(&str),
6194) -> anyhow::Result<()> {
6195 device.seek(SeekFrom::Start(partition_offset + 1024))?;
6197 let mut vh_buf = [0u8; 512];
6198 device.read_exact(&mut vh_buf)?;
6199
6200 let sig = BigEndian::read_u16(&vh_buf[0..2]);
6201 if sig != HFS_PLUS_SIGNATURE && sig != HFSX_SIGNATURE {
6202 log("HFS+ resize: not an HFS+ volume, skipping");
6203 return Ok(());
6204 }
6205 let mut vh = HfsPlusVolumeHeader::parse(&vh_buf)?;
6206 let bs = vh.block_size;
6207 let old_total = vh.total_blocks;
6208 let new_total = (new_size_bytes / bs as u64) as u32;
6209 if new_total < 8 {
6210 anyhow::bail!("HFS+ resize: {new_size_bytes} bytes is too small for a volume");
6211 }
6212
6213 let af = &vh.allocation_file;
6215 if af.extents[0].block_count == 0 || af.extents[1].block_count != 0 {
6216 anyhow::bail!("HFS+ resize: the allocation file is not a single extent; refusing");
6217 }
6218 let (mut af_start, mut af_blocks) = (af.extents[0].start_block, af.extents[0].block_count);
6219 let mut bitmap = vec![0u8; af_blocks as usize * bs as usize];
6220 device.seek(SeekFrom::Start(
6221 partition_offset + af_start as u64 * bs as u64,
6222 ))?;
6223 device.read_exact(&mut bitmap)?;
6224
6225 let old_tail = alt_vh_blocks(old_total, bs);
6226 let new_tail = alt_vh_blocks(new_total, bs);
6227 if new_total < old_total {
6228 let highest = find_last_set_bit(&bitmap, old_tail.start).map_or(0, |b| b + 1);
6230 if highest > new_tail.start {
6231 anyhow::bail!(
6232 "HFS+ resize: new size {} blocks ends before the last allocated block ({})",
6233 new_total,
6234 highest
6235 );
6236 }
6237 }
6238
6239 let cap_bits = (bitmap.len() as u64 * 8).min(u32::MAX as u64) as u32;
6242 for b in old_tail.start..old_total.min(cap_bits) {
6243 bitmap_clear_bit_be(&mut bitmap, b);
6244 }
6245 for b in old_total..new_total.min(cap_bits) {
6246 bitmap_clear_bit_be(&mut bitmap, b);
6247 }
6248
6249 let needed_blocks = (new_total as u64).div_ceil(8).div_ceil(bs as u64) as u32;
6252 if needed_blocks > af_blocks {
6253 let mut grown = bitmap.clone();
6254 grown.resize(needed_blocks as usize * bs as usize, 0);
6255 let usable = new_tail.start;
6256 let extend_ok = (af_start + af_blocks..af_start + needed_blocks)
6257 .all(|b| b < usable && !bitmap_test_bit_be(&grown, b));
6258 let new_start = if extend_ok {
6259 af_start
6260 } else {
6261 hfs_common::bitmap_find_clear_run_be(&grown, usable, needed_blocks).ok_or_else(
6262 || {
6263 anyhow::anyhow!(
6264 "HFS+ resize: no free run of {needed_blocks} blocks for the allocation file"
6265 )
6266 },
6267 )?
6268 };
6269 if new_start != af_start {
6270 for b in af_start..af_start + af_blocks {
6271 bitmap_clear_bit_be(&mut grown, b);
6272 }
6273 }
6274 for b in new_start..new_start + needed_blocks {
6275 bitmap_set_bit_be(&mut grown, b);
6276 }
6277 log(&format!(
6278 "HFS+ resize: allocation file grows from {af_blocks} to {needed_blocks} blocks at \
6279 block {new_start}"
6280 ));
6281 bitmap = grown;
6282 af_start = new_start;
6283 af_blocks = needed_blocks;
6284 vh.allocation_file.extents[0] = ExtentDescriptor {
6285 start_block: af_start,
6286 block_count: af_blocks,
6287 };
6288 vh.allocation_file.total_blocks = af_blocks;
6289 vh.allocation_file.logical_size = af_blocks as u64 * bs as u64;
6290 }
6291 for b in new_tail.clone() {
6292 bitmap_set_bit_be(&mut bitmap, b);
6293 }
6294 let cap_bits = (bitmap.len() as u64 * 8).min(u32::MAX as u64) as u32;
6295 for b in new_total..cap_bits {
6296 bitmap_clear_bit_be(&mut bitmap, b);
6297 }
6298
6299 let used = count_used_blocks(&bitmap, new_total);
6300 vh.total_blocks = new_total;
6301 vh.free_blocks = new_total - used;
6302 if vh.next_allocation >= new_tail.start {
6303 vh.next_allocation = 0;
6304 }
6305 log(&format!(
6306 "HFS+ resize: {} -> {} blocks ({} free)",
6307 old_total, new_total, vh.free_blocks
6308 ));
6309
6310 device.seek(SeekFrom::Start(
6311 partition_offset + af_start as u64 * bs as u64,
6312 ))?;
6313 device.write_all(&bitmap)?;
6314 let out = vh.serialize();
6315 device.seek(SeekFrom::Start(partition_offset + 1024))?;
6316 device.write_all(&out)?;
6317 if new_size_bytes > 1024 {
6318 device.seek(SeekFrom::Start(partition_offset + new_size_bytes - 1024))?;
6319 device.write_all(&out)?;
6320 }
6321 device.flush()?;
6322 Ok(())
6323}
6324
6325pub fn mirror_alternate_volume_header(
6329 device: &mut (impl Read + Write + Seek),
6330 partition_offset: u64,
6331 partition_len: u64,
6332 log: &mut impl FnMut(&str),
6333) -> anyhow::Result<bool> {
6334 device.seek(SeekFrom::Start(partition_offset + 1024))?;
6335 let mut vh_buf = [0u8; 512];
6336 device.read_exact(&mut vh_buf)?;
6337 let sig = BigEndian::read_u16(&vh_buf[0..2]);
6338 if sig != HFS_PLUS_SIGNATURE && sig != HFSX_SIGNATURE {
6339 return Ok(false);
6340 }
6341 let block_size = BigEndian::read_u32(&vh_buf[40..44]) as u64;
6342 let total_blocks = BigEndian::read_u32(&vh_buf[44..48]) as u64;
6343 let volume_len = total_blocks * block_size;
6344 if partition_len < 2048 || volume_len >= partition_len {
6345 return Ok(false);
6346 }
6347 device.seek(SeekFrom::Start(partition_offset + partition_len - 1024))?;
6348 device.write_all(&vh_buf)?;
6349 device.flush()?;
6350 log(&format!(
6351 "HFS+ volume of {volume_len} bytes in a {partition_len}-byte partition: alternate \
6352 volume header mirrored at the partition end"
6353 ));
6354 Ok(true)
6355}
6356
6357fn alt_vh_blocks(total_blocks: u32, block_size: u32) -> std::ops::Range<u32> {
6360 if total_blocks == 0 {
6361 return 0..0;
6362 }
6363 let bytes = total_blocks as u64 * block_size as u64;
6364 let start = (bytes.saturating_sub(1024) / block_size as u64) as u32;
6365 start..total_blocks
6366}
6367
6368fn count_used_blocks(bitmap: &[u8], total_blocks: u32) -> u32 {
6370 let full = (total_blocks / 8) as usize;
6371 let mut used: u32 = bitmap[..full.min(bitmap.len())]
6372 .iter()
6373 .map(|b| b.count_ones())
6374 .sum();
6375 let keep = total_blocks % 8;
6376 if keep > 0 {
6377 if let Some(&last) = bitmap.get(full) {
6378 let mask = 0xFFu8 << (8 - keep);
6379 used += (last & mask).count_ones();
6380 }
6381 }
6382 used
6383}
6384
6385pub fn validate_hfsplus_integrity(
6387 device: &mut (impl Read + Seek),
6388 partition_offset: u64,
6389 log: &mut impl FnMut(&str),
6390) -> anyhow::Result<()> {
6391 device.seek(SeekFrom::Start(partition_offset + 1024))?;
6392 let mut vh_buf = [0u8; 512];
6393 device.read_exact(&mut vh_buf)?;
6394
6395 let sig = BigEndian::read_u16(&vh_buf[0..2]);
6396 if sig != HFS_PLUS_SIGNATURE && sig != HFSX_SIGNATURE {
6397 log("HFS+ validate: not an HFS+ volume, skipping");
6398 return Ok(());
6399 }
6400
6401 let block_size = BigEndian::read_u32(&vh_buf[40..44]);
6402 let total_blocks = BigEndian::read_u32(&vh_buf[44..48]);
6403
6404 if !block_size.is_power_of_two() || block_size < 512 {
6405 anyhow::bail!("HFS+ validate: invalid block size {block_size}");
6406 }
6407 if total_blocks == 0 {
6408 anyhow::bail!("HFS+ validate: zero total blocks");
6409 }
6410
6411 log(&format!(
6412 "HFS+ validate: OK ({total_blocks} blocks, {block_size} block size)"
6413 ));
6414 Ok(())
6415}
6416
6417pub fn resolve_apple_hfs<R: Read + Seek>(
6429 reader: &mut R,
6430 partition_offset: u64,
6431) -> (&'static str, u64) {
6432 if reader
6437 .seek(SeekFrom::Start(partition_offset + 1024))
6438 .is_err()
6439 {
6440 return ("unknown", partition_offset);
6441 }
6442 let mut buf = [0u8; 512];
6443 if reader.read_exact(&mut buf).is_err() {
6444 return ("unknown", partition_offset);
6445 }
6446 let sig = u16::from_be_bytes([buf[0], buf[1]]);
6447 match sig {
6448 0x4244 => {
6449 let embedded_sig = u16::from_be_bytes([buf[124], buf[125]]);
6451 if embedded_sig == 0x482B {
6452 let block_size = u32::from_be_bytes([buf[20], buf[21], buf[22], buf[23]]) as u64;
6457 let first_alloc_block = u16::from_be_bytes([buf[28], buf[29]]) as u64;
6458 let embedded_start = u16::from_be_bytes([buf[126], buf[127]]) as u64;
6459 let hfsplus_offset =
6461 partition_offset + first_alloc_block * 512 + embedded_start * block_size;
6462 ("hfsplus", hfsplus_offset)
6463 } else {
6464 ("hfs", partition_offset)
6465 }
6466 }
6467 0x482B | 0x4858 => ("hfsplus", partition_offset),
6469 _ => ("unknown", partition_offset),
6470 }
6471}
6472
6473#[cfg(test)]
6474#[allow(clippy::identity_op)] mod tests {
6476 use super::*;
6477
6478 #[test]
6481 fn thousand_file_churn_keeps_the_catalog_clean() {
6482 use std::io::Cursor;
6483 let img = create_blank_hfsplus(64 * 1024 * 1024, 4096, "Churn", false);
6484 let mut fs = HfsPlusFilesystem::open(Cursor::new(img), 0).unwrap();
6485 fs.prepare_for_edit().unwrap();
6486 let root = fs.root().unwrap();
6487 let clean = |fs: &mut HfsPlusFilesystem<Cursor<Vec<u8>>>, stage: &str| {
6488 fs.sync_metadata().unwrap();
6489 let r = fs.fsck().expect("hfsplus has fsck").unwrap();
6490 assert!(r.errors.is_empty(), "{stage}: {:?}", r.errors);
6491 };
6492 let put = |fs: &mut HfsPlusFilesystem<Cursor<Vec<u8>>>, name: &str, data: &[u8]| {
6493 fs.create_file(
6494 &root,
6495 name,
6496 &mut Cursor::new(data),
6497 data.len() as u64,
6498 &CreateFileOptions::default(),
6499 )
6500 .unwrap_or_else(|e| panic!("creating {name}: {e}"))
6501 };
6502 let free0 = fs.vh.free_blocks;
6503 let trees0 = fs.vh.catalog_file.total_blocks + fs.vh.extents_file.total_blocks;
6504 for i in 0..1000 {
6505 put(
6506 &mut fs,
6507 &format!("f{i:04}.txt"),
6508 format!("file {i:04}").as_bytes(),
6509 );
6510 }
6511 clean(&mut fs, "after 1000 creates");
6512 let extra = put(&mut fs, "extra.bin", &[7u8; 5000]);
6513 clean(&mut fs, "after one more");
6514 fs.delete_entry(&root, &extra).unwrap();
6515 clean(&mut fs, "after deleting it");
6516 for e in fs.list_directory(&root).unwrap() {
6517 fs.delete_entry(&root, &e).unwrap();
6518 }
6519 let trees = fs.vh.catalog_file.total_blocks + fs.vh.extents_file.total_blocks;
6521 assert_eq!(
6522 fs.vh.free_blocks + (trees - trees0),
6523 free0,
6524 "every block came back"
6525 );
6526 clean(&mut fs, "after deleting the 1000");
6527 put(&mut fs, "last.bin", &[9u8; 100]);
6528 clean(&mut fs, "after the last create");
6529 let names: Vec<String> = fs
6530 .list_directory(&root)
6531 .unwrap()
6532 .into_iter()
6533 .map(|e| e.name)
6534 .collect();
6535 assert_eq!(names, ["last.bin"]);
6536 }
6537
6538 #[test]
6541 fn blank_btree_headers_carry_apples_attribute_bits() {
6542 let img = create_blank_hfsplus(16 * 1024 * 1024, 4096, "Attrs", false);
6543 let vh = &img[1024..1536];
6544 let block_size = BigEndian::read_u32(&vh[0x28..0x2c]) as usize;
6545 let attrs = |fork: usize| {
6549 let start = BigEndian::read_u32(&vh[fork + 0x10..fork + 0x14]) as usize;
6550 BigEndian::read_u32(&img[start * block_size + 52..start * block_size + 56])
6551 };
6552 assert_eq!(attrs(0xC0), hfs_common::KBT_BIG_KEYS_MASK, "extents");
6553 let variable = hfs_common::KBT_BIG_KEYS_MASK | hfs_common::KBT_VARIABLE_INDEX_KEYS_MASK;
6554 assert_eq!(attrs(0x110), variable, "catalog");
6555 }
6556
6557 #[test]
6558 fn test_volume_header_parse() {
6559 let mut data = [0u8; 512];
6560 BigEndian::write_u16(&mut data[0..2], HFS_PLUS_SIGNATURE);
6561 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();
6567 assert_eq!(vh.signature, HFS_PLUS_SIGNATURE);
6568 assert_eq!(vh.block_size, 4096);
6569 assert_eq!(vh.total_blocks, 100000);
6570 assert_eq!(vh.free_blocks, 30000);
6571 assert!(!vh.is_hfsx());
6572 }
6573
6574 #[test]
6575 fn test_hfsx_signature() {
6576 let mut data = [0u8; 512];
6577 BigEndian::write_u16(&mut data[0..2], HFSX_SIGNATURE);
6578 BigEndian::write_u32(&mut data[40..44], 4096);
6579 BigEndian::write_u32(&mut data[44..48], 100000);
6580 BigEndian::write_u32(&mut data[48..52], 30000);
6581
6582 let vh = HfsPlusVolumeHeader::parse(&data).unwrap();
6583 assert!(vh.is_hfsx());
6584 }
6585
6586 #[test]
6587 fn test_decode_utf16be() {
6588 let data = [0x00, 0x48, 0x00, 0x65, 0x00, 0x6C, 0x00, 0x6C, 0x00, 0x6F];
6590 assert_eq!(decode_utf16be(&data), "Hello");
6591 }
6592
6593 #[test]
6594 fn test_decode_utf16be_with_non_ascii() {
6595 let data = [0x00, 0xC4];
6597 assert_eq!(decode_utf16be(&data), "Ä");
6598 }
6599
6600 #[test]
6601 fn test_extent_descriptor() {
6602 let mut data = [0u8; 8];
6603 BigEndian::write_u32(&mut data[0..4], 100);
6604 BigEndian::write_u32(&mut data[4..8], 50);
6605
6606 let ext = ExtentDescriptor::parse(&data);
6607 assert_eq!(ext.start_block, 100);
6608 assert_eq!(ext.block_count, 50);
6609 assert!(!ext.is_empty());
6610 }
6611
6612 #[test]
6613 fn test_find_last_set_bit() {
6614 let bitmap = [0b10100000, 0b00001000]; assert_eq!(find_last_set_bit(&bitmap, 16), Some(12));
6616
6617 let empty = [0u8; 2];
6618 assert_eq!(find_last_set_bit(&empty, 16), None);
6619 }
6620
6621 #[test]
6622 fn test_find_last_set_bit_dense_byte() {
6623 assert_eq!(find_last_set_bit(&[0b11111100], 8), Some(5));
6628 assert_eq!(find_last_set_bit(&[0b11111111], 8), Some(7));
6629 assert_eq!(find_last_set_bit(&[0b00000001], 8), Some(7));
6630 assert_eq!(find_last_set_bit(&[0b10000000], 8), Some(0));
6631 assert_eq!(find_last_set_bit(&[0b11111100, 0b00000000], 16), Some(5));
6633 assert_eq!(find_last_set_bit(&[0b11111111], 4), Some(3));
6636 }
6637
6638 #[test]
6639 fn test_volume_header_serialize_roundtrip() {
6640 let mut data = [0u8; 512];
6641 BigEndian::write_u16(&mut data[0..2], HFS_PLUS_SIGNATURE);
6642 BigEndian::write_u16(&mut data[2..4], 4);
6643 BigEndian::write_u32(&mut data[40..44], 4096);
6644 BigEndian::write_u32(&mut data[44..48], 100);
6645 BigEndian::write_u32(&mut data[48..52], 50);
6646 BigEndian::write_u32(&mut data[64..68], 10); BigEndian::write_u32(&mut data[80..84], 42); let vh = HfsPlusVolumeHeader::parse(&data).unwrap();
6650 assert_eq!(vh.next_catalog_id, 10);
6651 assert_eq!(vh.finder_info[0], 42);
6652
6653 let serialized = vh.serialize();
6654 let vh2 = HfsPlusVolumeHeader::parse(&serialized).unwrap();
6655 assert_eq!(vh2.block_size, 4096);
6656 assert_eq!(vh2.total_blocks, 100);
6657 assert_eq!(vh2.free_blocks, 50);
6658 assert_eq!(vh2.next_catalog_id, 10);
6659 assert_eq!(vh2.finder_info[0], 42);
6660 }
6661
6662 #[test]
6663 fn test_fork_data_serialize_roundtrip() {
6664 let fork = ForkData {
6665 logical_size: 12345,
6666 clump_size: 0,
6667 total_blocks: 3,
6668 extents: [
6669 ExtentDescriptor {
6670 start_block: 10,
6671 block_count: 3,
6672 },
6673 ExtentDescriptor {
6674 start_block: 0,
6675 block_count: 0,
6676 },
6677 ExtentDescriptor {
6678 start_block: 0,
6679 block_count: 0,
6680 },
6681 ExtentDescriptor {
6682 start_block: 0,
6683 block_count: 0,
6684 },
6685 ExtentDescriptor {
6686 start_block: 0,
6687 block_count: 0,
6688 },
6689 ExtentDescriptor {
6690 start_block: 0,
6691 block_count: 0,
6692 },
6693 ExtentDescriptor {
6694 start_block: 0,
6695 block_count: 0,
6696 },
6697 ExtentDescriptor {
6698 start_block: 0,
6699 block_count: 0,
6700 },
6701 ],
6702 };
6703 let mut buf = [0u8; 80];
6704 fork.serialize(&mut buf);
6705 let fork2 = ForkData::parse(&buf);
6706 assert_eq!(fork2.logical_size, 12345);
6707 assert_eq!(fork2.total_blocks, 3);
6708 assert_eq!(fork2.extents[0].start_block, 10);
6709 assert_eq!(fork2.extents[0].block_count, 3);
6710 }
6711
6712 fn make_editable_hfsplus_image() -> Vec<u8> {
6722 let block_size = 4096u32;
6723 let total_blocks = 256u32;
6724 let image_size = total_blocks as usize * block_size as usize; let mut img = vec![0u8; image_size];
6726
6727 let bitmap_block = 1u32;
6729 let catalog_start_block = 2u32;
6730 let catalog_blocks = 4u32;
6731 let alloc_blocks = 1 + 1 + catalog_blocks; let bitmap_data = &mut img[bitmap_block as usize * block_size as usize
6733 ..(bitmap_block + 1) as usize * block_size as usize];
6734 bitmap_data[0] = 0b11111100;
6736
6737 let node_size = 4096usize;
6739 let catalog_offset = catalog_start_block as usize * block_size as usize;
6740 let catalog_size = catalog_blocks as usize * block_size as usize;
6741
6742 let hdr_off = catalog_offset;
6744 img[hdr_off + 8] = 1; BigEndian::write_u16(&mut img[hdr_off + 10..hdr_off + 12], 3); let hr = hdr_off + 14;
6750 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;
6773
6774 let leaf_off = catalog_offset + node_size;
6776 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;
6784 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;
6789 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;
6801 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;
6808 BigEndian::write_u16(&mut img[lot - 2..lot], 14); let r1_rel = (r1_off - leaf_off) as u16;
6810 BigEndian::write_u16(&mut img[lot - 4..lot - 2], r1_rel); let free_rel = (r1_data + 10 - leaf_off) as u16;
6812 BigEndian::write_u16(&mut img[lot - 6..lot - 4], free_rel); let vh = HfsPlusVolumeHeader {
6816 signature: HFS_PLUS_SIGNATURE,
6817 version: 4,
6818 attributes: 0,
6819 last_mounted_version: 0,
6820 journal_info_block: 0,
6821 create_date: hfs_common::hfs_now(),
6822 modify_date: hfs_common::hfs_now(),
6823 backup_date: 0,
6824 checked_date: 0,
6825 file_count: 0,
6826 folder_count: 0, block_size,
6828 total_blocks,
6829 free_blocks: total_blocks - alloc_blocks,
6830 next_allocation: alloc_blocks,
6831 rsrc_clump_size: 0,
6832 data_clump_size: 0,
6833 next_catalog_id: 16, write_count: 0,
6835 encodings_bitmap: 0,
6836 finder_info: [0u32; 8],
6837 allocation_file: ForkData {
6838 logical_size: block_size as u64,
6839 clump_size: 0,
6840 total_blocks: 1,
6841 extents: {
6842 let mut e = [ExtentDescriptor {
6843 start_block: 0,
6844 block_count: 0,
6845 }; 8];
6846 e[0] = ExtentDescriptor {
6847 start_block: bitmap_block,
6848 block_count: 1,
6849 };
6850 e
6851 },
6852 },
6853 extents_file: ForkData::empty(),
6854 catalog_file: ForkData {
6855 logical_size: catalog_size as u64,
6856 clump_size: 0,
6857 total_blocks: catalog_blocks,
6858 extents: {
6859 let mut e = [ExtentDescriptor {
6860 start_block: 0,
6861 block_count: 0,
6862 }; 8];
6863 e[0] = ExtentDescriptor {
6864 start_block: catalog_start_block,
6865 block_count: catalog_blocks,
6866 };
6867 e
6868 },
6869 },
6870 attributes_file: ForkData::empty(),
6871 startup_file: ForkData::empty(),
6872 };
6873
6874 let vh_bytes = vh.serialize();
6875 img[1024..1024 + 512].copy_from_slice(&vh_bytes);
6876
6877 let backup_pos = image_size - 1024;
6879 img[backup_pos..backup_pos + 512].copy_from_slice(&vh_bytes);
6880
6881 img
6882 }
6883
6884 fn make_editable_hfsplus_image_with_extents_btree() -> Vec<u8> {
6896 let mut img = make_editable_hfsplus_image();
6899 let block_size = 4096u32;
6900 let node_size = 4096usize;
6901 let extents_start_block = 6u32;
6902 let extents_blocks = 4u32;
6903
6904 let bitmap_off = 1usize * block_size as usize;
6908 img[bitmap_off] = 0xFF; img[bitmap_off + 1] = 0b11000000; let hdr_off = extents_start_block as usize * block_size as usize;
6913 img[hdr_off + 8] = 1; BigEndian::write_u16(&mut img[hdr_off + 10..hdr_off + 12], 3); let hr = hdr_off + 14;
6918 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);
6924 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;
6931 BigEndian::write_u16(&mut img[ot - 2..ot], 14);
6932 BigEndian::write_u16(&mut img[ot - 4..ot - 2], 142);
6933 BigEndian::write_u16(&mut img[ot - 6..ot - 4], 270);
6934 BigEndian::write_u16(&mut img[ot - 8..ot - 6], 526);
6935
6936 img[hdr_off + 270] = 0b11000000;
6938
6939 let leaf_off = hdr_off + node_size;
6941 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;
6948 BigEndian::write_u16(&mut img[lot - 2..lot], 14);
6949
6950 let mut vh = HfsPlusVolumeHeader::parse(&img[1024..1536]).expect("parse seed VH");
6956 vh.extents_file = ForkData {
6957 logical_size: (extents_blocks as u64) * (block_size as u64),
6958 clump_size: 0,
6959 total_blocks: extents_blocks,
6960 extents: {
6961 let mut e = [ExtentDescriptor {
6962 start_block: 0,
6963 block_count: 0,
6964 }; 8];
6965 e[0] = ExtentDescriptor {
6966 start_block: extents_start_block,
6967 block_count: extents_blocks,
6968 };
6969 e
6970 },
6971 };
6972 vh.free_blocks -= extents_blocks;
6973 vh.next_allocation = 10;
6974 let vh_bytes = vh.serialize();
6975 img[1024..1536].copy_from_slice(&vh_bytes);
6976 let alt = img.len() - 1024;
6977 img[alt..alt + 512].copy_from_slice(&vh_bytes);
6978 img
6979 }
6980
6981 fn make_editable_hfsplus_image_with_inline_xattr(cnid: u32, value: &[u8]) -> Vec<u8> {
6992 let mut img = make_editable_hfsplus_image();
6993 let block_size = 4096u32;
6994 let node_size = 4096usize;
6995 let attr_start_block = 6u32;
6996 let attr_blocks = 2u32;
6997
6998 let bitmap_off = 1usize * block_size as usize;
7001 img[bitmap_off] = 0xFF; let hdr_off = attr_start_block as usize * block_size as usize;
7005 img[hdr_off + 8] = 1; BigEndian::write_u16(&mut img[hdr_off + 10..hdr_off + 12], 3);
7007
7008 let hr = hdr_off + 14;
7009 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);
7015 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;
7021 BigEndian::write_u16(&mut img[ot - 2..ot], 14);
7022 BigEndian::write_u16(&mut img[ot - 4..ot - 2], 142);
7023 BigEndian::write_u16(&mut img[ot - 6..ot - 4], 270);
7024 BigEndian::write_u16(&mut img[ot - 8..ot - 6], 526);
7025
7026 img[hdr_off + 270] = 0b11000000;
7028
7029 let leaf_off = hdr_off + node_size;
7031 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";
7038 let name_utf16: Vec<u16> = name.encode_utf16().collect();
7039 let name_byte_len = name_utf16.len() * 2;
7040 let key_body_len = 12 + name_byte_len;
7041 let data_section_len = 4 + 8 + 4 + value.len();
7043 let rec_total = 2 + key_body_len + data_section_len;
7044
7045 let r0 = leaf_off + 14;
7046 BigEndian::write_u16(&mut img[r0..r0 + 2], key_body_len as u16);
7047 BigEndian::write_u32(&mut img[r0 + 4..r0 + 8], cnid);
7049 BigEndian::write_u32(&mut img[r0 + 8..r0 + 12], 0); BigEndian::write_u16(&mut img[r0 + 12..r0 + 14], name_utf16.len() as u16);
7051 for (i, ch) in name_utf16.iter().enumerate() {
7052 BigEndian::write_u16(&mut img[r0 + 14 + i * 2..r0 + 14 + i * 2 + 2], *ch);
7053 }
7054 let data_off = r0 + 2 + key_body_len;
7055 BigEndian::write_u32(&mut img[data_off..data_off + 4], HFSPLUS_ATTR_INLINE_DATA);
7056 BigEndian::write_u32(&mut img[data_off + 12..data_off + 16], value.len() as u32);
7058 img[data_off + 16..data_off + 16 + value.len()].copy_from_slice(value);
7059
7060 let lot = leaf_off + node_size;
7062 BigEndian::write_u16(&mut img[lot - 2..lot], 14);
7063 BigEndian::write_u16(&mut img[lot - 4..lot - 2], (14 + rec_total) as u16);
7064
7065 let mut vh = HfsPlusVolumeHeader::parse(&img[1024..1536]).expect("parse seed VH");
7068 vh.attributes_file = ForkData {
7069 logical_size: (attr_blocks as u64) * (block_size as u64),
7070 clump_size: 0,
7071 total_blocks: attr_blocks,
7072 extents: {
7073 let mut e = [ExtentDescriptor {
7074 start_block: 0,
7075 block_count: 0,
7076 }; 8];
7077 e[0] = ExtentDescriptor {
7078 start_block: attr_start_block,
7079 block_count: attr_blocks,
7080 };
7081 e
7082 },
7083 };
7084 vh.free_blocks -= attr_blocks;
7085 vh.next_allocation = 8;
7086 let vh_bytes = vh.serialize();
7087 img[1024..1536].copy_from_slice(&vh_bytes);
7088 let alt = img.len() - 1024;
7089 img[alt..alt + 512].copy_from_slice(&vh_bytes);
7090 img
7091 }
7092
7093 fn make_hfsplus_image_with_label(volume_name: &str, num_children: usize) -> Vec<u8> {
7100 let block_size = 4096u32;
7101 let total_blocks = 256u32;
7102 let image_size = total_blocks as usize * block_size as usize;
7103 let mut img = vec![0u8; image_size];
7104
7105 let bitmap_block = 1u32;
7107 let catalog_start_block = 2u32;
7108 let catalog_blocks = 4u32;
7109 img[bitmap_block as usize * block_size as usize] = 0b11111100;
7111
7112 let node_size = 4096usize;
7113 let catalog_offset = catalog_start_block as usize * block_size as usize;
7114
7115 let hdr = catalog_offset;
7117 img[hdr + 8] = 1; BigEndian::write_u16(&mut img[hdr + 10..hdr + 12], 3);
7119 let hr = hdr + 14;
7120 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);
7123 BigEndian::write_u32(&mut img[hr + 10..hr + 14], 1); BigEndian::write_u32(&mut img[hr + 14..hr + 18], 2);
7126 BigEndian::write_u16(&mut img[hr + 18..hr + 20], node_size as u16);
7127 BigEndian::write_u16(&mut img[hr + 20..hr + 22], 516);
7128 BigEndian::write_u32(&mut img[hr + 22..hr + 26], 4);
7129 BigEndian::write_u32(&mut img[hr + 26..hr + 30], 0);
7130 let ot = hdr + node_size;
7132 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;
7141 img[leaf + 8] = 0xFF; img[leaf + 9] = 1; let total_recs = 1 + num_children;
7144 BigEndian::write_u16(&mut img[leaf + 10..leaf + 12], total_recs as u16);
7145
7146 let name_units: Vec<u16> = volume_name.encode_utf16().collect();
7149 let name_bytes = name_units.len() * 2;
7150 let r0 = leaf + 14;
7151 let key_len = 6 + name_bytes; BigEndian::write_u16(&mut img[r0..r0 + 2], key_len as u16);
7153 BigEndian::write_u32(&mut img[r0 + 2..r0 + 6], 1); BigEndian::write_u16(&mut img[r0 + 6..r0 + 8], name_units.len() as u16);
7155 for (i, u) in name_units.iter().enumerate() {
7156 BigEndian::write_u16(&mut img[r0 + 8 + i * 2..r0 + 10 + i * 2], *u);
7157 }
7158 let r0_data = r0 + 2 + key_len + ((2 + key_len) % 2); BigEndian::write_i16(&mut img[r0_data..r0_data + 2], CATALOG_FOLDER);
7160 BigEndian::write_u32(&mut img[r0_data + 8..r0_data + 12], 2); let mut rec_offsets = vec![14u16];
7167 let mut cursor = r0_data + 88;
7168 for i in 0..num_children {
7169 let child_name = format!("c{i}");
7170 let cn_units: Vec<u16> = child_name.encode_utf16().collect();
7171 let key_len = 6 + cn_units.len() * 2;
7172 let off = cursor;
7173 BigEndian::write_u16(&mut img[off..off + 2], key_len as u16);
7174 BigEndian::write_u32(&mut img[off + 2..off + 6], 2);
7175 BigEndian::write_u16(&mut img[off + 6..off + 8], cn_units.len() as u16);
7176 for (j, u) in cn_units.iter().enumerate() {
7177 BigEndian::write_u16(&mut img[off + 8 + j * 2..off + 10 + j * 2], *u);
7178 }
7179 let data = off + 2 + key_len + ((2 + key_len) % 2);
7182 BigEndian::write_i16(&mut img[data..data + 2], CATALOG_FOLDER);
7183 rec_offsets.push((off - leaf) as u16);
7184 cursor = data + 88;
7185 }
7186
7187 let lot = leaf + node_size;
7189 for (i, off) in rec_offsets.iter().enumerate() {
7190 BigEndian::write_u16(&mut img[lot - 2 - i * 2..lot - i * 2], *off);
7191 }
7192 BigEndian::write_u16(
7193 &mut img[lot - 2 - rec_offsets.len() * 2..lot - rec_offsets.len() * 2],
7194 (cursor - leaf) as u16,
7195 );
7196
7197 let vh = HfsPlusVolumeHeader {
7199 signature: HFS_PLUS_SIGNATURE,
7200 version: 4,
7201 attributes: 0,
7202 last_mounted_version: 0,
7203 journal_info_block: 0,
7204 create_date: 0,
7205 modify_date: 0,
7206 backup_date: 0,
7207 checked_date: 0,
7208 file_count: 0,
7209 folder_count: num_children as u32,
7210 block_size,
7211 total_blocks,
7212 free_blocks: total_blocks - 6,
7213 next_allocation: 6,
7214 rsrc_clump_size: 0,
7215 data_clump_size: 0,
7216 next_catalog_id: (16 + num_children) as u32,
7217 write_count: 0,
7218 encodings_bitmap: 0,
7219 finder_info: [0; 8],
7220 allocation_file: ForkData {
7221 logical_size: block_size as u64,
7222 clump_size: 0,
7223 total_blocks: 1,
7224 extents: {
7225 let mut e = [ExtentDescriptor {
7226 start_block: 0,
7227 block_count: 0,
7228 }; 8];
7229 e[0] = ExtentDescriptor {
7230 start_block: bitmap_block,
7231 block_count: 1,
7232 };
7233 e
7234 },
7235 },
7236 extents_file: ForkData::empty(),
7237 catalog_file: ForkData {
7238 logical_size: (catalog_blocks as u64) * (block_size as u64),
7239 clump_size: 0,
7240 total_blocks: catalog_blocks,
7241 extents: {
7242 let mut e = [ExtentDescriptor {
7243 start_block: 0,
7244 block_count: 0,
7245 }; 8];
7246 e[0] = ExtentDescriptor {
7247 start_block: catalog_start_block,
7248 block_count: catalog_blocks,
7249 };
7250 e
7251 },
7252 },
7253 attributes_file: ForkData::empty(),
7254 startup_file: ForkData::empty(),
7255 };
7256 let vh_bytes = vh.serialize();
7257 img[1024..1024 + 512].copy_from_slice(&vh_bytes);
7258 let alt = image_size - 1024;
7259 img[alt..alt + 512].copy_from_slice(&vh_bytes);
7260 img
7261 }
7262
7263 #[test]
7264 fn test_probe_hfsplus_volume_label_reads_from_root_key() {
7265 let img = make_hfsplus_image_with_label("Ariel-backup", 20);
7270 let mut cursor = std::io::Cursor::new(img);
7271 let label = probe_hfsplus_volume_label(&mut cursor, 0);
7272 assert_eq!(label.as_deref(), Some("Ariel-backup"));
7273 }
7274
7275 #[test]
7276 fn test_probe_hfsplus_volume_label_short_name() {
7277 let img = make_hfsplus_image_with_label("X", 0);
7278 let mut cursor = std::io::Cursor::new(img);
7279 assert_eq!(
7280 probe_hfsplus_volume_label(&mut cursor, 0).as_deref(),
7281 Some("X")
7282 );
7283 }
7284
7285 #[test]
7286 fn test_prepare_for_edit_refuses_journaled_volume_without_valid_journal() {
7287 let mut img = make_editable_hfsplus_image();
7293 let attr_off = 1024 + 4; let mut attrs = BigEndian::read_u32(&img[attr_off..attr_off + 4]);
7295 attrs |= HFSPLUS_VOLUME_JOURNALED_BIT;
7296 BigEndian::write_u32(&mut img[attr_off..attr_off + 4], attrs);
7297
7298 let cursor = std::io::Cursor::new(img);
7299 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("read-only open should succeed");
7300 let err = fs
7301 .prepare_for_edit()
7302 .expect_err("journaled volume with no valid journal must refuse edit prep");
7303 match err {
7304 FilesystemError::Unsupported(msg) => assert!(msg.contains("journal")),
7305 other => panic!("expected Unsupported(journal...), got {other:?}"),
7306 }
7307 }
7308
7309 #[test]
7310 fn test_sync_metadata_stamps_clean_unmount_attributes() {
7311 let mut img = make_editable_hfsplus_image();
7317 let attr_off = 1024 + 4;
7318 let dirty = 0x80004000u32 | HFSPLUS_BOOT_VOLUME_INCONSISTENT_BIT;
7319 BigEndian::write_u32(&mut img[attr_off..attr_off + 4], dirty);
7320
7321 let cursor = std::io::Cursor::new(img);
7322 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("open");
7323 fs.prepare_for_edit().expect("prep");
7324 fs.sync_metadata().expect("sync");
7325
7326 let img = fs.reader.into_inner();
7327 let after = BigEndian::read_u32(&img[attr_off..attr_off + 4]);
7328 assert!(
7329 after & HFSPLUS_VOLUME_UNMOUNTED_BIT != 0,
7330 "unmounted bit must be set after sync, got 0x{after:08x}"
7331 );
7332 assert!(
7333 after & HFSPLUS_VOLUME_INCONSISTENT_BIT == 0,
7334 "inconsistent bit must be cleared, got 0x{after:08x}"
7335 );
7336 assert!(
7337 after & HFSPLUS_BOOT_VOLUME_INCONSISTENT_BIT == 0,
7338 "boot-inconsistent bit must be cleared, got 0x{after:08x}"
7339 );
7340 }
7341
7342 #[test]
7343 fn test_prepare_for_edit_clean_volume_succeeds() {
7344 let img = make_editable_hfsplus_image();
7345 let cursor = std::io::Cursor::new(img);
7346 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
7347 fs.prepare_for_edit()
7348 .expect("clean volume should accept edit prep");
7349 assert!(fs.xattr_cnids.as_ref().unwrap().is_empty());
7351 }
7352
7353 #[test]
7356 fn deleting_a_leafs_first_record_refreshes_the_separators() {
7357 use crate::hfs_common::{
7360 btree_record_range, btree_stale_index_keys, BTreeHeader, BTreeKeyFormat,
7361 };
7362 type Fs = HfsPlusFilesystem<std::io::Cursor<Vec<u8>>>;
7363 let img = create_blank_hfsplus_sized(64 * 1024 * 1024, 4096, "IKey", false, 16 << 20, 0);
7364 let mut fs = Fs::open(std::io::Cursor::new(img), 0).unwrap();
7365 fs.prepare_for_edit().unwrap();
7366 let root = fs.root().unwrap();
7367 let options = CreateFileOptions::default();
7368 for i in 0..1200 {
7369 let mut empty = std::io::Cursor::new(Vec::new());
7370 fs.create_file(&root, &format!("f{i:04}.txt"), &mut empty, 0, &options)
7371 .unwrap();
7372 }
7373 let node_size = fs.catalog_node_size();
7374 let header = BTreeHeader::read(fs.catalog_data());
7375 assert!(
7376 header.depth >= 2,
7377 "need an index level, depth {}",
7378 header.depth
7379 );
7380
7381 let first = header.first_leaf_node as usize;
7384 let second =
7385 BigEndian::read_u32(&fs.catalog_data()[first * node_size..first * node_size + 4])
7386 as usize;
7387 let name = {
7388 let node = &fs.catalog_data()[second * node_size..(second + 1) * node_size];
7389 let (s, e) = btree_record_range(node, node_size, 0);
7390 let rec = &node[s..e];
7391 let name_len = BigEndian::read_u16(&rec[6..8]) as usize;
7392 (0..name_len)
7393 .map(|i| BigEndian::read_u16(&rec[8 + 2 * i..10 + 2 * i]) as u8 as char)
7394 .collect::<String>()
7395 };
7396 assert!(
7397 name.starts_with('f'),
7398 "expected a file record first, got {name:?}"
7399 );
7400 let victim = fs
7401 .list_directory(&root)
7402 .unwrap()
7403 .into_iter()
7404 .find(|e| e.name == name)
7405 .expect("victim listed");
7406 fs.delete_entry(&root, &victim).unwrap();
7407
7408 let cmp = |a: &[u8], b: &[u8]| Fs::catalog_compare(a, b, false);
7409 let stale = btree_stale_index_keys(
7410 fs.catalog_data(),
7411 node_size,
7412 &BTreeKeyFormat::HFSPLUS_CATALOG,
7413 &cmp,
7414 );
7415 assert!(stale.is_empty(), "separators still stale: {stale:?}");
7416 let report = fs.fsck().unwrap().unwrap();
7417 assert!(
7418 report.errors.is_empty(),
7419 "{:?}",
7420 report
7421 .errors
7422 .iter()
7423 .map(|e| &e.message)
7424 .take(4)
7425 .collect::<Vec<_>>()
7426 );
7427 }
7428
7429 #[test]
7430 fn emptied_leaves_leave_the_index_too() {
7431 use crate::hfs_common::{btree_bitmap_test, btree_record_range, BTREE_INDEX_NODE};
7432 let img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "Leaves", false);
7433 let mut fs = HfsPlusFilesystem::open(std::io::Cursor::new(img), 0).unwrap();
7434 fs.prepare_for_edit().unwrap();
7435 let root = fs.root().unwrap();
7436 let names: Vec<String> = (0..220).map(|i| format!("leaf-file-{i:04}.txt")).collect();
7437 let fill = |fs: &mut HfsPlusFilesystem<std::io::Cursor<Vec<u8>>>| {
7438 for n in &names {
7439 let mut src = std::io::Cursor::new(n.as_bytes().to_vec());
7440 fs.create_file(
7441 &root,
7442 n,
7443 &mut src,
7444 n.len() as u64,
7445 &CreateFileOptions::default(),
7446 )
7447 .unwrap_or_else(|e| panic!("create {n}: {e:?}"));
7448 }
7449 };
7450 fill(&mut fs);
7451 assert!(
7452 fs.catalog_header.depth > 1,
7453 "the test needs a split catalog"
7454 );
7455
7456 for e in fs.list_directory(&root).unwrap() {
7457 fs.delete_entry(&root, &e).unwrap();
7458 }
7459 let node_size = fs.catalog_header.node_size as usize;
7461 let data = &fs.catalog_data;
7462 for idx in 1..(data.len() / node_size) as u32 {
7463 if !btree_bitmap_test(data, node_size, idx) {
7464 continue;
7465 }
7466 let off = idx as usize * node_size;
7467 let node = &data[off..off + node_size];
7468 if node[8] as i8 != BTREE_INDEX_NODE {
7469 continue;
7470 }
7471 let n = BigEndian::read_u16(&node[10..12]) as usize;
7472 for r in 0..n {
7473 let (_, end) = btree_record_range(node, node_size, r);
7474 let child = BigEndian::read_u32(&node[end - 4..end]);
7475 assert!(
7476 btree_bitmap_test(data, node_size, child),
7477 "index node {idx} points at free node {child}"
7478 );
7479 }
7480 }
7481
7482 fill(&mut fs);
7483 let listed = fs.list_directory(&root).unwrap();
7484 assert_eq!(listed.len(), names.len());
7485 for e in &listed {
7486 assert_eq!(fs.read_file(e, usize::MAX).unwrap(), e.name.as_bytes());
7487 }
7488 fs.sync_metadata().unwrap();
7489 let report = fs.fsck().unwrap().unwrap();
7490 assert!(report.errors.is_empty(), "{:?}", report.errors);
7491 }
7492
7493 #[test]
7494 fn test_create_blank_hfsplus_32mib() {
7495 let img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "MyVol", false);
7496 assert_eq!(img.len(), 32 * 1024 * 1024);
7497
7498 let cursor = std::io::Cursor::new(img);
7499 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("blank should open");
7500 assert_eq!(fs.fs_type(), "HFS+");
7501 assert_eq!(fs.label, "MyVol");
7502
7503 let root = fs.root().unwrap();
7504 let entries = fs.list_directory(&root).unwrap();
7505 assert!(entries.is_empty(), "freshly built blank must list empty");
7506
7507 assert_eq!(fs.vh.total_blocks, 8192);
7514 assert_eq!(fs.vh.free_blocks, 8192 - 47 - 1);
7515 fs.prepare_for_edit().expect("blank must accept edit prep");
7517 }
7518
7519 #[test]
7520 fn test_create_blank_hfsx_32mib_signature() {
7521 let img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "CSVol", true);
7522 let cursor = std::io::Cursor::new(img);
7523 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("HFSX blank should open");
7524 assert_eq!(fs.fs_type(), "HFSX");
7525 assert!(fs.vh.is_hfsx());
7526 assert_eq!(fs.label, "CSVol");
7527 let root = fs.root().unwrap();
7528 assert!(fs.list_directory(&root).unwrap().is_empty());
7529 }
7530
7531 #[test]
7532 fn test_create_blank_hfsplus_supports_create_file() {
7533 let img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "Work", false);
7535 let cursor = std::io::Cursor::new(img);
7536 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
7537 fs.prepare_for_edit().unwrap();
7538 let root = fs.root().unwrap();
7539 let payload = b"hello world".to_vec();
7540 let mut data = std::io::Cursor::new(payload.clone());
7541 fs.create_file(
7542 &root,
7543 "hello.txt",
7544 &mut data,
7545 payload.len() as u64,
7546 &CreateFileOptions::default(),
7547 )
7548 .expect("create_file on blank");
7549 fs.sync_metadata().unwrap();
7550
7551 let img2 = fs.reader.into_inner();
7552 let mut cursor2 = std::io::Cursor::new(img2);
7553 let mut fs2 = HfsPlusFilesystem::open(&mut cursor2, 0).unwrap();
7554 let root2 = fs2.root().unwrap();
7555 let entries = fs2.list_directory(&root2).unwrap();
7556 assert_eq!(entries.len(), 1);
7557 assert_eq!(entries[0].name, "hello.txt");
7558 let got = fs2.read_file(&entries[0], 1024).unwrap();
7559 assert_eq!(got, payload);
7560 }
7561
7562 #[test]
7563 fn test_hfsplus_rename_round_trips() {
7564 let img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "Work", false);
7565 let mut fs = HfsPlusFilesystem::open(std::io::Cursor::new(img), 0).unwrap();
7566 fs.prepare_for_edit().unwrap();
7567 let root = fs.root().unwrap();
7568
7569 let payload = b"keep my bytes across a rename".to_vec();
7570 let mut data = std::io::Cursor::new(payload.clone());
7571 let fe = fs
7572 .create_file(
7573 &root,
7574 "before.txt",
7575 &mut data,
7576 payload.len() as u64,
7577 &CreateFileOptions::default(),
7578 )
7579 .unwrap();
7580 let cnid = fe.location;
7581 fs.create_directory(&root, "olddir", &CreateDirectoryOptions::default())
7582 .unwrap();
7583
7584 fs.rename(&root, &fe, "a much longer renamed file.txt")
7587 .unwrap();
7588 let old_dir = fs
7590 .list_directory(&root)
7591 .unwrap()
7592 .into_iter()
7593 .find(|e| e.name == "olddir")
7594 .unwrap();
7595 fs.rename(&root, &old_dir, "newdir").unwrap();
7596
7597 let mut t = std::io::Cursor::new(b"x".as_slice());
7599 fs.create_file(&root, "taken.txt", &mut t, 1, &CreateFileOptions::default())
7600 .unwrap();
7601 let renamed_now = fs
7602 .list_directory(&root)
7603 .unwrap()
7604 .into_iter()
7605 .find(|e| e.name == "a much longer renamed file.txt")
7606 .unwrap();
7607 assert!(matches!(
7608 fs.rename(&root, &renamed_now, "taken.txt"),
7609 Err(FilesystemError::AlreadyExists(_))
7610 ));
7611
7612 fs.sync_metadata().unwrap();
7614 let img2 = fs.reader.into_inner();
7615 let mut c2 = std::io::Cursor::new(img2);
7616 let mut fs2 = HfsPlusFilesystem::open(&mut c2, 0).unwrap();
7617 let root2 = fs2.root().unwrap();
7618 let entries = fs2.list_directory(&root2).unwrap();
7619 assert!(
7620 entries
7621 .iter()
7622 .any(|e| e.name == "a much longer renamed file.txt"),
7623 "{entries:?}"
7624 );
7625 assert!(
7626 !entries.iter().any(|e| e.name == "before.txt"),
7627 "{entries:?}"
7628 );
7629 let renamed = entries
7630 .iter()
7631 .find(|e| e.name == "a much longer renamed file.txt")
7632 .unwrap();
7633 assert_eq!(renamed.location, cnid);
7635 assert_eq!(fs2.read_file(renamed, 4096).unwrap(), payload);
7636 let newdir = entries.iter().find(|e| e.name == "newdir").unwrap();
7638 assert!(fs2.list_directory(newdir).is_ok());
7639 }
7640
7641 #[test]
7642 fn test_create_file_stamps_dates() {
7643 let img = make_editable_hfsplus_image();
7647 let cursor = std::io::Cursor::new(img);
7648 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
7649 fs.prepare_for_edit().unwrap();
7650
7651 let now_before = hfs_common::hfs_now();
7652 let root = fs.root().unwrap();
7653 let mut data = std::io::Cursor::new(b"hi".as_slice());
7654 let entry = fs
7655 .create_file(
7656 &root,
7657 "dated.txt",
7658 &mut data,
7659 2,
7660 &CreateFileOptions::default(),
7661 )
7662 .unwrap();
7663 let now_after = hfs_common::hfs_now();
7664
7665 let cnid = entry.location as u32;
7667 let (_n, _r, f_off) = fs.find_catalog_record(2, "dated.txt").unwrap();
7668 let fkey_len = BigEndian::read_u16(&fs.catalog_data[f_off..f_off + 2]) as usize;
7669 let mut frec = f_off + 2 + fkey_len;
7670 if !frec.is_multiple_of(2) {
7671 frec += 1;
7672 }
7673 let create_date = BigEndian::read_u32(&fs.catalog_data[frec + 12..frec + 16]);
7674 let content_mod = BigEndian::read_u32(&fs.catalog_data[frec + 16..frec + 20]);
7675 let attr_mod = BigEndian::read_u32(&fs.catalog_data[frec + 20..frec + 24]);
7676 let access = BigEndian::read_u32(&fs.catalog_data[frec + 24..frec + 28]);
7677
7678 for (label, value) in [
7679 ("createDate", create_date),
7680 ("contentModDate", content_mod),
7681 ("attributeModDate", attr_mod),
7682 ("accessDate", access),
7683 ] {
7684 assert!(
7685 value >= now_before && value <= now_after + 1,
7686 "{label} {value} not in [{now_before}, {now_after}+1]"
7687 );
7688 }
7689
7690 assert_eq!(fs.vh.file_count, 1);
7692 let _ = cnid;
7693 }
7694
7695 #[test]
7696 fn test_set_type_creator_bumps_attribute_mod_date() {
7697 let img = make_editable_hfsplus_image();
7701 let cursor = std::io::Cursor::new(img);
7702 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
7703 fs.prepare_for_edit().unwrap();
7704
7705 let root = fs.root().unwrap();
7706 let mut data = std::io::Cursor::new(b"x".as_slice());
7707 let entry = fs
7708 .create_file(&root, "a.txt", &mut data, 1, &CreateFileOptions::default())
7709 .unwrap();
7710
7711 let (_n, _r, f_off) = fs.find_catalog_record(2, "a.txt").unwrap();
7712 let fkey_len = BigEndian::read_u16(&fs.catalog_data[f_off..f_off + 2]) as usize;
7713 let mut frec = f_off + 2 + fkey_len;
7714 if !frec.is_multiple_of(2) {
7715 frec += 1;
7716 }
7717 let content_before = BigEndian::read_u32(&fs.catalog_data[frec + 16..frec + 20]);
7718
7719 BigEndian::write_u32(
7723 &mut fs.catalog_data[frec + 16..frec + 20],
7724 content_before - 60,
7725 );
7726 BigEndian::write_u32(
7727 &mut fs.catalog_data[frec + 20..frec + 24],
7728 content_before - 60,
7729 );
7730
7731 let now_before = hfs_common::hfs_now();
7732 fs.set_type_creator(&entry, "TEXT", "ttxt").unwrap();
7733 let now_after = hfs_common::hfs_now();
7734
7735 let content_after = BigEndian::read_u32(&fs.catalog_data[frec + 16..frec + 20]);
7736 let attr_after = BigEndian::read_u32(&fs.catalog_data[frec + 20..frec + 24]);
7737 assert_eq!(
7738 content_after,
7739 content_before - 60,
7740 "contentModDate must not move on attribute-only change"
7741 );
7742 assert!(
7743 attr_after >= now_before && attr_after <= now_after + 1,
7744 "attributeModDate {attr_after} not in [{now_before}, {now_after}+1]"
7745 );
7746 }
7747
7748 #[test]
7749 fn test_extents_overflow_round_trip_12_extents() {
7750 let img = make_editable_hfsplus_image_with_extents_btree();
7755 let cursor = std::io::Cursor::new(img);
7756 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
7757 fs.prepare_for_edit().unwrap();
7758
7759 fs.ensure_bitmap().unwrap();
7763 let bitmap = fs.bitmap.as_mut().unwrap();
7764 for byte in bitmap.iter_mut().take(32) {
7765 *byte = 0xFF;
7766 }
7767 let free_positions: Vec<u32> = (0..12).map(|i| 11 + 2 * i as u32).collect();
7768 for &blk in &free_positions {
7769 bitmap_clear_bit_be(bitmap, blk);
7770 }
7771 fs.vh.free_blocks = 12;
7772
7773 let block_size = fs.vh.block_size as usize;
7774 let payload: Vec<u8> = (0..(12 * block_size)).map(|i| (i % 251) as u8).collect();
7775 let mut reader = std::io::Cursor::new(payload.clone());
7776 let root = fs.root().unwrap();
7777 let entry = fs
7778 .create_file(
7779 &root,
7780 "frag12.bin",
7781 &mut reader,
7782 payload.len() as u64,
7783 &CreateFileOptions::default(),
7784 )
7785 .expect("12-extent allocation should succeed via overflow B-tree");
7786
7787 let cnid = entry.location as u32;
7788 let (data_fork, _rsrc) = fs.find_file_by_id(cnid).unwrap();
7789 for slot in &data_fork.extents {
7791 assert_eq!(slot.block_count, 1);
7792 }
7793 assert_eq!(data_fork.total_blocks, 12);
7794
7795 fs.sync_metadata().unwrap();
7797
7798 let img_after = fs.reader.into_inner();
7801 let mut cursor2 = std::io::Cursor::new(img_after);
7802 let mut fs2 = HfsPlusFilesystem::open(&mut cursor2, 0).unwrap();
7803 let root2 = fs2.root().unwrap();
7804 let entries = fs2.list_directory(&root2).unwrap();
7805 let reopened = entries.iter().find(|e| e.name == "frag12.bin").unwrap();
7806 let got = fs2.read_file(reopened, payload.len() + 1).unwrap();
7807 assert_eq!(got.len(), payload.len());
7808 assert_eq!(got, payload);
7809 }
7810
7811 #[test]
7812 fn test_allocate_extents_fragmented_bitmap() {
7813 let img = make_editable_hfsplus_image();
7817 let cursor = std::io::Cursor::new(img);
7818 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
7819 fs.prepare_for_edit().unwrap();
7820
7821 fs.ensure_bitmap().unwrap();
7825 let bitmap = fs.bitmap.as_mut().unwrap();
7826 for byte in bitmap.iter_mut().take(32) {
7827 *byte = 0xFF;
7828 }
7829 for blk in [7u32, 9, 11, 13] {
7831 bitmap_clear_bit_be(bitmap, blk);
7832 }
7833 fs.vh.free_blocks = 4;
7834
7835 let block_size = fs.vh.block_size as usize;
7836 let payload: Vec<u8> = (0..(4 * block_size)).map(|i| (i % 251) as u8).collect();
7837 let mut reader = std::io::Cursor::new(payload.clone());
7838 let root = fs.root().unwrap();
7839 let entry = fs
7840 .create_file(
7841 &root,
7842 "frag.bin",
7843 &mut reader,
7844 payload.len() as u64,
7845 &CreateFileOptions::default(),
7846 )
7847 .expect("fragmented allocation should succeed");
7848
7849 let cnid = entry.location as u32;
7850 let (data_fork, _rsrc) = fs.find_file_by_id(cnid).expect("file record present");
7851
7852 for (i, expected_start) in [7u32, 9, 11, 13].iter().enumerate() {
7854 assert_eq!(data_fork.extents[i].block_count, 1, "slot {i}");
7855 assert_eq!(
7856 data_fork.extents[i].start_block, *expected_start,
7857 "slot {i}"
7858 );
7859 }
7860 for slot in &data_fork.extents[4..] {
7861 assert!(slot.is_empty(), "slot past 4 should be empty");
7862 }
7863 assert_eq!(data_fork.total_blocks, 4);
7864
7865 let mut got = Vec::new();
7867 fs.write_file_to(&entry, &mut got).unwrap();
7868 assert_eq!(got, payload);
7869 assert_eq!(fs.vh.free_blocks, 0);
7870 }
7871
7872 #[test]
7873 fn test_snapshot_rollback_on_disk_full_create_file() {
7874 let img = make_editable_hfsplus_image();
7880 let cursor = std::io::Cursor::new(img);
7881 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
7882 fs.prepare_for_edit().unwrap();
7883
7884 let next_cnid_before = fs.vh.next_catalog_id;
7885 let free_blocks_before = fs.vh.free_blocks;
7886 let file_count_before = fs.vh.file_count;
7887 let catalog_before = fs.catalog_data.clone();
7888
7889 let root = fs.root().unwrap();
7890 let big_len: u64 = 251 * 4096 + 1; let big_buf = vec![0u8; big_len as usize];
7892 let mut reader = std::io::Cursor::new(big_buf);
7893 let err = fs
7894 .create_file(
7895 &root,
7896 "too-big.bin",
7897 &mut reader,
7898 big_len,
7899 &CreateFileOptions::default(),
7900 )
7901 .expect_err("create_file should fail with DiskFull");
7902 assert!(matches!(err, FilesystemError::DiskFull(_)), "got {err:?}");
7903
7904 assert_eq!(fs.vh.next_catalog_id, next_cnid_before);
7905 assert_eq!(fs.vh.free_blocks, free_blocks_before);
7906 assert_eq!(fs.vh.file_count, file_count_before);
7907 assert_eq!(fs.catalog_data, catalog_before);
7908 }
7909
7910 #[test]
7911 fn test_set_xattr_round_trip_and_delete_cleanup() {
7912 let sentinel_cnid = 99u32;
7921 let img = make_editable_hfsplus_image_with_inline_xattr(sentinel_cnid, &[0xAA; 8]);
7922 let cursor = std::io::Cursor::new(img);
7923 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
7924 fs.prepare_for_edit().unwrap();
7925
7926 let root = fs.root().unwrap();
7927 let mut data = std::io::Cursor::new(b"x".as_slice());
7928 let entry = fs
7929 .create_file(
7930 &root,
7931 "with-xattr.txt",
7932 &mut data,
7933 1,
7934 &CreateFileOptions::default(),
7935 )
7936 .expect("create_file");
7937 let cnid = entry.location as u32;
7938
7939 let pairs: [(&str, &[u8]); 3] = [
7940 ("com.apple.FinderInfo", &[1u8; 32]),
7941 ("com.apple.metadata:_kMDItemUserTags", b"\x00bplist00 tag"),
7942 ("user.short", b"hi"),
7943 ];
7944 for (name, value) in pairs.iter() {
7945 fs.set_xattr(cnid, name, value).expect("set_xattr");
7946 }
7947 fs.sync_metadata().unwrap();
7948
7949 let img2 = fs.reader.into_inner();
7951 let mut cursor2 = std::io::Cursor::new(img2);
7952 let mut fs2 = HfsPlusFilesystem::open(&mut cursor2, 0).unwrap();
7953 let mut got = fs2.list_xattrs(cnid).expect("list_xattrs");
7954 got.sort_by(|a, b| a.name.cmp(&b.name));
7955 let mut want: Vec<(String, Vec<u8>)> = pairs
7956 .iter()
7957 .map(|(n, v)| (n.to_string(), v.to_vec()))
7958 .collect();
7959 want.sort_by(|a, b| a.0.cmp(&b.0));
7960 assert_eq!(got.len(), want.len());
7961 for (rec, (wname, wval)) in got.iter().zip(want.iter()) {
7962 assert_eq!(rec.name, *wname);
7963 match &rec.kind {
7964 XattrKind::Inline(bytes) => assert_eq!(bytes, wval),
7965 other => panic!("expected Inline xattr, got {other:?}"),
7966 }
7967 }
7968
7969 let sentinel = fs2
7971 .list_xattrs(sentinel_cnid)
7972 .expect("sentinel list_xattrs");
7973 assert_eq!(sentinel.len(), 1, "sentinel record should survive");
7974
7975 fs2.prepare_for_edit().unwrap();
7977 let root2 = fs2.root().unwrap();
7978 let listing = fs2.list_directory(&root2).unwrap();
7979 let target = listing
7980 .iter()
7981 .find(|e| e.name == "with-xattr.txt")
7982 .cloned()
7983 .expect("entry on reopened volume");
7984 fs2.delete_entry(&root2, &target).expect("delete");
7985 let after = fs2.list_xattrs(cnid).expect("post-delete list_xattrs");
7986 assert!(
7987 after.is_empty(),
7988 "expected 0 records after delete, got {}",
7989 after.len()
7990 );
7991 let sentinel_after = fs2
7992 .list_xattrs(sentinel_cnid)
7993 .expect("sentinel post-delete");
7994 assert_eq!(sentinel_after.len(), 1, "sentinel must survive delete");
7995 }
7996
7997 #[test]
7998 fn test_remove_xattr_not_found_and_cleanup() {
7999 let cnid = 17u32;
8002 let img = make_editable_hfsplus_image_with_inline_xattr(cnid, &[0u8; 16]);
8003 let cursor = std::io::Cursor::new(img);
8004 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
8005 fs.prepare_for_edit().unwrap();
8006 assert!(fs.xattr_cnids.as_ref().unwrap().contains(&cnid));
8007
8008 let err = fs
8009 .remove_xattr(cnid, "com.apple.does-not-exist")
8010 .expect_err("missing xattr must error");
8011 assert!(matches!(err, FilesystemError::NotFound(_)));
8012
8013 fs.remove_xattr(cnid, "com.apple.FinderInfo")
8014 .expect("remove existing xattr");
8015 assert!(
8016 !fs.xattr_cnids.as_ref().unwrap().contains(&cnid),
8017 "cnid should be dropped from cache once its last xattr is gone"
8018 );
8019 }
8020
8021 #[test]
8022 fn test_list_xattrs_round_trips_inline_finder_info() {
8023 let cnid = 17u32;
8027 let finder_info: Vec<u8> = (0..32u8).collect();
8028 let img = make_editable_hfsplus_image_with_inline_xattr(cnid, &finder_info);
8029 let cursor = std::io::Cursor::new(img);
8030 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
8031
8032 let recs = fs.list_xattrs(cnid).expect("list_xattrs cnid=17");
8033 assert_eq!(recs.len(), 1, "expected exactly one xattr record");
8034 let rec = &recs[0];
8035 assert_eq!(rec.name, "com.apple.FinderInfo");
8036 assert_eq!(rec.start_block, 0);
8037 match &rec.kind {
8038 XattrKind::Inline(bytes) => assert_eq!(bytes, &finder_info),
8039 other => panic!("expected Inline xattr, got {other:?}"),
8040 }
8041
8042 let none = fs.list_xattrs(99).expect("list_xattrs cnid=99");
8044 assert!(none.is_empty(), "expected no xattrs for cnid=99");
8045
8046 let bare = make_editable_hfsplus_image();
8048 let mut bare_fs = HfsPlusFilesystem::open(std::io::Cursor::new(bare), 0).unwrap();
8049 let empty = bare_fs.list_xattrs(cnid).expect("list_xattrs no attr file");
8050 assert!(empty.is_empty(), "no attributes file → no records");
8051 }
8052
8053 #[test]
8054 fn test_hardlink_resolution_two_links_one_inode() {
8055 const PAYLOAD: &[u8] = b"INODE_PAYLOAD_42\n";
8060 const I_NODE_NUM: u32 = 99;
8061
8062 let mut img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "Links", false);
8063
8064 let inode_cnid: u32;
8070 {
8071 let cursor = std::io::Cursor::new(&mut img);
8072 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("open blank");
8073 fs.prepare_for_edit().expect("prepare_for_edit");
8074 let root = fs.root().expect("root");
8075
8076 let private_dir = fs
8077 .create_directory(
8078 &root,
8079 &hfsplus_private_dir_name(),
8080 &CreateDirectoryOptions::default(),
8081 )
8082 .expect("create private dir");
8083
8084 let mut inode_data = std::io::Cursor::new(PAYLOAD);
8085 let inode_entry = fs
8086 .create_file(
8087 &private_dir,
8088 &format!("iNode{I_NODE_NUM}"),
8089 &mut inode_data,
8090 PAYLOAD.len() as u64,
8091 &CreateFileOptions::default(),
8092 )
8093 .expect("create inode file");
8094 inode_cnid = inode_entry.location as u32;
8095
8096 for link_name in ["linkA", "linkB"] {
8097 let mut empty = std::io::Cursor::new(&[][..]);
8098 fs.create_file(
8099 &root,
8100 link_name,
8101 &mut empty,
8102 0,
8103 &CreateFileOptions {
8104 os_type: Some(*b"hlnk"),
8105 os_creator: Some(*b"hfs+"),
8106 ..Default::default()
8107 },
8108 )
8109 .expect("create link stub");
8110
8111 let (_node, _rec, abs) = fs
8114 .find_catalog_record(2, link_name)
8115 .expect("link stub in catalog");
8116 let key_len = BigEndian::read_u16(&fs.catalog_data[abs..abs + 2]) as usize;
8117 let mut body_start = abs + 2 + key_len;
8118 if !body_start.is_multiple_of(2) {
8119 body_start += 1;
8120 }
8121 BigEndian::write_u32(
8122 &mut fs.catalog_data[body_start + 44..body_start + 48],
8123 I_NODE_NUM,
8124 );
8125 }
8126
8127 fs.sync_metadata().expect("sync");
8128 }
8129
8130 let cursor = std::io::Cursor::new(&mut img);
8133 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("re-open");
8134 let root = fs.root().expect("root");
8135 let entries = fs.list_directory(&root).expect("list root");
8136 let link_a = entries
8137 .iter()
8138 .find(|e| e.name == "linkA")
8139 .cloned()
8140 .expect("linkA");
8141 let link_b = entries
8142 .iter()
8143 .find(|e| e.name == "linkB")
8144 .cloned()
8145 .expect("linkB");
8146
8147 assert_eq!(
8148 link_a.link_target_cnid,
8149 Some(inode_cnid as u64),
8150 "linkA must resolve to inode CNID"
8151 );
8152 assert_eq!(
8153 link_b.link_target_cnid,
8154 Some(inode_cnid as u64),
8155 "linkB must resolve to inode CNID"
8156 );
8157 assert_eq!(link_a.size, PAYLOAD.len() as u64);
8160 assert_eq!(link_b.size, PAYLOAD.len() as u64);
8161
8162 let bytes_a = fs.read_file(&link_a, usize::MAX).expect("read linkA");
8163 let bytes_b = fs.read_file(&link_b, usize::MAX).expect("read linkB");
8164 assert_eq!(bytes_a, PAYLOAD);
8165 assert_eq!(bytes_b, PAYLOAD);
8166
8167 let private = entries
8171 .iter()
8172 .find(|e| e.name == hfsplus_private_dir_name())
8173 .expect("private dir at root");
8174 assert!(private.link_target_cnid.is_none());
8175 }
8176
8177 #[test]
8178 fn test_hardlink_delete_decrements_then_frees_inode() {
8179 const PAYLOAD: &[u8] = b"INODE_DATA_FOR_DELETE\n";
8183 const I_NODE_NUM: u32 = 7;
8184
8185 let mut img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "DelLinks", false);
8186
8187 let inode_cnid: u32;
8190 let private_cnid: u32;
8191 let payload_blocks_used: u32;
8192 let free_after_build: u32;
8193 let free_with_inode: u32;
8194 {
8195 let cursor = std::io::Cursor::new(&mut img);
8196 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("open blank");
8197 fs.prepare_for_edit().expect("prepare_for_edit");
8198 free_after_build = fs.vh.free_blocks;
8199 let root = fs.root().expect("root");
8200 let private_dir = fs
8201 .create_directory(
8202 &root,
8203 &hfsplus_private_dir_name(),
8204 &CreateDirectoryOptions::default(),
8205 )
8206 .expect("create private dir");
8207 private_cnid = private_dir.location as u32;
8208
8209 let mut inode_data = std::io::Cursor::new(PAYLOAD);
8210 let inode_entry = fs
8211 .create_file(
8212 &private_dir,
8213 &format!("iNode{I_NODE_NUM}"),
8214 &mut inode_data,
8215 PAYLOAD.len() as u64,
8216 &CreateFileOptions::default(),
8217 )
8218 .expect("create inode file");
8219 inode_cnid = inode_entry.location as u32;
8220 free_with_inode = fs.vh.free_blocks;
8221 payload_blocks_used = free_after_build - free_with_inode;
8222 assert!(
8223 payload_blocks_used >= 1,
8224 "inode payload must consume at least one block"
8225 );
8226
8227 {
8229 let (_, _, abs) = fs
8230 .find_catalog_record(private_cnid, &format!("iNode{I_NODE_NUM}"))
8231 .expect("inode catalog record");
8232 let key_len = BigEndian::read_u16(&fs.catalog_data[abs..abs + 2]) as usize;
8233 let mut body = abs + 2 + key_len;
8234 if !body.is_multiple_of(2) {
8235 body += 1;
8236 }
8237 BigEndian::write_u32(&mut fs.catalog_data[body + 44..body + 48], 2);
8238 }
8239
8240 for link_name in ["linkA", "linkB"] {
8241 let mut empty = std::io::Cursor::new(&[][..]);
8242 fs.create_file(
8243 &root,
8244 link_name,
8245 &mut empty,
8246 0,
8247 &CreateFileOptions {
8248 os_type: Some(*b"hlnk"),
8249 os_creator: Some(*b"hfs+"),
8250 ..Default::default()
8251 },
8252 )
8253 .expect("create stub");
8254 let (_, _, abs) = fs.find_catalog_record(2, link_name).expect("stub");
8255 let key_len = BigEndian::read_u16(&fs.catalog_data[abs..abs + 2]) as usize;
8256 let mut body = abs + 2 + key_len;
8257 if !body.is_multiple_of(2) {
8258 body += 1;
8259 }
8260 BigEndian::write_u32(&mut fs.catalog_data[body + 44..body + 48], I_NODE_NUM);
8261 }
8262 fs.sync_metadata().expect("sync");
8263 }
8264
8265 let cursor = std::io::Cursor::new(&mut img);
8268 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("re-open");
8269 fs.prepare_for_edit().expect("prepare_for_edit");
8270 let root = fs.root().expect("root");
8271 let entries = fs.list_directory(&root).expect("list");
8272 let link_a = entries
8273 .iter()
8274 .find(|e| e.name == "linkA")
8275 .cloned()
8276 .expect("linkA");
8277 let link_b = entries
8278 .iter()
8279 .find(|e| e.name == "linkB")
8280 .cloned()
8281 .expect("linkB");
8282 let free_before_delete = fs.vh.free_blocks;
8283
8284 fs.delete_entry(&root, &link_a).expect("delete linkA");
8285 let cnt_after_first = {
8287 let (_, _, abs) = fs
8288 .find_catalog_record(private_cnid, &format!("iNode{I_NODE_NUM}"))
8289 .expect("inode still present");
8290 let key_len = BigEndian::read_u16(&fs.catalog_data[abs..abs + 2]) as usize;
8291 let mut body = abs + 2 + key_len;
8292 if !body.is_multiple_of(2) {
8293 body += 1;
8294 }
8295 BigEndian::read_u32(&fs.catalog_data[body + 44..body + 48])
8296 };
8297 assert_eq!(cnt_after_first, 1, "linkCount should decrement to 1");
8298 assert_eq!(
8299 fs.vh.free_blocks, free_before_delete,
8300 "no blocks freed while inode still has refs"
8301 );
8302 let bytes_b = fs.read_file(&link_b, usize::MAX).expect("read linkB");
8304 assert_eq!(bytes_b, PAYLOAD);
8305
8306 fs.delete_entry(&root, &link_b).expect("delete linkB");
8307 let private_children = fs.list_children(private_cnid).expect("list private dir");
8309 assert!(
8310 private_children.is_empty(),
8311 "iNode entry should be removed once linkCount hits 0; got {} children",
8312 private_children.len()
8313 );
8314 assert!(
8315 fs.find_file_by_id(inode_cnid).is_none(),
8316 "inode catalog row should be gone"
8317 );
8318 assert_eq!(
8319 fs.vh.free_blocks,
8320 free_before_delete + payload_blocks_used,
8321 "inode payload blocks should be reclaimed"
8322 );
8323 }
8324
8325 #[test]
8326 fn test_hardlink_capture_and_replay_dedupes_inode() {
8327 const PAYLOAD: &[u8] = b"shared inode contents (3 names)\n";
8334 const I_NODE_NUM: u32 = 17;
8335
8336 let mut source_img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "Src", false);
8338 let source_inode_cnid: u32;
8339 {
8340 let cursor = std::io::Cursor::new(&mut source_img);
8341 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("open src blank");
8342 fs.prepare_for_edit().expect("prepare_for_edit");
8343 let mut data = std::io::Cursor::new(PAYLOAD);
8344 let inode = fs
8345 .create_hardlink_inode(
8346 I_NODE_NUM,
8347 &mut data,
8348 PAYLOAD.len() as u64,
8349 &CreateFileOptions::default(),
8350 )
8351 .expect("create source inode");
8352 source_inode_cnid = inode.location as u32;
8353 let private_cnid = fs.find_private_dir_cnid().unwrap().expect("private dir");
8356 let _ = fs
8357 .adjust_inode_link_count(private_cnid, I_NODE_NUM, 0)
8358 .expect("init linkCount");
8359 let root = fs.root().expect("root");
8360 for name in ["a.txt", "b.txt", "c.txt"] {
8361 fs.create_hardlink(&root, name, I_NODE_NUM)
8362 .expect("create stub");
8363 }
8364 fs.sync_metadata().expect("sync src");
8365 }
8366
8367 let cursor = std::io::Cursor::new(&mut source_img);
8369 let mut src_fs = HfsPlusFilesystem::open(cursor, 0).expect("re-open src");
8370 let snap =
8371 crate::hfsplus_clone::SourceCatalogSnapshot::capture(&mut src_fs).expect("capture");
8372
8373 let inodes: Vec<_> = snap.files.iter().filter(|f| f.is_inode).collect();
8375 assert_eq!(inodes.len(), 1, "expected exactly one inode in snapshot");
8376 let inode = inodes[0];
8377 assert_eq!(inode.cnid, source_inode_cnid);
8378 assert_eq!(inode.inode_num, Some(I_NODE_NUM));
8379 assert_eq!(inode.data_fork_size, PAYLOAD.len() as u64);
8380 let inode_payload = {
8382 use std::io::Read;
8383 let mut buf = Vec::new();
8384 src_fs
8385 .fork_stream_reader(inode.cnid, 0x00, &inode.data_fork_raw)
8386 .expect("fork reader")
8387 .read_to_end(&mut buf)
8388 .expect("read fork");
8389 buf
8390 };
8391 assert_eq!(inode_payload, PAYLOAD);
8392
8393 let user_files: Vec<_> = snap.files.iter().filter(|f| !f.is_inode).collect();
8394 assert!(
8395 user_files.is_empty(),
8396 "user-tree files should be empty (the three stubs are in `hardlinks`); \
8397 got {:?}",
8398 user_files.iter().map(|f| &f.name).collect::<Vec<_>>()
8399 );
8400
8401 assert_eq!(snap.hardlinks.len(), 3, "expected 3 hardlink stubs");
8402 for h in &snap.hardlinks {
8403 assert_eq!(h.inode_num, I_NODE_NUM);
8404 assert_eq!(h.inode_source_cnid, source_inode_cnid);
8405 assert_eq!(h.parent_cnid, 2, "stubs live at root");
8406 }
8407
8408 let mut target_img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "Tgt", false);
8410 let target_inode_cnid: u32;
8411 {
8412 let cursor = std::io::Cursor::new(&mut target_img);
8413 let mut tgt = HfsPlusFilesystem::open(cursor, 0).expect("open tgt blank");
8414 tgt.prepare_for_edit().expect("prepare_for_edit");
8415
8416 let mut data = std::io::Cursor::new(&inode_payload);
8417 let target_inode = tgt
8418 .create_hardlink_inode(
8419 inode.inode_num.unwrap(),
8420 &mut data,
8421 inode.data_fork_size,
8422 &CreateFileOptions::default(),
8423 )
8424 .expect("replay inode");
8425 target_inode_cnid = target_inode.location as u32;
8426
8427 let root = tgt.root().expect("tgt root");
8428 for h in &snap.hardlinks {
8429 tgt.create_hardlink(&root, &h.name, h.inode_num)
8430 .expect("replay stub");
8431 }
8432 tgt.sync_metadata().expect("sync tgt");
8433 }
8434
8435 let cursor = std::io::Cursor::new(&mut target_img);
8437 let mut tgt_fs = HfsPlusFilesystem::open(cursor, 0).expect("re-open tgt");
8438 let root = tgt_fs.root().expect("tgt root");
8439 let entries = tgt_fs.list_directory(&root).expect("list tgt root");
8440
8441 let stubs: Vec<&FileEntry> = entries
8444 .iter()
8445 .filter(|e| ["a.txt", "b.txt", "c.txt"].contains(&e.name.as_str()))
8446 .collect();
8447 assert_eq!(stubs.len(), 3);
8448 for stub in &stubs {
8449 assert_eq!(
8450 stub.link_target_cnid,
8451 Some(target_inode_cnid as u64),
8452 "stub {} must resolve to the single target inode",
8453 stub.name
8454 );
8455 assert_eq!(stub.size, PAYLOAD.len() as u64);
8456 }
8457 for stub in &stubs {
8458 let bytes = tgt_fs.read_file(stub, usize::MAX).expect("read tgt stub");
8459 assert_eq!(bytes, PAYLOAD);
8460 }
8461
8462 let private_cnid = tgt_fs
8464 .find_private_dir_cnid()
8465 .unwrap()
8466 .expect("private dir");
8467 let inode_name = format!("iNode{I_NODE_NUM}");
8468 let (_, _, abs) = tgt_fs
8469 .find_catalog_record(private_cnid, &inode_name)
8470 .expect("inode in private dir");
8471 let key_len = BigEndian::read_u16(&tgt_fs.catalog_data[abs..abs + 2]) as usize;
8472 let mut body = abs + 2 + key_len;
8473 if !body.is_multiple_of(2) {
8474 body += 1;
8475 }
8476 let link_count = BigEndian::read_u32(&tgt_fs.catalog_data[body + 44..body + 48]);
8477 assert_eq!(link_count, 3, "inode linkCount must equal stub count");
8478 }
8479
8480 #[test]
8481 fn test_dir_hardlink_capture_and_replay_resolves_to_inode() {
8482 const PAYLOAD: &[u8] = b"file inside dir-hardlinked folder\n";
8489 const D_INODE_NUM: u32 = 13;
8490
8491 let mut source_img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "DhSrc", false);
8493 let source_inode_cnid: u32;
8494 {
8495 let cursor = std::io::Cursor::new(&mut source_img);
8496 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("open src");
8497 fs.prepare_for_edit().expect("prepare_for_edit");
8498 let inode_dir = fs
8499 .create_dir_hardlink_inode(D_INODE_NUM, &CreateDirectoryOptions::default())
8500 .expect("create dir inode");
8501 source_inode_cnid = inode_dir.location as u32;
8502 let mut data = std::io::Cursor::new(PAYLOAD);
8505 fs.create_file(
8506 &inode_dir,
8507 "inside.txt",
8508 &mut data,
8509 PAYLOAD.len() as u64,
8510 &CreateFileOptions::default(),
8511 )
8512 .expect("create file in dir inode");
8513 let dir_priv = fs
8515 .find_dir_private_dir_cnid()
8516 .unwrap()
8517 .expect("dir private");
8518 let _ = fs
8519 .adjust_dir_inode_link_count(dir_priv, D_INODE_NUM, 0)
8520 .expect("init dir linkCount");
8521 let root = fs.root().expect("root");
8522 for name in ["alpha", "beta"] {
8523 fs.create_dir_hardlink(&root, name, D_INODE_NUM)
8524 .expect("create dir hardlink stub");
8525 }
8526 fs.sync_metadata().expect("sync src");
8527 }
8528
8529 let cursor = std::io::Cursor::new(&mut source_img);
8531 let mut src_fs = HfsPlusFilesystem::open(cursor, 0).expect("re-open src");
8532 let snap =
8533 crate::hfsplus_clone::SourceCatalogSnapshot::capture(&mut src_fs).expect("capture");
8534
8535 let dir_inodes: Vec<_> = snap.dirs.iter().filter(|d| d.is_dir_inode).collect();
8537 assert_eq!(
8538 dir_inodes.len(),
8539 1,
8540 "expected exactly one dir inode in snapshot; got {}",
8541 dir_inodes.len()
8542 );
8543 let dir_inode = dir_inodes[0];
8544 assert_eq!(dir_inode.cnid, source_inode_cnid);
8545 assert_eq!(dir_inode.inode_num, Some(D_INODE_NUM));
8546
8547 assert_eq!(snap.dir_hardlinks.len(), 2, "expected 2 dir-hardlink stubs");
8548 for h in &snap.dir_hardlinks {
8549 assert_eq!(h.inode_num, D_INODE_NUM);
8550 assert_eq!(h.inode_dir_source_cnid, source_inode_cnid);
8551 assert_eq!(h.parent_cnid, 2, "stubs live at root");
8552 }
8553
8554 let mut target_img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "DhTgt", false);
8556 let target_inode_cnid: u32;
8557 {
8558 let cursor = std::io::Cursor::new(&mut target_img);
8559 let mut tgt = HfsPlusFilesystem::open(cursor, 0).expect("open tgt");
8560 tgt.prepare_for_edit().expect("prepare_for_edit");
8561
8562 let inode = tgt
8564 .create_dir_hardlink_inode(
8565 dir_inode.inode_num.unwrap(),
8566 &CreateDirectoryOptions::default(),
8567 )
8568 .expect("replay dir inode");
8569 target_inode_cnid = inode.location as u32;
8570
8571 let mut data = std::io::Cursor::new(PAYLOAD);
8574 tgt.create_file(
8575 &inode,
8576 "inside.txt",
8577 &mut data,
8578 PAYLOAD.len() as u64,
8579 &CreateFileOptions::default(),
8580 )
8581 .expect("replay child file");
8582
8583 let root = tgt.root().expect("tgt root");
8585 for h in &snap.dir_hardlinks {
8586 tgt.create_dir_hardlink(&root, &h.name, h.inode_num)
8587 .expect("replay dir stub");
8588 }
8589 tgt.sync_metadata().expect("sync tgt");
8590 }
8591
8592 let cursor = std::io::Cursor::new(&mut target_img);
8594 let mut tgt_fs = HfsPlusFilesystem::open(cursor, 0).expect("re-open tgt");
8595 let root = tgt_fs.root().expect("tgt root");
8596 let entries = tgt_fs.list_directory(&root).expect("list tgt root");
8597
8598 for stub_name in ["alpha", "beta"] {
8601 let stub = entries
8602 .iter()
8603 .find(|e| e.name == stub_name)
8604 .cloned()
8605 .unwrap_or_else(|| panic!("stub {stub_name} missing from target root"));
8606 assert!(
8607 stub.is_directory(),
8608 "stub {stub_name} must surface as a directory"
8609 );
8610 assert_eq!(
8611 stub.link_target_cnid,
8612 Some(target_inode_cnid as u64),
8613 "stub {stub_name} must resolve to the single target inode"
8614 );
8615
8616 let children = tgt_fs.list_directory(&stub).expect("list stub");
8617 let inside = children
8618 .iter()
8619 .find(|c| c.name == "inside.txt")
8620 .unwrap_or_else(|| panic!("inside.txt missing under {stub_name}"));
8621 let bytes = tgt_fs
8622 .read_file(inside, usize::MAX)
8623 .expect("read inside.txt via stub");
8624 assert_eq!(bytes, PAYLOAD);
8625 }
8626
8627 let dir_priv = tgt_fs
8629 .find_dir_private_dir_cnid()
8630 .unwrap()
8631 .expect("tgt dir private");
8632 let inode_name = format!("dir_{D_INODE_NUM}");
8633 let (_, _, abs) = tgt_fs
8634 .find_catalog_record(dir_priv, &inode_name)
8635 .expect("dir inode in private dir");
8636 let key_len = BigEndian::read_u16(&tgt_fs.catalog_data[abs..abs + 2]) as usize;
8637 let mut body = abs + 2 + key_len;
8638 if !body.is_multiple_of(2) {
8639 body += 1;
8640 }
8641 let link_count = BigEndian::read_u32(&tgt_fs.catalog_data[body + 44..body + 48]);
8642 assert_eq!(link_count, 2, "dir inode linkCount must equal stub count");
8643 }
8644
8645 #[test]
8646 fn test_hfsx_case_sensitive_allows_distinct_case_siblings() {
8647 const PAYLOAD: &[u8] = b"hi\n";
8653
8654 {
8656 let mut img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "HFSX", true);
8657 let cursor = std::io::Cursor::new(&mut img);
8658 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("open hfsx");
8659 assert!(
8660 fs.case_sensitive(),
8661 "fresh HFSX blank must be case-sensitive"
8662 );
8663 fs.prepare_for_edit().expect("prepare");
8664 let root = fs.root().expect("root");
8665
8666 let mut data = std::io::Cursor::new(PAYLOAD);
8667 fs.create_file(
8668 &root,
8669 "Foo",
8670 &mut data,
8671 PAYLOAD.len() as u64,
8672 &CreateFileOptions::default(),
8673 )
8674 .expect("create Foo");
8675
8676 let mut data = std::io::Cursor::new(PAYLOAD);
8677 fs.create_file(
8678 &root,
8679 "foo",
8680 &mut data,
8681 PAYLOAD.len() as u64,
8682 &CreateFileOptions::default(),
8683 )
8684 .expect("create foo on HFSX must succeed");
8685
8686 fs.sync_metadata().expect("sync");
8687 let entries = fs.list_directory(&root).expect("list");
8689 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
8690 assert!(names.contains(&"Foo"), "Foo missing: {names:?}");
8691 assert!(names.contains(&"foo"), "foo missing: {names:?}");
8692 }
8693
8694 {
8696 let mut img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "HFSPLUS", false);
8697 let cursor = std::io::Cursor::new(&mut img);
8698 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("open hfs+");
8699 assert!(
8700 !fs.case_sensitive(),
8701 "plain HFS+ blank must be case-folding"
8702 );
8703 fs.prepare_for_edit().expect("prepare");
8704 let root = fs.root().expect("root");
8705
8706 let mut data = std::io::Cursor::new(PAYLOAD);
8707 fs.create_file(
8708 &root,
8709 "Foo",
8710 &mut data,
8711 PAYLOAD.len() as u64,
8712 &CreateFileOptions::default(),
8713 )
8714 .expect("create Foo");
8715
8716 let mut data = std::io::Cursor::new(PAYLOAD);
8717 let err = fs
8718 .create_file(
8719 &root,
8720 "foo",
8721 &mut data,
8722 PAYLOAD.len() as u64,
8723 &CreateFileOptions::default(),
8724 )
8725 .expect_err("create foo must fail on case-folding HFS+");
8726 match err {
8727 FilesystemError::AlreadyExists(_) => {}
8728 other => panic!("expected AlreadyExists, got {other:?}"),
8729 }
8730 }
8731 }
8732
8733 #[test]
8734 fn test_clone_capture_carries_xattrs_byte_equal() {
8735 let cnid = 99u32;
8745 let seed_value: Vec<u8> = (0..32u8).collect();
8746 let extra_value: Vec<u8> = b"\x00bplist00 demo".to_vec();
8747 let img = make_editable_hfsplus_image_with_inline_xattr(cnid, &seed_value);
8748
8749 let cursor = std::io::Cursor::new(img);
8752 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
8753 fs.prepare_for_edit().unwrap();
8754 fs.set_xattr(cnid, "com.apple.metadata:demo", &extra_value)
8755 .expect("set extra xattr on sentinel cnid");
8756 fs.set_xattr(2u32, "com.apple.FinderInfo", &seed_value)
8757 .expect("set xattr on root dir");
8758 let root = fs.root().expect("root");
8763 let mut data = std::io::Cursor::new(b"hi".as_slice());
8764 fs.create_file(
8765 &root,
8766 "carrier.txt",
8767 &mut data,
8768 2,
8769 &CreateFileOptions::default(),
8770 )
8771 .expect("create_file");
8772 fs.sync_metadata().unwrap();
8773 let img2 = fs.reader.into_inner();
8774
8775 let mut cursor2 = std::io::Cursor::new(img2);
8776 let mut fs2 = HfsPlusFilesystem::open(&mut cursor2, 0).unwrap();
8777 let snap = crate::hfsplus_clone::SourceCatalogSnapshot::capture(&mut fs2).expect("capture");
8778
8779 let carrier = snap
8782 .files
8783 .iter()
8784 .find(|f| f.name == "carrier.txt")
8785 .expect("carrier file in snapshot");
8786 assert!(
8787 carrier.xattrs.is_empty(),
8788 "fresh file should have no xattrs, got {}",
8789 carrier.xattrs.len()
8790 );
8791
8792 let root_dir = snap
8794 .dirs
8795 .iter()
8796 .find(|d| d.cnid == 2)
8797 .expect("root dir in snapshot");
8798 assert_eq!(root_dir.xattrs.len(), 1, "expected 1 xattr on root dir");
8799 assert_eq!(root_dir.xattrs[0].name, "com.apple.FinderInfo");
8800 match &root_dir.xattrs[0].kind {
8801 XattrKind::Inline(bytes) => assert_eq!(bytes, &seed_value),
8802 other => panic!("expected inline FinderInfo on root, got {other:?}"),
8803 }
8804
8805 let mut sentinel = fs2.list_xattrs(cnid).expect("list_xattrs sentinel");
8809 sentinel.sort_by(|a, b| a.name.cmp(&b.name));
8810 assert_eq!(sentinel.len(), 2, "sentinel should carry 2 xattrs");
8811 let mut want: Vec<(String, Vec<u8>)> = vec![
8812 ("com.apple.FinderInfo".into(), seed_value.clone()),
8813 ("com.apple.metadata:demo".into(), extra_value.clone()),
8814 ];
8815 want.sort_by(|a, b| a.0.cmp(&b.0));
8816 for (rec, (wn, wv)) in sentinel.iter().zip(want.iter()) {
8817 assert_eq!(rec.name, *wn);
8818 match &rec.kind {
8819 XattrKind::Inline(bytes) => assert_eq!(bytes, wv),
8820 other => panic!("expected inline, got {other:?}"),
8821 }
8822 }
8823 }
8824
8825 #[test]
8826 fn test_hfsplus_editable_open_and_free_space() {
8827 let img = make_editable_hfsplus_image();
8828 let cursor = std::io::Cursor::new(img);
8829 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
8830 assert_eq!(fs.fs_type(), "HFS+");
8831 let free = fs.free_space().unwrap();
8832 assert!(free > 0);
8833 assert_eq!(free, 250 * 4096);
8835 }
8836
8837 #[test]
8838 fn test_hfsplus_defragmented_minimum_size() {
8839 let img = make_editable_hfsplus_image();
8842 let cursor = std::io::Cursor::new(img);
8843 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
8844
8845 let total = fs.total_size();
8846 let in_place = fs.last_data_byte().unwrap();
8847 let defrag = fs.defragmented_minimum_size().unwrap();
8848
8849 assert_eq!(total, 256 * 4096);
8850 assert_eq!(in_place, 7 * 4096);
8853 assert_eq!(defrag, 19 * 4096);
8857 assert!(defrag <= total);
8858 }
8859
8860 #[test]
8861 fn test_hfsplus_defragmented_min_smaller_on_fragmented() {
8862 let mut img = make_editable_hfsplus_image();
8867 let block_size = 4096usize;
8868 let bitmap_byte = block_size + 25; img[bitmap_byte] |= 0b1000_0000;
8872 let free_off = 1024 + 48;
8875 let free = BigEndian::read_u32(&img[free_off..free_off + 4]);
8876 BigEndian::write_u32(&mut img[free_off..free_off + 4], free - 1);
8877 let image_size = img.len();
8879 let alt_free_off = image_size - 1024 + 48;
8880 BigEndian::write_u32(&mut img[alt_free_off..alt_free_off + 4], free - 1);
8881
8882 let cursor = std::io::Cursor::new(img);
8883 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
8884
8885 let in_place = fs.last_data_byte().unwrap();
8886 let defrag = fs.defragmented_minimum_size().unwrap();
8887
8888 assert_eq!(in_place, 202 * 4096);
8891 assert_eq!(defrag, 20 * 4096);
8894 assert!(defrag < in_place);
8895 }
8896
8897 #[test]
8898 fn test_hfsplus_editable_create_file_and_read() {
8899 let img = make_editable_hfsplus_image();
8900 let cursor = std::io::Cursor::new(img);
8901 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
8902
8903 let root = fs.root().unwrap();
8904 let test_data = b"Hello, HFS+ World!";
8905 let mut data_reader = std::io::Cursor::new(test_data.as_slice());
8906
8907 let options = CreateFileOptions::default();
8908 let fe = fs
8909 .create_file(
8910 &root,
8911 "test.txt",
8912 &mut data_reader,
8913 test_data.len() as u64,
8914 &options,
8915 )
8916 .unwrap();
8917
8918 assert_eq!(fe.name, "test.txt");
8919 assert_eq!(fe.size, test_data.len() as u64);
8920
8921 let entries = fs.list_directory(&root).unwrap();
8923 assert!(entries.iter().any(|e| e.name == "test.txt"));
8924
8925 let read_back = fs.read_file(&fe, 1024).unwrap();
8927 assert_eq!(&read_back, test_data);
8928 }
8929
8930 #[test]
8931 fn test_hfsplus_editable_create_directory() {
8932 let img = make_editable_hfsplus_image();
8933 let cursor = std::io::Cursor::new(img);
8934 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
8935
8936 let root = fs.root().unwrap();
8937 let options = CreateDirectoryOptions::default();
8938 let dir = fs.create_directory(&root, "TestDir", &options).unwrap();
8939
8940 assert_eq!(dir.name, "TestDir");
8941 assert!(dir.is_directory());
8942
8943 let entries = fs.list_directory(&root).unwrap();
8944 assert!(entries
8945 .iter()
8946 .any(|e| e.name == "TestDir" && e.is_directory()));
8947 }
8948
8949 #[test]
8950 fn test_hfsplus_editable_delete_file() {
8951 let img = make_editable_hfsplus_image();
8952 let cursor = std::io::Cursor::new(img);
8953 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
8954
8955 let root = fs.root().unwrap();
8956 let test_data = b"delete me";
8957 let mut data_reader = std::io::Cursor::new(test_data.as_slice());
8958 let options = CreateFileOptions::default();
8959 let fe = fs
8960 .create_file(
8961 &root,
8962 "gone.txt",
8963 &mut data_reader,
8964 test_data.len() as u64,
8965 &options,
8966 )
8967 .unwrap();
8968
8969 let free_before = fs.free_space().unwrap();
8970
8971 fs.delete_entry(&root, &fe).unwrap();
8973
8974 let entries = fs.list_directory(&root).unwrap();
8976 assert!(!entries.iter().any(|e| e.name == "gone.txt"));
8977
8978 let free_after = fs.free_space().unwrap();
8980 assert!(free_after > free_before);
8981 }
8982
8983 #[test]
8984 fn test_hfsplus_editable_duplicate_name_rejected() {
8985 let img = make_editable_hfsplus_image();
8986 let cursor = std::io::Cursor::new(img);
8987 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
8988
8989 let root = fs.root().unwrap();
8990 let test_data = b"first";
8991 let mut r1 = std::io::Cursor::new(test_data.as_slice());
8992 let options = CreateFileOptions::default();
8993 fs.create_file(&root, "dup.txt", &mut r1, 5, &options)
8994 .unwrap();
8995
8996 let mut r2 = std::io::Cursor::new(test_data.as_slice());
8998 let result = fs.create_file(&root, "dup.txt", &mut r2, 5, &options);
8999 assert!(matches!(result, Err(FilesystemError::AlreadyExists(_))));
9000 }
9001
9002 #[test]
9003 fn test_hfsplus_editable_blessed_folder() {
9004 let img = make_editable_hfsplus_image();
9005 let cursor = std::io::Cursor::new(img);
9006 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
9007
9008 assert!(fs.blessed_system_folder().is_none());
9010
9011 let root = fs.root().unwrap();
9013 let options = CreateDirectoryOptions::default();
9014 let sys_dir = fs
9015 .create_directory(&root, "System Folder", &options)
9016 .unwrap();
9017
9018 fs.set_blessed_folder(&sys_dir).unwrap();
9020
9021 let blessed = fs.blessed_system_folder();
9023 assert!(blessed.is_some());
9024 let (cnid, name) = blessed.unwrap();
9025 assert_eq!(cnid, sys_dir.location);
9026 assert_eq!(name, "System Folder");
9027 }
9028
9029 #[test]
9030 fn test_hfsplus_editable_type_creator_auto_detect() {
9031 let img = make_editable_hfsplus_image();
9032 let cursor = std::io::Cursor::new(img);
9033 let mut fs = HfsPlusFilesystem::open(cursor, 0).unwrap();
9034
9035 let root = fs.root().unwrap();
9036 let test_data = b"text content";
9037 let mut data_reader = std::io::Cursor::new(test_data.as_slice());
9038 let options = CreateFileOptions::default();
9039 let fe = fs
9040 .create_file(
9041 &root,
9042 "hello.txt",
9043 &mut data_reader,
9044 test_data.len() as u64,
9045 &options,
9046 )
9047 .unwrap();
9048
9049 assert!(fe.type_code.is_some());
9051 assert_eq!(fe.type_code, Some(*b"TEXT"));
9052 }
9053
9054 #[test]
9055 fn test_hfsplus_catalog_variable_index_keys_grow_multilevel() {
9056 use crate::hfs_common::{
9073 btree_find_record, btree_insert_full, BTreeHeader, BTreeKeyFormat,
9074 };
9075 use byteorder::{BigEndian, ByteOrder};
9076 type Fs = HfsPlusFilesystem<std::io::Cursor<Vec<u8>>>;
9077
9078 let node_size = 512usize;
9079 let total_nodes = 1800u32;
9082 let mut buf = vec![0u8; total_nodes as usize * node_size];
9083 write_blank_btree_header_node(
9084 &mut buf[0..node_size],
9085 node_size,
9086 0,
9087 total_nodes,
9088 total_nodes - 2,
9089 516,
9090 KEY_COMPARE_CASE_FOLDING,
9091 );
9092 write_empty_leaf_node(&mut buf[node_size..2 * node_size]);
9093
9094 let kf = BTreeKeyFormat::HFSPLUS_CATALOG;
9095 let cmp = |a: &[u8], b: &[u8]| Fs::catalog_compare(a, b, false);
9096
9097 const DIRS: u32 = 50;
9102 const N: u32 = 2500;
9103 let mut inserted: Vec<(u32, String)> = Vec::with_capacity(N as usize);
9104 for i in 0..N {
9105 let hash = i.wrapping_mul(2_654_435_761);
9106 let parent = 16 + (hash % DIRS); let name = format!("f{hash:08x}");
9108 let mut rec = Fs::build_catalog_key(parent, &name);
9109 rec.extend_from_slice(&[0u8; 40]);
9112 btree_insert_full(&mut buf, &rec, &kf, &cmp)
9113 .unwrap_or_else(|e| panic!("insert #{i} (parent {parent}, {name}): {e}"));
9114 inserted.push((parent, name));
9115 }
9116
9117 let header = BTreeHeader::read(&buf);
9119 assert!(
9120 header.depth >= 3,
9121 "catalog did not reach depth >= 3 (got {}); test needs a deeper tree to \
9122 exercise index-node splits",
9123 header.depth
9124 );
9125 assert_eq!(
9126 header.leaf_records, N,
9127 "leaf_records header count drifted from inserts"
9128 );
9129
9130 let mut keys: Vec<Vec<u8>> = Vec::with_capacity(N as usize);
9134 crate::hfs_common::walk_leaf_records::<(), _>(
9135 &buf,
9136 header.first_leaf_node,
9137 node_size,
9138 |_n, _r, _o, rec| {
9139 let kl = BigEndian::read_u16(&rec[0..2]) as usize;
9140 keys.push(rec[..(2 + kl).min(rec.len())].to_vec());
9141 None
9142 },
9143 );
9144 assert_eq!(
9145 keys.len(),
9146 N as usize,
9147 "leaf walk recovered {} of {N} records — chain is broken or has cycles",
9148 keys.len()
9149 );
9150 for w in keys.windows(2) {
9151 assert_eq!(
9152 cmp(&w[0], &w[1]),
9153 std::cmp::Ordering::Less,
9154 "catalog leaves are not strictly ascending — separator/descent corruption"
9155 );
9156 }
9157
9158 fn key_extract(rec: &[u8]) -> &[u8] {
9163 let kl = BigEndian::read_u16(&rec[0..2]) as usize;
9164 &rec[..(2 + kl).min(rec.len())]
9165 }
9166 for (parent, name) in &inserted {
9167 let search = Fs::build_catalog_key(*parent, name);
9168 let (found, _chain) = btree_find_record(&buf, &header, &search, &key_extract, &cmp);
9169 assert!(
9170 found.is_some(),
9171 "record (parent {parent}, {name}) not findable by descent — misrouted index"
9172 );
9173 }
9174 }
9175
9176 #[test]
9177 fn test_hfsplus_blank_sized_catalog_20k_inserts_fsck_clean() {
9178 use crate::hfs_common::BTreeHeader;
9186 type Fs = HfsPlusFilesystem<std::io::Cursor<Vec<u8>>>;
9187
9188 let img =
9191 create_blank_hfsplus_sized(64 * 1024 * 1024, 4096, "PutShuffle", false, 16 << 20, 0);
9192 let mut fs = Fs::open(std::io::Cursor::new(img), 0).unwrap();
9193 fs.prepare_for_edit().unwrap();
9194
9195 const DIRS: u32 = 50;
9197 let root = fs.root().unwrap();
9198 let mut dir_ids = Vec::with_capacity(DIRS as usize);
9199 for d in 0..DIRS {
9200 let de = fs
9201 .create_directory(&root, &format!("dir{d:03}"), &Default::default())
9202 .unwrap();
9203 dir_ids.push(de.location as u32);
9204 }
9205 let base_cnid = fs.vh.next_catalog_id;
9206
9207 let n: u32 = 20_000;
9215 let mut per_dir = vec![0i32; DIRS as usize];
9216 let empty_fork = ForkData::empty();
9217 for i in 0..n {
9218 let hash = i.wrapping_mul(2_654_435_761);
9219 let d = (hash % DIRS) as usize;
9220 let name = format!("f{hash:08x}");
9221 let mut kr = Fs::build_catalog_key(dir_ids[d], &name);
9222 if !kr.len().is_multiple_of(2) {
9223 kr.push(0);
9224 }
9225 kr.extend_from_slice(&Fs::build_file_record_with_dates(
9226 base_cnid + i,
9227 &empty_fork,
9228 &empty_fork,
9229 &[0u8; 4],
9230 &[0u8; 4],
9231 None,
9232 0,
9233 ));
9234 fs.insert_catalog_record(&kr)
9235 .unwrap_or_else(|e| panic!("insert #{i} into dir{d}: {e}"));
9236 per_dir[d] += 1;
9237 }
9238 fs.vh.file_count += n;
9241 fs.vh.next_catalog_id = base_cnid + n;
9242 for (d, &cnt) in per_dir.iter().enumerate() {
9243 fs.update_parent_valence(dir_ids[d], cnt).unwrap();
9244 }
9245
9246 let h = BTreeHeader::read(fs.catalog_data());
9248 assert!(
9249 h.depth >= 3,
9250 "catalog depth {} < 3 after 20k inserts",
9251 h.depth
9252 );
9253 let result = fs.fsck().unwrap().unwrap();
9257 assert!(
9258 result.errors.is_empty(),
9259 "fsck found {} errors after 20k inserts: {:?}",
9260 result.errors.len(),
9261 result
9262 .errors
9263 .iter()
9264 .take(8)
9265 .map(|e| &e.message)
9266 .collect::<Vec<_>>()
9267 );
9268 }
9269
9270 #[test]
9271 fn test_hfsplus_catalog_grows_map_nodes_past_cap_phase_c() {
9272 use crate::hfs_common::{btree_bitmap_segments, BTreeHeader};
9280 type Fs = HfsPlusFilesystem<std::io::Cursor<Vec<u8>>>;
9281 let node_size = 4096usize;
9282 let header_cap = ((node_size - 256) * 8) as u32; let min_catalog = (header_cap - 16) * node_size as u32;
9286 let img =
9287 create_blank_hfsplus_sized(180 * 1024 * 1024, 4096, "MapCap", false, min_catalog, 0);
9288 let mut fs = Fs::open(std::io::Cursor::new(img), 0).unwrap();
9289 fs.prepare_for_edit().unwrap();
9290
9291 let start = BTreeHeader::read(fs.catalog_data()).total_nodes;
9292 assert!(
9293 start <= header_cap && start > header_cap - 64,
9294 "blank catalog {start} not near cap"
9295 );
9296 assert_eq!(
9298 btree_bitmap_segments(fs.catalog_data(), node_size).len(),
9299 1,
9300 "fresh near-cap catalog should have no map nodes"
9301 );
9302
9303 let mut guard = 0;
9305 while BTreeHeader::read(fs.catalog_data()).total_nodes <= header_cap {
9306 fs.grow_btree_fork(BTreeFork::Catalog).unwrap();
9307 guard += 1;
9308 assert!(guard < 64, "too many grows to cross the cap");
9309 }
9310 let h = BTreeHeader::read(fs.catalog_data());
9311 assert!(
9312 h.total_nodes > header_cap,
9313 "did not cross the cap: {}",
9314 h.total_nodes
9315 );
9316 let segs = btree_bitmap_segments(fs.catalog_data(), node_size);
9318 assert!(
9319 segs.len() >= 2,
9320 "expected a map-node segment after crossing the cap, got {} segment(s)",
9321 segs.len()
9322 );
9323
9324 fs.sync_metadata().unwrap();
9326 let result = fs.fsck().unwrap().unwrap();
9327 assert!(
9328 result.errors.is_empty(),
9329 "fsck after map-node grow: {:?}",
9330 result
9331 .errors
9332 .iter()
9333 .take(8)
9334 .map(|e| &e.message)
9335 .collect::<Vec<_>>()
9336 );
9337
9338 let img = fs.reader.get_ref().clone();
9340 if let Ok(path) = std::env::var("RB_EMIT_MAPC_IMAGE") {
9341 std::fs::write(&path, &img).unwrap();
9342 }
9343 let fs2 = Fs::open(std::io::Cursor::new(img), 0).unwrap();
9344 assert_eq!(
9345 BTreeHeader::read(fs2.catalog_data()).total_nodes,
9346 h.total_nodes,
9347 "reopen lost grown nodes"
9348 );
9349 assert!(
9350 btree_bitmap_segments(fs2.catalog_data(), node_size).len() >= 2,
9351 "reopen lost the map node"
9352 );
9353 }
9354
9355 #[test]
9356 fn test_hfsplus_catalog_grow_spills_to_overflow_phase_b() {
9357 type Fs = HfsPlusFilesystem<std::io::Cursor<Vec<u8>>>;
9365 let img = create_blank_hfsplus_sized(16 * 1024 * 1024, 4096, "SpillVol", false, 0, 0);
9366 let mut fs = Fs::open(std::io::Cursor::new(img), 0).unwrap();
9367 fs.prepare_for_edit().unwrap();
9368 let root = fs.root().unwrap();
9369 let mut dirs = Vec::new();
9370 for d in 0..20 {
9371 dirs.push(
9372 fs.create_directory(&root, &format!("d{d}"), &Default::default())
9373 .unwrap(),
9374 );
9375 }
9376 const N: u32 = 1200;
9377 for i in 0..N {
9378 let d = &dirs[(i % 20) as usize];
9379 let mut data = std::io::Cursor::new(vec![b'x']);
9380 fs.create_file(
9381 d,
9382 &format!("f{i:05}.txt"),
9383 &mut data,
9384 1,
9385 &Default::default(),
9386 )
9387 .unwrap_or_else(|e| panic!("create_file #{i} (grow+spill should cover it): {e}"));
9388 }
9389
9390 let inline: u32 = fs
9393 .vh
9394 .catalog_file
9395 .extents
9396 .iter()
9397 .map(|e| e.block_count)
9398 .sum();
9399 let inline_count = fs
9400 .vh
9401 .catalog_file
9402 .extents
9403 .iter()
9404 .filter(|e| e.block_count != 0)
9405 .count();
9406 assert_eq!(
9407 inline_count, 8,
9408 "catalog should have filled all 8 inline extents"
9409 );
9410 assert!(
9411 fs.vh.catalog_file.total_blocks > inline,
9412 "catalog total_blocks {} should exceed inline-extent blocks {inline} (overflow used)",
9413 fs.vh.catalog_file.total_blocks
9414 );
9415
9416 fs.sync_metadata().unwrap();
9417 let result = fs.fsck().unwrap().unwrap();
9418 assert!(
9419 result.errors.is_empty(),
9420 "fsck after overflow spill: {:?}",
9421 result
9422 .errors
9423 .iter()
9424 .take(8)
9425 .map(|e| &e.message)
9426 .collect::<Vec<_>>()
9427 );
9428
9429 let img = fs.reader.get_ref().clone();
9432 if let Ok(path) = std::env::var("RB_EMIT_SPILL_IMAGE") {
9433 std::fs::write(&path, &img).unwrap();
9434 }
9435 let mut fs2 = Fs::open(std::io::Cursor::new(img), 0).unwrap();
9436 let r2 = fs2.root().unwrap();
9437 let mut total = 0;
9438 for de in fs2.list_directory(&r2).unwrap() {
9439 if de.is_directory() {
9440 total += fs2.list_directory(&de).unwrap().len();
9441 }
9442 }
9443 assert_eq!(total as u32, N, "reopen lost files: {total} != {N}");
9444 }
9445
9446 #[test]
9451 fn the_alternate_volume_header_follows_the_partition_end_when_known() {
9452 use disk_core::filesystem::{CreateFileOptions, EditableFilesystem, Filesystem};
9453 const VOLUME: u64 = 8 * 1024 * 1024;
9454 const PARTITION: u64 = VOLUME + 6 * 1024;
9455 let mut img = create_blank_hfsplus(VOLUME, 4096, "Slack", false);
9456 img.resize(PARTITION as usize, 0);
9457 {
9458 let mut fs =
9459 HfsPlusFilesystem::open_sized(std::io::Cursor::new(&mut img), 0, Some(PARTITION))
9460 .unwrap();
9461 fs.prepare_for_edit().unwrap();
9462 let root = fs.root().unwrap();
9463 fs.create_file(&root, "a", &mut &b"x"[..], 1, &CreateFileOptions::default())
9464 .unwrap();
9465 fs.sync_metadata().unwrap();
9466 }
9467 let primary = &img[1024..1536];
9468 let alt = &img[PARTITION as usize - 1024..PARTITION as usize - 512];
9469 assert_eq!(
9470 &alt[0..2],
9471 b"H+",
9472 "alternate volume header missing at the partition end"
9473 );
9474 assert_eq!(primary, alt);
9475 }
9476
9477 #[test]
9479 fn create_file_writes_the_finder_flags_it_is_given() {
9480 use disk_core::filesystem::{CreateFileOptions, EditableFilesystem, Filesystem};
9481 let mut img = create_blank_hfsplus(8 * 1024 * 1024, 4096, "Flags", false);
9482 let mut fs = HfsPlusFilesystem::open(std::io::Cursor::new(&mut img), 0).unwrap();
9483 fs.prepare_for_edit().unwrap();
9484 let root = fs.root().unwrap();
9485 let opts = CreateFileOptions {
9486 os_type: Some(*b"APPL"),
9487 os_creator: Some(*b"ttxt"),
9488 finder_flags: Some(0x2000),
9489 ..Default::default()
9490 };
9491 fs.create_file(&root, "app", &mut &b"x"[..], 1, &opts)
9492 .unwrap();
9493 fs.sync_metadata().unwrap();
9494 let entry = fs
9495 .list_directory(&root)
9496 .unwrap()
9497 .into_iter()
9498 .find(|e| e.name == "app")
9499 .unwrap();
9500 assert_eq!(entry.finder_flags, Some(0x2000));
9501 }
9502
9503 #[test]
9504 fn a_grow_allocates_the_blocks_the_new_nodes_occupy() {
9505 assert_eq!(btree_grow_blocks(10, 8, 4096, 8192), 4);
9507 assert_eq!(btree_grow_blocks(4, 8, 1024, 4096), 2);
9509 assert_eq!(btree_grow_blocks(11, 8, 4096, 8192), 4);
9511 assert_eq!(btree_grow_blocks(11, 1, 4096, 8192), 0);
9512 assert_eq!(btree_grow_blocks(10, 8, 4096, 512), 64);
9514 assert_eq!(btree_grow_blocks(10, 8, 4096, 4096), 8);
9515 }
9516
9517 #[test]
9518 fn test_hfsplus_catalog_grows_fork_on_full_phase_a() {
9519 use crate::hfs_common::BTreeHeader;
9525 type Fs = HfsPlusFilesystem<std::io::Cursor<Vec<u8>>>;
9526
9527 let img = create_blank_hfsplus_sized(16 * 1024 * 1024, 4096, "GrowVol", false, 0, 0);
9530 let mut fs = Fs::open(std::io::Cursor::new(img), 0).unwrap();
9531 fs.prepare_for_edit().unwrap();
9532
9533 let start_nodes = BTreeHeader::read(fs.catalog_data()).total_nodes;
9534
9535 const DIRS: u32 = 20;
9536 let root = fs.root().unwrap();
9537 let mut dir_ids = Vec::with_capacity(DIRS as usize);
9538 for d in 0..DIRS {
9539 let de = fs
9540 .create_directory(&root, &format!("dir{d:03}"), &Default::default())
9541 .unwrap();
9542 dir_ids.push(de.location as u32);
9543 }
9544 let base_cnid = fs.vh.next_catalog_id;
9545
9546 let n: u32 = 6_000;
9547 let mut per_dir = vec![0i32; DIRS as usize];
9548 let empty_fork = ForkData::empty();
9549 for i in 0..n {
9550 let hash = i.wrapping_mul(2_654_435_761);
9551 let d = (hash % DIRS) as usize;
9552 let name = format!("f{hash:08x}");
9553 let mut kr = Fs::build_catalog_key(dir_ids[d], &name);
9554 if !kr.len().is_multiple_of(2) {
9555 kr.push(0);
9556 }
9557 kr.extend_from_slice(&Fs::build_file_record_with_dates(
9558 base_cnid + i,
9559 &empty_fork,
9560 &empty_fork,
9561 &[0u8; 4],
9562 &[0u8; 4],
9563 None,
9564 0,
9565 ));
9566 fs.insert_catalog_record(&kr)
9567 .unwrap_or_else(|e| panic!("insert #{i} into dir{d} (grow should cover it): {e}"));
9568 per_dir[d] += 1;
9569 }
9570 fs.vh.file_count += n;
9571 fs.vh.next_catalog_id = base_cnid + n;
9572 for (d, &cnt) in per_dir.iter().enumerate() {
9573 fs.update_parent_valence(dir_ids[d], cnt).unwrap();
9574 }
9575
9576 let end_nodes = BTreeHeader::read(fs.catalog_data()).total_nodes;
9578 assert!(
9579 end_nodes >= start_nodes + 16,
9580 "catalog total_nodes only went {start_nodes} -> {end_nodes}; expected >= 2 grows"
9581 );
9582 let cat_extents = fs
9584 .vh
9585 .catalog_file
9586 .extents
9587 .iter()
9588 .filter(|e| e.block_count != 0)
9589 .count();
9590 assert_eq!(
9591 cat_extents, 1,
9592 "contiguous tail growth should keep the catalog fork at 1 extent, got {cat_extents}"
9593 );
9594
9595 fs.sync_metadata().unwrap();
9597
9598 let result = fs.fsck().unwrap().unwrap();
9600 assert!(
9601 result.errors.is_empty(),
9602 "fsck found {} errors after grow: {:?}",
9603 result.errors.len(),
9604 result
9605 .errors
9606 .iter()
9607 .take(8)
9608 .map(|e| &e.message)
9609 .collect::<Vec<_>>()
9610 );
9611
9612 let img = fs.reader.get_ref().clone();
9615 if let Ok(path) = std::env::var("RB_EMIT_GROW_IMAGE") {
9618 std::fs::write(&path, &img).unwrap();
9619 }
9620 let mut fs2 = Fs::open(std::io::Cursor::new(img), 0).unwrap();
9621 let reopened_nodes = BTreeHeader::read(fs2.catalog_data()).total_nodes;
9622 assert_eq!(reopened_nodes, end_nodes, "reopen lost grown nodes");
9623 let r2 = fs2.root().unwrap();
9624 let dirs = fs2.list_directory(&r2).unwrap();
9625 assert_eq!(dirs.len(), DIRS as usize, "reopen lost directories");
9626 }
9627
9628 #[test]
9629 fn test_hfsplus_extents_overflow_grows_multilevel() {
9630 use crate::hfs_common::{
9637 btree_find_record, btree_insert_full, BTreeHeader, BTreeKeyFormat,
9638 };
9639 type Fs = HfsPlusFilesystem<std::io::Cursor<Vec<u8>>>;
9640
9641 let node_size = 512usize;
9642 let total_nodes = 1800u32;
9643 let mut buf = vec![0u8; total_nodes as usize * node_size];
9644 write_blank_btree_header_node(
9645 &mut buf[0..node_size],
9646 node_size,
9647 0,
9648 total_nodes,
9649 total_nodes - 2,
9650 10,
9651 0,
9652 );
9653 write_empty_leaf_node(&mut buf[node_size..2 * node_size]);
9654
9655 let kf = BTreeKeyFormat::HFSPLUS_EXTENTS;
9656 const N: u32 = 600;
9660 let mut ids: Vec<u32> = Vec::with_capacity(N as usize);
9661 for i in 0..N {
9662 let file_id = i.wrapping_mul(2_654_435_761);
9663 let rec = Fs::build_extents_overflow_record(file_id, 0, 8, &[]);
9664 btree_insert_full(&mut buf, &rec, &kf, &Fs::extents_compare)
9665 .unwrap_or_else(|e| panic!("extents insert #{i} (file {file_id}): {e}"));
9666 ids.push(file_id);
9667 }
9668
9669 let header = BTreeHeader::read(&buf);
9670 assert!(
9671 header.depth >= 2,
9672 "extents-overflow tree stayed depth {} — split past a leaf level failed",
9673 header.depth
9674 );
9675 assert_eq!(header.leaf_records, N);
9676
9677 let mut recs: Vec<Vec<u8>> = Vec::with_capacity(N as usize);
9678 crate::hfs_common::walk_leaf_records::<(), _>(
9679 &buf,
9680 header.first_leaf_node,
9681 node_size,
9682 |_n, _r, _o, r| {
9683 recs.push(r.to_vec());
9684 None
9685 },
9686 );
9687 assert_eq!(recs.len(), N as usize, "leaf walk lost records");
9688 for w in recs.windows(2) {
9689 assert_eq!(
9690 Fs::extents_compare(&w[0], &w[1]),
9691 std::cmp::Ordering::Less,
9692 "extents-overflow leaves out of order — bad separator"
9693 );
9694 }
9695
9696 fn ext_key(rec: &[u8]) -> &[u8] {
9698 &rec[..12.min(rec.len())]
9699 }
9700 let ext_cmp = |a: &[u8], b: &[u8]| Fs::extents_compare(a, b);
9701 for &file_id in &ids {
9702 let search = Fs::build_extents_overflow_record(file_id, 0, 8, &[]);
9703 let (found, _) = btree_find_record(&buf, &header, &search, &ext_key, &ext_cmp);
9704 assert!(
9705 found.is_some(),
9706 "extents record for file {file_id} not findable"
9707 );
9708 }
9709 }
9710
9711 #[test]
9712 fn test_hfsplus_attributes_grows_multilevel() {
9713 use crate::hfs_common::{
9719 btree_find_record, btree_insert_full, BTreeHeader, BTreeKeyFormat,
9720 };
9721 use byteorder::{BigEndian, ByteOrder};
9722 type Fs = HfsPlusFilesystem<std::io::Cursor<Vec<u8>>>;
9723
9724 let node_size = 512usize;
9725 let total_nodes = 1800u32;
9726 let mut buf = vec![0u8; total_nodes as usize * node_size];
9727 write_blank_btree_header_node(
9728 &mut buf[0..node_size],
9729 node_size,
9730 0,
9731 total_nodes,
9732 total_nodes - 2,
9733 264,
9734 0,
9735 );
9736 write_empty_leaf_node(&mut buf[node_size..2 * node_size]);
9737
9738 let kf = BTreeKeyFormat::HFSPLUS_ATTRIBUTES;
9739 const N: u32 = 600;
9740 let value = [0xABu8; 8];
9741 let mut cnids: Vec<u32> = Vec::with_capacity(N as usize);
9742 for i in 0..N {
9743 let cnid = i.wrapping_mul(2_654_435_761);
9744 let rec = Fs::build_inline_attr_record(cnid, "xattr", &value);
9745 btree_insert_full(&mut buf, &rec, &kf, &Fs::attr_compare)
9746 .unwrap_or_else(|e| panic!("attr insert #{i} (cnid {cnid}): {e}"));
9747 cnids.push(cnid);
9748 }
9749
9750 let header = BTreeHeader::read(&buf);
9751 assert!(
9752 header.depth >= 2,
9753 "attributes tree stayed depth {} — split past a leaf level failed",
9754 header.depth
9755 );
9756 assert_eq!(header.leaf_records, N);
9757
9758 let mut recs: Vec<Vec<u8>> = Vec::with_capacity(N as usize);
9759 crate::hfs_common::walk_leaf_records::<(), _>(
9760 &buf,
9761 header.first_leaf_node,
9762 node_size,
9763 |_n, _r, _o, r| {
9764 recs.push(r.to_vec());
9765 None
9766 },
9767 );
9768 assert_eq!(recs.len(), N as usize, "leaf walk lost records");
9769 for w in recs.windows(2) {
9770 assert_eq!(
9771 Fs::attr_compare(&w[0], &w[1]),
9772 std::cmp::Ordering::Less,
9773 "attributes leaves out of order — bad separator"
9774 );
9775 }
9776
9777 fn attr_key(rec: &[u8]) -> &[u8] {
9778 let kl = BigEndian::read_u16(&rec[0..2]) as usize;
9779 &rec[..(2 + kl).min(rec.len())]
9780 }
9781 let attr_cmp = |a: &[u8], b: &[u8]| Fs::attr_compare(a, b);
9782 for &cnid in &cnids {
9783 let search = Fs::build_inline_attr_record(cnid, "xattr", &value);
9784 let (found, _) = btree_find_record(&buf, &header, &search, &attr_key, &attr_cmp);
9785 assert!(found.is_some(), "attr record for cnid {cnid} not findable");
9786 }
9787 }
9788
9789 #[test]
9790 fn test_hfsplus_fragmented_file_splits_extents_overflow_btree_real_path() {
9791 type Fs = HfsPlusFilesystem<std::io::Cursor<Vec<u8>>>;
9799 let img = create_blank_hfsplus_sized(32 << 20, 4096, "Frag", false, 0, 256 << 10);
9802 let mut fs = Fs::open(std::io::Cursor::new(img), 0).unwrap();
9803 fs.prepare_for_edit().unwrap();
9804
9805 fs.ensure_bitmap().unwrap();
9811 let block_size = fs.vh.block_size as usize;
9812 let n_blocks: u32 = 520;
9813 let first_free = fs.vh.next_allocation;
9814 {
9815 let bitmap = fs.bitmap.as_mut().unwrap();
9816 for b in bitmap.iter_mut() {
9817 *b = 0xFF;
9818 }
9819 let mut blk = first_free + 1;
9820 for _ in 0..n_blocks {
9821 bitmap_clear_bit_be(bitmap, blk);
9822 blk += 2; }
9824 }
9825 fs.vh.free_blocks = n_blocks;
9826
9827 let payload: Vec<u8> = (0..(n_blocks as usize * block_size))
9828 .map(|i| (i % 251) as u8)
9829 .collect();
9830 let mut reader = std::io::Cursor::new(payload.clone());
9831 let root = fs.root().unwrap();
9832 let entry = fs
9833 .create_file(
9834 &root,
9835 "frag.bin",
9836 &mut reader,
9837 payload.len() as u64,
9838 &CreateFileOptions::default(),
9839 )
9840 .expect("fragmented file create should succeed via a splitting overflow B-tree");
9841
9842 let depth =
9844 crate::hfs_common::BTreeHeader::read(fs.extents_overflow_data.as_ref().unwrap()).depth;
9845 assert!(
9846 depth >= 2,
9847 "extents-overflow tree depth {depth} < 2 — 64 overflow records did not split it"
9848 );
9849
9850 let cnid = entry.location as u32;
9852 let (data_fork, _rsrc) = fs.find_file_by_id(cnid).unwrap();
9853 assert_eq!(data_fork.total_blocks, n_blocks);
9854
9855 let got = fs.read_file(&entry, payload.len() + 1).unwrap();
9857 assert_eq!(got.len(), payload.len());
9858 assert_eq!(
9859 got, payload,
9860 "fragmented read-back mismatch through split overflow tree"
9861 );
9862 }
9863
9864 #[test]
9865 fn deleting_the_last_overflow_record_retires_the_extents_tree() {
9866 use crate::hfs_common::{btree_lonely_empty_leaves, BTreeHeader};
9869 type Fs = HfsPlusFilesystem<std::io::Cursor<Vec<u8>>>;
9870 let img = create_blank_hfsplus_sized(32 << 20, 4096, "Retire", false, 0, 256 << 10);
9871 let mut fs = Fs::open(std::io::Cursor::new(img), 0).unwrap();
9872 fs.prepare_for_edit().unwrap();
9873
9874 fs.ensure_bitmap().unwrap();
9876 let block_size = fs.vh.block_size as usize;
9877 let n_blocks: u32 = 520;
9878 let first_free = fs.vh.next_allocation;
9879 {
9880 let bitmap = fs.bitmap.as_mut().unwrap();
9881 for b in bitmap.iter_mut() {
9882 *b = 0xFF;
9883 }
9884 let mut blk = first_free + 1;
9885 for _ in 0..n_blocks {
9886 bitmap_clear_bit_be(bitmap, blk);
9887 blk += 2;
9888 }
9889 }
9890 fs.vh.free_blocks = n_blocks;
9891 let payload: Vec<u8> = (0..(n_blocks as usize * block_size))
9892 .map(|i| (i % 251) as u8)
9893 .collect();
9894 let root = fs.root().unwrap();
9895 let entry = fs
9896 .create_file(
9897 &root,
9898 "frag.bin",
9899 &mut std::io::Cursor::new(payload.clone()),
9900 payload.len() as u64,
9901 &CreateFileOptions::default(),
9902 )
9903 .unwrap();
9904 let before = BTreeHeader::read(fs.extents_overflow_data.as_ref().unwrap());
9905 assert!(
9906 before.depth >= 2 && before.leaf_records > 0,
9907 "depth {} leaf_records {}",
9908 before.depth,
9909 before.leaf_records
9910 );
9911
9912 fs.delete_entry(&root, &entry).unwrap();
9913
9914 let tree = fs.extents_overflow_data.as_ref().unwrap();
9915 let h = BTreeHeader::read(tree);
9916 assert_eq!((h.leaf_records, h.root_node, h.depth), (0, 0, 0));
9917 assert_eq!(
9918 h.free_nodes,
9919 h.total_nodes - 1,
9920 "only the header node stays"
9921 );
9922 assert!(btree_lonely_empty_leaves(tree, h.node_size as usize).is_empty());
9925 }
9926
9927 #[test]
9928 fn a_grow_moves_the_alternate_header_blocks_and_the_bitmap() {
9929 type Fs = HfsPlusFilesystem<std::io::Cursor<Vec<u8>>>;
9933 for (bs, old_mib, new_mib) in [(4096u32, 24u64, 30u64), (2048u32, 32u64, 64u64)] {
9934 let mut img = create_blank_hfsplus(old_mib << 20, bs, "Grow", false);
9935 img.resize((new_mib << 20) as usize, 0);
9936 let mut cur = std::io::Cursor::new(img);
9937 let mut log = |_: &str| {};
9938 resize_hfsplus_in_place(&mut cur, 0, new_mib << 20, &mut log).unwrap();
9939
9940 let mut fs = Fs::open_sized(cur, 0, Some(new_mib << 20)).unwrap();
9941 let total = fs.vh.total_blocks;
9942 assert_eq!(total as u64, (new_mib << 20) / bs as u64, "bs {bs}");
9943 let bitmap = fs.read_allocation_bitmap_for_fsck().unwrap();
9944 let old_total = ((old_mib << 20) / bs as u64) as u32;
9945 for b in alt_vh_blocks(old_total, bs) {
9946 assert!(
9947 !bitmap_test_bit_be(&bitmap, b),
9948 "bs {bs}: old tail block {b} still marked"
9949 );
9950 }
9951 for b in alt_vh_blocks(total, bs) {
9952 assert!(
9953 bitmap_test_bit_be(&bitmap, b),
9954 "bs {bs}: new tail block {b} is free"
9955 );
9956 }
9957 let used = (0..total)
9958 .filter(|&b| bitmap_test_bit_be(&bitmap, b))
9959 .count() as u32;
9960 assert_eq!(fs.vh.free_blocks, total - used, "bs {bs}");
9961 assert!(
9962 fs.vh.allocation_file.total_blocks as u64 * bs as u64 * 8 >= total as u64,
9963 "bs {bs}: allocation file too small for {total} blocks"
9964 );
9965 let report = fs.fsck().unwrap().unwrap();
9966 assert!(
9967 report.errors.is_empty(),
9968 "bs {bs}: {:?}",
9969 report
9970 .errors
9971 .iter()
9972 .map(|e| &e.message)
9973 .take(4)
9974 .collect::<Vec<_>>()
9975 );
9976 }
9977 }
9978
9979 #[test]
9980 fn test_hfsplus_catalog_shuffled_inserts_pack_densely() {
9981 use crate::hfs_common::{btree_insert_full, BTreeHeader, BTreeKeyFormat};
9988 use byteorder::{BigEndian, ByteOrder};
9989 type Fs = HfsPlusFilesystem<std::io::Cursor<Vec<u8>>>;
9990
9991 let node_size = 512usize;
9992 let total_nodes = 1800u32;
9993 let mut buf = vec![0u8; total_nodes as usize * node_size];
9994 write_blank_btree_header_node(
9995 &mut buf[0..node_size],
9996 node_size,
9997 0,
9998 total_nodes,
9999 total_nodes - 2,
10000 516,
10001 KEY_COMPARE_CASE_FOLDING,
10002 );
10003 write_empty_leaf_node(&mut buf[node_size..2 * node_size]);
10004
10005 let kf = BTreeKeyFormat::HFSPLUS_CATALOG;
10006 let cmp = |a: &[u8], b: &[u8]| Fs::catalog_compare(a, b, false);
10007 const DIRS: u32 = 40;
10008 const N: u32 = 3000;
10009 for i in 0..N {
10010 let hash = i.wrapping_mul(2_654_435_761);
10011 let parent = 16 + (hash % DIRS);
10012 let name = format!("f{hash:08x}");
10013 let mut rec = Fs::build_catalog_key(parent, &name);
10014 rec.extend_from_slice(&[0u8; 40]);
10015 btree_insert_full(&mut buf, &rec, &kf, &cmp)
10016 .unwrap_or_else(|e| panic!("insert #{i}: {e}"));
10017 }
10018
10019 let header = BTreeHeader::read(&buf);
10021 assert!(
10022 header.depth >= 3,
10023 "need a multi-level tree to judge packing"
10024 );
10025 let mut used_total = 0usize;
10026 let mut leaves = 0usize;
10027 let mut node = header.first_leaf_node;
10028 let mut seen = std::collections::HashSet::new();
10029 while node != 0 && seen.insert(node) {
10030 let off = node as usize * node_size;
10031 let num = BigEndian::read_u16(&buf[off + 10..off + 12]) as usize;
10032 let free_off_pos = off + node_size - 2 * (num + 1);
10034 let free_off = BigEndian::read_u16(&buf[free_off_pos..free_off_pos + 2]) as usize;
10035 used_total += free_off + 2 * (num + 1);
10036 leaves += 1;
10037 node = BigEndian::read_u32(&buf[off..off + 4]); }
10039 let occupancy = used_total as f64 / (leaves * node_size) as f64;
10041 assert!(
10042 occupancy >= 0.80,
10043 "shuffled multi-dir leaf occupancy {occupancy:.3} (<0.80) over {leaves} leaves — \
10044 the rotation is not packing densely"
10045 );
10046 }
10047}