use std::io::Read;
use crate::Result;
use crate::block::BlockDevice;
use super::btree::{BTNODE_FIXED_KV_SIZE, BTNODE_LEAF, BTNODE_ROOT, BTREE_INFO_SIZE};
use super::checksum::fletcher64;
use super::jrec::{
APFS_TYPE_DIR_REC, APFS_TYPE_DSTREAM_ID, APFS_TYPE_FILE_EXTENT, APFS_TYPE_INODE,
APFS_TYPE_XATTR, DT_DIR, DT_LNK, DT_REG, INO_EXT_TYPE_DSTREAM, J_INODE_VAL_FIXED_SIZE,
OBJ_ID_MASK, OBJ_TYPE_SHIFT,
};
use super::obj::{
OBJECT_TYPE_BTREE, OBJECT_TYPE_BTREE_NODE, OBJECT_TYPE_CHECKPOINT_MAP, OBJECT_TYPE_FS,
OBJECT_TYPE_FSTREE, OBJECT_TYPE_NX_SUPERBLOCK, OBJECT_TYPE_OMAP,
};
use super::superblock::{APFS_MAGIC, NX_MAGIC, NX_MAX_FILE_SYSTEMS};
const OBJECT_TYPE_SPACEMAN: u32 = 0x0000_0005;
const OBJ_PHYSICAL: u32 = 0x4000_0000;
const OBJ_EPHEMERAL: u32 = 0x8000_0000;
const COPY_BUF: usize = 64 * 1024;
const WRITE_XID: u64 = 2;
const DSTREAM_ID_SHARES_INODE: bool = true;
const XATTR_DATA_EMBEDDED: u16 = 0x0002;
pub const APFS_XATTR_MAX_EMBEDDED_SIZE: usize = 3804;
const FS_LEAF_VID_BASE: u64 = 0x1_0000;
#[derive(Clone)]
struct FsRecord {
key: Vec<u8>,
val: Vec<u8>,
}
fn fs_record_sort_key(rec: &FsRecord) -> (u64, u8, Vec<u8>) {
let hdr = u64::from_le_bytes(rec.key[0..8].try_into().unwrap());
let oid = hdr & OBJ_ID_MASK;
let kind = (hdr >> OBJ_TYPE_SHIFT) as u8;
let tail = if rec.key.len() > 8 {
rec.key[8..].to_vec()
} else {
Vec::new()
};
(oid, kind, tail)
}
pub struct ApfsWriter<'a> {
dev: &'a mut dyn BlockDevice,
block_size: u32,
total_blocks: u64,
next_block: u64,
bump_block_start: u64,
volume_name: String,
container_uuid: [u8; 16],
volume_uuid: [u8; 16],
records: Vec<FsRecord>,
next_oid: u64,
num_files: u64,
num_directories: u64,
num_symlinks: u64,
finished: bool,
}
impl<'a> ApfsWriter<'a> {
pub fn new(
dev: &'a mut dyn BlockDevice,
total_blocks: u64,
block_size: u32,
volume_name: &str,
) -> Result<Self> {
if !(512..=65_536).contains(&block_size) || !block_size.is_power_of_two() {
return Err(crate::Error::InvalidArgument(format!(
"apfs writer: block_size {block_size} is not a sensible power of two"
)));
}
let needed = total_blocks.checked_mul(block_size as u64).ok_or_else(|| {
crate::Error::InvalidArgument("apfs writer: total_blocks * block_size overflows".into())
})?;
if needed > dev.total_size() {
return Err(crate::Error::InvalidArgument(format!(
"apfs writer: image needs {} bytes but dev is {} bytes",
needed,
dev.total_size()
)));
}
if total_blocks < 32 {
return Err(crate::Error::InvalidArgument(
"apfs writer: need at least 32 blocks".into(),
));
}
let container_uuid = derive_uuid(volume_name.as_bytes(), b"container");
let volume_uuid = derive_uuid(volume_name.as_bytes(), b"volume");
let bump_block_start: u64 = 7;
let mut w = Self {
dev,
block_size,
total_blocks,
next_block: bump_block_start,
bump_block_start,
volume_name: volume_name.to_string(),
container_uuid,
volume_uuid,
records: Vec::new(),
next_oid: 16, num_files: 0,
num_directories: 0,
num_symlinks: 0,
finished: false,
};
w.add_inode_record(2, 0, mode_dir(0o755), 0)?;
Ok(w)
}
pub fn add_dir(&mut self, parent_oid: u64, name: &str, mode: u16) -> Result<u64> {
let oid = self.alloc_oid();
self.add_drec(parent_oid, name, oid, DT_DIR)?;
self.add_inode_record(oid, parent_oid, mode_dir(mode), 0)?;
self.num_directories += 1;
Ok(oid)
}
pub fn add_symlink(
&mut self,
parent_oid: u64,
name: &str,
mode: u16,
target: &str,
) -> Result<u64> {
let oid = self.alloc_oid();
self.add_drec(parent_oid, name, oid, DT_LNK)?;
let target_bytes = target.as_bytes();
let extent_paddr = self.allocate_extent_for_size(target_bytes.len() as u64)?;
let extent_len_blocks = self.bytes_to_blocks(target_bytes.len() as u64);
let mut block = vec![0u8; self.block_size as usize];
for i in 0..extent_len_blocks {
let off = (i as usize) * self.block_size as usize;
let end = (off + self.block_size as usize).min(target_bytes.len());
if off < target_bytes.len() {
block.fill(0);
let chunk = &target_bytes[off..end];
block[..chunk.len()].copy_from_slice(chunk);
self.write_block(extent_paddr + i, &block)?;
}
}
self.add_file_extent(oid, 0, target_bytes.len() as u64, extent_paddr)?;
self.add_inode_record(oid, parent_oid, mode_lnk(mode), target_bytes.len() as u64)?;
self.num_symlinks += 1;
Ok(oid)
}
pub fn add_file_from_reader<R: Read>(
&mut self,
parent_oid: u64,
name: &str,
mode: u16,
reader: &mut R,
size: u64,
) -> Result<u64> {
let oid = self.alloc_oid();
self.add_drec(parent_oid, name, oid, DT_REG)?;
if size == 0 {
self.add_inode_record(oid, parent_oid, mode_reg(mode), 0)?;
self.num_files += 1;
return Ok(oid);
}
let extent_paddr = self.allocate_extent_for_size(size)?;
let blocks = self.bytes_to_blocks(size);
let mut block_buf = vec![0u8; self.block_size as usize];
let mut scratch = vec![0u8; COPY_BUF];
let mut bytes_left = size;
let mut block_idx: u64 = 0;
let mut block_off: usize = 0;
block_buf.fill(0);
while bytes_left > 0 {
let want = scratch.len().min(bytes_left as usize);
let mut got = 0;
while got < want {
let n = reader
.read(&mut scratch[got..want])
.map_err(crate::Error::Io)?;
if n == 0 {
break;
}
got += n;
}
if got == 0 {
break;
}
let mut consumed = 0;
while consumed < got {
let space = self.block_size as usize - block_off;
let n = space.min(got - consumed);
block_buf[block_off..block_off + n]
.copy_from_slice(&scratch[consumed..consumed + n]);
block_off += n;
consumed += n;
bytes_left -= n as u64;
if block_off == self.block_size as usize {
self.write_block(extent_paddr + block_idx, &block_buf)?;
block_idx += 1;
block_off = 0;
block_buf.fill(0);
}
}
}
if block_off > 0 {
for b in &mut block_buf[block_off..] {
*b = 0;
}
self.write_block(extent_paddr + block_idx, &block_buf)?;
block_idx += 1;
}
while block_idx < blocks {
block_buf.fill(0);
self.write_block(extent_paddr + block_idx, &block_buf)?;
block_idx += 1;
}
self.add_file_extent(oid, 0, size, extent_paddr)?;
self.add_inode_record(oid, parent_oid, mode_reg(mode), size)?;
self.num_files += 1;
Ok(oid)
}
pub fn add_xattr(&mut self, parent_oid: u64, name: &str, value: &[u8]) -> Result<()> {
if value.len() > APFS_XATTR_MAX_EMBEDDED_SIZE {
return Err(crate::Error::Unsupported(format!(
"apfs writer: xattr value of {} bytes exceeds embedded limit ({}); \
dstream xattrs are not supported",
value.len(),
APFS_XATTR_MAX_EMBEDDED_SIZE
)));
}
let name_bytes = name.as_bytes();
let nlen = name_bytes.len() + 1; if nlen > u16::MAX as usize {
return Err(crate::Error::InvalidArgument(
"apfs writer: xattr name too long".into(),
));
}
let mut key = Vec::with_capacity(10 + nlen);
let hdr = ((APFS_TYPE_XATTR as u64) << OBJ_TYPE_SHIFT) | (parent_oid & OBJ_ID_MASK);
key.extend_from_slice(&hdr.to_le_bytes());
key.extend_from_slice(&(nlen as u16).to_le_bytes());
key.extend_from_slice(name_bytes);
key.push(0);
let mut val = Vec::with_capacity(4 + value.len());
val.extend_from_slice(&XATTR_DATA_EMBEDDED.to_le_bytes());
val.extend_from_slice(&(value.len() as u16).to_le_bytes());
val.extend_from_slice(value);
self.records.push(FsRecord { key, val });
Ok(())
}
pub fn finish(mut self) -> Result<()> {
if self.finished {
return Err(crate::Error::Unsupported(
"apfs writer: finish() called twice".into(),
));
}
self.finished = true;
self.records.sort_by(|a, b| {
let ka = fs_record_sort_key(a);
let kb = fs_record_sort_key(b);
ka.cmp(&kb)
});
let bs = self.block_size as usize;
let nxsb_label_paddr: u64 = 0;
let chkmap_paddr: u64 = 1;
let nxsb_live_paddr: u64 = 2;
let spaceman_paddr: u64 = 3;
let cont_omap_paddr: u64 = 4;
let apsb_paddr: u64 = 5;
let vol_omap_paddr: u64 = 6;
let volume_vid: u64 = 1024;
let spaceman_vid: u64 = 512;
let reaper_vid: u64 = 513;
let fsroot_vid: u64 = 2;
let leaf_payload_cap = leaf_payload_capacity(bs);
let leaves = pack_records_into_leaves(&self.records, leaf_payload_cap)?;
let fs_leaf_paddrs: Vec<u64> = (0..leaves.len())
.map(|_| self.alloc_block())
.collect::<Result<Vec<_>>>()?;
let (fsroot_paddr, vol_omap_entries) = if leaves.len() <= 1 {
let leaf_paddr = fs_leaf_paddrs.first().copied().unwrap_or(0);
(leaf_paddr, vec![(fsroot_vid, WRITE_XID, leaf_paddr)])
} else {
let mut entries = Vec::with_capacity(leaves.len() + 1);
let mut child_vids = Vec::with_capacity(leaves.len());
for (i, &paddr) in fs_leaf_paddrs.iter().enumerate() {
let vid = FS_LEAF_VID_BASE + i as u64;
entries.push((vid, WRITE_XID, paddr));
child_vids.push(vid);
}
let root_paddr = self.alloc_block()?;
entries.push((fsroot_vid, WRITE_XID, root_paddr));
entries.sort_by_key(|e| e.0);
(root_paddr, entries)
};
for (i, leaf_records) in leaves.iter().enumerate() {
let is_root = leaves.len() == 1;
let vid = if is_root {
fsroot_vid
} else {
FS_LEAF_VID_BASE + i as u64
};
let leaf_block = build_fs_leaf(leaf_records, bs, vid, is_root)?;
self.write_block(fs_leaf_paddrs[i], &leaf_block)?;
}
if leaves.len() > 1 {
let mut sep_entries: Vec<(Vec<u8>, u64)> = Vec::with_capacity(leaves.len());
for (i, leaf) in leaves.iter().enumerate() {
let sep_key = leaf[0].key.clone();
let vid = FS_LEAF_VID_BASE + i as u64;
sep_entries.push((sep_key, vid));
}
let internal_block = build_fs_internal_root(&sep_entries, bs, fsroot_vid)?;
self.write_block(fsroot_paddr, &internal_block)?;
}
let vol_omap_root_paddr = self.write_omap_tree(&vol_omap_entries)?;
let vol_omap_phys = build_omap_phys(bs, vol_omap_paddr, vol_omap_root_paddr)?;
self.write_block(vol_omap_paddr, &vol_omap_phys)?;
let apsb_block = self.build_apsb(bs, apsb_paddr, vol_omap_paddr, fsroot_vid)?;
self.write_block(apsb_paddr, &apsb_block)?;
let spaceman_block = build_spaceman_stub(bs, spaceman_vid)?;
self.write_block(spaceman_paddr, &spaceman_block)?;
let cont_omap_root_paddr = self.write_omap_tree(&[(volume_vid, WRITE_XID, apsb_paddr)])?;
let cont_omap_phys = build_omap_phys(bs, cont_omap_paddr, cont_omap_root_paddr)?;
self.write_block(cont_omap_paddr, &cont_omap_phys)?;
let chkmap = build_chkmap_stub(bs)?;
self.write_block(chkmap_paddr, &chkmap)?;
let nxsb = self.build_nxsb(
bs,
nxsb_live_paddr,
cont_omap_paddr,
spaceman_vid,
reaper_vid,
volume_vid,
)?;
self.write_block(nxsb_live_paddr, &nxsb)?;
let nxsb_label = self.build_nxsb(
bs,
nxsb_label_paddr,
cont_omap_paddr,
spaceman_vid,
reaper_vid,
volume_vid,
)?;
self.write_block(nxsb_label_paddr, &nxsb_label)?;
let _ = fsroot_paddr;
Ok(())
}
fn alloc_oid(&mut self) -> u64 {
let o = self.next_oid;
self.next_oid = self.next_oid.checked_add(1).unwrap_or(o);
o
}
fn bytes_to_blocks(&self, n: u64) -> u64 {
let bs = self.block_size as u64;
n.div_ceil(bs).max(1)
}
fn alloc_block(&mut self) -> Result<u64> {
if self.next_block >= self.total_blocks {
return Err(crate::Error::InvalidArgument(format!(
"apfs writer: image full at {} blocks",
self.total_blocks
)));
}
let p = self.next_block;
self.next_block += 1;
Ok(p)
}
fn allocate_extent_for_size(&mut self, size: u64) -> Result<u64> {
if size == 0 {
return Err(crate::Error::InvalidArgument(
"apfs writer: cannot allocate a zero-length extent".into(),
));
}
let blocks = self.bytes_to_blocks(size);
let start = self.next_block;
let end = start
.checked_add(blocks)
.ok_or_else(|| crate::Error::InvalidArgument("apfs writer: extent oob".into()))?;
if end > self.total_blocks {
return Err(crate::Error::InvalidArgument(format!(
"apfs writer: extent of {blocks} blocks past end of image"
)));
}
self.next_block = end;
Ok(start)
}
fn write_block(&mut self, paddr: u64, buf: &[u8]) -> Result<()> {
let off = paddr.saturating_mul(self.block_size as u64);
self.dev.write_at(off, buf)
}
fn add_drec(&mut self, parent_oid: u64, name: &str, target_oid: u64, dtype: u16) -> Result<()> {
let nlen = name.len() + 1; if nlen > u16::MAX as usize {
return Err(crate::Error::InvalidArgument(
"apfs writer: directory entry name too long".into(),
));
}
let mut key = Vec::with_capacity(10 + nlen);
let hdr = ((APFS_TYPE_DIR_REC as u64) << OBJ_TYPE_SHIFT) | (parent_oid & OBJ_ID_MASK);
key.extend_from_slice(&hdr.to_le_bytes());
key.extend_from_slice(&(nlen as u16).to_le_bytes());
key.extend_from_slice(name.as_bytes());
key.push(0);
let mut val = vec![0u8; 18];
val[0..8].copy_from_slice(&target_oid.to_le_bytes());
val[16..18].copy_from_slice(&dtype.to_le_bytes());
self.records.push(FsRecord { key, val });
Ok(())
}
fn add_inode_record(
&mut self,
oid: u64,
parent_oid: u64,
mode: u16,
dstream_size: u64,
) -> Result<()> {
let has_dstream =
dstream_size > 0 || (mode & 0o170_000 == 0o100_000) || (mode & 0o170_000 == 0o120_000);
let mut val = vec![0u8; J_INODE_VAL_FIXED_SIZE];
val[0..8].copy_from_slice(&parent_oid.to_le_bytes());
let private_id = if has_dstream && DSTREAM_ID_SHARES_INODE {
oid
} else {
0
};
val[8..16].copy_from_slice(&private_id.to_le_bytes());
let nlink: i32 = if mode & 0o170_000 == 0o040_000 { 2 } else { 1 };
val[56..60].copy_from_slice(&nlink.to_le_bytes());
val[80..82].copy_from_slice(&mode.to_le_bytes());
val[84..92].copy_from_slice(&dstream_size.to_le_bytes());
if has_dstream {
let mut xfields = Vec::new();
xfields.extend_from_slice(&1u16.to_le_bytes()); xfields.extend_from_slice(&40u16.to_le_bytes());
xfields.push(INO_EXT_TYPE_DSTREAM);
xfields.push(0);
xfields.extend_from_slice(&40u16.to_le_bytes());
let mut ds = [0u8; 40];
ds[0..8].copy_from_slice(&dstream_size.to_le_bytes());
let bs = self.block_size as u64;
let alloc = dstream_size.div_ceil(bs) * bs;
ds[8..16].copy_from_slice(&alloc.to_le_bytes());
xfields.extend_from_slice(&ds);
val.extend_from_slice(&xfields);
}
let mut key = vec![0u8; 8];
let hdr = ((APFS_TYPE_INODE as u64) << OBJ_TYPE_SHIFT) | (oid & OBJ_ID_MASK);
key.copy_from_slice(&hdr.to_le_bytes());
self.records.push(FsRecord { key, val });
Ok(())
}
fn add_file_extent(
&mut self,
dstream_oid: u64,
logical_addr: u64,
length: u64,
phys_block: u64,
) -> Result<()> {
let mut key = vec![0u8; 16];
let hdr = ((APFS_TYPE_FILE_EXTENT as u64) << OBJ_TYPE_SHIFT) | (dstream_oid & OBJ_ID_MASK);
key[0..8].copy_from_slice(&hdr.to_le_bytes());
key[8..16].copy_from_slice(&logical_addr.to_le_bytes());
let mut val = vec![0u8; 24];
let bs = self.block_size as u64;
let alloc = length.div_ceil(bs) * bs;
val[0..8].copy_from_slice(&alloc.to_le_bytes()); val[8..16].copy_from_slice(&phys_block.to_le_bytes());
self.records.push(FsRecord { key, val });
let mut dkey = vec![0u8; 8];
let dhdr = ((APFS_TYPE_DSTREAM_ID as u64) << OBJ_TYPE_SHIFT) | (dstream_oid & OBJ_ID_MASK);
dkey.copy_from_slice(&dhdr.to_le_bytes());
let dval = vec![0u8; 4]; self.records.push(FsRecord {
key: dkey,
val: dval,
});
Ok(())
}
fn write_omap_tree(&mut self, entries: &[(u64, u64, u64)]) -> Result<u64> {
let bs = self.block_size as usize;
let leaves = pack_omap_into_leaves(entries, omap_leaf_capacity(bs))?;
if leaves.len() == 1 {
let paddr = self.alloc_block()?;
let block = build_omap_leaf_node(bs, &leaves[0], true)?;
self.write_block(paddr, &block)?;
return Ok(paddr);
}
let mut leaf_paddrs: Vec<u64> = Vec::with_capacity(leaves.len());
let mut sep_entries: Vec<((u64, u64), u64)> = Vec::with_capacity(leaves.len());
for chunk in &leaves {
let paddr = self.alloc_block()?;
let block = build_omap_leaf_node(bs, chunk, false)?;
self.write_block(paddr, &block)?;
leaf_paddrs.push(paddr);
sep_entries.push(((chunk[0].0, chunk[0].1), paddr));
}
let root_paddr = self.alloc_block()?;
let root_block = build_omap_internal_root(bs, &sep_entries)?;
self.write_block(root_paddr, &root_block)?;
Ok(root_paddr)
}
fn build_apsb(
&self,
bs: usize,
apsb_paddr: u64,
vol_omap_paddr: u64,
fsroot_vid: u64,
) -> Result<Vec<u8>> {
let mut buf = vec![0u8; bs];
buf[8..16].copy_from_slice(&apsb_paddr.to_le_bytes()); buf[16..24].copy_from_slice(&WRITE_XID.to_le_bytes());
buf[24..28].copy_from_slice(&OBJECT_TYPE_FS.to_le_bytes());
buf[32..36].copy_from_slice(&APFS_MAGIC.to_le_bytes());
buf[36..40].copy_from_slice(&0u32.to_le_bytes()); buf[116..120].copy_from_slice(&(OBJECT_TYPE_BTREE).to_le_bytes());
buf[120..124].copy_from_slice(&(OBJECT_TYPE_BTREE).to_le_bytes());
buf[124..128].copy_from_slice(&(OBJECT_TYPE_BTREE).to_le_bytes());
buf[128..136].copy_from_slice(&vol_omap_paddr.to_le_bytes()); buf[136..144].copy_from_slice(&fsroot_vid.to_le_bytes()); buf[144..152].copy_from_slice(&0u64.to_le_bytes()); buf[152..160].copy_from_slice(&0u64.to_le_bytes()); buf[160..168].copy_from_slice(&0u64.to_le_bytes()); buf[168..176].copy_from_slice(&0u64.to_le_bytes()); buf[176..184].copy_from_slice(&self.next_oid.to_le_bytes()); buf[184..192].copy_from_slice(&self.num_files.to_le_bytes());
buf[192..200].copy_from_slice(&self.num_directories.to_le_bytes());
buf[200..208].copy_from_slice(&self.num_symlinks.to_le_bytes());
buf[240..256].copy_from_slice(&self.volume_uuid);
const APFS_FS_UNENCRYPTED: u64 = 0x0000_0001;
buf[264..272].copy_from_slice(&APFS_FS_UNENCRYPTED.to_le_bytes());
let name_bytes = self.volume_name.as_bytes();
let n = name_bytes.len().min(255);
buf[704..704 + n].copy_from_slice(&name_bytes[..n]);
buf[704 + n] = 0;
sign_block(&mut buf);
Ok(buf)
}
fn build_nxsb(
&self,
bs: usize,
paddr: u64,
cont_omap_paddr: u64,
spaceman_vid: u64,
reaper_vid: u64,
volume_vid: u64,
) -> Result<Vec<u8>> {
let mut buf = vec![0u8; bs];
buf[8..16].copy_from_slice(&paddr.to_le_bytes()); buf[16..24].copy_from_slice(&WRITE_XID.to_le_bytes());
buf[24..28].copy_from_slice(&(OBJECT_TYPE_NX_SUPERBLOCK | OBJ_EPHEMERAL).to_le_bytes());
buf[32..36].copy_from_slice(&NX_MAGIC.to_le_bytes());
buf[36..40].copy_from_slice(&self.block_size.to_le_bytes());
buf[40..48].copy_from_slice(&self.total_blocks.to_le_bytes());
buf[72..88].copy_from_slice(&self.container_uuid);
buf[88..96].copy_from_slice(&(self.next_oid + 1024).to_le_bytes()); buf[96..104].copy_from_slice(&(WRITE_XID + 1).to_le_bytes()); buf[104..108].copy_from_slice(&2u32.to_le_bytes()); buf[108..112].copy_from_slice(&1u32.to_le_bytes()); buf[112..120].copy_from_slice(&1u64.to_le_bytes()); buf[120..128].copy_from_slice(&3u64.to_le_bytes()); buf[128..132].copy_from_slice(&2u32.to_le_bytes()); buf[132..136].copy_from_slice(&1u32.to_le_bytes()); buf[136..140].copy_from_slice(&0u32.to_le_bytes()); buf[140..144].copy_from_slice(&2u32.to_le_bytes()); buf[144..148].copy_from_slice(&0u32.to_le_bytes()); buf[148..152].copy_from_slice(&0u32.to_le_bytes()); buf[152..160].copy_from_slice(&spaceman_vid.to_le_bytes());
buf[160..168].copy_from_slice(&cont_omap_paddr.to_le_bytes()); buf[168..176].copy_from_slice(&reaper_vid.to_le_bytes()); buf[176..180].copy_from_slice(&0u32.to_le_bytes()); buf[180..184].copy_from_slice(&(NX_MAX_FILE_SYSTEMS as u32).to_le_bytes());
buf[184..192].copy_from_slice(&volume_vid.to_le_bytes());
let _ = self.bump_block_start;
sign_block(&mut buf);
Ok(buf)
}
}
fn sign_block(buf: &mut [u8]) {
let cksum = fletcher64(buf);
buf[0..8].copy_from_slice(&cksum.to_le_bytes());
}
fn build_omap_phys(bs: usize, paddr: u64, tree_paddr: u64) -> Result<Vec<u8>> {
let mut buf = vec![0u8; bs];
buf[8..16].copy_from_slice(&paddr.to_le_bytes()); buf[16..24].copy_from_slice(&WRITE_XID.to_le_bytes()); buf[24..28].copy_from_slice(&(OBJECT_TYPE_OMAP | OBJ_PHYSICAL).to_le_bytes());
buf[40..44].copy_from_slice(&(OBJECT_TYPE_BTREE | OBJ_PHYSICAL).to_le_bytes()); buf[48..56].copy_from_slice(&tree_paddr.to_le_bytes());
sign_block(&mut buf);
Ok(buf)
}
fn omap_leaf_capacity(bs: usize) -> usize {
bs - 56 - BTREE_INFO_SIZE
}
fn pack_omap_into_leaves(
entries: &[(u64, u64, u64)],
cap: usize,
) -> Result<Vec<Vec<(u64, u64, u64)>>> {
let per = 4 + 16 + 16;
let max_per_leaf = cap / per;
if max_per_leaf == 0 {
return Err(crate::Error::Unsupported(
"apfs writer: block too small to fit any omap entry".into(),
));
}
if entries.is_empty() {
return Ok(vec![Vec::new()]);
}
let mut out: Vec<Vec<(u64, u64, u64)>> = Vec::new();
for chunk in entries.chunks(max_per_leaf) {
out.push(chunk.to_vec());
}
Ok(out)
}
fn build_omap_leaf_node(bs: usize, entries: &[(u64, u64, u64)], is_root: bool) -> Result<Vec<u8>> {
let mut block = vec![0u8; bs];
let obj_type = if is_root {
OBJECT_TYPE_BTREE | OBJ_PHYSICAL
} else {
OBJECT_TYPE_BTREE_NODE | OBJ_PHYSICAL
};
block[16..24].copy_from_slice(&WRITE_XID.to_le_bytes());
block[24..28].copy_from_slice(&obj_type.to_le_bytes());
let mut flags = BTNODE_LEAF | BTNODE_FIXED_KV_SIZE;
if is_root {
flags |= BTNODE_ROOT;
}
block[32..34].copy_from_slice(&flags.to_le_bytes());
block[34..36].copy_from_slice(&0u16.to_le_bytes()); block[36..40].copy_from_slice(&(entries.len() as u32).to_le_bytes());
let toc_len = entries.len() * 4;
block[40..42].copy_from_slice(&0u16.to_le_bytes());
block[42..44].copy_from_slice(&(toc_len as u16).to_le_bytes());
let toc_base = 56;
let keys_start = toc_base + toc_len;
let vals_end = if is_root { bs - BTREE_INFO_SIZE } else { bs };
if entries.len() * 32 + toc_len + 56 > vals_end {
return Err(crate::Error::Unsupported(
"apfs writer: omap leaf overflowed single block".into(),
));
}
for (i, &(oid, xid, paddr)) in entries.iter().enumerate() {
let k_off = (i * 16) as u16;
let v_off = ((i + 1) * 16) as u16;
block[toc_base + i * 4..toc_base + i * 4 + 2].copy_from_slice(&k_off.to_le_bytes());
block[toc_base + i * 4 + 2..toc_base + i * 4 + 4].copy_from_slice(&v_off.to_le_bytes());
let ks = keys_start + k_off as usize;
block[ks..ks + 8].copy_from_slice(&oid.to_le_bytes());
block[ks + 8..ks + 16].copy_from_slice(&xid.to_le_bytes());
let vs = vals_end - v_off as usize;
block[vs + 8..vs + 16].copy_from_slice(&paddr.to_le_bytes());
}
if is_root {
let info_off = bs - BTREE_INFO_SIZE;
block[info_off + 8..info_off + 12].copy_from_slice(&16u32.to_le_bytes());
block[info_off + 12..info_off + 16].copy_from_slice(&16u32.to_le_bytes());
}
sign_block(&mut block);
Ok(block)
}
fn build_omap_internal_root(bs: usize, entries: &[((u64, u64), u64)]) -> Result<Vec<u8>> {
let mut block = vec![0u8; bs];
block[16..24].copy_from_slice(&WRITE_XID.to_le_bytes());
block[24..28].copy_from_slice(&(OBJECT_TYPE_BTREE | OBJ_PHYSICAL).to_le_bytes());
let flags = BTNODE_ROOT | BTNODE_FIXED_KV_SIZE;
block[32..34].copy_from_slice(&flags.to_le_bytes());
block[34..36].copy_from_slice(&1u16.to_le_bytes()); block[36..40].copy_from_slice(&(entries.len() as u32).to_le_bytes());
let toc_len = entries.len() * 4;
block[40..42].copy_from_slice(&0u16.to_le_bytes());
block[42..44].copy_from_slice(&(toc_len as u16).to_le_bytes());
let toc_base = 56;
let keys_start = toc_base + toc_len;
let vals_end = bs - BTREE_INFO_SIZE;
if entries.len() * 28 + 56 + BTREE_INFO_SIZE > bs {
return Err(crate::Error::Unsupported(format!(
"apfs writer: {} omap internal entries overflow one block",
entries.len()
)));
}
for (i, &((oid, xid), child_paddr)) in entries.iter().enumerate() {
let k_off = (i * 16) as u16;
let v_off = ((i + 1) * 8) as u16;
block[toc_base + i * 4..toc_base + i * 4 + 2].copy_from_slice(&k_off.to_le_bytes());
block[toc_base + i * 4 + 2..toc_base + i * 4 + 4].copy_from_slice(&v_off.to_le_bytes());
let ks = keys_start + k_off as usize;
block[ks..ks + 8].copy_from_slice(&oid.to_le_bytes());
block[ks + 8..ks + 16].copy_from_slice(&xid.to_le_bytes());
let vs = vals_end - v_off as usize;
block[vs..vs + 8].copy_from_slice(&child_paddr.to_le_bytes());
}
let info_off = bs - BTREE_INFO_SIZE;
block[info_off + 8..info_off + 12].copy_from_slice(&16u32.to_le_bytes());
block[info_off + 12..info_off + 16].copy_from_slice(&16u32.to_le_bytes());
sign_block(&mut block);
Ok(block)
}
fn leaf_payload_capacity(bs: usize) -> usize {
bs - 56 - BTREE_INFO_SIZE
}
fn pack_records_into_leaves(records: &[FsRecord], cap: usize) -> Result<Vec<Vec<FsRecord>>> {
if records.is_empty() {
return Ok(vec![Vec::new()]);
}
let mut leaves: Vec<Vec<FsRecord>> = Vec::new();
let mut cur: Vec<FsRecord> = Vec::new();
let mut cur_bytes: usize = 0;
for r in records {
let needed = 8 + r.key.len() + r.val.len();
if needed > cap {
return Err(crate::Error::Unsupported(format!(
"apfs writer: single fs-tree record ({} bytes) does not fit in a leaf (cap {})",
needed, cap
)));
}
if cur_bytes + needed > cap && !cur.is_empty() {
leaves.push(std::mem::take(&mut cur));
cur_bytes = 0;
}
cur.push(r.clone());
cur_bytes += needed;
}
if !cur.is_empty() {
leaves.push(cur);
}
Ok(leaves)
}
fn build_fs_leaf(records: &[FsRecord], bs: usize, vid: u64, is_root: bool) -> Result<Vec<u8>> {
let mut block = vec![0u8; bs];
block[8..16].copy_from_slice(&vid.to_le_bytes());
block[16..24].copy_from_slice(&WRITE_XID.to_le_bytes());
let obj_type = if is_root {
OBJECT_TYPE_BTREE
} else {
OBJECT_TYPE_BTREE_NODE
};
block[24..28].copy_from_slice(&obj_type.to_le_bytes());
block[28..32].copy_from_slice(&OBJECT_TYPE_FSTREE.to_le_bytes());
let mut flags = BTNODE_LEAF;
if is_root {
flags |= BTNODE_ROOT;
}
block[32..34].copy_from_slice(&flags.to_le_bytes());
block[34..36].copy_from_slice(&0u16.to_le_bytes()); block[36..40].copy_from_slice(&(records.len() as u32).to_le_bytes());
let toc_len = records.len() * 8;
block[40..42].copy_from_slice(&0u16.to_le_bytes());
block[42..44].copy_from_slice(&(toc_len as u16).to_le_bytes());
let toc_base = 56;
let keys_start = toc_base + toc_len;
let vals_end = if is_root { bs - BTREE_INFO_SIZE } else { bs };
let mut total_keys = 0usize;
let mut total_vals = 0usize;
for r in records {
total_keys += r.key.len();
total_vals += r.val.len();
}
if keys_start + total_keys + total_vals > vals_end {
return Err(crate::Error::Unsupported(format!(
"apfs writer: {} fs-tree records don't fit in one leaf (need {} key bytes + {} val bytes)",
records.len(),
total_keys,
total_vals,
)));
}
let mut k_cursor: usize = 0;
let mut v_cursor_back: usize = 0;
for (i, r) in records.iter().enumerate() {
let k_off = k_cursor as u16;
let k_len = r.key.len() as u16;
v_cursor_back += r.val.len();
let v_off = v_cursor_back as u16;
let v_len = r.val.len() as u16;
block[toc_base + i * 8..toc_base + i * 8 + 2].copy_from_slice(&k_off.to_le_bytes());
block[toc_base + i * 8 + 2..toc_base + i * 8 + 4].copy_from_slice(&k_len.to_le_bytes());
block[toc_base + i * 8 + 4..toc_base + i * 8 + 6].copy_from_slice(&v_off.to_le_bytes());
block[toc_base + i * 8 + 6..toc_base + i * 8 + 8].copy_from_slice(&v_len.to_le_bytes());
let ks = keys_start + k_off as usize;
block[ks..ks + r.key.len()].copy_from_slice(&r.key);
let vs = vals_end - v_off as usize;
block[vs..vs + r.val.len()].copy_from_slice(&r.val);
k_cursor += r.key.len();
}
if is_root {
let info_off = bs - BTREE_INFO_SIZE;
block[info_off + 24..info_off + 32].copy_from_slice(&(records.len() as u64).to_le_bytes());
}
sign_block(&mut block);
Ok(block)
}
fn build_fs_internal_root(entries: &[(Vec<u8>, u64)], bs: usize, root_vid: u64) -> Result<Vec<u8>> {
let mut block = vec![0u8; bs];
block[8..16].copy_from_slice(&root_vid.to_le_bytes());
block[16..24].copy_from_slice(&WRITE_XID.to_le_bytes());
block[24..28].copy_from_slice(&OBJECT_TYPE_BTREE.to_le_bytes());
block[28..32].copy_from_slice(&OBJECT_TYPE_FSTREE.to_le_bytes());
let flags = BTNODE_ROOT; block[32..34].copy_from_slice(&flags.to_le_bytes());
block[34..36].copy_from_slice(&1u16.to_le_bytes()); block[36..40].copy_from_slice(&(entries.len() as u32).to_le_bytes());
let toc_len = entries.len() * 8;
block[40..42].copy_from_slice(&0u16.to_le_bytes());
block[42..44].copy_from_slice(&(toc_len as u16).to_le_bytes());
let toc_base = 56;
let keys_start = toc_base + toc_len;
let vals_end = bs - BTREE_INFO_SIZE;
let mut total_keys = 0usize;
for (kb, _) in entries {
total_keys += kb.len();
}
let total_vals = entries.len() * 8;
if keys_start + total_keys + total_vals > vals_end {
return Err(crate::Error::Unsupported(format!(
"apfs writer: {} fs-tree internal entries don't fit in one root \
(need {} key bytes + {} val bytes)",
entries.len(),
total_keys,
total_vals,
)));
}
let mut k_cursor: usize = 0;
let mut v_cursor_back: usize = 0;
for (i, (kb, child_vid)) in entries.iter().enumerate() {
let k_off = k_cursor as u16;
let k_len = kb.len() as u16;
v_cursor_back += 8;
let v_off = v_cursor_back as u16;
let v_len = 8u16;
block[toc_base + i * 8..toc_base + i * 8 + 2].copy_from_slice(&k_off.to_le_bytes());
block[toc_base + i * 8 + 2..toc_base + i * 8 + 4].copy_from_slice(&k_len.to_le_bytes());
block[toc_base + i * 8 + 4..toc_base + i * 8 + 6].copy_from_slice(&v_off.to_le_bytes());
block[toc_base + i * 8 + 6..toc_base + i * 8 + 8].copy_from_slice(&v_len.to_le_bytes());
let ks = keys_start + k_off as usize;
block[ks..ks + kb.len()].copy_from_slice(kb);
let vs = vals_end - v_off as usize;
block[vs..vs + 8].copy_from_slice(&child_vid.to_le_bytes());
k_cursor += kb.len();
}
let info_off = bs - BTREE_INFO_SIZE;
block[info_off + 24..info_off + 32].copy_from_slice(&(entries.len() as u64).to_le_bytes());
sign_block(&mut block);
Ok(block)
}
fn build_spaceman_stub(bs: usize, oid: u64) -> Result<Vec<u8>> {
let mut buf = vec![0u8; bs];
buf[8..16].copy_from_slice(&oid.to_le_bytes());
buf[16..24].copy_from_slice(&WRITE_XID.to_le_bytes());
buf[24..28].copy_from_slice(&(OBJECT_TYPE_SPACEMAN | OBJ_EPHEMERAL).to_le_bytes());
sign_block(&mut buf);
Ok(buf)
}
fn build_chkmap_stub(bs: usize) -> Result<Vec<u8>> {
let mut buf = vec![0u8; bs];
buf[24..28].copy_from_slice(&(OBJECT_TYPE_CHECKPOINT_MAP | OBJ_PHYSICAL).to_le_bytes());
buf[16..24].copy_from_slice(&WRITE_XID.to_le_bytes());
sign_block(&mut buf);
Ok(buf)
}
fn derive_uuid(name: &[u8], salt: &[u8]) -> [u8; 16] {
let mut seed: u64 = 0x6a09_e667_f3bc_c908;
for &b in name.iter().chain(salt.iter()) {
seed = seed.wrapping_mul(0x0100_0193).wrapping_add(b as u64);
seed ^= seed >> 27;
}
let mut out = [0u8; 16];
for chunk in out.chunks_mut(8) {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
chunk.copy_from_slice(&seed.to_le_bytes());
}
out[6] = (out[6] & 0x0f) | 0x40;
out[8] = (out[8] & 0x3f) | 0x80;
out
}
fn mode_reg(perm: u16) -> u16 {
0o100_000 | (perm & 0o7777)
}
fn mode_dir(perm: u16) -> u16 {
0o040_000 | (perm & 0o7777)
}
fn mode_lnk(perm: u16) -> u16 {
0o120_000 | (perm & 0o7777)
}
#[cfg(test)]
mod tests {
use super::super::Apfs;
use super::*;
use crate::block::MemoryBackend;
use std::io::{Cursor, Read};
#[test]
fn write_then_read_single_small_file() {
let total_blocks = 64u64;
let bs = 4096u32;
let mut dev = MemoryBackend::new(total_blocks * bs as u64);
{
let mut w = ApfsWriter::new(&mut dev, total_blocks, bs, "TestVol").unwrap();
let data = b"hello apfs world";
let mut r = Cursor::new(data);
w.add_file_from_reader(2, "hi.txt", 0o644, &mut r, data.len() as u64)
.unwrap();
w.finish().unwrap();
}
let apfs = Apfs::open(&mut dev).expect("opens the new image");
assert_eq!(apfs.volume_name(), "TestVol");
let entries = apfs.list_path(&mut dev, "/").expect("list root");
let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
assert!(names.contains(&"hi.txt"), "got names: {names:?}");
let mut reader = apfs.open_file_reader(&mut dev, "/hi.txt").unwrap();
let mut out = Vec::new();
reader.read_to_end(&mut out).unwrap();
assert_eq!(out, b"hello apfs world");
}
#[test]
fn write_then_read_nested_dir() {
let total_blocks = 64u64;
let bs = 4096u32;
let mut dev = MemoryBackend::new(total_blocks * bs as u64);
{
let mut w = ApfsWriter::new(&mut dev, total_blocks, bs, "Vol").unwrap();
let dir = w.add_dir(2, "sub", 0o755).unwrap();
let data = b"nested";
let mut r = Cursor::new(data);
w.add_file_from_reader(dir, "f", 0o644, &mut r, data.len() as u64)
.unwrap();
w.finish().unwrap();
}
let apfs = Apfs::open(&mut dev).unwrap();
let root = apfs.list_path(&mut dev, "/").unwrap();
assert!(root.iter().any(|e| e.name == "sub"));
let sub = apfs.list_path(&mut dev, "/sub").unwrap();
assert!(sub.iter().any(|e| e.name == "f"));
let mut r = apfs.open_file_reader(&mut dev, "/sub/f").unwrap();
let mut out = Vec::new();
r.read_to_end(&mut out).unwrap();
assert_eq!(out, b"nested");
}
#[test]
fn write_then_read_symlink() {
let total_blocks = 64u64;
let bs = 4096u32;
let mut dev = MemoryBackend::new(total_blocks * bs as u64);
{
let mut w = ApfsWriter::new(&mut dev, total_blocks, bs, "Vol").unwrap();
w.add_symlink(2, "link", 0o777, "/dev/null").unwrap();
w.finish().unwrap();
}
let apfs = Apfs::open(&mut dev).unwrap();
let entries = apfs.list_path(&mut dev, "/").unwrap();
let link = entries.iter().find(|e| e.name == "link").unwrap();
assert!(matches!(link.kind, crate::fs::EntryKind::Symlink));
}
#[test]
fn list_and_open_single_volume_slot() {
let total_blocks = 64u64;
let bs = 4096u32;
let mut dev = MemoryBackend::new(total_blocks * bs as u64);
{
let w = ApfsWriter::new(&mut dev, total_blocks, bs, "OnlyVol").unwrap();
w.finish().unwrap();
}
let vols = Apfs::list_volumes(&mut dev).unwrap();
assert_eq!(vols.len(), 1);
assert_eq!(vols[0].name, "OnlyVol");
assert!(!vols[0].encrypted);
assert_eq!(vols[0].index, 0);
let apfs = Apfs::open_volume(&mut dev, 0).unwrap();
assert_eq!(apfs.volume_name(), "OnlyVol");
let e = Apfs::open_volume(&mut dev, 5).unwrap_err();
assert!(matches!(e, crate::Error::InvalidArgument(_)));
}
#[test]
fn list_snapshots_empty_when_no_tree() {
let total_blocks = 64u64;
let bs = 4096u32;
let mut dev = MemoryBackend::new(total_blocks * bs as u64);
{
let w = ApfsWriter::new(&mut dev, total_blocks, bs, "Vol").unwrap();
w.finish().unwrap();
}
let apfs = Apfs::open(&mut dev).unwrap();
let snaps = apfs.list_snapshots(&mut dev).unwrap();
assert!(snaps.is_empty());
let e = apfs.open_snapshot(&mut dev, 42).unwrap_err();
assert!(matches!(e, crate::Error::InvalidArgument(_)));
}
#[test]
fn write_then_read_multiblock_file() {
let total_blocks = 128u64;
let bs = 4096u32;
let mut dev = MemoryBackend::new(total_blocks * bs as u64);
let payload: Vec<u8> = (0..(20 * 1024)).map(|i| (i % 256) as u8).collect();
{
let mut w = ApfsWriter::new(&mut dev, total_blocks, bs, "Vol").unwrap();
let mut r = Cursor::new(payload.clone());
w.add_file_from_reader(2, "blob", 0o644, &mut r, payload.len() as u64)
.unwrap();
w.finish().unwrap();
}
let apfs = Apfs::open(&mut dev).unwrap();
let mut r = apfs.open_file_reader(&mut dev, "/blob").unwrap();
let mut out = Vec::new();
r.read_to_end(&mut out).unwrap();
assert_eq!(out, payload);
}
#[test]
fn write_then_read_embedded_xattrs() {
let total_blocks = 64u64;
let bs = 4096u32;
let mut dev = MemoryBackend::new(total_blocks * bs as u64);
{
let mut w = ApfsWriter::new(&mut dev, total_blocks, bs, "Vol").unwrap();
let mut r = Cursor::new(b"x");
w.add_file_from_reader(2, "f", 0o644, &mut r, 1).unwrap();
w.add_xattr(2, "user.note", b"hello world").unwrap();
w.add_xattr(2, "user.lang", b"en_US").unwrap();
w.finish().unwrap();
}
let apfs = Apfs::open(&mut dev).unwrap();
let xs = apfs.read_xattrs(&mut dev, "/").unwrap();
assert_eq!(
xs.get("user.note").map(Vec::as_slice),
Some(&b"hello world"[..])
);
assert_eq!(xs.get("user.lang").map(Vec::as_slice), Some(&b"en_US"[..]));
}
#[test]
fn xattr_attached_to_file_path() {
let total_blocks = 64u64;
let bs = 4096u32;
let mut dev = MemoryBackend::new(total_blocks * bs as u64);
{
let mut w = ApfsWriter::new(&mut dev, total_blocks, bs, "Vol").unwrap();
let mut r = Cursor::new(b"data");
let oid = w
.add_file_from_reader(2, "doc.txt", 0o644, &mut r, 4)
.unwrap();
w.add_xattr(oid, "com.apple.lastuseddate#PS", &[1, 2, 3, 4])
.unwrap();
w.finish().unwrap();
}
let apfs = Apfs::open(&mut dev).unwrap();
let root_xs = apfs.read_xattrs(&mut dev, "/").unwrap();
assert!(root_xs.is_empty());
let xs = apfs.read_xattrs(&mut dev, "/doc.txt").unwrap();
assert_eq!(
xs.get("com.apple.lastuseddate#PS").map(Vec::as_slice),
Some(&[1u8, 2, 3, 4][..])
);
}
#[test]
fn xattr_too_big_unsupported() {
let total_blocks = 64u64;
let bs = 4096u32;
let mut dev = MemoryBackend::new(total_blocks * bs as u64);
let mut w = ApfsWriter::new(&mut dev, total_blocks, bs, "Vol").unwrap();
let big = vec![0u8; APFS_XATTR_MAX_EMBEDDED_SIZE + 1];
let e = w.add_xattr(2, "user.too_big", &big).unwrap_err();
assert!(matches!(e, crate::Error::Unsupported(_)));
}
#[test]
fn write_then_read_multi_leaf_fs_tree() {
let total_blocks = 256u64;
let bs = 4096u32;
let mut dev = MemoryBackend::new(total_blocks * bs as u64);
let n_files: usize = 80; let payloads: Vec<Vec<u8>> = (0..n_files)
.map(|i| format!("file-{i:03}-payload").into_bytes())
.collect();
{
let mut w = ApfsWriter::new(&mut dev, total_blocks, bs, "Vol").unwrap();
for (i, p) in payloads.iter().enumerate() {
let name = format!("f{i:03}");
let mut r = Cursor::new(p.clone());
w.add_file_from_reader(2, &name, 0o644, &mut r, p.len() as u64)
.unwrap();
}
w.finish().unwrap();
}
let apfs = Apfs::open(&mut dev).unwrap();
let entries = apfs.list_path(&mut dev, "/").unwrap();
assert!(
entries.len() >= n_files,
"expected at least {} entries, got {}",
n_files,
entries.len()
);
for &i in &[0usize, 1, n_files / 2, n_files - 1] {
let name = format!("/f{i:03}");
let mut r = apfs.open_file_reader(&mut dev, &name).unwrap();
let mut out = Vec::new();
r.read_to_end(&mut out).unwrap();
assert_eq!(out, payloads[i], "mismatch for {name}");
}
}
#[test]
fn pack_records_splits_on_capacity() {
let r = |k: u8, v_len: usize| FsRecord {
key: vec![k; 8],
val: vec![0u8; v_len],
};
let recs = vec![r(1, 100), r(2, 100), r(3, 100)];
let leaves = pack_records_into_leaves(&recs, 116).unwrap();
assert_eq!(leaves.len(), 3);
let leaves = pack_records_into_leaves(&recs, 232).unwrap();
assert_eq!(leaves.len(), 2);
assert_eq!(leaves[0].len(), 2);
assert_eq!(leaves[1].len(), 1);
}
}