1#[cfg(feature = "rust173-polyfill")]
20use disk_core::rust173::IntIsMultipleOf as _;
21use std::io::{Read, Seek, Write};
22
23use byteorder::{BigEndian, ByteOrder};
24
25use crate::hfs_common::walk_leaf_records;
26use crate::hfsplus::{HfsPlusFilesystem, RecordMetadata, XattrKind, XattrRecord};
27use disk_core::entry::FileEntry;
28use disk_core::filesystem::{
29 CreateDirectoryOptions, CreateFileOptions, EditableFilesystem, Filesystem, FilesystemError,
30};
31use std::collections::{HashMap, HashSet, VecDeque};
32
33const HFSPLUS_FORK_DATA: u8 = 0x00;
36const HFSPLUS_FORK_RESOURCE: u8 = 0xFF;
37
38const CATALOG_FOLDER: i16 = 1;
40const CATALOG_FILE: i16 = 2;
41
42const HARDLINK_FILE_TYPE: &[u8; 4] = b"hlnk";
46const HARDLINK_FILE_CREATOR: &[u8; 4] = b"hfs+";
47const HARDLINK_DIR_TYPE: &[u8; 4] = b"fdrp";
48const HARDLINK_DIR_CREATOR: &[u8; 4] = b"MACS";
49
50#[derive(Debug, Clone)]
52pub struct SourceVolumeSnapshot {
53 pub label: String,
55 pub signature: u16,
58 pub attributes: u32,
61 pub create_date: u32,
62 pub modify_date: u32,
63 pub backup_date: u32,
64 pub checked_date: u32,
65 pub finder_info: [u32; 8],
69 pub block_size: u32,
72 pub total_blocks: u32,
74 pub file_count: u32,
75 pub folder_count: u32,
76 pub boot_blocks: [u8; 1024],
79 pub catalog_total_blocks: u32,
83 pub extents_total_blocks: u32,
87 pub attributes_total_blocks: u32,
91}
92
93#[derive(Debug, Clone)]
95pub struct SourceDirSnapshot {
96 pub name: String,
97 pub name_utf16: Vec<u16>,
101 pub cnid: u32,
104 pub parent_cnid: u32,
105 pub valence: u32,
107 pub flags: u16,
109 pub finder_info: [u8; 16],
111 pub extended_finder_info: [u8; 16],
113 pub bsd_info: [u8; 16],
115 pub create_date: u32,
116 pub content_mod_date: u32,
117 pub attribute_mod_date: u32,
118 pub access_date: u32,
119 pub backup_date: u32,
120 pub text_encoding: u32,
121 pub(crate) xattrs: Vec<XattrRecord>,
128 pub is_dir_inode: bool,
134 pub inode_num: Option<u32>,
137}
138
139#[derive(Debug, Clone)]
141pub struct SourceFileSnapshot {
142 pub name: String,
143 pub name_utf16: Vec<u16>,
144 pub cnid: u32,
146 pub parent_cnid: u32,
147 pub flags: u16,
148 pub finder_info: [u8; 16],
152 pub extended_finder_info: [u8; 16],
154 pub bsd_info: [u8; 16],
156 pub create_date: u32,
157 pub content_mod_date: u32,
158 pub attribute_mod_date: u32,
159 pub access_date: u32,
160 pub backup_date: u32,
161 pub text_encoding: u32,
162 pub data_fork_raw: [u8; 80],
167 pub rsrc_fork_raw: [u8; 80],
169 pub data_fork_size: u64,
172 pub rsrc_fork_size: u64,
174 pub(crate) xattrs: Vec<XattrRecord>,
177 pub is_inode: bool,
185 pub inode_num: Option<u32>,
190}
191
192#[derive(Debug, Clone)]
195pub struct SourceHardlinkSnapshot {
196 pub name: String,
198 pub name_utf16: Vec<u16>,
199 pub cnid: u32,
201 pub parent_cnid: u32,
203 pub inode_source_cnid: u32,
207 pub inode_num: u32,
210 pub flags: u16,
211 pub finder_info: [u8; 16],
212 pub extended_finder_info: [u8; 16],
213 pub bsd_info: [u8; 16],
214 pub create_date: u32,
215 pub content_mod_date: u32,
216 pub attribute_mod_date: u32,
217 pub access_date: u32,
218 pub backup_date: u32,
219 pub text_encoding: u32,
220 pub(crate) xattrs: Vec<XattrRecord>,
221}
222
223impl SourceFileSnapshot {
224 pub fn type_code(&self) -> String {
226 decode_fourcc(&self.finder_info[0..4])
227 }
228
229 pub fn creator_code(&self) -> String {
231 decode_fourcc(&self.finder_info[4..8])
232 }
233}
234
235#[derive(Debug, Clone)]
239pub struct SourceDirHardlinkSnapshot {
240 pub name: String,
241 pub name_utf16: Vec<u16>,
242 pub cnid: u32,
243 pub parent_cnid: u32,
244 pub inode_dir_source_cnid: u32,
248 pub inode_num: u32,
249 pub flags: u16,
250 pub finder_info: [u8; 16],
251 pub extended_finder_info: [u8; 16],
252 pub bsd_info: [u8; 16],
253 pub create_date: u32,
254 pub content_mod_date: u32,
255 pub attribute_mod_date: u32,
256 pub access_date: u32,
257 pub backup_date: u32,
258 pub text_encoding: u32,
259 pub(crate) xattrs: Vec<XattrRecord>,
260}
261
262#[derive(Debug, Clone)]
266pub struct SourceCatalogSnapshot {
267 pub volume: SourceVolumeSnapshot,
268 pub dirs: Vec<SourceDirSnapshot>,
269 pub files: Vec<SourceFileSnapshot>,
274 pub hardlinks: Vec<SourceHardlinkSnapshot>,
277 pub dir_hardlinks: Vec<SourceDirHardlinkSnapshot>,
280}
281
282impl SourceVolumeSnapshot {
283 pub fn capture<R: Read + Seek>(fs: &mut HfsPlusFilesystem<R>) -> Result<Self, FilesystemError> {
286 let boot_blocks = fs.read_boot_blocks()?;
287 Ok(SourceVolumeSnapshot {
288 label: fs.label().to_string(),
289 signature: fs.signature(),
290 attributes: fs.vh_attributes(),
291 create_date: fs.vh_create_date(),
292 modify_date: fs.vh_modify_date(),
293 backup_date: fs.vh_backup_date(),
294 checked_date: fs.vh_checked_date(),
295 finder_info: fs.vh_finder_info(),
296 block_size: fs.block_size(),
297 total_blocks: fs.total_blocks(),
298 file_count: fs.vh_file_count(),
299 folder_count: fs.vh_folder_count(),
300 boot_blocks,
301 catalog_total_blocks: fs.vh_catalog_total_blocks(),
302 extents_total_blocks: fs.vh_extents_total_blocks(),
303 attributes_total_blocks: fs.vh_attributes_total_blocks(),
304 })
305 }
306}
307
308impl SourceCatalogSnapshot {
309 pub fn capture<R: Read + Seek>(fs: &mut HfsPlusFilesystem<R>) -> Result<Self, FilesystemError> {
319 let volume = SourceVolumeSnapshot::capture(fs)?;
320
321 let mut dirs: Vec<SourceDirSnapshot> = Vec::new();
322 let mut files: Vec<SourceFileSnapshot> = Vec::new();
323 let mut pending_forks: Vec<([u8; 80], [u8; 80])> = Vec::new();
324 let mut pending_links: Vec<PendingHardlink> = Vec::new();
325 let mut pending_dir_links: Vec<PendingDirHardlink> = Vec::new();
326 let mut walk_err: Option<FilesystemError> = None;
327 {
328 let catalog = fs.catalog_data();
329 let node_size = fs.catalog_node_size();
330 let first_leaf = fs.catalog_first_leaf();
331 walk_leaf_records(
332 catalog,
333 first_leaf,
334 node_size,
335 |_node_idx, _rec_idx, _abs_off, rec| -> Option<()> {
336 match parse_catalog_record(rec) {
337 Ok(ParsedRecord::Folder(d)) => {
338 dirs.push(d);
339 None
340 }
341 Ok(ParsedRecord::File {
342 file,
343 data_fork,
344 rsrc_fork,
345 }) => {
346 files.push(file);
347 pending_forks.push((data_fork, rsrc_fork));
348 None
349 }
350 Ok(ParsedRecord::Hardlink(p)) => {
351 pending_links.push(p);
352 None
353 }
354 Ok(ParsedRecord::DirHardlink(p)) => {
355 pending_dir_links.push(p);
356 None
357 }
358 Ok(ParsedRecord::Thread) | Ok(ParsedRecord::Skip) => None,
359 Err(e) => {
360 walk_err = Some(e);
361 Some(())
362 }
363 }
364 },
365 );
366 }
367 if let Some(e) = walk_err {
368 return Err(e);
369 }
370
371 let private_name = hfsplus_private_dir_name();
376 let dir_private_name = hfsplus_dir_private_dir_name();
377 let private_cnid = dirs.iter().find_map(|d| {
378 if d.name == private_name {
379 Some(d.cnid)
380 } else {
381 None
382 }
383 });
384 let dir_private_cnid = dirs.iter().find_map(|d| {
385 if d.name == dir_private_name {
386 Some(d.cnid)
387 } else {
388 None
389 }
390 });
391 let mut inode_num_to_source_cnid: std::collections::HashMap<u32, u32> =
392 std::collections::HashMap::new();
393 let mut dir_inode_num_to_source_cnid: std::collections::HashMap<u32, u32> =
394 std::collections::HashMap::new();
395 if let Some(pcnid) = dir_private_cnid {
396 for d in dirs.iter_mut() {
397 if d.parent_cnid != pcnid {
398 continue;
399 }
400 let Some(rest) = d.name.strip_prefix("dir_") else {
401 continue;
402 };
403 let Ok(num) = rest.parse::<u32>() else {
404 continue;
405 };
406 d.is_dir_inode = true;
407 d.inode_num = Some(num);
408 dir_inode_num_to_source_cnid.insert(num, d.cnid);
409 }
410 }
411 if let Some(pcnid) = private_cnid {
412 for f in files.iter_mut() {
413 if f.parent_cnid != pcnid {
414 continue;
415 }
416 let Some(rest) = f.name.strip_prefix("iNode") else {
417 continue;
418 };
419 let Ok(num) = rest.parse::<u32>() else {
420 continue;
421 };
422 f.is_inode = true;
423 f.inode_num = Some(num);
424 inode_num_to_source_cnid.insert(num, f.cnid);
425 }
426 }
427
428 let mut hardlinks: Vec<SourceHardlinkSnapshot> = Vec::with_capacity(pending_links.len());
436 for p in pending_links {
437 let Some(inode_source_cnid) = inode_num_to_source_cnid.get(&p.inode_num).copied()
438 else {
439 files.push(SourceFileSnapshot {
440 name: p.name,
441 name_utf16: p.name_utf16,
442 cnid: p.cnid,
443 parent_cnid: p.parent_cnid,
444 flags: p.flags,
445 finder_info: p.finder_info,
446 extended_finder_info: p.extended_finder_info,
447 bsd_info: p.bsd_info,
448 create_date: p.create_date,
449 content_mod_date: p.content_mod_date,
450 attribute_mod_date: p.attribute_mod_date,
451 access_date: p.access_date,
452 backup_date: p.backup_date,
453 text_encoding: p.text_encoding,
454 data_fork_raw: p.data_fork_raw,
455 rsrc_fork_raw: p.rsrc_fork_raw,
456 data_fork_size: p.data_fork_size,
457 rsrc_fork_size: p.rsrc_fork_size,
458 xattrs: Vec::new(),
459 is_inode: false,
460 inode_num: None,
461 });
462 continue;
463 };
464 hardlinks.push(SourceHardlinkSnapshot {
465 name: p.name,
466 name_utf16: p.name_utf16,
467 cnid: p.cnid,
468 parent_cnid: p.parent_cnid,
469 inode_source_cnid,
470 inode_num: p.inode_num,
471 flags: p.flags,
472 finder_info: p.finder_info,
473 extended_finder_info: p.extended_finder_info,
474 bsd_info: p.bsd_info,
475 create_date: p.create_date,
476 content_mod_date: p.content_mod_date,
477 attribute_mod_date: p.attribute_mod_date,
478 access_date: p.access_date,
479 backup_date: p.backup_date,
480 text_encoding: p.text_encoding,
481 xattrs: Vec::new(),
482 });
483 }
484
485 let mut dir_hardlinks: Vec<SourceDirHardlinkSnapshot> =
486 Vec::with_capacity(pending_dir_links.len());
487 for p in pending_dir_links {
488 let Some(inode_dir_source_cnid) =
489 dir_inode_num_to_source_cnid.get(&p.inode_num).copied()
490 else {
491 files.push(SourceFileSnapshot {
499 name: p.name,
500 name_utf16: p.name_utf16,
501 cnid: p.cnid,
502 parent_cnid: p.parent_cnid,
503 flags: p.flags,
504 finder_info: p.finder_info,
505 extended_finder_info: p.extended_finder_info,
506 bsd_info: p.bsd_info,
507 create_date: p.create_date,
508 content_mod_date: p.content_mod_date,
509 attribute_mod_date: p.attribute_mod_date,
510 access_date: p.access_date,
511 backup_date: p.backup_date,
512 text_encoding: p.text_encoding,
513 data_fork_raw: p.data_fork_raw,
514 rsrc_fork_raw: p.rsrc_fork_raw,
515 data_fork_size: p.data_fork_size,
516 rsrc_fork_size: p.rsrc_fork_size,
517 xattrs: Vec::new(),
518 is_inode: false,
519 inode_num: None,
520 });
521 continue;
522 };
523 dir_hardlinks.push(SourceDirHardlinkSnapshot {
524 name: p.name,
525 name_utf16: p.name_utf16,
526 cnid: p.cnid,
527 parent_cnid: p.parent_cnid,
528 inode_dir_source_cnid,
529 inode_num: p.inode_num,
530 flags: p.flags,
531 finder_info: p.finder_info,
532 extended_finder_info: p.extended_finder_info,
533 bsd_info: p.bsd_info,
534 create_date: p.create_date,
535 content_mod_date: p.content_mod_date,
536 attribute_mod_date: p.attribute_mod_date,
537 access_date: p.access_date,
538 backup_date: p.backup_date,
539 text_encoding: p.text_encoding,
540 xattrs: Vec::new(),
541 });
542 }
543
544 for (file, (data_fork, rsrc_fork)) in files.iter_mut().zip(pending_forks) {
548 file.data_fork_size = BigEndian::read_u64(&data_fork[0..8]);
549 file.rsrc_fork_size = BigEndian::read_u64(&rsrc_fork[0..8]);
550 file.data_fork_raw = data_fork;
551 file.rsrc_fork_raw = rsrc_fork;
552 file.xattrs = fs.list_xattrs(file.cnid)?;
553 }
554 for dir in dirs.iter_mut() {
555 dir.xattrs = fs.list_xattrs(dir.cnid)?;
556 }
557 for link in hardlinks.iter_mut() {
558 link.xattrs = fs.list_xattrs(link.cnid)?;
559 }
560 for link in dir_hardlinks.iter_mut() {
561 link.xattrs = fs.list_xattrs(link.cnid)?;
562 }
563
564 Ok(SourceCatalogSnapshot {
565 volume,
566 dirs,
567 files,
568 hardlinks,
569 dir_hardlinks,
570 })
571 }
572}
573
574enum ParsedRecord {
578 Folder(SourceDirSnapshot),
579 File {
580 file: SourceFileSnapshot,
581 data_fork: [u8; 80],
582 rsrc_fork: [u8; 80],
583 },
584 Hardlink(PendingHardlink),
585 DirHardlink(PendingDirHardlink),
586 Thread,
587 Skip,
588}
589
590struct PendingHardlink {
594 name: String,
595 name_utf16: Vec<u16>,
596 cnid: u32,
597 parent_cnid: u32,
598 inode_num: u32,
599 flags: u16,
600 finder_info: [u8; 16],
601 extended_finder_info: [u8; 16],
602 bsd_info: [u8; 16],
603 create_date: u32,
604 content_mod_date: u32,
605 attribute_mod_date: u32,
606 access_date: u32,
607 backup_date: u32,
608 text_encoding: u32,
609 data_fork_raw: [u8; 80],
614 rsrc_fork_raw: [u8; 80],
615 data_fork_size: u64,
616 rsrc_fork_size: u64,
617}
618
619struct PendingDirHardlink {
622 name: String,
623 name_utf16: Vec<u16>,
624 cnid: u32,
625 parent_cnid: u32,
626 inode_num: u32,
627 flags: u16,
628 finder_info: [u8; 16],
629 extended_finder_info: [u8; 16],
630 bsd_info: [u8; 16],
631 create_date: u32,
632 content_mod_date: u32,
633 attribute_mod_date: u32,
634 access_date: u32,
635 backup_date: u32,
636 text_encoding: u32,
637 data_fork_raw: [u8; 80],
644 rsrc_fork_raw: [u8; 80],
645 data_fork_size: u64,
646 rsrc_fork_size: u64,
647}
648
649fn hfsplus_dir_private_dir_name() -> String {
651 let mut s = String::with_capacity(28 + 1);
652 s.push_str(".HFS+ Private Directory Data");
653 s.push('\u{D}');
654 s
655}
656
657fn hfsplus_private_dir_name() -> String {
660 let mut s = String::with_capacity(4 + 17);
661 for _ in 0..4 {
662 s.push('\u{0}');
663 }
664 s.push_str("HFS+ Private Data");
665 s
666}
667
668fn parse_catalog_record(rec: &[u8]) -> Result<ParsedRecord, FilesystemError> {
669 if rec.len() < 4 {
670 return Ok(ParsedRecord::Skip);
671 }
672 let key_len = BigEndian::read_u16(&rec[0..2]) as usize;
673 if key_len < 6 || 2 + key_len > rec.len() {
674 return Ok(ParsedRecord::Skip);
675 }
676 let key = &rec[2..2 + key_len];
677 let parent_cnid = BigEndian::read_u32(&key[0..4]);
678 let name_length = BigEndian::read_u16(&key[4..6]) as usize;
679 if 6 + name_length * 2 > key.len() {
680 return Ok(ParsedRecord::Skip);
681 }
682 let name_bytes = &key[6..6 + name_length * 2];
683 let name_utf16: Vec<u16> = (0..name_length)
684 .map(|i| BigEndian::read_u16(&name_bytes[i * 2..i * 2 + 2]))
685 .collect();
686 let name = String::from_utf16(&name_utf16).unwrap_or_else(|_| {
687 name_utf16
688 .iter()
689 .map(|&u| char::from_u32(u as u32).unwrap_or('?'))
690 .collect()
691 });
692
693 let mut body_start = 2 + key_len;
695 if !body_start.is_multiple_of(2) {
696 body_start += 1;
697 }
698 if body_start + 2 > rec.len() {
699 return Ok(ParsedRecord::Skip);
700 }
701 let body = &rec[body_start..];
702 let record_type = BigEndian::read_i16(&body[0..2]);
703
704 match record_type {
705 CATALOG_FOLDER => {
706 if body.len() < 88 {
707 return Ok(ParsedRecord::Skip);
708 }
709 let flags = BigEndian::read_u16(&body[2..4]);
710 let valence = BigEndian::read_u32(&body[4..8]);
711 let cnid = BigEndian::read_u32(&body[8..12]);
712 let create_date = BigEndian::read_u32(&body[12..16]);
713 let content_mod_date = BigEndian::read_u32(&body[16..20]);
714 let attribute_mod_date = BigEndian::read_u32(&body[20..24]);
715 let access_date = BigEndian::read_u32(&body[24..28]);
716 let backup_date = BigEndian::read_u32(&body[28..32]);
717 let mut bsd_info = [0u8; 16];
718 bsd_info.copy_from_slice(&body[32..48]);
719 let mut finder_info = [0u8; 16];
720 finder_info.copy_from_slice(&body[48..64]);
721 let mut extended_finder_info = [0u8; 16];
722 extended_finder_info.copy_from_slice(&body[64..80]);
723 let text_encoding = BigEndian::read_u32(&body[80..84]);
724
725 Ok(ParsedRecord::Folder(SourceDirSnapshot {
731 name,
732 name_utf16,
733 cnid,
734 parent_cnid,
735 valence,
736 flags,
737 finder_info,
738 extended_finder_info,
739 bsd_info,
740 create_date,
741 content_mod_date,
742 attribute_mod_date,
743 access_date,
744 backup_date,
745 text_encoding,
746 xattrs: Vec::new(),
747 is_dir_inode: false,
748 inode_num: None,
749 }))
750 }
751 CATALOG_FILE => {
752 if body.len() < 248 {
753 return Ok(ParsedRecord::Skip);
754 }
755 let flags = BigEndian::read_u16(&body[2..4]);
756 let cnid = BigEndian::read_u32(&body[8..12]);
758 let create_date = BigEndian::read_u32(&body[12..16]);
759 let content_mod_date = BigEndian::read_u32(&body[16..20]);
760 let attribute_mod_date = BigEndian::read_u32(&body[20..24]);
761 let access_date = BigEndian::read_u32(&body[24..28]);
762 let backup_date = BigEndian::read_u32(&body[28..32]);
763 let mut bsd_info = [0u8; 16];
764 bsd_info.copy_from_slice(&body[32..48]);
765 let mut finder_info = [0u8; 16];
766 finder_info.copy_from_slice(&body[48..64]);
767 let mut extended_finder_info = [0u8; 16];
768 extended_finder_info.copy_from_slice(&body[64..80]);
769 let text_encoding = BigEndian::read_u32(&body[80..84]);
770 let mut data_fork = [0u8; 80];
772 data_fork.copy_from_slice(&body[88..168]);
773 let mut rsrc_fork = [0u8; 80];
774 rsrc_fork.copy_from_slice(&body[168..248]);
775
776 if &finder_info[0..4] == HARDLINK_FILE_TYPE
781 && &finder_info[4..8] == HARDLINK_FILE_CREATOR
782 {
783 let inode_num = BigEndian::read_u32(&body[44..48]);
784 let data_fork_size = BigEndian::read_u64(&data_fork[0..8]);
785 let rsrc_fork_size = BigEndian::read_u64(&rsrc_fork[0..8]);
786 return Ok(ParsedRecord::Hardlink(PendingHardlink {
787 name,
788 name_utf16,
789 cnid,
790 parent_cnid,
791 inode_num,
792 flags,
793 finder_info,
794 extended_finder_info,
795 bsd_info,
796 create_date,
797 content_mod_date,
798 attribute_mod_date,
799 access_date,
800 backup_date,
801 text_encoding,
802 data_fork_raw: data_fork,
803 rsrc_fork_raw: rsrc_fork,
804 data_fork_size,
805 rsrc_fork_size,
806 }));
807 }
808
809 if &finder_info[0..4] == HARDLINK_DIR_TYPE && &finder_info[4..8] == HARDLINK_DIR_CREATOR
826 {
827 let inode_num = BigEndian::read_u32(&body[44..48]);
828 let data_fork_size = BigEndian::read_u64(&data_fork[0..8]);
829 let rsrc_fork_size = BigEndian::read_u64(&rsrc_fork[0..8]);
830 return Ok(ParsedRecord::DirHardlink(PendingDirHardlink {
831 name,
832 name_utf16,
833 cnid,
834 parent_cnid,
835 inode_num,
836 flags,
837 finder_info,
838 extended_finder_info,
839 bsd_info,
840 create_date,
841 content_mod_date,
842 attribute_mod_date,
843 access_date,
844 backup_date,
845 text_encoding,
846 data_fork_raw: data_fork,
847 rsrc_fork_raw: rsrc_fork,
848 data_fork_size,
849 rsrc_fork_size,
850 }));
851 }
852
853 Ok(ParsedRecord::File {
854 file: SourceFileSnapshot {
855 name,
856 name_utf16,
857 cnid,
858 parent_cnid,
859 flags,
860 finder_info,
861 extended_finder_info,
862 bsd_info,
863 create_date,
864 content_mod_date,
865 attribute_mod_date,
866 access_date,
867 backup_date,
868 text_encoding,
869 data_fork_raw: [0u8; 80],
870 rsrc_fork_raw: [0u8; 80],
871 data_fork_size: 0,
872 rsrc_fork_size: 0,
873 xattrs: Vec::new(),
874 is_inode: false,
875 inode_num: None,
876 },
877 data_fork,
878 rsrc_fork,
879 })
880 }
881 3 | 4 => Ok(ParsedRecord::Thread),
883 _ => Ok(ParsedRecord::Skip),
884 }
885}
886
887fn decode_fourcc(data: &[u8]) -> String {
890 data.iter()
891 .map(|&b| {
892 if b.is_ascii_graphic() || b == b' ' {
893 b as char
894 } else {
895 '.'
896 }
897 })
898 .collect()
899}
900
901#[derive(Debug, Default, Clone)]
904pub struct HfsPlusCloneReport {
905 pub files_copied: u64,
906 pub dirs_copied: u64,
907 pub data_bytes_copied: u64,
908 pub rsrc_bytes_copied: u64,
909 pub xattrs_copied: u64,
910 pub hardlinks_copied: u64,
911 pub dir_hardlinks_copied: u64,
912 pub skipped: Vec<(String, String)>,
915}
916
917pub fn clone_hfsplus_volume<RS, RT>(
940 source: &mut HfsPlusFilesystem<RS>,
941 target: &mut HfsPlusFilesystem<RT>,
942) -> Result<HfsPlusCloneReport, FilesystemError>
943where
944 RS: Read + Seek + Send,
945 RT: Read + Write + Seek + Send,
946{
947 let snapshot = SourceCatalogSnapshot::capture(source)?;
948 let mut report = HfsPlusCloneReport::default();
949
950 target.set_volume_dates(
955 snapshot.volume.create_date,
956 snapshot.volume.modify_date,
957 snapshot.volume.backup_date,
958 snapshot.volume.checked_date,
959 );
960 target.set_boot_blocks(&snapshot.volume.boot_blocks);
961
962 let mut dirs_by_parent: HashMap<u32, Vec<&SourceDirSnapshot>> = HashMap::new();
966 for d in &snapshot.dirs {
967 if d.cnid == 2 || d.is_dir_inode {
968 continue;
969 }
970 if d.parent_cnid == 2
975 && (d.name == hfsplus_private_dir_name() || d.name == hfsplus_dir_private_dir_name())
976 {
977 continue;
978 }
979 dirs_by_parent.entry(d.parent_cnid).or_default().push(d);
980 }
981 let mut files_by_parent: HashMap<u32, Vec<&SourceFileSnapshot>> = HashMap::new();
982 for f in &snapshot.files {
983 if f.is_inode {
984 continue;
985 }
986 files_by_parent.entry(f.parent_cnid).or_default().push(f);
987 }
988 let mut hardlinks_by_parent: HashMap<u32, Vec<&SourceHardlinkSnapshot>> = HashMap::new();
989 for h in &snapshot.hardlinks {
990 hardlinks_by_parent
991 .entry(h.parent_cnid)
992 .or_default()
993 .push(h);
994 }
995 let mut dir_hardlinks_by_parent: HashMap<u32, Vec<&SourceDirHardlinkSnapshot>> = HashMap::new();
996 for h in &snapshot.dir_hardlinks {
997 dir_hardlinks_by_parent
998 .entry(h.parent_cnid)
999 .or_default()
1000 .push(h);
1001 }
1002
1003 for f in snapshot.files.iter().filter(|f| f.is_inode) {
1008 let inode_num = f.inode_num.expect("inode without inode_num");
1009 let inode_entry = {
1010 let mut data =
1011 source.fork_stream_reader(f.cnid, HFSPLUS_FORK_DATA, &f.data_fork_raw)?;
1012 target.create_hardlink_inode(
1013 inode_num,
1014 &mut data,
1015 f.data_fork_size,
1016 &CreateFileOptions::default(),
1017 )?
1018 };
1019 if f.rsrc_fork_size > 0 {
1020 let mut rsrc =
1021 source.fork_stream_reader(f.cnid, HFSPLUS_FORK_RESOURCE, &f.rsrc_fork_raw)?;
1022 target.write_resource_fork(&inode_entry, &mut rsrc, f.rsrc_fork_size)?;
1023 }
1024 report.data_bytes_copied += f.data_fork_size;
1025 report.rsrc_bytes_copied += f.rsrc_fork_size;
1026 stamp_file_metadata(target, &inode_entry, f, &mut report)?;
1027 }
1028 for d in snapshot.dirs.iter().filter(|d| d.is_dir_inode) {
1029 let inode_num = d.inode_num.expect("dir-inode without inode_num");
1030 let inode_entry =
1031 target.create_dir_hardlink_inode(inode_num, &CreateDirectoryOptions::default())?;
1032 stamp_dir_metadata(target, &inode_entry, d, &mut report)?;
1033 }
1034
1035 let mut cnid_map: HashMap<u32, u32> = HashMap::new();
1040 cnid_map.insert(2, 2);
1041 let target_root = target.root()?;
1042 let mut entry_map: HashMap<u32, FileEntry> = HashMap::new();
1043 entry_map.insert(2, target_root.clone());
1044
1045 if let Some(root_src) = snapshot.dirs.iter().find(|d| d.cnid == 2) {
1046 stamp_dir_metadata(target, &target_root, root_src, &mut report)?;
1047 }
1048
1049 let mut queue: VecDeque<u32> = VecDeque::new();
1050 queue.push_back(2);
1051
1052 while let Some(src_parent_cnid) = queue.pop_front() {
1053 let parent_entry = entry_map.get(&src_parent_cnid).cloned().ok_or_else(|| {
1054 FilesystemError::InvalidData(format!(
1055 "clone: missing parent entry for source CNID {src_parent_cnid}"
1056 ))
1057 })?;
1058
1059 if let Some(children) = dirs_by_parent.remove(&src_parent_cnid) {
1060 for d in children {
1061 let new_dir = target.create_directory(
1062 &parent_entry,
1063 &d.name,
1064 &CreateDirectoryOptions::default(),
1065 )?;
1066 stamp_dir_metadata(target, &new_dir, d, &mut report)?;
1067 cnid_map.insert(d.cnid, new_dir.location as u32);
1068 entry_map.insert(d.cnid, new_dir);
1069 report.dirs_copied += 1;
1070 queue.push_back(d.cnid);
1071 }
1072 }
1073
1074 if let Some(children) = files_by_parent.remove(&src_parent_cnid) {
1075 for f in children {
1076 let new_file = {
1077 let mut data =
1078 source.fork_stream_reader(f.cnid, HFSPLUS_FORK_DATA, &f.data_fork_raw)?;
1079 target.create_file(
1080 &parent_entry,
1081 &f.name,
1082 &mut data,
1083 f.data_fork_size,
1084 &CreateFileOptions {
1085 type_code: Some(f.type_code()),
1086 creator_code: Some(f.creator_code()),
1087 ..Default::default()
1088 },
1089 )?
1090 };
1091 report.data_bytes_copied += f.data_fork_size;
1092 if f.rsrc_fork_size > 0 {
1093 let mut rsrc = source.fork_stream_reader(
1094 f.cnid,
1095 HFSPLUS_FORK_RESOURCE,
1096 &f.rsrc_fork_raw,
1097 )?;
1098 target.write_resource_fork(&new_file, &mut rsrc, f.rsrc_fork_size)?;
1099 report.rsrc_bytes_copied += f.rsrc_fork_size;
1100 }
1101 stamp_file_metadata(target, &new_file, f, &mut report)?;
1102 cnid_map.insert(f.cnid, new_file.location as u32);
1103 report.files_copied += 1;
1104 }
1105 }
1106
1107 if let Some(children) = hardlinks_by_parent.remove(&src_parent_cnid) {
1108 for h in children {
1109 let stub = target.create_hardlink(&parent_entry, &h.name, h.inode_num)?;
1110 replay_xattrs(
1111 target,
1112 stub.location as u32,
1113 &h.xattrs,
1114 &h.name,
1115 &mut report,
1116 )?;
1117 report.hardlinks_copied += 1;
1118 }
1119 }
1120
1121 if let Some(children) = dir_hardlinks_by_parent.remove(&src_parent_cnid) {
1122 for h in children {
1123 let stub = target.create_dir_hardlink(&parent_entry, &h.name, h.inode_num)?;
1124 replay_xattrs(
1125 target,
1126 stub.location as u32,
1127 &h.xattrs,
1128 &h.name,
1129 &mut report,
1130 )?;
1131 report.dir_hardlinks_copied += 1;
1132 }
1133 }
1134 }
1135
1136 let known_source_cnids: HashSet<u32> = cnid_map.keys().copied().collect();
1142 let mut new_finder_info = [0u32; 8];
1143 for (i, slot) in new_finder_info.iter_mut().enumerate() {
1144 if i == 6 || i == 7 {
1145 continue;
1146 }
1147 let src_val = snapshot.volume.finder_info[i];
1148 *slot = if known_source_cnids.contains(&src_val) {
1149 *cnid_map.get(&src_val).unwrap()
1150 } else {
1151 src_val
1152 };
1153 }
1154 target.set_volume_finder_info(new_finder_info);
1155
1156 target.sync_metadata()?;
1157
1158 if let Some(fsck) = target.fsck() {
1162 let result = fsck?;
1163 if !result.errors.is_empty() {
1164 let summary: Vec<String> = result
1165 .errors
1166 .iter()
1167 .take(5)
1168 .map(|i| i.code.clone())
1169 .collect();
1170 return Err(FilesystemError::InvalidData(format!(
1171 "clone target failed fsck: {} error(s); first: [{}]",
1172 result.errors.len(),
1173 summary.join(", "),
1174 )));
1175 }
1176 }
1177
1178 Ok(report)
1179}
1180
1181fn metadata_from_file(f: &SourceFileSnapshot) -> RecordMetadata {
1183 RecordMetadata {
1184 create_date: f.create_date,
1185 content_mod_date: f.content_mod_date,
1186 attribute_mod_date: f.attribute_mod_date,
1187 access_date: f.access_date,
1188 backup_date: f.backup_date,
1189 bsd_info: f.bsd_info,
1190 finder_info: f.finder_info,
1191 extended_finder_info: f.extended_finder_info,
1192 text_encoding: f.text_encoding,
1193 }
1194}
1195
1196fn metadata_from_dir(d: &SourceDirSnapshot) -> RecordMetadata {
1198 RecordMetadata {
1199 create_date: d.create_date,
1200 content_mod_date: d.content_mod_date,
1201 attribute_mod_date: d.attribute_mod_date,
1202 access_date: d.access_date,
1203 backup_date: d.backup_date,
1204 bsd_info: d.bsd_info,
1205 finder_info: d.finder_info,
1206 extended_finder_info: d.extended_finder_info,
1207 text_encoding: d.text_encoding,
1208 }
1209}
1210
1211fn stamp_file_metadata<RT>(
1212 target: &mut HfsPlusFilesystem<RT>,
1213 entry: &FileEntry,
1214 f: &SourceFileSnapshot,
1215 report: &mut HfsPlusCloneReport,
1216) -> Result<(), FilesystemError>
1217where
1218 RT: Read + Write + Seek + Send,
1219{
1220 let cnid = entry.location as u32;
1221 target.set_record_metadata(cnid, &metadata_from_file(f))?;
1222 replay_xattrs(target, cnid, &f.xattrs, &f.name, report)?;
1223 Ok(())
1224}
1225
1226fn stamp_dir_metadata<RT>(
1227 target: &mut HfsPlusFilesystem<RT>,
1228 entry: &FileEntry,
1229 d: &SourceDirSnapshot,
1230 report: &mut HfsPlusCloneReport,
1231) -> Result<(), FilesystemError>
1232where
1233 RT: Read + Write + Seek + Send,
1234{
1235 let cnid = entry.location as u32;
1236 target.set_record_metadata(cnid, &metadata_from_dir(d))?;
1237 replay_xattrs(target, cnid, &d.xattrs, &d.name, report)?;
1238 Ok(())
1239}
1240
1241fn replay_xattrs<RT>(
1246 target: &mut HfsPlusFilesystem<RT>,
1247 cnid: u32,
1248 xattrs: &[XattrRecord],
1249 label: &str,
1250 report: &mut HfsPlusCloneReport,
1251) -> Result<(), FilesystemError>
1252where
1253 RT: Read + Write + Seek + Send,
1254{
1255 for rec in xattrs {
1256 match &rec.kind {
1257 XattrKind::Inline(value) => {
1258 target.set_xattr(cnid, &rec.name, value)?;
1259 report.xattrs_copied += 1;
1260 }
1261 XattrKind::Fork(_) | XattrKind::Extents(_) => {
1262 report.skipped.push((
1263 format!("{label}/xattr:{}", rec.name),
1264 "fork-style xattr — target only supports inline payloads".into(),
1265 ));
1266 }
1267 }
1268 }
1269 Ok(())
1270}
1271
1272#[cfg(test)]
1273mod tests {
1274 use std::io::Cursor;
1275
1276 use super::*;
1277 use crate::hfsplus::{create_blank_hfsplus, HfsPlusFilesystem};
1278
1279 #[test]
1280 fn capture_blank_volume_only_has_root_dir() {
1281 let mut img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "Empty", false);
1282 let cursor = Cursor::new(&mut img);
1283 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("open blank");
1284 let snap = SourceCatalogSnapshot::capture(&mut fs).expect("capture");
1285 assert_eq!(snap.volume.label, "Empty");
1286 assert_eq!(snap.volume.block_size, 4096);
1287 let root = snap
1290 .dirs
1291 .iter()
1292 .find(|d| d.parent_cnid == 1)
1293 .expect("root dir");
1294 assert_eq!(root.cnid, 2);
1295 assert_eq!(root.name, "Empty");
1296 let user_dirs: Vec<_> = snap.dirs.iter().filter(|d| d.parent_cnid != 1).collect();
1297 assert!(
1298 user_dirs.is_empty(),
1299 "blank should have no user dirs, got: {:?}",
1300 user_dirs.iter().map(|d| &d.name).collect::<Vec<_>>()
1301 );
1302 }
1303
1304 #[test]
1305 fn capture_walks_dirs_and_files_and_streams_forks() {
1306 let mut img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "Work", false);
1307 {
1308 let cursor = Cursor::new(&mut img);
1309 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("open blank");
1310 fs.prepare_for_edit().expect("prepare_for_edit");
1311 let root = fs.root().expect("root entry");
1312 let _docs = fs
1313 .create_directory(&root, "Docs", &CreateDirectoryOptions::default())
1314 .expect("create dir");
1315 let payload: &[u8] = b"hello world\n";
1316 let mut data = std::io::Cursor::new(payload);
1317 fs.create_file(
1318 &root,
1319 "note.txt",
1320 &mut data,
1321 payload.len() as u64,
1322 &CreateFileOptions::default(),
1323 )
1324 .expect("create file");
1325 fs.sync_metadata().expect("sync");
1326 }
1327
1328 let cursor = Cursor::new(&mut img);
1329 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("re-open");
1330 let snap = SourceCatalogSnapshot::capture(&mut fs).expect("capture");
1331
1332 assert_eq!(snap.volume.label, "Work");
1333 let user_dirs: Vec<&SourceDirSnapshot> =
1335 snap.dirs.iter().filter(|d| d.parent_cnid != 1).collect();
1336 assert_eq!(user_dirs.len(), 1, "expected one user directory");
1337 let docs = user_dirs[0];
1338 assert_eq!(docs.name, "Docs");
1339 assert_eq!(docs.parent_cnid, 2);
1340
1341 assert_eq!(snap.files.len(), 1, "expected one user file");
1342 let note = &snap.files[0];
1343 assert_eq!(note.name, "note.txt");
1344 assert_eq!(note.parent_cnid, 2);
1345 assert_eq!(note.data_fork_size, b"hello world\n".len() as u64);
1346 assert_eq!(note.rsrc_fork_size, 0);
1347 let cnid = note.cnid;
1349 let raw = note.data_fork_raw;
1350 let mut buf = Vec::new();
1351 fs.fork_stream_reader(cnid, 0x00, &raw)
1352 .expect("fork reader")
1353 .read_to_end(&mut buf)
1354 .expect("read fork");
1355 assert_eq!(buf, b"hello world\n");
1356 }
1357
1358 #[test]
1365 fn capture_no_xattrs_yields_empty_vec() {
1366 let mut img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "Plain", false);
1367 {
1368 let cursor = Cursor::new(&mut img);
1369 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("open");
1370 fs.prepare_for_edit().expect("prepare_for_edit");
1371 let root = fs.root().expect("root");
1372 let mut data = std::io::Cursor::new(&b"x"[..]);
1373 fs.create_file(&root, "x.txt", &mut data, 1, &CreateFileOptions::default())
1374 .expect("create");
1375 fs.sync_metadata().expect("sync");
1376 }
1377 let cursor = Cursor::new(&mut img);
1378 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("re-open");
1379 let snap = SourceCatalogSnapshot::capture(&mut fs).expect("capture");
1380 for f in &snap.files {
1381 assert!(f.xattrs.is_empty(), "file {} has xattrs", f.name);
1382 }
1383 for d in &snap.dirs {
1384 assert!(d.xattrs.is_empty(), "dir {} has xattrs", d.name);
1385 }
1386 }
1387
1388 #[test]
1393 fn clone_round_trip_basic_tree() {
1394 let mut src_img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "Source", false);
1396 let payload: &[u8] = b"contents of note.txt\n";
1397 let rsrc: &[u8] = b"resource fork bytes";
1398 let xval = b"xattr-payload";
1399 {
1400 let cursor = Cursor::new(&mut src_img);
1401 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("open src");
1402 fs.prepare_for_edit().expect("prepare src");
1403 let root = fs.root().expect("root");
1404 let docs = fs
1405 .create_directory(&root, "Docs", &CreateDirectoryOptions::default())
1406 .expect("mkdir Docs");
1407 let mut data = Cursor::new(payload);
1408 let note = fs
1409 .create_file(
1410 &docs,
1411 "note.txt",
1412 &mut data,
1413 payload.len() as u64,
1414 &CreateFileOptions::default(),
1415 )
1416 .expect("create note");
1417 let mut rcur = Cursor::new(rsrc);
1418 fs.write_resource_fork(¬e, &mut rcur, rsrc.len() as u64)
1419 .expect("write rsrc");
1420 let _ = xval;
1425 fs.sync_metadata().expect("sync src");
1426 }
1427
1428 let mut tgt_img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "Target", false);
1430
1431 let report = {
1433 let src_cur = Cursor::new(&mut src_img);
1434 let mut src_fs = HfsPlusFilesystem::open(src_cur, 0).expect("open src for clone");
1435 let tgt_cur = Cursor::new(&mut tgt_img);
1436 let mut tgt_fs = HfsPlusFilesystem::open(tgt_cur, 0).expect("open tgt");
1437 tgt_fs.prepare_for_edit().expect("prepare tgt");
1438 clone_hfsplus_volume(&mut src_fs, &mut tgt_fs).expect("clone")
1439 };
1440 assert_eq!(report.dirs_copied, 1, "expected 1 dir copied");
1441 assert_eq!(report.files_copied, 1, "expected 1 file copied");
1442 assert_eq!(report.data_bytes_copied, payload.len() as u64);
1443 assert_eq!(report.rsrc_bytes_copied, rsrc.len() as u64);
1444
1445 let cursor = Cursor::new(&mut tgt_img);
1447 let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("re-open tgt");
1448 let root = fs.root().expect("tgt root");
1449 let children = fs.list_directory(&root).expect("list root");
1450 let docs = children
1451 .iter()
1452 .find(|e| e.name == "Docs" && e.is_directory())
1453 .expect("Docs in tgt");
1454 let docs_children = fs.list_directory(docs).expect("list Docs");
1455 let note = docs_children
1456 .iter()
1457 .find(|e| e.name == "note.txt")
1458 .expect("note in tgt");
1459 let read_data = fs.read_file(note, payload.len()).expect("read data");
1460 assert_eq!(read_data, payload);
1461 let mut rsrc_buf: Vec<u8> = Vec::new();
1462 fs.write_resource_fork_to(note, &mut rsrc_buf)
1463 .expect("read rsrc");
1464 assert_eq!(rsrc_buf, rsrc);
1465
1466 let result = fs.fsck().expect("fsck supported").expect("fsck ok");
1470 let codes: Vec<&str> = result.errors.iter().map(|e| e.code.as_str()).collect();
1471 assert!(result.is_clean(), "tgt fsck dirty: {codes:?}");
1472 }
1473}