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