#[cfg(feature = "rust173-polyfill")]
use disk_core::rust173::IntIsMultipleOf as _;
use std::collections::{HashMap, HashSet, VecDeque};
use std::io::{Read, Seek, Write};
use byteorder::{BigEndian, ByteOrder};
use crate::hfs::{HfsExtDescriptor, HfsFilesystem};
use disk_core::entry::FileEntry;
use disk_core::filesystem::FilesystemError;
use disk_core::filesystem::{
CreateDirectoryOptions, CreateFileOptions, EditableFilesystem, Filesystem,
};
const CATALOG_DIR: i8 = 1;
const CATALOG_FILE: i8 = 2;
#[derive(Debug, Clone)]
pub struct SourceVolumeSnapshot {
pub volume_name: String,
pub volume_name_raw: Vec<u8>,
pub create_date: u32,
pub modify_date: u32,
pub backup_date: u32,
pub finder_info: [u32; 8],
pub boot_blocks: [u8; 1024],
pub block_size: u32,
pub total_blocks: u16,
}
#[derive(Debug, Clone)]
pub struct SourceFileSnapshot {
pub name: String,
pub name_raw: Vec<u8>,
pub cnid: u32,
pub parent_cnid: u32,
pub flags: u8,
pub finfo: [u8; 16],
pub fxinfo: [u8; 16],
pub create_date: u32,
pub modify_date: u32,
pub backup_date: u32,
pub data_size: u32,
pub data_extents: [HfsExtDescriptor; 3],
pub rsrc_size: u32,
pub rsrc_extents: [HfsExtDescriptor; 3],
}
impl SourceFileSnapshot {
pub fn is_locked(&self) -> bool {
self.flags & 0x01 != 0
}
}
#[derive(Debug, Clone)]
pub struct SourceDirSnapshot {
pub name: String,
pub name_raw: Vec<u8>,
pub cnid: u32,
pub parent_cnid: u32,
pub dinfo: [u8; 16],
pub dxinfo: [u8; 16],
pub create_date: u32,
pub modify_date: u32,
pub backup_date: u32,
}
#[derive(Debug, Clone)]
pub struct SourceCatalogSnapshot {
pub volume: SourceVolumeSnapshot,
pub dirs: Vec<SourceDirSnapshot>,
pub files: Vec<SourceFileSnapshot>,
}
impl SourceVolumeSnapshot {
pub fn capture<R: Read + Seek>(fs: &mut HfsFilesystem<R>) -> Result<Self, FilesystemError> {
let boot_blocks = fs.read_boot_blocks()?;
let mdb = fs.mdb();
let backup_date = BigEndian::read_u32(&mdb.raw_sector[64..68]);
Ok(SourceVolumeSnapshot {
volume_name: mdb.volume_name.clone(),
volume_name_raw: mdb.volume_name_raw.clone(),
create_date: mdb.create_date,
modify_date: mdb.modify_date,
backup_date,
finder_info: mdb.finder_info,
boot_blocks,
block_size: mdb.block_size,
total_blocks: mdb.total_blocks,
})
}
}
impl SourceCatalogSnapshot {
pub fn capture<R: Read + Seek>(fs: &mut HfsFilesystem<R>) -> Result<Self, FilesystemError> {
let volume = SourceVolumeSnapshot::capture(fs)?;
let (dirs, files) = walk_catalog(fs.catalog_data());
Ok(SourceCatalogSnapshot {
volume,
dirs,
files,
})
}
}
fn walk_catalog(catalog: &[u8]) -> (Vec<SourceDirSnapshot>, Vec<SourceFileSnapshot>) {
let mut dirs = Vec::new();
let mut files = Vec::new();
if catalog.len() < 512 {
return (dirs, files);
}
let node_size = BigEndian::read_u16(&catalog[32..34]) as usize;
if node_size < crate::hfs_common::BTREE_MIN_NODE_SIZE || catalog.len() < node_size {
return (dirs, files);
}
let mut node_idx = BigEndian::read_u32(&catalog[24..28]);
while node_idx != 0 {
let offset = node_idx as usize * node_size;
if offset + node_size > catalog.len() {
break;
}
let node = &catalog[offset..offset + node_size];
let next_node = BigEndian::read_u32(&node[0..4]);
let kind = node[8] as i8;
if kind != -1 {
break;
}
let num_records = BigEndian::read_u16(&node[10..12]) as usize;
for i in 0..num_records {
let offset_pos = node_size - 2 * (i + 1);
if offset_pos + 2 > node.len() {
break;
}
let rec_offset = BigEndian::read_u16(&node[offset_pos..offset_pos + 2]) as usize;
if rec_offset + 7 > node.len() {
continue;
}
let key_len = node[rec_offset] as usize;
if key_len < 6 || rec_offset + 1 + key_len > node.len() {
continue;
}
let parent_id = BigEndian::read_u32(&node[rec_offset + 2..rec_offset + 6]);
let name_len = node[rec_offset + 6] as usize;
let name_end = rec_offset + 7 + name_len;
if name_end > node.len() {
continue;
}
let name_raw = node[rec_offset + 7..name_end].to_vec();
let mut rec_data_offset = rec_offset + 1 + key_len;
if !rec_data_offset.is_multiple_of(2) {
rec_data_offset += 1;
}
if rec_data_offset + 2 > node.len() {
continue;
}
let rec_type = node[rec_data_offset] as i8;
match rec_type {
CATALOG_DIR => {
if rec_data_offset + 70 > node.len() {
continue;
}
let rec = &node[rec_data_offset..rec_data_offset + 70];
let cnid = BigEndian::read_u32(&rec[6..10]);
let create_date = BigEndian::read_u32(&rec[10..14]);
let modify_date = BigEndian::read_u32(&rec[14..18]);
let backup_date = BigEndian::read_u32(&rec[18..22]);
let mut dinfo = [0u8; 16];
dinfo.copy_from_slice(&rec[22..38]);
let mut dxinfo = [0u8; 16];
dxinfo.copy_from_slice(&rec[38..54]);
dirs.push(SourceDirSnapshot {
name: crate::hfs::mac_roman_to_utf8(&name_raw),
name_raw,
cnid,
parent_cnid: parent_id,
dinfo,
dxinfo,
create_date,
modify_date,
backup_date,
});
}
CATALOG_FILE => {
if rec_data_offset + 102 > node.len() {
continue;
}
let rec = &node[rec_data_offset..rec_data_offset + 102];
let flags = rec[2];
let mut finfo = [0u8; 16];
finfo.copy_from_slice(&rec[4..20]);
let cnid = BigEndian::read_u32(&rec[20..24]);
let data_size = BigEndian::read_u32(&rec[26..30]);
let rsrc_size = BigEndian::read_u32(&rec[36..40]);
let create_date = BigEndian::read_u32(&rec[44..48]);
let modify_date = BigEndian::read_u32(&rec[48..52]);
let backup_date = BigEndian::read_u32(&rec[52..56]);
let mut fxinfo = [0u8; 16];
fxinfo.copy_from_slice(&rec[56..72]);
let mut data_extents = [HfsExtDescriptor {
start_block: 0,
block_count: 0,
}; 3];
for j in 0..3 {
data_extents[j] = HfsExtDescriptor::parse(&rec[74 + j * 4..78 + j * 4]);
}
let mut rsrc_extents = [HfsExtDescriptor {
start_block: 0,
block_count: 0,
}; 3];
for j in 0..3 {
rsrc_extents[j] = HfsExtDescriptor::parse(&rec[86 + j * 4..90 + j * 4]);
}
files.push(SourceFileSnapshot {
name: crate::hfs::mac_roman_to_utf8(&name_raw),
name_raw,
cnid,
parent_cnid: parent_id,
flags,
finfo,
fxinfo,
create_date,
modify_date,
backup_date,
data_size,
data_extents,
rsrc_size,
rsrc_extents,
});
}
_ => {}
}
}
node_idx = next_node;
}
(dirs, files)
}
#[derive(Debug, Default)]
pub struct CloneReport {
pub files_copied: u64,
pub dirs_copied: u64,
pub data_bytes_copied: u64,
pub rsrc_bytes_copied: u64,
pub skipped: Vec<(String, String)>,
pub errors: Vec<(String, String)>,
}
pub fn clone_hfs_volume<RS, RT>(
source: &mut HfsFilesystem<RS>,
target: &mut HfsFilesystem<RT>,
) -> Result<CloneReport, FilesystemError>
where
RS: Read + Seek + Send,
RT: Read + Write + Seek + Send,
{
let snapshot = SourceCatalogSnapshot::capture(source)?;
let mut report = CloneReport::default();
target.set_volume_dates(
snapshot.volume.create_date,
snapshot.volume.modify_date,
snapshot.volume.backup_date,
);
target.set_boot_blocks(&snapshot.volume.boot_blocks);
let mut dirs_by_parent: HashMap<u32, Vec<&SourceDirSnapshot>> = HashMap::new();
for d in &snapshot.dirs {
if d.cnid == 2 {
continue; }
dirs_by_parent.entry(d.parent_cnid).or_default().push(d);
}
let mut files_by_parent: HashMap<u32, Vec<&SourceFileSnapshot>> = HashMap::new();
for f in &snapshot.files {
files_by_parent.entry(f.parent_cnid).or_default().push(f);
}
let mut cnid_map: HashMap<u32, u32> = HashMap::new();
cnid_map.insert(2, 2);
let target_root = target.root()?;
let mut entry_map: HashMap<u32, FileEntry> = HashMap::new();
entry_map.insert(2, target_root.clone());
if let Some(root_src) = snapshot.dirs.iter().find(|d| d.cnid == 2) {
target.set_directory_finder_info(&target_root, root_src.dinfo, root_src.dxinfo)?;
target.set_dates(
&target_root,
root_src.create_date,
root_src.modify_date,
root_src.backup_date,
)?;
}
let mut queue: VecDeque<u32> = VecDeque::new();
queue.push_back(2);
while let Some(src_parent_cnid) = queue.pop_front() {
let parent_entry = entry_map.get(&src_parent_cnid).cloned().ok_or_else(|| {
FilesystemError::InvalidData(format!(
"clone: missing parent entry for source CNID {src_parent_cnid}"
))
})?;
if let Some(children) = dirs_by_parent.remove(&src_parent_cnid) {
for d in children {
let new_dir = target.create_directory(
&parent_entry,
&d.name,
&CreateDirectoryOptions::default(),
)?;
target.set_directory_finder_info(&new_dir, d.dinfo, d.dxinfo)?;
target.set_dates(&new_dir, d.create_date, d.modify_date, d.backup_date)?;
cnid_map.insert(d.cnid, new_dir.location as u32);
entry_map.insert(d.cnid, new_dir);
report.dirs_copied += 1;
queue.push_back(d.cnid);
}
}
if let Some(children) = files_by_parent.remove(&src_parent_cnid) {
for f in children {
clone_one_file(source, target, &parent_entry, f, &mut cnid_map, &mut report)?;
}
}
}
let known_source_cnids: HashSet<u32> = cnid_map.keys().copied().collect();
let mut new_finder_info = [0u8; 32];
for i in 0..8 {
if i == 6 || i == 7 {
continue; }
let src_val = snapshot.volume.finder_info[i];
let mapped = if known_source_cnids.contains(&src_val) {
*cnid_map.get(&src_val).unwrap()
} else {
src_val
};
BigEndian::write_u32(&mut new_finder_info[i * 4..(i + 1) * 4], mapped);
}
target.set_volume_finder_info(&new_finder_info);
target.sync_metadata()?;
Ok(report)
}
fn clone_one_file<RS, RT>(
source: &mut HfsFilesystem<RS>,
target: &mut HfsFilesystem<RT>,
parent_entry: &FileEntry,
f: &SourceFileSnapshot,
cnid_map: &mut HashMap<u32, u32>,
report: &mut CloneReport,
) -> Result<(), FilesystemError>
where
RS: Read + Seek + Send,
RT: Read + Write + Seek + Send,
{
let mut source_entry = FileEntry::new_file(
f.name.clone(),
String::new(),
f.data_size as u64,
f.cnid as u64,
);
if f.rsrc_size > 0 {
source_entry.resource_fork_size = Some(f.rsrc_size as u64);
}
let new_file = if f.data_size > 0 {
disk_core::stream_pipe::with_file_reader(source, &source_entry, |r| {
target.create_file(
parent_entry,
&f.name,
r,
f.data_size as u64,
&CreateFileOptions::default(),
)
})?
} else {
target.create_file(
parent_entry,
&f.name,
&mut std::io::empty(),
0,
&CreateFileOptions::default(),
)?
};
report.data_bytes_copied += f.data_size as u64;
if f.rsrc_size > 0 {
let mut rsrc = Vec::with_capacity(f.rsrc_size as usize);
source.write_resource_fork_to(&source_entry, &mut rsrc)?;
let mut rsrc_cursor = std::io::Cursor::new(&rsrc);
target.write_resource_fork(&new_file, &mut rsrc_cursor, f.rsrc_size as u64)?;
report.rsrc_bytes_copied += f.rsrc_size as u64;
}
target.set_finder_info(&new_file, f.finfo, f.fxinfo)?;
target.set_dates(&new_file, f.create_date, f.modify_date, f.backup_date)?;
if f.is_locked() {
target.set_file_locked(&new_file, true)?;
}
cnid_map.insert(f.cnid, new_file.location as u32);
report.files_copied += 1;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hfs::HfsFilesystem;
use byteorder::{BigEndian, ByteOrder};
use disk_core::filesystem::{CreateFileOptions, EditableFilesystem, Filesystem};
use std::io::Cursor;
fn make_test_image() -> Vec<u8> {
{
let block_size = 4096u32;
let total_blocks: u16 = 128;
let first_alloc_block: u16 = 5;
let alloc_start = first_alloc_block as usize * 512;
let image_size = alloc_start + total_blocks as usize * block_size as usize;
let mut img = vec![0u8; image_size];
for (i, slot) in img[..1024].iter_mut().enumerate() {
*slot = (i as u8).wrapping_mul(7).wrapping_add(3);
}
img[3 * 512] = 0b11110000;
let node_size = 4096usize;
let catalog_offset = alloc_start;
img[catalog_offset + 8] = 1; BigEndian::write_u16(&mut img[catalog_offset + 10..catalog_offset + 12], 3);
let hr = catalog_offset + 14;
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], 37);
BigEndian::write_u32(&mut img[hr + 22..hr + 26], 4);
BigEndian::write_u32(&mut img[hr + 26..hr + 30], 2);
let ot = catalog_offset + 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[catalog_offset + 270] = 0b11000000;
let leaf_off = catalog_offset + node_size;
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;
img[r0_off] = 6;
img[r0_off + 1] = 0;
BigEndian::write_u32(&mut img[r0_off + 2..r0_off + 6], 1);
img[r0_off + 6] = 0;
img[r0_off + 7] = 0;
let r0_data = r0_off + 8;
img[r0_data] = 1; BigEndian::write_u32(&mut img[r0_data + 6..r0_data + 10], 2);
BigEndian::write_u32(&mut img[r0_data + 10..r0_data + 14], 0x11111111);
BigEndian::write_u32(&mut img[r0_data + 14..r0_data + 18], 0x22222222);
BigEndian::write_u32(&mut img[r0_data + 18..r0_data + 22], 0x33333333);
img[r0_data + 22..r0_data + 38].copy_from_slice(b"DINFO_ROOT______");
img[r0_data + 38..r0_data + 54].copy_from_slice(b"DXINFO_ROOT_____");
let r1_off = r0_data + 70;
img[r1_off] = 6;
img[r1_off + 1] = 0;
BigEndian::write_u32(&mut img[r1_off + 2..r1_off + 6], 2);
img[r1_off + 6] = 0;
img[r1_off + 7] = 0;
let r1_data = r1_off + 8;
img[r1_data] = 3; BigEndian::write_u32(&mut img[r1_data + 10..r1_data + 14], 1);
img[r1_data + 14] = 0;
let lot = leaf_off + node_size;
BigEndian::write_u16(&mut img[lot - 2..lot], 14);
let r1_rel = (r1_off - leaf_off) as u16;
BigEndian::write_u16(&mut img[lot - 4..lot - 2], r1_rel);
let free_rel = (r1_data + 46 - leaf_off) as u16;
BigEndian::write_u16(&mut img[lot - 6..lot - 4], free_rel);
let mdb_off = 1024;
for b in img[mdb_off..mdb_off + 512].iter_mut() {
*b = 0;
}
let mut mdb = [0u8; 512];
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);
BigEndian::write_u32(&mut mdb[20..24], block_size);
BigEndian::write_u16(&mut mdb[28..30], first_alloc_block);
BigEndian::write_u32(&mut mdb[30..34], 16);
BigEndian::write_u16(&mut mdb[34..36], total_blocks - 4);
mdb[36] = 7;
mdb[37..44].copy_from_slice(b"TestVol");
BigEndian::write_u32(&mut mdb[64..68], 0xDEADBEEF);
BigEndian::write_u32(&mut mdb[92..96], 99);
BigEndian::write_u32(&mut mdb[92 + 12..92 + 16], 77);
BigEndian::write_u32(&mut mdb[146..150], 4 * block_size);
BigEndian::write_u16(&mut mdb[150..152], 0);
BigEndian::write_u16(&mut mdb[152..154], 4);
img[mdb_off..mdb_off + 512].copy_from_slice(&mdb);
img
}
}
#[test]
fn volume_snapshot_roundtrip() {
let img = make_test_image();
let mut fs = HfsFilesystem::open(Cursor::new(img.clone()), 0).unwrap();
let snap = SourceVolumeSnapshot::capture(&mut fs).unwrap();
assert_eq!(snap.volume_name, "TestVol");
assert_eq!(snap.volume_name_raw, b"TestVol");
assert_eq!(snap.create_date, 0xAABBCCDD);
assert_eq!(snap.modify_date, 0x11223344);
assert_eq!(snap.backup_date, 0xDEADBEEF);
assert_eq!(snap.finder_info[0], 99);
assert_eq!(snap.finder_info[3], 77);
assert_eq!(snap.block_size, 4096);
assert_eq!(snap.total_blocks, 128);
let mut expected = [0u8; 1024];
for (i, slot) in expected.iter_mut().enumerate() {
*slot = (i as u8).wrapping_mul(7).wrapping_add(3);
}
assert_eq!(snap.boot_blocks, expected);
}
#[test]
fn dir_snapshot_captures_dinfo_and_dates() {
let img = make_test_image();
let mut fs = HfsFilesystem::open(Cursor::new(img.clone()), 0).unwrap();
let snap = SourceCatalogSnapshot::capture(&mut fs).unwrap();
let root = snap
.dirs
.iter()
.find(|d| d.cnid == 2)
.expect("root dir captured");
assert_eq!(root.parent_cnid, 1);
assert_eq!(root.create_date, 0x11111111);
assert_eq!(root.modify_date, 0x22222222);
assert_eq!(root.backup_date, 0x33333333);
assert_eq!(&root.dinfo, b"DINFO_ROOT______");
assert_eq!(&root.dxinfo, b"DXINFO_ROOT_____");
assert!(snap.files.is_empty());
}
#[test]
fn file_snapshot_captures_finfo_dates_and_extents() {
let mut backing: Vec<u8> = make_test_image();
{
let mut fs = HfsFilesystem::open(Cursor::new(&mut backing), 0).unwrap();
let root = fs.root().unwrap();
let opts = CreateFileOptions {
os_type: Some(*b"TEXT"),
os_creator: Some(*b"ttxt"),
..Default::default()
};
let payload = b"hello world".to_vec();
let mut src = payload.as_slice();
fs.create_file(&root, "greet", &mut src, payload.len() as u64, &opts)
.unwrap();
fs.sync_metadata().unwrap();
}
let mut fs = HfsFilesystem::open(Cursor::new(&mut backing), 0).unwrap();
let snap = SourceCatalogSnapshot::capture(&mut fs).unwrap();
let f = snap
.files
.iter()
.find(|f| f.name == "greet")
.expect("greet captured");
assert_eq!(f.parent_cnid, 2);
assert_eq!(&f.finfo[0..4], b"TEXT");
assert_eq!(&f.finfo[4..8], b"ttxt");
assert_eq!(f.data_size, 11);
assert!(f.data_extents[0].block_count >= 1);
assert!(f.create_date != 0);
assert!(f.modify_date != 0);
assert!(!f.is_locked());
}
fn build_populated_source() -> Vec<u8> {
use crate::hfs::create_blank_hfs;
use disk_core::filesystem::{CreateDirectoryOptions, ResourceForkSource};
let mut backing = create_blank_hfs(1024 * 1024, 4096, "Source").unwrap();
{
let mut fs = HfsFilesystem::open(Cursor::new(&mut backing), 0).unwrap();
let root = fs.root().unwrap();
let sysf = fs
.create_directory(&root, "System Folder", &CreateDirectoryOptions::default())
.unwrap();
let apps = fs
.create_directory(&root, "Apps", &CreateDirectoryOptions::default())
.unwrap();
let sub = fs
.create_directory(&apps, "Sub", &CreateDirectoryOptions::default())
.unwrap();
let mut dinfo = [0u8; 16];
dinfo[..4].copy_from_slice(&[0x12, 0x34, 0x56, 0x78]);
let mut dxinfo = [0u8; 16];
dxinfo[..4].copy_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD]);
fs.set_directory_finder_info(&apps, dinfo, dxinfo).unwrap();
fs.set_dates(&apps, 0x10000001, 0x10000002, 0x10000003)
.unwrap();
let payload = b"hello world".to_vec();
let mut data = payload.as_slice();
let opts = CreateFileOptions {
os_type: Some(*b"TEXT"),
os_creator: Some(*b"ttxt"),
..Default::default()
};
let note = fs
.create_file(&apps, "note.txt", &mut data, payload.len() as u64, &opts)
.unwrap();
fs.set_dates(¬e, 0x20000001, 0x20000002, 0x20000003)
.unwrap();
let big = vec![0x42u8; 6000];
let mut data = big.as_slice();
let opts = CreateFileOptions {
os_type: Some(*b"BINA"),
os_creator: Some(*b"hexd"),
..Default::default()
};
fs.create_file(&sub, "data.bin", &mut data, big.len() as u64, &opts)
.unwrap();
let rsrc = b"RESOURCE_FORK_BYTES".to_vec();
let payload = b"with rsrc".to_vec();
let mut data = payload.as_slice();
let opts = CreateFileOptions {
os_type: Some(*b"APPL"),
os_creator: Some(*b"MACS"),
resource_fork: Some(ResourceForkSource::Data(rsrc.clone())),
..Default::default()
};
let _ = fs
.create_file(&root, "App", &mut data, payload.len() as u64, &opts)
.unwrap();
let payload = b"locked content".to_vec();
let mut data = payload.as_slice();
let locked_file = fs
.create_file(
&root,
"ReadOnly",
&mut data,
payload.len() as u64,
&CreateFileOptions::default(),
)
.unwrap();
fs.set_file_locked(&locked_file, true).unwrap();
fs.set_blessed_folder(&sysf).unwrap();
fs.set_volume_dates(0x30000001, 0x30000002, 0x30000003);
fs.sync_metadata().unwrap();
}
backing
}
fn find_child<R: Read + Seek + Send>(
fs: &mut HfsFilesystem<R>,
parent: &FileEntry,
name: &str,
) -> FileEntry {
let kids = fs.list_directory(parent).unwrap();
kids.into_iter()
.find(|e| e.name == name)
.unwrap_or_else(|| panic!("entry '{name}' not found under {}", parent.path))
}
#[test]
fn clone_round_trip_basic() {
let mut source_bytes = build_populated_source();
let mut target_bytes =
crate::hfs::create_blank_hfs(2 * 1024 * 1024, 8192, "Target").unwrap();
{
let mut source = HfsFilesystem::open(Cursor::new(&mut source_bytes), 0).unwrap();
let mut target = HfsFilesystem::open(Cursor::new(&mut target_bytes), 0).unwrap();
let report = clone_hfs_volume(&mut source, &mut target).unwrap();
assert_eq!(report.files_copied, 4);
assert_eq!(report.dirs_copied, 3);
assert!(report.data_bytes_copied >= 6000 + 11);
assert_eq!(
report.rsrc_bytes_copied,
b"RESOURCE_FORK_BYTES".len() as u64
);
}
let mut fs = HfsFilesystem::open(Cursor::new(&target_bytes), 0).unwrap();
let root = fs.root().unwrap();
let kids: Vec<String> = fs
.list_directory(&root)
.unwrap()
.into_iter()
.map(|e| e.name)
.collect();
assert!(kids.contains(&"System Folder".to_string()));
assert!(kids.contains(&"Apps".to_string()));
assert!(kids.contains(&"App".to_string()));
assert!(kids.contains(&"ReadOnly".to_string()));
let apps = find_child(&mut fs, &root, "Apps");
let apps_kids: Vec<String> = fs
.list_directory(&apps)
.unwrap()
.into_iter()
.map(|e| e.name)
.collect();
assert!(apps_kids.contains(&"note.txt".to_string()));
assert!(apps_kids.contains(&"Sub".to_string()));
let sub = find_child(&mut fs, &apps, "Sub");
let data_bin = find_child(&mut fs, &sub, "data.bin");
assert_eq!(data_bin.size, 6000);
let bytes = fs.read_file(&data_bin, 6000).unwrap();
assert_eq!(bytes, vec![0x42u8; 6000]);
assert_eq!(data_bin.type_code, Some(*b"BINA"));
assert_eq!(data_bin.creator_code, Some(*b"hexd"));
let note = find_child(&mut fs, &apps, "note.txt");
let bytes = fs.read_file(¬e, 32).unwrap();
assert_eq!(bytes, b"hello world");
assert_eq!(note.type_code, Some(*b"TEXT"));
assert_eq!(note.creator_code, Some(*b"ttxt"));
let app = find_child(&mut fs, &root, "App");
assert_eq!(app.resource_fork_size, Some(19));
let mut buf = Vec::new();
fs.write_resource_fork_to(&app, &mut buf).unwrap();
assert_eq!(buf, b"RESOURCE_FORK_BYTES");
let blessed = fs.blessed_system_folder().expect("blessed folder set");
let sys_folder = find_child(&mut fs, &root, "System Folder");
assert_eq!(blessed.0, sys_folder.location);
assert_eq!(blessed.1, "System Folder");
assert_eq!(fs.volume_label(), Some("Target"));
let fsck = fs.fsck().expect("HFS supports fsck");
assert!(
fsck.is_clean(),
"fsck errors on cloned volume: {:?}",
fsck.errors
.iter()
.map(|e| e.code.as_str())
.collect::<Vec<_>>()
);
}
#[test]
fn clone_preserves_locked_flag_and_dates() {
let mut source_bytes = build_populated_source();
let mut target_bytes =
crate::hfs::create_blank_hfs(2 * 1024 * 1024, 8192, "Target").unwrap();
{
let mut source = HfsFilesystem::open(Cursor::new(&mut source_bytes), 0).unwrap();
let mut target = HfsFilesystem::open(Cursor::new(&mut target_bytes), 0).unwrap();
clone_hfs_volume(&mut source, &mut target).unwrap();
}
let mut fs = HfsFilesystem::open(Cursor::new(&target_bytes), 0).unwrap();
let snap = SourceCatalogSnapshot::capture(&mut fs).unwrap();
let locked = snap
.files
.iter()
.find(|f| f.name == "ReadOnly")
.expect("ReadOnly file present");
assert!(locked.is_locked(), "locked flag must survive clone");
let note = snap
.files
.iter()
.find(|f| f.name == "note.txt")
.expect("note.txt present");
assert_eq!(note.create_date, 0x20000001);
assert_eq!(note.modify_date, 0x20000002);
assert_eq!(note.backup_date, 0x20000003);
let apps = snap
.dirs
.iter()
.find(|d| d.name == "Apps")
.expect("Apps dir present");
assert_eq!(apps.create_date, 0x10000001);
assert_eq!(apps.modify_date, 0x10000002);
assert_eq!(apps.backup_date, 0x10000003);
assert_eq!(&apps.dinfo[..4], &[0x12, 0x34, 0x56, 0x78]);
assert_eq!(&apps.dxinfo[..4], &[0xAA, 0xBB, 0xCC, 0xDD]);
assert_eq!(snap.volume.create_date, 0x30000001);
assert_eq!(snap.volume.modify_date, 0x30000002);
assert_eq!(snap.volume.backup_date, 0x30000003);
}
}