#[cfg(feature = "rust173-polyfill")]
use disk_core::rust173::IntIsMultipleOf as _;
use std::io::{Read, Seek, Write};
use byteorder::{BigEndian, ByteOrder};
use crate::hfs_common::walk_leaf_records;
use crate::hfsplus::{HfsPlusFilesystem, RecordMetadata, XattrKind, XattrRecord};
use disk_core::entry::FileEntry;
use disk_core::filesystem::{
CreateDirectoryOptions, CreateFileOptions, EditableFilesystem, Filesystem, FilesystemError,
};
use std::collections::{HashMap, HashSet, VecDeque};
const HFSPLUS_FORK_DATA: u8 = 0x00;
const HFSPLUS_FORK_RESOURCE: u8 = 0xFF;
const CATALOG_FOLDER: i16 = 1;
const CATALOG_FILE: i16 = 2;
const HARDLINK_FILE_TYPE: &[u8; 4] = b"hlnk";
const HARDLINK_FILE_CREATOR: &[u8; 4] = b"hfs+";
const HARDLINK_DIR_TYPE: &[u8; 4] = b"fdrp";
const HARDLINK_DIR_CREATOR: &[u8; 4] = b"MACS";
#[derive(Debug, Clone)]
pub struct SourceVolumeSnapshot {
pub label: String,
pub signature: u16,
pub attributes: u32,
pub create_date: u32,
pub modify_date: u32,
pub backup_date: u32,
pub checked_date: u32,
pub finder_info: [u32; 8],
pub block_size: u32,
pub total_blocks: u32,
pub file_count: u32,
pub folder_count: u32,
pub boot_blocks: [u8; 1024],
pub catalog_total_blocks: u32,
pub extents_total_blocks: u32,
pub attributes_total_blocks: u32,
}
#[derive(Debug, Clone)]
pub struct SourceDirSnapshot {
pub name: String,
pub name_utf16: Vec<u16>,
pub cnid: u32,
pub parent_cnid: u32,
pub valence: u32,
pub flags: u16,
pub finder_info: [u8; 16],
pub extended_finder_info: [u8; 16],
pub bsd_info: [u8; 16],
pub create_date: u32,
pub content_mod_date: u32,
pub attribute_mod_date: u32,
pub access_date: u32,
pub backup_date: u32,
pub text_encoding: u32,
pub(crate) xattrs: Vec<XattrRecord>,
pub is_dir_inode: bool,
pub inode_num: Option<u32>,
}
#[derive(Debug, Clone)]
pub struct SourceFileSnapshot {
pub name: String,
pub name_utf16: Vec<u16>,
pub cnid: u32,
pub parent_cnid: u32,
pub flags: u16,
pub finder_info: [u8; 16],
pub extended_finder_info: [u8; 16],
pub bsd_info: [u8; 16],
pub create_date: u32,
pub content_mod_date: u32,
pub attribute_mod_date: u32,
pub access_date: u32,
pub backup_date: u32,
pub text_encoding: u32,
pub data_fork_raw: [u8; 80],
pub rsrc_fork_raw: [u8; 80],
pub data_fork_size: u64,
pub rsrc_fork_size: u64,
pub(crate) xattrs: Vec<XattrRecord>,
pub is_inode: bool,
pub inode_num: Option<u32>,
}
#[derive(Debug, Clone)]
pub struct SourceHardlinkSnapshot {
pub name: String,
pub name_utf16: Vec<u16>,
pub cnid: u32,
pub parent_cnid: u32,
pub inode_source_cnid: u32,
pub inode_num: u32,
pub flags: u16,
pub finder_info: [u8; 16],
pub extended_finder_info: [u8; 16],
pub bsd_info: [u8; 16],
pub create_date: u32,
pub content_mod_date: u32,
pub attribute_mod_date: u32,
pub access_date: u32,
pub backup_date: u32,
pub text_encoding: u32,
pub(crate) xattrs: Vec<XattrRecord>,
}
impl SourceFileSnapshot {
pub fn type_code(&self) -> String {
decode_fourcc(&self.finder_info[0..4])
}
pub fn creator_code(&self) -> String {
decode_fourcc(&self.finder_info[4..8])
}
}
#[derive(Debug, Clone)]
pub struct SourceDirHardlinkSnapshot {
pub name: String,
pub name_utf16: Vec<u16>,
pub cnid: u32,
pub parent_cnid: u32,
pub inode_dir_source_cnid: u32,
pub inode_num: u32,
pub flags: u16,
pub finder_info: [u8; 16],
pub extended_finder_info: [u8; 16],
pub bsd_info: [u8; 16],
pub create_date: u32,
pub content_mod_date: u32,
pub attribute_mod_date: u32,
pub access_date: u32,
pub backup_date: u32,
pub text_encoding: u32,
pub(crate) xattrs: Vec<XattrRecord>,
}
#[derive(Debug, Clone)]
pub struct SourceCatalogSnapshot {
pub volume: SourceVolumeSnapshot,
pub dirs: Vec<SourceDirSnapshot>,
pub files: Vec<SourceFileSnapshot>,
pub hardlinks: Vec<SourceHardlinkSnapshot>,
pub dir_hardlinks: Vec<SourceDirHardlinkSnapshot>,
}
impl SourceVolumeSnapshot {
pub fn capture<R: Read + Seek>(fs: &mut HfsPlusFilesystem<R>) -> Result<Self, FilesystemError> {
let boot_blocks = fs.read_boot_blocks()?;
Ok(SourceVolumeSnapshot {
label: fs.label().to_string(),
signature: fs.signature(),
attributes: fs.vh_attributes(),
create_date: fs.vh_create_date(),
modify_date: fs.vh_modify_date(),
backup_date: fs.vh_backup_date(),
checked_date: fs.vh_checked_date(),
finder_info: fs.vh_finder_info(),
block_size: fs.block_size(),
total_blocks: fs.total_blocks(),
file_count: fs.vh_file_count(),
folder_count: fs.vh_folder_count(),
boot_blocks,
catalog_total_blocks: fs.vh_catalog_total_blocks(),
extents_total_blocks: fs.vh_extents_total_blocks(),
attributes_total_blocks: fs.vh_attributes_total_blocks(),
})
}
}
impl SourceCatalogSnapshot {
pub fn capture<R: Read + Seek>(fs: &mut HfsPlusFilesystem<R>) -> Result<Self, FilesystemError> {
let volume = SourceVolumeSnapshot::capture(fs)?;
let mut dirs: Vec<SourceDirSnapshot> = Vec::new();
let mut files: Vec<SourceFileSnapshot> = Vec::new();
let mut pending_forks: Vec<([u8; 80], [u8; 80])> = Vec::new();
let mut pending_links: Vec<PendingHardlink> = Vec::new();
let mut pending_dir_links: Vec<PendingDirHardlink> = Vec::new();
let mut walk_err: Option<FilesystemError> = None;
{
let catalog = fs.catalog_data();
let node_size = fs.catalog_node_size();
let first_leaf = fs.catalog_first_leaf();
walk_leaf_records(
catalog,
first_leaf,
node_size,
|_node_idx, _rec_idx, _abs_off, rec| -> Option<()> {
match parse_catalog_record(rec) {
Ok(ParsedRecord::Folder(d)) => {
dirs.push(d);
None
}
Ok(ParsedRecord::File {
file,
data_fork,
rsrc_fork,
}) => {
files.push(file);
pending_forks.push((data_fork, rsrc_fork));
None
}
Ok(ParsedRecord::Hardlink(p)) => {
pending_links.push(p);
None
}
Ok(ParsedRecord::DirHardlink(p)) => {
pending_dir_links.push(p);
None
}
Ok(ParsedRecord::Thread) | Ok(ParsedRecord::Skip) => None,
Err(e) => {
walk_err = Some(e);
Some(())
}
}
},
);
}
if let Some(e) = walk_err {
return Err(e);
}
let private_name = hfsplus_private_dir_name();
let dir_private_name = hfsplus_dir_private_dir_name();
let private_cnid = dirs.iter().find_map(|d| {
if d.name == private_name {
Some(d.cnid)
} else {
None
}
});
let dir_private_cnid = dirs.iter().find_map(|d| {
if d.name == dir_private_name {
Some(d.cnid)
} else {
None
}
});
let mut inode_num_to_source_cnid: std::collections::HashMap<u32, u32> =
std::collections::HashMap::new();
let mut dir_inode_num_to_source_cnid: std::collections::HashMap<u32, u32> =
std::collections::HashMap::new();
if let Some(pcnid) = dir_private_cnid {
for d in dirs.iter_mut() {
if d.parent_cnid != pcnid {
continue;
}
let Some(rest) = d.name.strip_prefix("dir_") else {
continue;
};
let Ok(num) = rest.parse::<u32>() else {
continue;
};
d.is_dir_inode = true;
d.inode_num = Some(num);
dir_inode_num_to_source_cnid.insert(num, d.cnid);
}
}
if let Some(pcnid) = private_cnid {
for f in files.iter_mut() {
if f.parent_cnid != pcnid {
continue;
}
let Some(rest) = f.name.strip_prefix("iNode") else {
continue;
};
let Ok(num) = rest.parse::<u32>() else {
continue;
};
f.is_inode = true;
f.inode_num = Some(num);
inode_num_to_source_cnid.insert(num, f.cnid);
}
}
let mut hardlinks: Vec<SourceHardlinkSnapshot> = Vec::with_capacity(pending_links.len());
for p in pending_links {
let Some(inode_source_cnid) = inode_num_to_source_cnid.get(&p.inode_num).copied()
else {
files.push(SourceFileSnapshot {
name: p.name,
name_utf16: p.name_utf16,
cnid: p.cnid,
parent_cnid: p.parent_cnid,
flags: p.flags,
finder_info: p.finder_info,
extended_finder_info: p.extended_finder_info,
bsd_info: p.bsd_info,
create_date: p.create_date,
content_mod_date: p.content_mod_date,
attribute_mod_date: p.attribute_mod_date,
access_date: p.access_date,
backup_date: p.backup_date,
text_encoding: p.text_encoding,
data_fork_raw: p.data_fork_raw,
rsrc_fork_raw: p.rsrc_fork_raw,
data_fork_size: p.data_fork_size,
rsrc_fork_size: p.rsrc_fork_size,
xattrs: Vec::new(),
is_inode: false,
inode_num: None,
});
continue;
};
hardlinks.push(SourceHardlinkSnapshot {
name: p.name,
name_utf16: p.name_utf16,
cnid: p.cnid,
parent_cnid: p.parent_cnid,
inode_source_cnid,
inode_num: p.inode_num,
flags: p.flags,
finder_info: p.finder_info,
extended_finder_info: p.extended_finder_info,
bsd_info: p.bsd_info,
create_date: p.create_date,
content_mod_date: p.content_mod_date,
attribute_mod_date: p.attribute_mod_date,
access_date: p.access_date,
backup_date: p.backup_date,
text_encoding: p.text_encoding,
xattrs: Vec::new(),
});
}
let mut dir_hardlinks: Vec<SourceDirHardlinkSnapshot> =
Vec::with_capacity(pending_dir_links.len());
for p in pending_dir_links {
let Some(inode_dir_source_cnid) =
dir_inode_num_to_source_cnid.get(&p.inode_num).copied()
else {
files.push(SourceFileSnapshot {
name: p.name,
name_utf16: p.name_utf16,
cnid: p.cnid,
parent_cnid: p.parent_cnid,
flags: p.flags,
finder_info: p.finder_info,
extended_finder_info: p.extended_finder_info,
bsd_info: p.bsd_info,
create_date: p.create_date,
content_mod_date: p.content_mod_date,
attribute_mod_date: p.attribute_mod_date,
access_date: p.access_date,
backup_date: p.backup_date,
text_encoding: p.text_encoding,
data_fork_raw: p.data_fork_raw,
rsrc_fork_raw: p.rsrc_fork_raw,
data_fork_size: p.data_fork_size,
rsrc_fork_size: p.rsrc_fork_size,
xattrs: Vec::new(),
is_inode: false,
inode_num: None,
});
continue;
};
dir_hardlinks.push(SourceDirHardlinkSnapshot {
name: p.name,
name_utf16: p.name_utf16,
cnid: p.cnid,
parent_cnid: p.parent_cnid,
inode_dir_source_cnid,
inode_num: p.inode_num,
flags: p.flags,
finder_info: p.finder_info,
extended_finder_info: p.extended_finder_info,
bsd_info: p.bsd_info,
create_date: p.create_date,
content_mod_date: p.content_mod_date,
attribute_mod_date: p.attribute_mod_date,
access_date: p.access_date,
backup_date: p.backup_date,
text_encoding: p.text_encoding,
xattrs: Vec::new(),
});
}
for (file, (data_fork, rsrc_fork)) in files.iter_mut().zip(pending_forks) {
file.data_fork_size = BigEndian::read_u64(&data_fork[0..8]);
file.rsrc_fork_size = BigEndian::read_u64(&rsrc_fork[0..8]);
file.data_fork_raw = data_fork;
file.rsrc_fork_raw = rsrc_fork;
file.xattrs = fs.list_xattrs(file.cnid)?;
}
for dir in dirs.iter_mut() {
dir.xattrs = fs.list_xattrs(dir.cnid)?;
}
for link in hardlinks.iter_mut() {
link.xattrs = fs.list_xattrs(link.cnid)?;
}
for link in dir_hardlinks.iter_mut() {
link.xattrs = fs.list_xattrs(link.cnid)?;
}
Ok(SourceCatalogSnapshot {
volume,
dirs,
files,
hardlinks,
dir_hardlinks,
})
}
}
enum ParsedRecord {
Folder(SourceDirSnapshot),
File {
file: SourceFileSnapshot,
data_fork: [u8; 80],
rsrc_fork: [u8; 80],
},
Hardlink(PendingHardlink),
DirHardlink(PendingDirHardlink),
Thread,
Skip,
}
struct PendingHardlink {
name: String,
name_utf16: Vec<u16>,
cnid: u32,
parent_cnid: u32,
inode_num: u32,
flags: u16,
finder_info: [u8; 16],
extended_finder_info: [u8; 16],
bsd_info: [u8; 16],
create_date: u32,
content_mod_date: u32,
attribute_mod_date: u32,
access_date: u32,
backup_date: u32,
text_encoding: u32,
data_fork_raw: [u8; 80],
rsrc_fork_raw: [u8; 80],
data_fork_size: u64,
rsrc_fork_size: u64,
}
struct PendingDirHardlink {
name: String,
name_utf16: Vec<u16>,
cnid: u32,
parent_cnid: u32,
inode_num: u32,
flags: u16,
finder_info: [u8; 16],
extended_finder_info: [u8; 16],
bsd_info: [u8; 16],
create_date: u32,
content_mod_date: u32,
attribute_mod_date: u32,
access_date: u32,
backup_date: u32,
text_encoding: u32,
data_fork_raw: [u8; 80],
rsrc_fork_raw: [u8; 80],
data_fork_size: u64,
rsrc_fork_size: u64,
}
fn hfsplus_dir_private_dir_name() -> String {
let mut s = String::with_capacity(28 + 1);
s.push_str(".HFS+ Private Directory Data");
s.push('\u{D}');
s
}
fn hfsplus_private_dir_name() -> String {
let mut s = String::with_capacity(4 + 17);
for _ in 0..4 {
s.push('\u{0}');
}
s.push_str("HFS+ Private Data");
s
}
fn parse_catalog_record(rec: &[u8]) -> Result<ParsedRecord, FilesystemError> {
if rec.len() < 4 {
return Ok(ParsedRecord::Skip);
}
let key_len = BigEndian::read_u16(&rec[0..2]) as usize;
if key_len < 6 || 2 + key_len > rec.len() {
return Ok(ParsedRecord::Skip);
}
let key = &rec[2..2 + key_len];
let parent_cnid = BigEndian::read_u32(&key[0..4]);
let name_length = BigEndian::read_u16(&key[4..6]) as usize;
if 6 + name_length * 2 > key.len() {
return Ok(ParsedRecord::Skip);
}
let name_bytes = &key[6..6 + name_length * 2];
let name_utf16: Vec<u16> = (0..name_length)
.map(|i| BigEndian::read_u16(&name_bytes[i * 2..i * 2 + 2]))
.collect();
let name = String::from_utf16(&name_utf16).unwrap_or_else(|_| {
name_utf16
.iter()
.map(|&u| char::from_u32(u as u32).unwrap_or('?'))
.collect()
});
let mut body_start = 2 + key_len;
if !body_start.is_multiple_of(2) {
body_start += 1;
}
if body_start + 2 > rec.len() {
return Ok(ParsedRecord::Skip);
}
let body = &rec[body_start..];
let record_type = BigEndian::read_i16(&body[0..2]);
match record_type {
CATALOG_FOLDER => {
if body.len() < 88 {
return Ok(ParsedRecord::Skip);
}
let flags = BigEndian::read_u16(&body[2..4]);
let valence = BigEndian::read_u32(&body[4..8]);
let cnid = BigEndian::read_u32(&body[8..12]);
let create_date = BigEndian::read_u32(&body[12..16]);
let content_mod_date = BigEndian::read_u32(&body[16..20]);
let attribute_mod_date = BigEndian::read_u32(&body[20..24]);
let access_date = BigEndian::read_u32(&body[24..28]);
let backup_date = BigEndian::read_u32(&body[28..32]);
let mut bsd_info = [0u8; 16];
bsd_info.copy_from_slice(&body[32..48]);
let mut finder_info = [0u8; 16];
finder_info.copy_from_slice(&body[48..64]);
let mut extended_finder_info = [0u8; 16];
extended_finder_info.copy_from_slice(&body[64..80]);
let text_encoding = BigEndian::read_u32(&body[80..84]);
Ok(ParsedRecord::Folder(SourceDirSnapshot {
name,
name_utf16,
cnid,
parent_cnid,
valence,
flags,
finder_info,
extended_finder_info,
bsd_info,
create_date,
content_mod_date,
attribute_mod_date,
access_date,
backup_date,
text_encoding,
xattrs: Vec::new(),
is_dir_inode: false,
inode_num: None,
}))
}
CATALOG_FILE => {
if body.len() < 248 {
return Ok(ParsedRecord::Skip);
}
let flags = BigEndian::read_u16(&body[2..4]);
let cnid = BigEndian::read_u32(&body[8..12]);
let create_date = BigEndian::read_u32(&body[12..16]);
let content_mod_date = BigEndian::read_u32(&body[16..20]);
let attribute_mod_date = BigEndian::read_u32(&body[20..24]);
let access_date = BigEndian::read_u32(&body[24..28]);
let backup_date = BigEndian::read_u32(&body[28..32]);
let mut bsd_info = [0u8; 16];
bsd_info.copy_from_slice(&body[32..48]);
let mut finder_info = [0u8; 16];
finder_info.copy_from_slice(&body[48..64]);
let mut extended_finder_info = [0u8; 16];
extended_finder_info.copy_from_slice(&body[64..80]);
let text_encoding = BigEndian::read_u32(&body[80..84]);
let mut data_fork = [0u8; 80];
data_fork.copy_from_slice(&body[88..168]);
let mut rsrc_fork = [0u8; 80];
rsrc_fork.copy_from_slice(&body[168..248]);
if &finder_info[0..4] == HARDLINK_FILE_TYPE
&& &finder_info[4..8] == HARDLINK_FILE_CREATOR
{
let inode_num = BigEndian::read_u32(&body[44..48]);
let data_fork_size = BigEndian::read_u64(&data_fork[0..8]);
let rsrc_fork_size = BigEndian::read_u64(&rsrc_fork[0..8]);
return Ok(ParsedRecord::Hardlink(PendingHardlink {
name,
name_utf16,
cnid,
parent_cnid,
inode_num,
flags,
finder_info,
extended_finder_info,
bsd_info,
create_date,
content_mod_date,
attribute_mod_date,
access_date,
backup_date,
text_encoding,
data_fork_raw: data_fork,
rsrc_fork_raw: rsrc_fork,
data_fork_size,
rsrc_fork_size,
}));
}
if &finder_info[0..4] == HARDLINK_DIR_TYPE && &finder_info[4..8] == HARDLINK_DIR_CREATOR
{
let inode_num = BigEndian::read_u32(&body[44..48]);
let data_fork_size = BigEndian::read_u64(&data_fork[0..8]);
let rsrc_fork_size = BigEndian::read_u64(&rsrc_fork[0..8]);
return Ok(ParsedRecord::DirHardlink(PendingDirHardlink {
name,
name_utf16,
cnid,
parent_cnid,
inode_num,
flags,
finder_info,
extended_finder_info,
bsd_info,
create_date,
content_mod_date,
attribute_mod_date,
access_date,
backup_date,
text_encoding,
data_fork_raw: data_fork,
rsrc_fork_raw: rsrc_fork,
data_fork_size,
rsrc_fork_size,
}));
}
Ok(ParsedRecord::File {
file: SourceFileSnapshot {
name,
name_utf16,
cnid,
parent_cnid,
flags,
finder_info,
extended_finder_info,
bsd_info,
create_date,
content_mod_date,
attribute_mod_date,
access_date,
backup_date,
text_encoding,
data_fork_raw: [0u8; 80],
rsrc_fork_raw: [0u8; 80],
data_fork_size: 0,
rsrc_fork_size: 0,
xattrs: Vec::new(),
is_inode: false,
inode_num: None,
},
data_fork,
rsrc_fork,
})
}
3 | 4 => Ok(ParsedRecord::Thread),
_ => Ok(ParsedRecord::Skip),
}
}
fn decode_fourcc(data: &[u8]) -> String {
data.iter()
.map(|&b| {
if b.is_ascii_graphic() || b == b' ' {
b as char
} else {
'.'
}
})
.collect()
}
#[derive(Debug, Default, Clone)]
pub struct HfsPlusCloneReport {
pub files_copied: u64,
pub dirs_copied: u64,
pub data_bytes_copied: u64,
pub rsrc_bytes_copied: u64,
pub xattrs_copied: u64,
pub hardlinks_copied: u64,
pub dir_hardlinks_copied: u64,
pub skipped: Vec<(String, String)>,
}
pub fn clone_hfsplus_volume<RS, RT>(
source: &mut HfsPlusFilesystem<RS>,
target: &mut HfsPlusFilesystem<RT>,
) -> Result<HfsPlusCloneReport, FilesystemError>
where
RS: Read + Seek + Send,
RT: Read + Write + Seek + Send,
{
let snapshot = SourceCatalogSnapshot::capture(source)?;
let mut report = HfsPlusCloneReport::default();
target.set_volume_dates(
snapshot.volume.create_date,
snapshot.volume.modify_date,
snapshot.volume.backup_date,
snapshot.volume.checked_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 || d.is_dir_inode {
continue;
}
if d.parent_cnid == 2
&& (d.name == hfsplus_private_dir_name() || d.name == hfsplus_dir_private_dir_name())
{
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 {
if f.is_inode {
continue;
}
files_by_parent.entry(f.parent_cnid).or_default().push(f);
}
let mut hardlinks_by_parent: HashMap<u32, Vec<&SourceHardlinkSnapshot>> = HashMap::new();
for h in &snapshot.hardlinks {
hardlinks_by_parent
.entry(h.parent_cnid)
.or_default()
.push(h);
}
let mut dir_hardlinks_by_parent: HashMap<u32, Vec<&SourceDirHardlinkSnapshot>> = HashMap::new();
for h in &snapshot.dir_hardlinks {
dir_hardlinks_by_parent
.entry(h.parent_cnid)
.or_default()
.push(h);
}
for f in snapshot.files.iter().filter(|f| f.is_inode) {
let inode_num = f.inode_num.expect("inode without inode_num");
let inode_entry = {
let mut data =
source.fork_stream_reader(f.cnid, HFSPLUS_FORK_DATA, &f.data_fork_raw)?;
target.create_hardlink_inode(
inode_num,
&mut data,
f.data_fork_size,
&CreateFileOptions::default(),
)?
};
if f.rsrc_fork_size > 0 {
let mut rsrc =
source.fork_stream_reader(f.cnid, HFSPLUS_FORK_RESOURCE, &f.rsrc_fork_raw)?;
target.write_resource_fork(&inode_entry, &mut rsrc, f.rsrc_fork_size)?;
}
report.data_bytes_copied += f.data_fork_size;
report.rsrc_bytes_copied += f.rsrc_fork_size;
stamp_file_metadata(target, &inode_entry, f, &mut report)?;
}
for d in snapshot.dirs.iter().filter(|d| d.is_dir_inode) {
let inode_num = d.inode_num.expect("dir-inode without inode_num");
let inode_entry =
target.create_dir_hardlink_inode(inode_num, &CreateDirectoryOptions::default())?;
stamp_dir_metadata(target, &inode_entry, d, &mut report)?;
}
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) {
stamp_dir_metadata(target, &target_root, root_src, &mut report)?;
}
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(),
)?;
stamp_dir_metadata(target, &new_dir, d, &mut report)?;
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 {
let new_file = {
let mut data =
source.fork_stream_reader(f.cnid, HFSPLUS_FORK_DATA, &f.data_fork_raw)?;
target.create_file(
&parent_entry,
&f.name,
&mut data,
f.data_fork_size,
&CreateFileOptions {
type_code: Some(f.type_code()),
creator_code: Some(f.creator_code()),
..Default::default()
},
)?
};
report.data_bytes_copied += f.data_fork_size;
if f.rsrc_fork_size > 0 {
let mut rsrc = source.fork_stream_reader(
f.cnid,
HFSPLUS_FORK_RESOURCE,
&f.rsrc_fork_raw,
)?;
target.write_resource_fork(&new_file, &mut rsrc, f.rsrc_fork_size)?;
report.rsrc_bytes_copied += f.rsrc_fork_size;
}
stamp_file_metadata(target, &new_file, f, &mut report)?;
cnid_map.insert(f.cnid, new_file.location as u32);
report.files_copied += 1;
}
}
if let Some(children) = hardlinks_by_parent.remove(&src_parent_cnid) {
for h in children {
let stub = target.create_hardlink(&parent_entry, &h.name, h.inode_num)?;
replay_xattrs(
target,
stub.location as u32,
&h.xattrs,
&h.name,
&mut report,
)?;
report.hardlinks_copied += 1;
}
}
if let Some(children) = dir_hardlinks_by_parent.remove(&src_parent_cnid) {
for h in children {
let stub = target.create_dir_hardlink(&parent_entry, &h.name, h.inode_num)?;
replay_xattrs(
target,
stub.location as u32,
&h.xattrs,
&h.name,
&mut report,
)?;
report.dir_hardlinks_copied += 1;
}
}
}
let known_source_cnids: HashSet<u32> = cnid_map.keys().copied().collect();
let mut new_finder_info = [0u32; 8];
for (i, slot) in new_finder_info.iter_mut().enumerate() {
if i == 6 || i == 7 {
continue;
}
let src_val = snapshot.volume.finder_info[i];
*slot = if known_source_cnids.contains(&src_val) {
*cnid_map.get(&src_val).unwrap()
} else {
src_val
};
}
target.set_volume_finder_info(new_finder_info);
target.sync_metadata()?;
if let Some(fsck) = target.fsck() {
let result = fsck?;
if !result.errors.is_empty() {
let summary: Vec<String> = result
.errors
.iter()
.take(5)
.map(|i| i.code.clone())
.collect();
return Err(FilesystemError::InvalidData(format!(
"clone target failed fsck: {} error(s); first: [{}]",
result.errors.len(),
summary.join(", "),
)));
}
}
Ok(report)
}
fn metadata_from_file(f: &SourceFileSnapshot) -> RecordMetadata {
RecordMetadata {
create_date: f.create_date,
content_mod_date: f.content_mod_date,
attribute_mod_date: f.attribute_mod_date,
access_date: f.access_date,
backup_date: f.backup_date,
bsd_info: f.bsd_info,
finder_info: f.finder_info,
extended_finder_info: f.extended_finder_info,
text_encoding: f.text_encoding,
}
}
fn metadata_from_dir(d: &SourceDirSnapshot) -> RecordMetadata {
RecordMetadata {
create_date: d.create_date,
content_mod_date: d.content_mod_date,
attribute_mod_date: d.attribute_mod_date,
access_date: d.access_date,
backup_date: d.backup_date,
bsd_info: d.bsd_info,
finder_info: d.finder_info,
extended_finder_info: d.extended_finder_info,
text_encoding: d.text_encoding,
}
}
fn stamp_file_metadata<RT>(
target: &mut HfsPlusFilesystem<RT>,
entry: &FileEntry,
f: &SourceFileSnapshot,
report: &mut HfsPlusCloneReport,
) -> Result<(), FilesystemError>
where
RT: Read + Write + Seek + Send,
{
let cnid = entry.location as u32;
target.set_record_metadata(cnid, &metadata_from_file(f))?;
replay_xattrs(target, cnid, &f.xattrs, &f.name, report)?;
Ok(())
}
fn stamp_dir_metadata<RT>(
target: &mut HfsPlusFilesystem<RT>,
entry: &FileEntry,
d: &SourceDirSnapshot,
report: &mut HfsPlusCloneReport,
) -> Result<(), FilesystemError>
where
RT: Read + Write + Seek + Send,
{
let cnid = entry.location as u32;
target.set_record_metadata(cnid, &metadata_from_dir(d))?;
replay_xattrs(target, cnid, &d.xattrs, &d.name, report)?;
Ok(())
}
fn replay_xattrs<RT>(
target: &mut HfsPlusFilesystem<RT>,
cnid: u32,
xattrs: &[XattrRecord],
label: &str,
report: &mut HfsPlusCloneReport,
) -> Result<(), FilesystemError>
where
RT: Read + Write + Seek + Send,
{
for rec in xattrs {
match &rec.kind {
XattrKind::Inline(value) => {
target.set_xattr(cnid, &rec.name, value)?;
report.xattrs_copied += 1;
}
XattrKind::Fork(_) | XattrKind::Extents(_) => {
report.skipped.push((
format!("{label}/xattr:{}", rec.name),
"fork-style xattr — target only supports inline payloads".into(),
));
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use super::*;
use crate::hfsplus::{create_blank_hfsplus, HfsPlusFilesystem};
#[test]
fn capture_blank_volume_only_has_root_dir() {
let mut img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "Empty", false);
let cursor = Cursor::new(&mut img);
let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("open blank");
let snap = SourceCatalogSnapshot::capture(&mut fs).expect("capture");
assert_eq!(snap.volume.label, "Empty");
assert_eq!(snap.volume.block_size, 4096);
let root = snap
.dirs
.iter()
.find(|d| d.parent_cnid == 1)
.expect("root dir");
assert_eq!(root.cnid, 2);
assert_eq!(root.name, "Empty");
let user_dirs: Vec<_> = snap.dirs.iter().filter(|d| d.parent_cnid != 1).collect();
assert!(
user_dirs.is_empty(),
"blank should have no user dirs, got: {:?}",
user_dirs.iter().map(|d| &d.name).collect::<Vec<_>>()
);
}
#[test]
fn capture_walks_dirs_and_files_and_streams_forks() {
let mut img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "Work", false);
{
let cursor = Cursor::new(&mut img);
let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("open blank");
fs.prepare_for_edit().expect("prepare_for_edit");
let root = fs.root().expect("root entry");
let _docs = fs
.create_directory(&root, "Docs", &CreateDirectoryOptions::default())
.expect("create dir");
let payload: &[u8] = b"hello world\n";
let mut data = std::io::Cursor::new(payload);
fs.create_file(
&root,
"note.txt",
&mut data,
payload.len() as u64,
&CreateFileOptions::default(),
)
.expect("create file");
fs.sync_metadata().expect("sync");
}
let cursor = Cursor::new(&mut img);
let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("re-open");
let snap = SourceCatalogSnapshot::capture(&mut fs).expect("capture");
assert_eq!(snap.volume.label, "Work");
let user_dirs: Vec<&SourceDirSnapshot> =
snap.dirs.iter().filter(|d| d.parent_cnid != 1).collect();
assert_eq!(user_dirs.len(), 1, "expected one user directory");
let docs = user_dirs[0];
assert_eq!(docs.name, "Docs");
assert_eq!(docs.parent_cnid, 2);
assert_eq!(snap.files.len(), 1, "expected one user file");
let note = &snap.files[0];
assert_eq!(note.name, "note.txt");
assert_eq!(note.parent_cnid, 2);
assert_eq!(note.data_fork_size, b"hello world\n".len() as u64);
assert_eq!(note.rsrc_fork_size, 0);
let cnid = note.cnid;
let raw = note.data_fork_raw;
let mut buf = Vec::new();
fs.fork_stream_reader(cnid, 0x00, &raw)
.expect("fork reader")
.read_to_end(&mut buf)
.expect("read fork");
assert_eq!(buf, b"hello world\n");
}
#[test]
fn capture_no_xattrs_yields_empty_vec() {
let mut img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "Plain", false);
{
let cursor = Cursor::new(&mut img);
let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("open");
fs.prepare_for_edit().expect("prepare_for_edit");
let root = fs.root().expect("root");
let mut data = std::io::Cursor::new(&b"x"[..]);
fs.create_file(&root, "x.txt", &mut data, 1, &CreateFileOptions::default())
.expect("create");
fs.sync_metadata().expect("sync");
}
let cursor = Cursor::new(&mut img);
let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("re-open");
let snap = SourceCatalogSnapshot::capture(&mut fs).expect("capture");
for f in &snap.files {
assert!(f.xattrs.is_empty(), "file {} has xattrs", f.name);
}
for d in &snap.dirs {
assert!(d.xattrs.is_empty(), "dir {} has xattrs", d.name);
}
}
#[test]
fn clone_round_trip_basic_tree() {
let mut src_img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "Source", false);
let payload: &[u8] = b"contents of note.txt\n";
let rsrc: &[u8] = b"resource fork bytes";
let xval = b"xattr-payload";
{
let cursor = Cursor::new(&mut src_img);
let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("open src");
fs.prepare_for_edit().expect("prepare src");
let root = fs.root().expect("root");
let docs = fs
.create_directory(&root, "Docs", &CreateDirectoryOptions::default())
.expect("mkdir Docs");
let mut data = Cursor::new(payload);
let note = fs
.create_file(
&docs,
"note.txt",
&mut data,
payload.len() as u64,
&CreateFileOptions::default(),
)
.expect("create note");
let mut rcur = Cursor::new(rsrc);
fs.write_resource_fork(¬e, &mut rcur, rsrc.len() as u64)
.expect("write rsrc");
let _ = xval;
fs.sync_metadata().expect("sync src");
}
let mut tgt_img = create_blank_hfsplus(32 * 1024 * 1024, 4096, "Target", false);
let report = {
let src_cur = Cursor::new(&mut src_img);
let mut src_fs = HfsPlusFilesystem::open(src_cur, 0).expect("open src for clone");
let tgt_cur = Cursor::new(&mut tgt_img);
let mut tgt_fs = HfsPlusFilesystem::open(tgt_cur, 0).expect("open tgt");
tgt_fs.prepare_for_edit().expect("prepare tgt");
clone_hfsplus_volume(&mut src_fs, &mut tgt_fs).expect("clone")
};
assert_eq!(report.dirs_copied, 1, "expected 1 dir copied");
assert_eq!(report.files_copied, 1, "expected 1 file copied");
assert_eq!(report.data_bytes_copied, payload.len() as u64);
assert_eq!(report.rsrc_bytes_copied, rsrc.len() as u64);
let cursor = Cursor::new(&mut tgt_img);
let mut fs = HfsPlusFilesystem::open(cursor, 0).expect("re-open tgt");
let root = fs.root().expect("tgt root");
let children = fs.list_directory(&root).expect("list root");
let docs = children
.iter()
.find(|e| e.name == "Docs" && e.is_directory())
.expect("Docs in tgt");
let docs_children = fs.list_directory(docs).expect("list Docs");
let note = docs_children
.iter()
.find(|e| e.name == "note.txt")
.expect("note in tgt");
let read_data = fs.read_file(note, payload.len()).expect("read data");
assert_eq!(read_data, payload);
let mut rsrc_buf: Vec<u8> = Vec::new();
fs.write_resource_fork_to(note, &mut rsrc_buf)
.expect("read rsrc");
assert_eq!(rsrc_buf, rsrc);
let result = fs.fsck().expect("fsck supported").expect("fsck ok");
let codes: Vec<&str> = result.errors.iter().map(|e| e.code.as_str()).collect();
assert!(result.is_clean(), "tgt fsck dirty: {codes:?}");
}
}