1#[cfg(feature = "rust173-polyfill")]
9use disk_core::rust173::IntIsMultipleOf as _;
10use std::collections::{HashMap, HashSet, VecDeque};
11use std::io::{Read, Seek, Write};
12
13use byteorder::{BigEndian, ByteOrder};
14
15use crate::hfs::{HfsExtDescriptor, HfsFilesystem};
16use disk_core::entry::FileEntry;
17use disk_core::filesystem::FilesystemError;
18use disk_core::filesystem::{
19 CreateDirectoryOptions, CreateFileOptions, EditableFilesystem, Filesystem,
20};
21
22const CATALOG_DIR: i8 = 1;
24const CATALOG_FILE: i8 = 2;
25
26#[derive(Debug, Clone)]
28pub struct SourceVolumeSnapshot {
29 pub volume_name: String,
31 pub volume_name_raw: Vec<u8>,
33 pub create_date: u32,
35 pub modify_date: u32,
37 pub backup_date: u32,
39 pub finder_info: [u32; 8],
43 pub boot_blocks: [u8; 1024],
45 pub block_size: u32,
47 pub total_blocks: u16,
49}
50
51#[derive(Debug, Clone)]
53pub struct SourceFileSnapshot {
54 pub name: String,
55 pub name_raw: Vec<u8>,
56 pub cnid: u32,
59 pub parent_cnid: u32,
60 pub flags: u8,
62 pub finfo: [u8; 16],
64 pub fxinfo: [u8; 16],
66 pub create_date: u32,
67 pub modify_date: u32,
68 pub backup_date: u32,
69 pub data_size: u32,
70 pub data_extents: [HfsExtDescriptor; 3],
71 pub rsrc_size: u32,
72 pub rsrc_extents: [HfsExtDescriptor; 3],
73}
74
75impl SourceFileSnapshot {
76 pub fn is_locked(&self) -> bool {
78 self.flags & 0x01 != 0
79 }
80}
81
82#[derive(Debug, Clone)]
84pub struct SourceDirSnapshot {
85 pub name: String,
86 pub name_raw: Vec<u8>,
87 pub cnid: u32,
89 pub parent_cnid: u32,
90 pub dinfo: [u8; 16],
92 pub dxinfo: [u8; 16],
94 pub create_date: u32,
95 pub modify_date: u32,
96 pub backup_date: u32,
97}
98
99#[derive(Debug, Clone)]
105pub struct SourceCatalogSnapshot {
106 pub volume: SourceVolumeSnapshot,
107 pub dirs: Vec<SourceDirSnapshot>,
108 pub files: Vec<SourceFileSnapshot>,
109}
110
111impl SourceVolumeSnapshot {
112 pub fn capture<R: Read + Seek>(fs: &mut HfsFilesystem<R>) -> Result<Self, FilesystemError> {
115 let boot_blocks = fs.read_boot_blocks()?;
116 let mdb = fs.mdb();
117 let backup_date = BigEndian::read_u32(&mdb.raw_sector[64..68]);
120 Ok(SourceVolumeSnapshot {
121 volume_name: mdb.volume_name.clone(),
122 volume_name_raw: mdb.volume_name_raw.clone(),
123 create_date: mdb.create_date,
124 modify_date: mdb.modify_date,
125 backup_date,
126 finder_info: mdb.finder_info,
127 boot_blocks,
128 block_size: mdb.block_size,
129 total_blocks: mdb.total_blocks,
130 })
131 }
132}
133
134impl SourceCatalogSnapshot {
135 pub fn capture<R: Read + Seek>(fs: &mut HfsFilesystem<R>) -> Result<Self, FilesystemError> {
139 let volume = SourceVolumeSnapshot::capture(fs)?;
140 let (dirs, files) = walk_catalog(fs.catalog_data());
141 Ok(SourceCatalogSnapshot {
142 volume,
143 dirs,
144 files,
145 })
146 }
147}
148
149fn walk_catalog(catalog: &[u8]) -> (Vec<SourceDirSnapshot>, Vec<SourceFileSnapshot>) {
152 let mut dirs = Vec::new();
153 let mut files = Vec::new();
154
155 if catalog.len() < 512 {
156 return (dirs, files);
157 }
158 let node_size = BigEndian::read_u16(&catalog[32..34]) as usize;
159 if node_size == 0 || catalog.len() < node_size {
160 return (dirs, files);
161 }
162 let mut node_idx = BigEndian::read_u32(&catalog[24..28]);
165
166 while node_idx != 0 {
167 let offset = node_idx as usize * node_size;
168 if offset + node_size > catalog.len() {
169 break;
170 }
171 let node = &catalog[offset..offset + node_size];
172 let next_node = BigEndian::read_u32(&node[0..4]);
173 let kind = node[8] as i8;
174 if kind != -1 {
175 break;
176 }
177 let num_records = BigEndian::read_u16(&node[10..12]) as usize;
178
179 for i in 0..num_records {
180 let offset_pos = node_size - 2 * (i + 1);
181 if offset_pos + 2 > node.len() {
182 break;
183 }
184 let rec_offset = BigEndian::read_u16(&node[offset_pos..offset_pos + 2]) as usize;
185 if rec_offset + 7 > node.len() {
186 continue;
187 }
188 let key_len = node[rec_offset] as usize;
189 if key_len < 6 || rec_offset + 1 + key_len > node.len() {
190 continue;
191 }
192 let parent_id = BigEndian::read_u32(&node[rec_offset + 2..rec_offset + 6]);
193 let name_len = node[rec_offset + 6] as usize;
194 let name_end = rec_offset + 7 + name_len;
195 if name_end > node.len() {
196 continue;
197 }
198 let name_raw = node[rec_offset + 7..name_end].to_vec();
199
200 let mut rec_data_offset = rec_offset + 1 + key_len;
201 if !rec_data_offset.is_multiple_of(2) {
202 rec_data_offset += 1;
203 }
204 if rec_data_offset + 2 > node.len() {
205 continue;
206 }
207 let rec_type = node[rec_data_offset] as i8;
208
209 match rec_type {
210 CATALOG_DIR => {
211 if rec_data_offset + 70 > node.len() {
212 continue;
213 }
214 let rec = &node[rec_data_offset..rec_data_offset + 70];
215 let cnid = BigEndian::read_u32(&rec[6..10]);
216 let create_date = BigEndian::read_u32(&rec[10..14]);
217 let modify_date = BigEndian::read_u32(&rec[14..18]);
218 let backup_date = BigEndian::read_u32(&rec[18..22]);
219 let mut dinfo = [0u8; 16];
220 dinfo.copy_from_slice(&rec[22..38]);
221 let mut dxinfo = [0u8; 16];
222 dxinfo.copy_from_slice(&rec[38..54]);
223 dirs.push(SourceDirSnapshot {
224 name: crate::hfs::mac_roman_to_utf8(&name_raw),
225 name_raw,
226 cnid,
227 parent_cnid: parent_id,
228 dinfo,
229 dxinfo,
230 create_date,
231 modify_date,
232 backup_date,
233 });
234 }
235 CATALOG_FILE => {
236 if rec_data_offset + 102 > node.len() {
237 continue;
238 }
239 let rec = &node[rec_data_offset..rec_data_offset + 102];
240 let flags = rec[2];
241 let mut finfo = [0u8; 16];
242 finfo.copy_from_slice(&rec[4..20]);
243 let cnid = BigEndian::read_u32(&rec[20..24]);
244 let data_size = BigEndian::read_u32(&rec[26..30]);
245 let rsrc_size = BigEndian::read_u32(&rec[36..40]);
246 let create_date = BigEndian::read_u32(&rec[44..48]);
247 let modify_date = BigEndian::read_u32(&rec[48..52]);
248 let backup_date = BigEndian::read_u32(&rec[52..56]);
249 let mut fxinfo = [0u8; 16];
250 fxinfo.copy_from_slice(&rec[56..72]);
251 let mut data_extents = [HfsExtDescriptor {
252 start_block: 0,
253 block_count: 0,
254 }; 3];
255 for j in 0..3 {
256 data_extents[j] = HfsExtDescriptor::parse(&rec[74 + j * 4..78 + j * 4]);
257 }
258 let mut rsrc_extents = [HfsExtDescriptor {
259 start_block: 0,
260 block_count: 0,
261 }; 3];
262 for j in 0..3 {
263 rsrc_extents[j] = HfsExtDescriptor::parse(&rec[86 + j * 4..90 + j * 4]);
264 }
265 files.push(SourceFileSnapshot {
266 name: crate::hfs::mac_roman_to_utf8(&name_raw),
267 name_raw,
268 cnid,
269 parent_cnid: parent_id,
270 flags,
271 finfo,
272 fxinfo,
273 create_date,
274 modify_date,
275 backup_date,
276 data_size,
277 data_extents,
278 rsrc_size,
279 rsrc_extents,
280 });
281 }
282 _ => {}
283 }
284 }
285
286 node_idx = next_node;
287 }
288
289 (dirs, files)
290}
291
292#[derive(Debug, Default)]
296pub struct CloneReport {
297 pub files_copied: u64,
298 pub dirs_copied: u64,
299 pub data_bytes_copied: u64,
300 pub rsrc_bytes_copied: u64,
301 pub skipped: Vec<(String, String)>,
303 pub errors: Vec<(String, String)>,
305}
306
307pub fn clone_hfs_volume<RS, RT>(
323 source: &mut HfsFilesystem<RS>,
324 target: &mut HfsFilesystem<RT>,
325) -> Result<CloneReport, FilesystemError>
326where
327 RS: Read + Seek + Send,
328 RT: Read + Write + Seek + Send,
329{
330 let snapshot = SourceCatalogSnapshot::capture(source)?;
331 let mut report = CloneReport::default();
332
333 target.set_volume_dates(
336 snapshot.volume.create_date,
337 snapshot.volume.modify_date,
338 snapshot.volume.backup_date,
339 );
340 target.set_boot_blocks(&snapshot.volume.boot_blocks);
341
342 let mut dirs_by_parent: HashMap<u32, Vec<&SourceDirSnapshot>> = HashMap::new();
345 for d in &snapshot.dirs {
346 if d.cnid == 2 {
347 continue; }
349 dirs_by_parent.entry(d.parent_cnid).or_default().push(d);
350 }
351 let mut files_by_parent: HashMap<u32, Vec<&SourceFileSnapshot>> = HashMap::new();
352 for f in &snapshot.files {
353 files_by_parent.entry(f.parent_cnid).or_default().push(f);
354 }
355
356 let mut cnid_map: HashMap<u32, u32> = HashMap::new();
358 cnid_map.insert(2, 2);
359
360 let target_root = target.root()?;
363 let mut entry_map: HashMap<u32, FileEntry> = HashMap::new();
364 entry_map.insert(2, target_root.clone());
365
366 if let Some(root_src) = snapshot.dirs.iter().find(|d| d.cnid == 2) {
368 target.set_directory_finder_info(&target_root, root_src.dinfo, root_src.dxinfo)?;
369 target.set_dates(
370 &target_root,
371 root_src.create_date,
372 root_src.modify_date,
373 root_src.backup_date,
374 )?;
375 }
376
377 let mut queue: VecDeque<u32> = VecDeque::new();
378 queue.push_back(2);
379
380 while let Some(src_parent_cnid) = queue.pop_front() {
381 let parent_entry = entry_map.get(&src_parent_cnid).cloned().ok_or_else(|| {
382 FilesystemError::InvalidData(format!(
383 "clone: missing parent entry for source CNID {src_parent_cnid}"
384 ))
385 })?;
386
387 if let Some(children) = dirs_by_parent.remove(&src_parent_cnid) {
390 for d in children {
391 let new_dir = target.create_directory(
392 &parent_entry,
393 &d.name,
394 &CreateDirectoryOptions::default(),
395 )?;
396 target.set_directory_finder_info(&new_dir, d.dinfo, d.dxinfo)?;
397 target.set_dates(&new_dir, d.create_date, d.modify_date, d.backup_date)?;
398 cnid_map.insert(d.cnid, new_dir.location as u32);
399 entry_map.insert(d.cnid, new_dir);
400 report.dirs_copied += 1;
401 queue.push_back(d.cnid);
402 }
403 }
404
405 if let Some(children) = files_by_parent.remove(&src_parent_cnid) {
406 for f in children {
407 clone_one_file(source, target, &parent_entry, f, &mut cnid_map, &mut report)?;
408 }
409 }
410 }
411
412 let known_source_cnids: HashSet<u32> = cnid_map.keys().copied().collect();
426 let mut new_finder_info = [0u8; 32];
427 for i in 0..8 {
428 if i == 6 || i == 7 {
429 continue; }
431 let src_val = snapshot.volume.finder_info[i];
432 let mapped = if known_source_cnids.contains(&src_val) {
433 *cnid_map.get(&src_val).unwrap()
434 } else {
435 src_val
436 };
437 BigEndian::write_u32(&mut new_finder_info[i * 4..(i + 1) * 4], mapped);
438 }
439 target.set_volume_finder_info(&new_finder_info);
440
441 target.sync_metadata()?;
442
443 Ok(report)
444}
445
446fn clone_one_file<RS, RT>(
449 source: &mut HfsFilesystem<RS>,
450 target: &mut HfsFilesystem<RT>,
451 parent_entry: &FileEntry,
452 f: &SourceFileSnapshot,
453 cnid_map: &mut HashMap<u32, u32>,
454 report: &mut CloneReport,
455) -> Result<(), FilesystemError>
456where
457 RS: Read + Seek + Send,
458 RT: Read + Write + Seek + Send,
459{
460 let mut source_entry = FileEntry::new_file(
461 f.name.clone(),
462 String::new(),
463 f.data_size as u64,
464 f.cnid as u64,
465 );
466 if f.rsrc_size > 0 {
467 source_entry.resource_fork_size = Some(f.rsrc_size as u64);
468 }
469
470 let new_file = if f.data_size > 0 {
472 disk_core::stream_pipe::with_file_reader(source, &source_entry, |r| {
473 target.create_file(
474 parent_entry,
475 &f.name,
476 r,
477 f.data_size as u64,
478 &CreateFileOptions::default(),
479 )
480 })?
481 } else {
482 target.create_file(
483 parent_entry,
484 &f.name,
485 &mut std::io::empty(),
486 0,
487 &CreateFileOptions::default(),
488 )?
489 };
490 report.data_bytes_copied += f.data_size as u64;
491
492 if f.rsrc_size > 0 {
493 let mut rsrc = Vec::with_capacity(f.rsrc_size as usize);
494 source.write_resource_fork_to(&source_entry, &mut rsrc)?;
495 let mut rsrc_cursor = std::io::Cursor::new(&rsrc);
496 target.write_resource_fork(&new_file, &mut rsrc_cursor, f.rsrc_size as u64)?;
497 report.rsrc_bytes_copied += f.rsrc_size as u64;
498 }
499
500 target.set_finder_info(&new_file, f.finfo, f.fxinfo)?;
501 target.set_dates(&new_file, f.create_date, f.modify_date, f.backup_date)?;
502 if f.is_locked() {
503 target.set_file_locked(&new_file, true)?;
504 }
505
506 cnid_map.insert(f.cnid, new_file.location as u32);
507 report.files_copied += 1;
508 Ok(())
509}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514 use crate::hfs::HfsFilesystem;
515 use byteorder::{BigEndian, ByteOrder};
516 use disk_core::filesystem::{CreateFileOptions, EditableFilesystem, Filesystem};
517 use std::io::Cursor;
518
519 fn make_test_image() -> Vec<u8> {
523 {
524 let block_size = 4096u32;
525 let total_blocks: u16 = 128;
526 let first_alloc_block: u16 = 5;
527 let alloc_start = first_alloc_block as usize * 512;
528 let image_size = alloc_start + total_blocks as usize * block_size as usize;
529 let mut img = vec![0u8; image_size];
530
531 for (i, slot) in img[..1024].iter_mut().enumerate() {
534 *slot = (i as u8).wrapping_mul(7).wrapping_add(3);
535 }
536
537 img[3 * 512] = 0b11110000;
539
540 let node_size = 4096usize;
542 let catalog_offset = alloc_start;
543 img[catalog_offset + 8] = 1; BigEndian::write_u16(&mut img[catalog_offset + 10..catalog_offset + 12], 3);
545 let hr = catalog_offset + 14;
546 BigEndian::write_u16(&mut img[hr..hr + 2], 1);
547 BigEndian::write_u32(&mut img[hr + 2..hr + 6], 1);
548 BigEndian::write_u32(&mut img[hr + 6..hr + 10], 2);
549 BigEndian::write_u32(&mut img[hr + 10..hr + 14], 1);
550 BigEndian::write_u32(&mut img[hr + 14..hr + 18], 1);
551 BigEndian::write_u16(&mut img[hr + 18..hr + 20], node_size as u16);
552 BigEndian::write_u16(&mut img[hr + 20..hr + 22], 37);
553 BigEndian::write_u32(&mut img[hr + 22..hr + 26], 4);
554 BigEndian::write_u32(&mut img[hr + 26..hr + 30], 2);
555 let ot = catalog_offset + node_size;
556 BigEndian::write_u16(&mut img[ot - 2..ot], 14);
557 BigEndian::write_u16(&mut img[ot - 4..ot - 2], 142);
558 BigEndian::write_u16(&mut img[ot - 6..ot - 4], 270);
559 BigEndian::write_u16(&mut img[ot - 8..ot - 6], 526);
560 img[catalog_offset + 270] = 0b11000000;
561
562 let leaf_off = catalog_offset + node_size;
563 img[leaf_off + 8] = 0xFF; img[leaf_off + 9] = 1;
565 BigEndian::write_u16(&mut img[leaf_off + 10..leaf_off + 12], 2);
566
567 let r0_off = leaf_off + 14;
568 img[r0_off] = 6;
569 img[r0_off + 1] = 0;
570 BigEndian::write_u32(&mut img[r0_off + 2..r0_off + 6], 1);
571 img[r0_off + 6] = 0;
572 img[r0_off + 7] = 0;
573 let r0_data = r0_off + 8;
574 img[r0_data] = 1; BigEndian::write_u32(&mut img[r0_data + 6..r0_data + 10], 2);
576 BigEndian::write_u32(&mut img[r0_data + 10..r0_data + 14], 0x11111111);
578 BigEndian::write_u32(&mut img[r0_data + 14..r0_data + 18], 0x22222222);
579 BigEndian::write_u32(&mut img[r0_data + 18..r0_data + 22], 0x33333333);
580 img[r0_data + 22..r0_data + 38].copy_from_slice(b"DINFO_ROOT______");
581 img[r0_data + 38..r0_data + 54].copy_from_slice(b"DXINFO_ROOT_____");
582
583 let r1_off = r0_data + 70;
584 img[r1_off] = 6;
585 img[r1_off + 1] = 0;
586 BigEndian::write_u32(&mut img[r1_off + 2..r1_off + 6], 2);
587 img[r1_off + 6] = 0;
588 img[r1_off + 7] = 0;
589 let r1_data = r1_off + 8;
590 img[r1_data] = 3; BigEndian::write_u32(&mut img[r1_data + 10..r1_data + 14], 1);
592 img[r1_data + 14] = 0;
593
594 let lot = leaf_off + node_size;
595 BigEndian::write_u16(&mut img[lot - 2..lot], 14);
596 let r1_rel = (r1_off - leaf_off) as u16;
597 BigEndian::write_u16(&mut img[lot - 4..lot - 2], r1_rel);
598 let free_rel = (r1_data + 46 - leaf_off) as u16;
599 BigEndian::write_u16(&mut img[lot - 6..lot - 4], free_rel);
600
601 let mdb_off = 1024;
604 for b in img[mdb_off..mdb_off + 512].iter_mut() {
605 *b = 0;
606 }
607 let mut mdb = [0u8; 512];
608 BigEndian::write_u16(&mut mdb[0..2], 0x4244); BigEndian::write_u32(&mut mdb[2..6], 0xAABBCCDD); BigEndian::write_u32(&mut mdb[6..10], 0x11223344); BigEndian::write_u16(&mut mdb[14..16], 3); BigEndian::write_u16(&mut mdb[18..20], total_blocks);
613 BigEndian::write_u32(&mut mdb[20..24], block_size);
614 BigEndian::write_u16(&mut mdb[28..30], first_alloc_block);
615 BigEndian::write_u32(&mut mdb[30..34], 16);
616 BigEndian::write_u16(&mut mdb[34..36], total_blocks - 4);
617 mdb[36] = 7;
618 mdb[37..44].copy_from_slice(b"TestVol");
619 BigEndian::write_u32(&mut mdb[64..68], 0xDEADBEEF);
621 BigEndian::write_u32(&mut mdb[92..96], 99);
623 BigEndian::write_u32(&mut mdb[92 + 12..92 + 16], 77);
625 BigEndian::write_u32(&mut mdb[146..150], 4 * block_size);
626 BigEndian::write_u16(&mut mdb[150..152], 0);
627 BigEndian::write_u16(&mut mdb[152..154], 4);
628 img[mdb_off..mdb_off + 512].copy_from_slice(&mdb);
629
630 img
631 }
632 }
633
634 #[test]
635 fn volume_snapshot_roundtrip() {
636 let img = make_test_image();
637 let mut fs = HfsFilesystem::open(Cursor::new(img.clone()), 0).unwrap();
638 let snap = SourceVolumeSnapshot::capture(&mut fs).unwrap();
639
640 assert_eq!(snap.volume_name, "TestVol");
641 assert_eq!(snap.volume_name_raw, b"TestVol");
642 assert_eq!(snap.create_date, 0xAABBCCDD);
643 assert_eq!(snap.modify_date, 0x11223344);
644 assert_eq!(snap.backup_date, 0xDEADBEEF);
645 assert_eq!(snap.finder_info[0], 99);
646 assert_eq!(snap.finder_info[3], 77);
647 assert_eq!(snap.block_size, 4096);
648 assert_eq!(snap.total_blocks, 128);
649
650 let mut expected = [0u8; 1024];
653 for (i, slot) in expected.iter_mut().enumerate() {
654 *slot = (i as u8).wrapping_mul(7).wrapping_add(3);
655 }
656 assert_eq!(snap.boot_blocks, expected);
657 }
658
659 #[test]
660 fn dir_snapshot_captures_dinfo_and_dates() {
661 let img = make_test_image();
662 let mut fs = HfsFilesystem::open(Cursor::new(img.clone()), 0).unwrap();
663 let snap = SourceCatalogSnapshot::capture(&mut fs).unwrap();
664
665 let root = snap
668 .dirs
669 .iter()
670 .find(|d| d.cnid == 2)
671 .expect("root dir captured");
672 assert_eq!(root.parent_cnid, 1);
673 assert_eq!(root.create_date, 0x11111111);
674 assert_eq!(root.modify_date, 0x22222222);
675 assert_eq!(root.backup_date, 0x33333333);
676 assert_eq!(&root.dinfo, b"DINFO_ROOT______");
677 assert_eq!(&root.dxinfo, b"DXINFO_ROOT_____");
678 assert!(snap.files.is_empty());
679 }
680
681 #[test]
682 fn file_snapshot_captures_finfo_dates_and_extents() {
683 let mut backing: Vec<u8> = make_test_image();
687 {
688 let mut fs = HfsFilesystem::open(Cursor::new(&mut backing), 0).unwrap();
689 let root = fs.root().unwrap();
690 let opts = CreateFileOptions {
691 os_type: Some(*b"TEXT"),
692 os_creator: Some(*b"ttxt"),
693 ..Default::default()
694 };
695 let payload = b"hello world".to_vec();
696 let mut src = payload.as_slice();
697 fs.create_file(&root, "greet", &mut src, payload.len() as u64, &opts)
698 .unwrap();
699 fs.sync_metadata().unwrap();
700 }
701
702 let mut fs = HfsFilesystem::open(Cursor::new(&mut backing), 0).unwrap();
703 let snap = SourceCatalogSnapshot::capture(&mut fs).unwrap();
704 let f = snap
705 .files
706 .iter()
707 .find(|f| f.name == "greet")
708 .expect("greet captured");
709 assert_eq!(f.parent_cnid, 2);
710 assert_eq!(&f.finfo[0..4], b"TEXT");
711 assert_eq!(&f.finfo[4..8], b"ttxt");
712 assert_eq!(f.data_size, 11);
713 assert!(f.data_extents[0].block_count >= 1);
714 assert!(f.create_date != 0);
715 assert!(f.modify_date != 0);
716 assert!(!f.is_locked());
717 }
718
719 fn build_populated_source() -> Vec<u8> {
723 use crate::hfs::create_blank_hfs;
724 use disk_core::filesystem::{CreateDirectoryOptions, ResourceForkSource};
725
726 let mut backing = create_blank_hfs(1024 * 1024, 4096, "Source").unwrap();
727 {
728 let mut fs = HfsFilesystem::open(Cursor::new(&mut backing), 0).unwrap();
729 let root = fs.root().unwrap();
730
731 let sysf = fs
732 .create_directory(&root, "System Folder", &CreateDirectoryOptions::default())
733 .unwrap();
734 let apps = fs
735 .create_directory(&root, "Apps", &CreateDirectoryOptions::default())
736 .unwrap();
737 let sub = fs
738 .create_directory(&apps, "Sub", &CreateDirectoryOptions::default())
739 .unwrap();
740
741 let mut dinfo = [0u8; 16];
744 dinfo[..4].copy_from_slice(&[0x12, 0x34, 0x56, 0x78]);
745 let mut dxinfo = [0u8; 16];
746 dxinfo[..4].copy_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD]);
747 fs.set_directory_finder_info(&apps, dinfo, dxinfo).unwrap();
748 fs.set_dates(&apps, 0x10000001, 0x10000002, 0x10000003)
749 .unwrap();
750
751 let payload = b"hello world".to_vec();
753 let mut data = payload.as_slice();
754 let opts = CreateFileOptions {
755 os_type: Some(*b"TEXT"),
756 os_creator: Some(*b"ttxt"),
757 ..Default::default()
758 };
759 let note = fs
760 .create_file(&apps, "note.txt", &mut data, payload.len() as u64, &opts)
761 .unwrap();
762 fs.set_dates(¬e, 0x20000001, 0x20000002, 0x20000003)
763 .unwrap();
764
765 let big = vec![0x42u8; 6000];
767 let mut data = big.as_slice();
768 let opts = CreateFileOptions {
769 os_type: Some(*b"BINA"),
770 os_creator: Some(*b"hexd"),
771 ..Default::default()
772 };
773 fs.create_file(&sub, "data.bin", &mut data, big.len() as u64, &opts)
774 .unwrap();
775
776 let rsrc = b"RESOURCE_FORK_BYTES".to_vec();
778 let payload = b"with rsrc".to_vec();
779 let mut data = payload.as_slice();
780 let opts = CreateFileOptions {
781 os_type: Some(*b"APPL"),
782 os_creator: Some(*b"MACS"),
783 resource_fork: Some(ResourceForkSource::Data(rsrc.clone())),
784 ..Default::default()
785 };
786 let _ = fs
787 .create_file(&root, "App", &mut data, payload.len() as u64, &opts)
788 .unwrap();
789
790 let payload = b"locked content".to_vec();
792 let mut data = payload.as_slice();
793 let locked_file = fs
794 .create_file(
795 &root,
796 "ReadOnly",
797 &mut data,
798 payload.len() as u64,
799 &CreateFileOptions::default(),
800 )
801 .unwrap();
802 fs.set_file_locked(&locked_file, true).unwrap();
803
804 fs.set_blessed_folder(&sysf).unwrap();
805
806 fs.set_volume_dates(0x30000001, 0x30000002, 0x30000003);
808
809 fs.sync_metadata().unwrap();
810 }
811 backing
812 }
813
814 fn find_child<R: Read + Seek + Send>(
815 fs: &mut HfsFilesystem<R>,
816 parent: &FileEntry,
817 name: &str,
818 ) -> FileEntry {
819 let kids = fs.list_directory(parent).unwrap();
820 kids.into_iter()
821 .find(|e| e.name == name)
822 .unwrap_or_else(|| panic!("entry '{name}' not found under {}", parent.path))
823 }
824
825 #[test]
826 fn clone_round_trip_basic() {
827 let mut source_bytes = build_populated_source();
828 let mut target_bytes =
831 crate::hfs::create_blank_hfs(2 * 1024 * 1024, 8192, "Target").unwrap();
832
833 {
834 let mut source = HfsFilesystem::open(Cursor::new(&mut source_bytes), 0).unwrap();
835 let mut target = HfsFilesystem::open(Cursor::new(&mut target_bytes), 0).unwrap();
836 let report = clone_hfs_volume(&mut source, &mut target).unwrap();
837 assert_eq!(report.files_copied, 4);
838 assert_eq!(report.dirs_copied, 3);
839 assert!(report.data_bytes_copied >= 6000 + 11);
840 assert_eq!(
841 report.rsrc_bytes_copied,
842 b"RESOURCE_FORK_BYTES".len() as u64
843 );
844 }
845
846 let mut fs = HfsFilesystem::open(Cursor::new(&target_bytes), 0).unwrap();
848 let root = fs.root().unwrap();
849 let kids: Vec<String> = fs
850 .list_directory(&root)
851 .unwrap()
852 .into_iter()
853 .map(|e| e.name)
854 .collect();
855 assert!(kids.contains(&"System Folder".to_string()));
856 assert!(kids.contains(&"Apps".to_string()));
857 assert!(kids.contains(&"App".to_string()));
858 assert!(kids.contains(&"ReadOnly".to_string()));
859
860 let apps = find_child(&mut fs, &root, "Apps");
862 let apps_kids: Vec<String> = fs
863 .list_directory(&apps)
864 .unwrap()
865 .into_iter()
866 .map(|e| e.name)
867 .collect();
868 assert!(apps_kids.contains(&"note.txt".to_string()));
869 assert!(apps_kids.contains(&"Sub".to_string()));
870
871 let sub = find_child(&mut fs, &apps, "Sub");
873 let data_bin = find_child(&mut fs, &sub, "data.bin");
874 assert_eq!(data_bin.size, 6000);
875 let bytes = fs.read_file(&data_bin, 6000).unwrap();
876 assert_eq!(bytes, vec![0x42u8; 6000]);
877 assert_eq!(data_bin.type_code, Some(*b"BINA"));
878 assert_eq!(data_bin.creator_code, Some(*b"hexd"));
879
880 let note = find_child(&mut fs, &apps, "note.txt");
882 let bytes = fs.read_file(¬e, 32).unwrap();
883 assert_eq!(bytes, b"hello world");
884 assert_eq!(note.type_code, Some(*b"TEXT"));
885 assert_eq!(note.creator_code, Some(*b"ttxt"));
886
887 let app = find_child(&mut fs, &root, "App");
889 assert_eq!(app.resource_fork_size, Some(19));
890 let mut buf = Vec::new();
891 fs.write_resource_fork_to(&app, &mut buf).unwrap();
892 assert_eq!(buf, b"RESOURCE_FORK_BYTES");
893
894 let blessed = fs.blessed_system_folder().expect("blessed folder set");
896 let sys_folder = find_child(&mut fs, &root, "System Folder");
897 assert_eq!(blessed.0, sys_folder.location);
898 assert_eq!(blessed.1, "System Folder");
899
900 assert_eq!(fs.volume_label(), Some("Target"));
902
903 let fsck = fs.fsck().expect("HFS supports fsck");
905 assert!(
906 fsck.is_clean(),
907 "fsck errors on cloned volume: {:?}",
908 fsck.errors
909 .iter()
910 .map(|e| e.code.as_str())
911 .collect::<Vec<_>>()
912 );
913 }
914
915 #[test]
916 fn clone_preserves_locked_flag_and_dates() {
917 let mut source_bytes = build_populated_source();
918 let mut target_bytes =
919 crate::hfs::create_blank_hfs(2 * 1024 * 1024, 8192, "Target").unwrap();
920 {
921 let mut source = HfsFilesystem::open(Cursor::new(&mut source_bytes), 0).unwrap();
922 let mut target = HfsFilesystem::open(Cursor::new(&mut target_bytes), 0).unwrap();
923 clone_hfs_volume(&mut source, &mut target).unwrap();
924 }
925
926 let mut fs = HfsFilesystem::open(Cursor::new(&target_bytes), 0).unwrap();
928 let snap = SourceCatalogSnapshot::capture(&mut fs).unwrap();
929 let locked = snap
930 .files
931 .iter()
932 .find(|f| f.name == "ReadOnly")
933 .expect("ReadOnly file present");
934 assert!(locked.is_locked(), "locked flag must survive clone");
935
936 let note = snap
937 .files
938 .iter()
939 .find(|f| f.name == "note.txt")
940 .expect("note.txt present");
941 assert_eq!(note.create_date, 0x20000001);
942 assert_eq!(note.modify_date, 0x20000002);
943 assert_eq!(note.backup_date, 0x20000003);
944
945 let apps = snap
946 .dirs
947 .iter()
948 .find(|d| d.name == "Apps")
949 .expect("Apps dir present");
950 assert_eq!(apps.create_date, 0x10000001);
951 assert_eq!(apps.modify_date, 0x10000002);
952 assert_eq!(apps.backup_date, 0x10000003);
953 assert_eq!(&apps.dinfo[..4], &[0x12, 0x34, 0x56, 0x78]);
954 assert_eq!(&apps.dxinfo[..4], &[0xAA, 0xBB, 0xCC, 0xDD]);
955
956 assert_eq!(snap.volume.create_date, 0x30000001);
958 assert_eq!(snap.volume.modify_date, 0x30000002);
959 assert_eq!(snap.volume.backup_date, 0x30000003);
960 }
961}