pub mod btree;
pub mod checksum;
pub mod fstree;
pub mod jrec;
pub mod obj;
pub mod omap;
pub(crate) mod rw;
pub mod snap;
pub(crate) mod spaceman;
pub mod superblock;
pub mod write;
use crate::Result;
use crate::block::BlockDevice;
use fstree::{DrecKeyLayout, FsKeyTarget, FsTreeCtx, RangeScan};
use jrec::{
APFS_TYPE_DIR_REC, APFS_TYPE_FILE_EXTENT, APFS_TYPE_INODE, APFS_TYPE_XATTR, DT_DIR, DT_LNK,
DT_REG, DrecKey, DrecVal, FileExtentVal, InodeVal, OBJ_ID_MASK, OBJ_TYPE_SHIFT,
};
use obj::{OBJECT_TYPE_MASK, ObjPhys};
use omap::{OmapPhys, lookup as omap_lookup};
use snap::{SnapMetaVal, decode_snap_meta_key};
use superblock::{ApfsSuperblock, NX_MAGIC, NxSuperblock};
const ROOT_DIR_INO: u64 = 2;
pub struct Apfs {
block_size: u32,
total_bytes: u64,
volume_name: String,
state: ApfsState,
}
enum ApfsState {
Read(ReadState),
PendingWrite(PendingWrite),
Write(WriteState),
}
pub(crate) struct WriteState {
pub(crate) read: ReadState,
pub(crate) volume_name: String,
pub(crate) container_uuid: [u8; 16],
pub(crate) volume_uuid: [u8; 16],
pub(crate) total_blocks: u64,
pub(crate) next_oid: u64,
pub(crate) num_files: u64,
pub(crate) num_directories: u64,
pub(crate) num_symlinks: u64,
pub(crate) cur_xid: u64,
pub(crate) next_xp_desc_slot: u64,
pub(crate) bump_high_water: u64,
}
pub(crate) struct ReadState {
snap_meta_tree_oid: u64,
volume_index: usize,
fsroot_block: Vec<u8>,
fs_ctx: std::cell::RefCell<FsTreeCtx>,
drec_layout: DrecKeyLayout,
}
struct PendingWrite {
total_blocks: u64,
dir_oid: std::collections::HashMap<std::path::PathBuf, u64>,
ops: Vec<PendingOp>,
next_oid: u64,
}
enum PendingOp {
Dir {
parent_oid: u64,
name: String,
mode: u16,
},
File {
parent_oid: u64,
name: String,
mode: u16,
data: Vec<u8>,
},
Symlink {
parent_oid: u64,
name: String,
mode: u16,
target: String,
},
}
impl std::fmt::Debug for Apfs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let state_name = match &self.state {
ApfsState::Read(_) => "Read",
ApfsState::PendingWrite(_) => "PendingWrite",
ApfsState::Write(_) => "Write",
};
f.debug_struct("Apfs")
.field("block_size", &self.block_size)
.field("total_bytes", &self.total_bytes)
.field("volume_name", &self.volume_name)
.field("state", &state_name)
.finish_non_exhaustive()
}
}
const APFS_INCOMPAT_NORMALIZATION_INSENSITIVE: u64 = 0x0000_0008;
#[derive(Debug, Clone)]
struct ContainerCtx {
live_sb: NxSuperblock,
omap_root: Vec<u8>,
block_size: u32,
total_bytes: u64,
}
#[derive(Debug, Clone)]
pub struct VolumeInfo {
pub index: usize,
pub vol_oid: u64,
pub name: String,
pub role: u16,
pub encrypted: bool,
pub uuid: [u8; 16],
}
#[derive(Debug, Clone)]
pub struct SnapshotInfo {
pub xid: u64,
pub name: String,
pub sblock_paddr: u64,
pub create_time: u64,
}
impl Apfs {
fn read_state(&self) -> Result<&ReadState> {
match &self.state {
ApfsState::Read(r) => Ok(r),
ApfsState::Write(w) => Ok(&w.read),
ApfsState::PendingWrite(_) => Err(crate::Error::Unsupported(
"apfs: filesystem is in pending-write mode; call flush() first".into(),
)),
}
}
pub fn format(
dev: &mut dyn BlockDevice,
total_blocks: u64,
block_size: u32,
volume_name: &str,
) -> Result<Self> {
let _ = write::ApfsWriter::new(dev, total_blocks, block_size, volume_name)?;
let mut dir_oid = std::collections::HashMap::new();
dir_oid.insert(std::path::PathBuf::from("/"), ROOT_DIR_INO);
Ok(Self {
block_size,
total_bytes: total_blocks.saturating_mul(block_size as u64),
volume_name: volume_name.to_string(),
state: ApfsState::PendingWrite(PendingWrite {
total_blocks,
dir_oid,
ops: Vec::new(),
next_oid: 16,
}),
})
}
pub fn open(dev: &mut dyn BlockDevice) -> Result<Self> {
let ctx = load_container(dev)?;
let vol_index = ctx
.live_sb
.fs_oid
.iter()
.position(|&o| o != 0)
.ok_or_else(|| {
crate::Error::InvalidImage("apfs: container has no volumes in nx_fs_oid".into())
})?;
Self::open_volume_with_ctx(dev, &ctx, vol_index, None)
}
pub fn open_writable(dev: &mut dyn BlockDevice) -> Result<Self> {
let read_only = Apfs::open(dev)?;
let ctx = load_container(dev)?;
let block_size = ctx.block_size;
let total_blocks = ctx.live_sb.block_count;
let total_bytes = ctx.total_bytes;
let container_uuid = ctx.live_sb.uuid;
let cur_xid = ctx.live_sb.obj.xid;
let next_xp_desc_slot = ctx.live_sb.xp_desc_base + ctx.live_sb.xp_desc_len as u64;
if next_xp_desc_slot >= ctx.live_sb.xp_desc_base + ctx.live_sb.xp_desc_blocks as u64 {
return Err(crate::Error::Unsupported(
"apfs: xp_desc area is full — checkpoint rotation isn't \
implemented yet"
.into(),
));
}
let (vol_index, apsb_paddr) = find_volume_paddr(dev, &ctx)?;
let mut apsb_block = vec![0u8; block_size as usize];
dev.read_at(apsb_paddr * block_size as u64, &mut apsb_block)?;
let apsb = ApfsSuperblock::decode(&apsb_block)?;
let next_oid = u64::from_le_bytes(apsb_block[176..184].try_into().unwrap());
let bump_high_water = read_spaceman_high_water(dev, &ctx).unwrap_or(total_blocks / 2);
let read = match read_only.state {
ApfsState::Read(r) => r,
_ => unreachable!("Apfs::open always returns Read"),
};
let _ = vol_index;
Ok(Self {
block_size,
total_bytes,
volume_name: apsb.volname.clone(),
state: ApfsState::Write(WriteState {
read,
volume_name: apsb.volname.clone(),
container_uuid,
volume_uuid: apsb.vol_uuid,
total_blocks,
next_oid,
num_files: apsb.num_files,
num_directories: apsb.num_directories,
num_symlinks: apsb.num_symlinks,
cur_xid,
next_xp_desc_slot,
bump_high_water,
}),
})
}
pub fn list_volumes(dev: &mut dyn BlockDevice) -> Result<Vec<VolumeInfo>> {
let ctx = load_container(dev)?;
let target_xid = ctx.live_sb.obj.xid;
let mut out = Vec::new();
for (i, &vol_oid) in ctx.live_sb.fs_oid.iter().enumerate() {
if vol_oid == 0 {
continue;
}
let mut dev_reader = DevReader {
dev,
block_size: ctx.block_size,
};
let vol_loc =
match omap_lookup(&ctx.omap_root, vol_oid, target_xid, &mut |paddr, buf| {
dev_reader.read(paddr, buf)
})? {
Some(v) => v,
None => continue,
};
let mut apsb_block = vec![0u8; ctx.block_size as usize];
dev_reader.read(vol_loc.paddr, &mut apsb_block)?;
let apsb = match ApfsSuperblock::decode(&apsb_block) {
Ok(s) => s,
Err(_) => continue,
};
const APFS_FS_UNENCRYPTED: u64 = 0x0000_0001;
let role = if apsb_block.len() >= 966 {
u16::from_le_bytes(apsb_block[964..966].try_into().unwrap())
} else {
0
};
out.push(VolumeInfo {
index: i,
vol_oid,
name: apsb.volname.clone(),
role,
encrypted: apsb.fs_flags & APFS_FS_UNENCRYPTED == 0,
uuid: apsb.vol_uuid,
});
}
Ok(out)
}
pub fn open_volume(dev: &mut dyn BlockDevice, index: usize) -> Result<Self> {
let ctx = load_container(dev)?;
Self::open_volume_with_ctx(dev, &ctx, index, None)
}
fn open_volume_with_ctx(
dev: &mut dyn BlockDevice,
ctx: &ContainerCtx,
index: usize,
snapshot: Option<(u64, u64)>,
) -> Result<Self> {
if index >= ctx.live_sb.fs_oid.len() {
return Err(crate::Error::InvalidArgument(format!(
"apfs: volume index {index} out of range"
)));
}
let vol_oid = ctx.live_sb.fs_oid[index];
if vol_oid == 0 {
return Err(crate::Error::InvalidArgument(format!(
"apfs: nx_fs_oid[{index}] is empty"
)));
}
let block_size = ctx.block_size;
let target_xid = ctx.live_sb.obj.xid;
let mut dev_reader = DevReader { dev, block_size };
let apsb_paddr = match snapshot {
Some((p, _)) => p,
None => {
let vol_loc =
omap_lookup(&ctx.omap_root, vol_oid, target_xid, &mut |paddr, buf| {
dev_reader.read(paddr, buf)
})?
.ok_or_else(|| {
crate::Error::InvalidImage(format!(
"apfs: container omap has no entry for volume oid {vol_oid:#x}"
))
})?;
vol_loc.paddr
}
};
let mut apsb_block = vec![0u8; block_size as usize];
dev_reader.read(apsb_paddr, &mut apsb_block)?;
let apsb = ApfsSuperblock::decode(&apsb_block)?;
const APFS_FS_UNENCRYPTED: u64 = 0x0000_0001;
if apsb.fs_flags & APFS_FS_UNENCRYPTED == 0 {
return Err(crate::Error::Unsupported(
"apfs: encrypted volumes are not supported (read)".into(),
));
}
const APFS_INCOMPAT_SEALED_VOLUME: u64 = 0x0000_0080;
if apsb.incompatible_features & APFS_INCOMPAT_SEALED_VOLUME != 0 {
return Err(crate::Error::Unsupported(
"apfs: sealed volumes (integrity hashes) are not supported".into(),
));
}
let vol_omap_phys =
read_object::<OmapPhys>(dev_reader.dev, apsb.omap_oid, block_size, OmapPhys::decode)?;
let mut vol_omap_root = vec![0u8; block_size as usize];
dev_reader.read(vol_omap_phys.tree_oid, &mut vol_omap_root)?;
let omap_xid = match snapshot {
Some((_, xid)) => xid,
None => apsb.obj.xid,
};
let fsroot_loc = omap_lookup(
&vol_omap_root,
apsb.root_tree_oid,
omap_xid,
&mut |paddr, buf| dev_reader.read(paddr, buf),
)?
.ok_or_else(|| {
crate::Error::InvalidImage(format!(
"apfs: volume omap has no entry for root_tree_oid {:#x} @ xid {omap_xid}",
apsb.root_tree_oid
))
})?;
let mut fsroot_block = vec![0u8; block_size as usize];
dev_reader.read(fsroot_loc.paddr, &mut fsroot_block)?;
let fsroot_obj = ObjPhys::decode(&fsroot_block)?;
let ot = fsroot_obj.type_and_flags & OBJECT_TYPE_MASK;
if ot != obj::OBJECT_TYPE_BTREE && ot != obj::OBJECT_TYPE_BTREE_NODE {
return Err(crate::Error::InvalidImage(format!(
"apfs: fsroot o_type {ot:#x} is not a btree"
)));
}
let drec_layout =
if apsb.incompatible_features & APFS_INCOMPAT_NORMALIZATION_INSENSITIVE != 0 {
DrecKeyLayout::Hashed
} else {
DrecKeyLayout::Plain
};
let fs_ctx = FsTreeCtx::new(vol_omap_root, omap_xid, block_size as usize);
Ok(Self {
block_size,
total_bytes: ctx.total_bytes,
volume_name: apsb.volname,
state: ApfsState::Read(ReadState {
snap_meta_tree_oid: apsb.snap_meta_tree_oid,
volume_index: index,
fsroot_block,
fs_ctx: std::cell::RefCell::new(fs_ctx),
drec_layout,
}),
})
}
pub fn list_snapshots(&self, dev: &mut dyn BlockDevice) -> Result<Vec<SnapshotInfo>> {
let rs = self.read_state()?;
if rs.snap_meta_tree_oid == 0 {
return Ok(Vec::new());
}
let mut root = vec![0u8; self.block_size as usize];
let off = rs.snap_meta_tree_oid.saturating_mul(self.block_size as u64);
dev.read_at(off, &mut root)?;
let node = btree::BTreeNode::decode(&root)?;
if !node.is_leaf() {
return Err(crate::Error::Unsupported(
"apfs: multi-level snapshot-metadata trees are not supported".into(),
));
}
let mut out = Vec::new();
for i in 0..node.nkeys {
let (kb, vb) = node.entry_at(i, 0, 0)?;
let (kind, xid) = match decode_snap_meta_key(kb) {
Ok(v) => v,
Err(_) => continue,
};
if kind != jrec::APFS_TYPE_SNAP_METADATA {
continue;
}
let meta = match SnapMetaVal::decode(vb) {
Ok(m) => m,
Err(_) => continue,
};
out.push(SnapshotInfo {
xid,
name: meta.name,
sblock_paddr: meta.sblock_oid,
create_time: meta.create_time,
});
}
Ok(out)
}
pub fn open_snapshot(&self, dev: &mut dyn BlockDevice, xid: u64) -> Result<Self> {
let vol_index = self.read_state()?.volume_index;
let snaps = self.list_snapshots(dev)?;
let snap = snaps
.iter()
.find(|s| s.xid == xid)
.ok_or_else(|| {
crate::Error::InvalidArgument(format!("apfs: no snapshot with xid {xid}"))
})?
.clone();
let ctx = load_container(dev)?;
Self::open_volume_with_ctx(dev, &ctx, vol_index, Some((snap.sblock_paddr, snap.xid)))
}
pub fn open_snapshot_by_name(&self, dev: &mut dyn BlockDevice, name: &str) -> Result<Self> {
let vol_index = self.read_state()?.volume_index;
let snaps = self.list_snapshots(dev)?;
let snap = snaps
.iter()
.find(|s| s.name == name)
.ok_or_else(|| {
crate::Error::InvalidArgument(format!("apfs: no snapshot named {name:?}"))
})?
.clone();
let ctx = load_container(dev)?;
Self::open_volume_with_ctx(dev, &ctx, vol_index, Some((snap.sblock_paddr, snap.xid)))
}
pub fn list_path(
&self,
dev: &mut dyn BlockDevice,
path: &str,
) -> Result<Vec<crate::fs::DirEntry>> {
let target_oid = self.resolve_path_to_oid(dev, path)?;
self.list_dir(dev, target_oid)
}
pub fn read_xattrs(
&self,
dev: &mut dyn BlockDevice,
path: &str,
) -> Result<std::collections::HashMap<String, Vec<u8>>> {
let target_oid = self.resolve_path_to_oid(dev, path)?;
let rs = self.read_state()?;
let target = FsKeyTarget {
oid: target_oid,
kind: APFS_TYPE_XATTR,
tail: &[],
drec_layout: rs.drec_layout,
};
let block_size = self.block_size;
let mut ctx = rs.fs_ctx.borrow_mut();
let mut out = std::collections::HashMap::new();
let mut scan = RangeScan::start(&rs.fsroot_block, &target, &mut ctx, &mut |paddr, buf| {
read_at_paddr(dev, paddr, block_size, buf)
})?;
while let Some((kb, vb)) = scan.next(&mut ctx, &mut |paddr, buf| {
read_at_paddr(dev, paddr, block_size, buf)
})? {
if kb.len() < 10 {
continue;
}
let nlen = u16::from_le_bytes(kb[8..10].try_into().unwrap()) as usize;
if 10 + nlen > kb.len() || nlen == 0 {
continue;
}
let raw = &kb[10..10 + nlen];
let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
let name = String::from_utf8_lossy(&raw[..end]).into_owned();
if vb.len() < 4 {
continue;
}
let flags = u16::from_le_bytes(vb[0..2].try_into().unwrap());
let xdata_len = u16::from_le_bytes(vb[2..4].try_into().unwrap()) as usize;
const XATTR_DATA_EMBEDDED: u16 = 0x0002;
if flags & XATTR_DATA_EMBEDDED == 0 {
continue;
}
if 4 + xdata_len > vb.len() {
continue;
}
let value = vb[4..4 + xdata_len].to_vec();
out.insert(name, value);
}
Ok(out)
}
pub fn open_file_reader<'a>(
&self,
dev: &'a mut dyn BlockDevice,
path: &str,
) -> Result<ApfsFileReader<'a>> {
let target_oid = self.resolve_path_to_oid(dev, path)?;
let (size, dstream_oid) = self.lookup_inode_size(dev, target_oid)?;
let extents = self.collect_extents(dev, dstream_oid.unwrap_or(target_oid))?;
Ok(ApfsFileReader {
dev,
block_size: self.block_size,
extents,
size,
cursor: 0,
})
}
pub fn chmod(&mut self, dev: &mut dyn BlockDevice, path: &str, mode_perms: u16) -> Result<()> {
let target_oid = self.resolve_path_to_oid(dev, path)?;
let new_perms = mode_perms & 0o7777;
rw::commit_with_mutator(self, dev, |records| {
patch_inode_record(records, target_oid, |val| {
if val.len() < jrec::J_INODE_VAL_FIXED_SIZE {
return;
}
let cur_mode = u16::from_le_bytes(val[80..82].try_into().unwrap());
let new_mode = (cur_mode & 0xF000) | new_perms;
val[80..82].copy_from_slice(&new_mode.to_le_bytes());
});
Ok(())
})
}
pub fn chown(
&mut self,
dev: &mut dyn BlockDevice,
path: &str,
owner: u32,
group: u32,
) -> Result<()> {
let target_oid = self.resolve_path_to_oid(dev, path)?;
rw::commit_with_mutator(self, dev, |records| {
patch_inode_record(records, target_oid, |val| {
if val.len() < jrec::J_INODE_VAL_FIXED_SIZE {
return;
}
val[72..76].copy_from_slice(&owner.to_le_bytes());
val[76..80].copy_from_slice(&group.to_le_bytes());
});
Ok(())
})
}
pub fn set_times(
&mut self,
dev: &mut dyn BlockDevice,
path: &str,
mtime_ns: Option<u64>,
ctime_ns: Option<u64>,
atime_ns: Option<u64>,
) -> Result<()> {
let target_oid = self.resolve_path_to_oid(dev, path)?;
rw::commit_with_mutator(self, dev, |records| {
patch_inode_record(records, target_oid, |val| {
if val.len() < jrec::J_INODE_VAL_FIXED_SIZE {
return;
}
if let Some(m) = mtime_ns {
val[24..32].copy_from_slice(&m.to_le_bytes());
}
if let Some(c) = ctime_ns {
val[32..40].copy_from_slice(&c.to_le_bytes());
}
if let Some(a) = atime_ns {
val[40..48].copy_from_slice(&a.to_le_bytes());
}
});
Ok(())
})
}
pub fn remove_path(&mut self, dev: &mut dyn BlockDevice, path: &str) -> Result<()> {
let (parent_oid, name) = self.resolve_parent_and_name(dev, path)?;
rw::commit_with_mutator(self, dev, |records| {
let (target_oid, dtype) = find_drec(records, parent_oid, &name).ok_or_else(|| {
crate::Error::InvalidArgument(format!(
"apfs: no such entry {name:?} under inode {parent_oid}"
))
})?;
if dtype == DT_DIR && drec_count_for(records, target_oid) > 0 {
return Err(crate::Error::InvalidArgument(format!(
"apfs: directory {name:?} is not empty"
)));
}
remove_drec(records, parent_oid, &name);
if dtype == DT_DIR {
remove_all_records_for_oid(records, target_oid);
patch_inode_record(records, parent_oid, |v| {
if v.len() >= jrec::J_INODE_VAL_FIXED_SIZE {
let cur = i32::from_le_bytes(v[56..60].try_into().unwrap());
v[56..60].copy_from_slice(&(cur - 1).to_le_bytes());
}
});
return Ok(());
}
let mut last_link = false;
patch_inode_record(records, target_oid, |v| {
if v.len() >= jrec::J_INODE_VAL_FIXED_SIZE {
let cur = i32::from_le_bytes(v[56..60].try_into().unwrap());
let new = cur.saturating_sub(1);
v[56..60].copy_from_slice(&new.to_le_bytes());
if new <= 0 {
last_link = true;
}
}
});
if last_link {
remove_all_records_for_oid(records, target_oid);
}
patch_inode_record(records, parent_oid, |v| {
if v.len() >= jrec::J_INODE_VAL_FIXED_SIZE {
let cur = i32::from_le_bytes(v[56..60].try_into().unwrap());
v[56..60].copy_from_slice(&(cur - 1).to_le_bytes());
}
});
Ok(())
})
}
pub fn rename(
&mut self,
dev: &mut dyn BlockDevice,
old_path: &str,
new_path: &str,
) -> Result<()> {
let (old_parent, old_name) = self.resolve_parent_and_name(dev, old_path)?;
let (new_parent, new_name) = self.resolve_parent_and_name(dev, new_path)?;
rw::commit_with_mutator(self, dev, |records| {
let (target_oid, dtype) =
find_drec(records, old_parent, &old_name).ok_or_else(|| {
crate::Error::InvalidArgument(format!(
"apfs: rename source {old_name:?} not found"
))
})?;
if find_drec(records, new_parent, &new_name).is_some() {
return Err(crate::Error::InvalidArgument(format!(
"apfs: rename target {new_name:?} already exists"
)));
}
remove_drec(records, old_parent, &old_name);
push_drec(records, new_parent, &new_name, target_oid, dtype);
if old_parent != new_parent {
patch_inode_record(records, old_parent, |v| {
if v.len() >= jrec::J_INODE_VAL_FIXED_SIZE {
let cur = i32::from_le_bytes(v[56..60].try_into().unwrap());
v[56..60].copy_from_slice(&(cur - 1).to_le_bytes());
}
});
patch_inode_record(records, new_parent, |v| {
if v.len() >= jrec::J_INODE_VAL_FIXED_SIZE {
let cur = i32::from_le_bytes(v[56..60].try_into().unwrap());
v[56..60].copy_from_slice(&(cur + 1).to_le_bytes());
}
});
if dtype == DT_DIR {
patch_inode_record(records, target_oid, |v| {
if v.len() >= 8 {
v[0..8].copy_from_slice(&new_parent.to_le_bytes());
}
});
}
}
Ok(())
})
}
pub fn link(
&mut self,
dev: &mut dyn BlockDevice,
existing_path: &str,
new_path: &str,
) -> Result<()> {
let target_oid = self.resolve_path_to_oid(dev, existing_path)?;
let (new_parent, new_name) = self.resolve_parent_and_name(dev, new_path)?;
let target_dtype = self.lookup_inode_dtype(dev, target_oid)?;
if target_dtype == DT_DIR {
return Err(crate::Error::InvalidArgument(
"apfs: cannot hardlink to a directory".into(),
));
}
rw::commit_with_mutator(self, dev, |records| {
if find_drec(records, new_parent, &new_name).is_some() {
return Err(crate::Error::InvalidArgument(format!(
"apfs: link target {new_name:?} already exists"
)));
}
push_drec(records, new_parent, &new_name, target_oid, target_dtype);
patch_inode_record(records, target_oid, |v| {
if v.len() >= jrec::J_INODE_VAL_FIXED_SIZE {
let cur = i32::from_le_bytes(v[56..60].try_into().unwrap());
v[56..60].copy_from_slice(&(cur + 1).to_le_bytes());
}
});
patch_inode_record(records, new_parent, |v| {
if v.len() >= jrec::J_INODE_VAL_FIXED_SIZE {
let cur = i32::from_le_bytes(v[56..60].try_into().unwrap());
v[56..60].copy_from_slice(&(cur + 1).to_le_bytes());
}
});
Ok(())
})
}
pub fn total_bytes(&self) -> u64 {
self.total_bytes
}
pub fn block_size(&self) -> u32 {
self.block_size
}
pub fn volume_name(&self) -> &str {
&self.volume_name
}
fn resolve_parent_and_name(
&self,
dev: &mut dyn BlockDevice,
path: &str,
) -> Result<(u64, String)> {
let comps: Vec<&str> = split_path(path);
if comps.is_empty() {
return Err(crate::Error::InvalidArgument(
"apfs: cannot operate on the root directory itself".into(),
));
}
let leaf = comps.last().unwrap().to_string();
let mut cur = ROOT_DIR_INO;
for part in &comps[..comps.len() - 1] {
cur = self.find_drec_child(dev, cur, part)?.ok_or_else(|| {
crate::Error::InvalidArgument(format!(
"apfs: no such entry {part:?} under {path:?}"
))
})?;
}
Ok((cur, leaf))
}
fn lookup_inode_dtype(&self, dev: &mut dyn BlockDevice, oid: u64) -> Result<u16> {
let rs = self.read_state()?;
let target = FsKeyTarget {
oid,
kind: APFS_TYPE_INODE,
tail: &[],
drec_layout: rs.drec_layout,
};
let block_size = self.block_size;
let mut ctx = rs.fs_ctx.borrow_mut();
let mut scan = RangeScan::start(&rs.fsroot_block, &target, &mut ctx, &mut |paddr, buf| {
read_at_paddr(dev, paddr, block_size, buf)
})?;
if let Some((_kb, vb)) = scan.next(&mut ctx, &mut |paddr, buf| {
read_at_paddr(dev, paddr, block_size, buf)
})? {
let ino = InodeVal::decode(&vb)?;
return Ok(match ino.mode & 0o170_000 {
0o040_000 => DT_DIR,
0o120_000 => DT_LNK,
_ => DT_REG,
});
}
Err(crate::Error::InvalidArgument(format!(
"apfs: no inode record for oid {oid:#x}"
)))
}
fn resolve_path_to_oid(&self, dev: &mut dyn BlockDevice, path: &str) -> Result<u64> {
let mut cur = ROOT_DIR_INO;
for part in split_path(path) {
cur = self.find_drec_child(dev, cur, part)?.ok_or_else(|| {
crate::Error::InvalidArgument(format!(
"apfs: no such entry {part:?} under {path:?}"
))
})?;
}
Ok(cur)
}
fn find_drec_child(
&self,
dev: &mut dyn BlockDevice,
parent_oid: u64,
name: &str,
) -> Result<Option<u64>> {
let rs = self.read_state()?;
let layout = rs.drec_layout;
let target = FsKeyTarget {
oid: parent_oid,
kind: APFS_TYPE_DIR_REC,
tail: &[],
drec_layout: layout,
};
let block_size = self.block_size;
let mut ctx = rs.fs_ctx.borrow_mut();
let mut scan = RangeScan::start(&rs.fsroot_block, &target, &mut ctx, &mut |paddr, buf| {
read_at_paddr(dev, paddr, block_size, buf)
})?;
while let Some((kb, vb)) = scan.next(&mut ctx, &mut |paddr, buf| {
read_at_paddr(dev, paddr, block_size, buf)
})? {
let key = match layout {
DrecKeyLayout::Hashed => {
DrecKey::decode_hashed(&kb).or_else(|_| DrecKey::decode_plain(&kb))?
}
DrecKeyLayout::Plain => {
DrecKey::decode_plain(&kb).or_else(|_| DrecKey::decode_hashed(&kb))?
}
};
if key.name == name {
let val = DrecVal::decode(&vb)?;
return Ok(Some(val.file_id));
}
}
Ok(None)
}
fn list_dir(
&self,
dev: &mut dyn BlockDevice,
dir_oid: u64,
) -> Result<Vec<crate::fs::DirEntry>> {
use crate::fs::{DirEntry as FsDirEntry, EntryKind};
let rs = self.read_state()?;
let layout = rs.drec_layout;
let target = FsKeyTarget {
oid: dir_oid,
kind: APFS_TYPE_DIR_REC,
tail: &[],
drec_layout: layout,
};
let block_size = self.block_size;
let mut ctx = rs.fs_ctx.borrow_mut();
let mut out = Vec::new();
let mut scan = RangeScan::start(&rs.fsroot_block, &target, &mut ctx, &mut |paddr, buf| {
read_at_paddr(dev, paddr, block_size, buf)
})?;
while let Some((kb, vb)) = scan.next(&mut ctx, &mut |paddr, buf| {
read_at_paddr(dev, paddr, block_size, buf)
})? {
let key = match layout {
DrecKeyLayout::Hashed => {
match DrecKey::decode_hashed(&kb).or_else(|_| DrecKey::decode_plain(&kb)) {
Ok(k) => k,
Err(_) => continue,
}
}
DrecKeyLayout::Plain => {
match DrecKey::decode_plain(&kb).or_else(|_| DrecKey::decode_hashed(&kb)) {
Ok(k) => k,
Err(_) => continue,
}
}
};
let val = match DrecVal::decode(&vb) {
Ok(v) => v,
Err(_) => continue,
};
let kind = match val.dtype() {
DT_DIR => EntryKind::Dir,
DT_REG => EntryKind::Regular,
DT_LNK => EntryKind::Symlink,
jrec::DT_FIFO => EntryKind::Fifo,
jrec::DT_CHR => EntryKind::Char,
jrec::DT_BLK => EntryKind::Block,
jrec::DT_SOCK => EntryKind::Socket,
_ => EntryKind::Unknown,
};
out.push(FsDirEntry {
name: key.name,
inode: val.file_id as u32,
kind,
size: 0,
});
}
Ok(out)
}
fn lookup_inode_size(&self, dev: &mut dyn BlockDevice, oid: u64) -> Result<(u64, Option<u64>)> {
let rs = self.read_state()?;
let target = FsKeyTarget {
oid,
kind: APFS_TYPE_INODE,
tail: &[],
drec_layout: rs.drec_layout,
};
let block_size = self.block_size;
let mut ctx = rs.fs_ctx.borrow_mut();
let mut scan = RangeScan::start(&rs.fsroot_block, &target, &mut ctx, &mut |paddr, buf| {
read_at_paddr(dev, paddr, block_size, buf)
})?;
if let Some((_kb, vb)) = scan.next(&mut ctx, &mut |paddr, buf| {
read_at_paddr(dev, paddr, block_size, buf)
})? {
let ino = InodeVal::decode(&vb)?;
const S_IFMT: u16 = 0o170_000;
const S_IFREG: u16 = 0o100_000;
const S_IFLNK: u16 = 0o120_000;
let mt = ino.mode & S_IFMT;
if mt != S_IFREG && mt != S_IFLNK {
return Err(crate::Error::InvalidArgument(format!(
"apfs: oid {oid:#x} is not a regular file (mode {:#o})",
ino.mode
)));
}
let size = ino.dstream.map(|d| d.size).unwrap_or(0);
let dstream_oid = if ino.private_id != 0 && ino.private_id != oid {
Some(ino.private_id)
} else {
None
};
return Ok((size, dstream_oid));
}
Err(crate::Error::InvalidArgument(format!(
"apfs: no inode record for oid {oid:#x}"
)))
}
fn collect_extents(
&self,
dev: &mut dyn BlockDevice,
dstream_oid: u64,
) -> Result<Vec<(u64, FileExtentVal)>> {
let rs = self.read_state()?;
let target = FsKeyTarget {
oid: dstream_oid,
kind: APFS_TYPE_FILE_EXTENT,
tail: &[],
drec_layout: rs.drec_layout,
};
let block_size = self.block_size;
let mut ctx = rs.fs_ctx.borrow_mut();
let mut out = Vec::new();
let mut scan = RangeScan::start(&rs.fsroot_block, &target, &mut ctx, &mut |paddr, buf| {
read_at_paddr(dev, paddr, block_size, buf)
})?;
while let Some((kb, vb)) = scan.next(&mut ctx, &mut |paddr, buf| {
read_at_paddr(dev, paddr, block_size, buf)
})? {
let logical_addr = if kb.len() >= 16 {
u64::from_le_bytes(kb[8..16].try_into().unwrap())
} else {
continue;
};
let val = match FileExtentVal::decode(&vb) {
Ok(v) => v,
Err(_) => continue,
};
out.push((logical_addr, val));
}
out.sort_by_key(|(la, _)| *la);
Ok(out)
}
}
impl crate::fs::Filesystem for Apfs {
fn create_file(
&mut self,
_dev: &mut dyn BlockDevice,
path: &std::path::Path,
src: crate::fs::FileSource,
meta: crate::fs::FileMeta,
) -> Result<()> {
let (mut reader, size) = src
.open()
.map_err(|e| crate::Error::Io(std::io::Error::other(e)))?;
let mut data = Vec::with_capacity(size.min(64 * 1024 * 1024) as usize);
let n = std::io::Read::read_to_end(&mut reader, &mut data)
.map_err(|e| crate::Error::Io(std::io::Error::other(e)))?;
if n as u64 != size {
data.resize(size as usize, 0);
}
let pw = pending_write_mut(&mut self.state)?;
let (parent_oid, name) = pw.resolve_parent(path)?;
pw.ops.push(PendingOp::File {
parent_oid,
name,
mode: meta.mode,
data,
});
pw.next_oid = pw.next_oid.saturating_add(1);
Ok(())
}
fn create_dir(
&mut self,
_dev: &mut dyn BlockDevice,
path: &std::path::Path,
meta: crate::fs::FileMeta,
) -> Result<()> {
let pw = pending_write_mut(&mut self.state)?;
let (parent_oid, name) = pw.resolve_parent(path)?;
let new_oid = pw.next_oid;
pw.next_oid = pw.next_oid.saturating_add(1);
pw.dir_oid.insert(path.to_path_buf(), new_oid);
pw.ops.push(PendingOp::Dir {
parent_oid,
name,
mode: meta.mode,
});
Ok(())
}
fn create_symlink(
&mut self,
_dev: &mut dyn BlockDevice,
path: &std::path::Path,
target: &std::path::Path,
meta: crate::fs::FileMeta,
) -> Result<()> {
let target_str = target
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("apfs: non-UTF-8 symlink target".into()))?
.to_string();
let pw = pending_write_mut(&mut self.state)?;
let (parent_oid, name) = pw.resolve_parent(path)?;
pw.ops.push(PendingOp::Symlink {
parent_oid,
name,
mode: meta.mode,
target: target_str,
});
pw.next_oid = pw.next_oid.saturating_add(1);
Ok(())
}
fn create_device(
&mut self,
_dev: &mut dyn BlockDevice,
_path: &std::path::Path,
_kind: crate::fs::DeviceKind,
_major: u32,
_minor: u32,
_meta: crate::fs::FileMeta,
) -> Result<()> {
Err(crate::Error::Unsupported(
"apfs: device nodes are not supported by the writer".into(),
))
}
fn remove(&mut self, _dev: &mut dyn BlockDevice, _path: &std::path::Path) -> Result<()> {
Err(crate::Error::Unsupported(
"apfs: remove is not supported (writer is single-pass)".into(),
))
}
fn list(
&mut self,
dev: &mut dyn BlockDevice,
path: &std::path::Path,
) -> Result<Vec<crate::fs::DirEntry>> {
let s = path
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("apfs: non-UTF-8 path".into()))?;
Apfs::list_path(self, dev, s)
}
fn read_file<'a>(
&'a mut self,
dev: &'a mut dyn BlockDevice,
path: &std::path::Path,
) -> Result<Box<dyn std::io::Read + 'a>> {
let s = path
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("apfs: non-UTF-8 path".into()))?;
let r = self.open_file_reader(dev, s)?;
Ok(Box::new(r))
}
fn open_file_ro<'a>(
&'a mut self,
dev: &'a mut dyn BlockDevice,
path: &std::path::Path,
) -> Result<Box<dyn crate::fs::FileReadHandle + 'a>> {
let s = path
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("apfs: non-UTF-8 path".into()))?;
let h = self.open_file_reader(dev, s)?;
Ok(Box::new(h))
}
fn flush(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
let pw = match std::mem::replace(
&mut self.state,
ApfsState::PendingWrite(PendingWrite {
total_blocks: 0,
dir_oid: std::collections::HashMap::new(),
ops: Vec::new(),
next_oid: 16,
}),
) {
ApfsState::Read(r) => {
self.state = ApfsState::Read(r);
return Ok(());
}
ApfsState::Write(w) => {
self.state = ApfsState::Write(w);
return Ok(());
}
ApfsState::PendingWrite(p) => p,
};
let block_size = self.block_size;
let volume_name = self.volume_name.clone();
{
let mut w = write::ApfsWriter::new(dev, pw.total_blocks, block_size, &volume_name)?;
for op in pw.ops {
match op {
PendingOp::Dir {
parent_oid,
name,
mode,
} => {
w.add_dir(parent_oid, &name, mode)?;
}
PendingOp::File {
parent_oid,
name,
mode,
data,
} => {
let len = data.len() as u64;
let mut r = std::io::Cursor::new(data);
w.add_file_from_reader(parent_oid, &name, mode, &mut r, len)?;
}
PendingOp::Symlink {
parent_oid,
name,
mode,
target,
} => {
w.add_symlink(parent_oid, &name, mode, &target)?;
}
}
}
w.finish()?;
}
let fresh = Apfs::open(dev)?;
debug_assert_eq!(fresh.block_size, self.block_size);
debug_assert_eq!(fresh.total_bytes, self.total_bytes);
self.state = fresh.state;
self.volume_name = fresh.volume_name;
Ok(())
}
fn mutation_capability(&self) -> crate::fs::MutationCapability {
match &self.state {
ApfsState::PendingWrite(_) => crate::fs::MutationCapability::WholeFileOnly,
ApfsState::Read(_) => crate::fs::MutationCapability::Immutable,
ApfsState::Write(_) => crate::fs::MutationCapability::Mutable,
}
}
fn open_file_rw<'a>(
&'a mut self,
dev: &'a mut dyn BlockDevice,
path: &std::path::Path,
flags: crate::fs::OpenFlags,
meta: Option<crate::fs::FileMeta>,
) -> Result<Box<dyn crate::fs::FileHandle + 'a>> {
let _ = meta;
if flags.create {
return Err(crate::Error::Unsupported(
"apfs: open_file_rw with create=true is not supported (open_writable \
only edits existing files)"
.into(),
));
}
let s = path
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("apfs: non-UTF-8 path".into()))?;
let h = rw::ApfsFileHandle::open(self, dev, s, flags)?;
Ok(Box::new(h))
}
}
impl crate::fs::FilesystemFactory for Apfs {
type FormatOpts = ApfsFormatOpts;
fn format(dev: &mut dyn BlockDevice, opts: &Self::FormatOpts) -> Result<Self> {
Apfs::format(dev, opts.total_blocks, opts.block_size, &opts.volume_name)
}
fn open(dev: &mut dyn BlockDevice) -> Result<Self> {
Apfs::open(dev)
}
}
#[derive(Debug, Clone)]
pub struct ApfsFormatOpts {
pub total_blocks: u64,
pub block_size: u32,
pub volume_name: String,
}
impl Default for ApfsFormatOpts {
fn default() -> Self {
Self {
total_blocks: 64,
block_size: 4096,
volume_name: "APFS".to_string(),
}
}
}
impl ApfsFormatOpts {
pub fn apply_options(&mut self, map: &mut crate::format_opts::OptionMap) -> crate::Result<()> {
if let Some(sz) = map.take_size("block_size")? {
self.block_size = sz as u32;
}
if let Some(n) = map.take_u64("total_blocks")? {
self.total_blocks = n;
}
if let Some(s) = map.take_str("volume_name") {
self.volume_name = s;
}
if let Some(s) = map.take_str("volume_label") {
self.volume_name = s;
}
Ok(())
}
}
fn pending_write_mut(state: &mut ApfsState) -> Result<&mut PendingWrite> {
match state {
ApfsState::PendingWrite(p) => Ok(p),
ApfsState::Read(_) => Err(crate::Error::Unsupported(
"apfs: filesystem has already been flushed; mutation after flush is not supported"
.into(),
)),
ApfsState::Write(_) => Err(crate::Error::Unsupported(
"apfs: filesystem is open for in-place writes (open_file_rw); \
create_* / remove are not supported in this mode"
.into(),
)),
}
}
impl PendingWrite {
fn resolve_parent(&self, path: &std::path::Path) -> Result<(u64, String)> {
let parent = path.parent().unwrap_or_else(|| std::path::Path::new("/"));
let parent_buf = if parent.as_os_str().is_empty() {
std::path::PathBuf::from("/")
} else {
parent.to_path_buf()
};
let leaf = path
.file_name()
.ok_or_else(|| crate::Error::InvalidArgument("apfs: empty leaf name".into()))?
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("apfs: non-UTF-8 leaf name".into()))?
.to_string();
let parent_oid = *self.dir_oid.get(&parent_buf).ok_or_else(|| {
crate::Error::InvalidArgument(format!(
"apfs: parent directory {:?} not found; call create_dir() for it first",
parent_buf
))
})?;
Ok((parent_oid, leaf))
}
}
fn read_at_paddr(
dev: &mut dyn BlockDevice,
paddr: u64,
block_size: u32,
buf: &mut [u8],
) -> Result<()> {
let off = paddr.saturating_mul(block_size as u64);
dev.read_at(off, buf)
}
pub struct ApfsFileReader<'a> {
dev: &'a mut dyn BlockDevice,
block_size: u32,
extents: Vec<(u64, FileExtentVal)>,
size: u64,
cursor: u64,
}
impl<'a> std::io::Seek for ApfsFileReader<'a> {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
let target_i128 = match pos {
std::io::SeekFrom::Start(n) => n as i128,
std::io::SeekFrom::Current(d) => self.cursor as i128 + d as i128,
std::io::SeekFrom::End(d) => self.size as i128 + d as i128,
};
if target_i128 < 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"apfs: seek to negative offset",
));
}
self.cursor = (target_i128 as u128).min(self.size as u128) as u64;
Ok(self.cursor)
}
}
impl<'a> crate::fs::FileReadHandle for ApfsFileReader<'a> {
fn len(&self) -> u64 {
self.size
}
}
impl<'a> std::io::Read for ApfsFileReader<'a> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.cursor >= self.size || buf.is_empty() {
return Ok(0);
}
let mut covering: Option<(u64, FileExtentVal)> = None;
for &(la, ev) in &self.extents {
if la <= self.cursor && self.cursor < la + ev.length {
covering = Some((la, ev));
break;
}
}
let (la, ev) = match covering {
Some(pair) => pair,
None => {
let next = self
.extents
.iter()
.map(|(la, _)| *la)
.find(|&la| la > self.cursor)
.unwrap_or(self.size);
let want = (next.min(self.size) - self.cursor).min(buf.len() as u64) as usize;
buf[..want].fill(0);
self.cursor += want as u64;
return Ok(want);
}
};
let off_in_extent = self.cursor - la;
let avail_in_extent = ev.length - off_in_extent;
let want = avail_in_extent
.min(buf.len() as u64)
.min(self.size - self.cursor) as usize;
if ev.phys_block_num == 0 {
buf[..want].fill(0);
} else {
let abs_off = ev.phys_block_num * self.block_size as u64 + off_in_extent;
self.dev
.read_at(abs_off, &mut buf[..want])
.map_err(std::io::Error::other)?;
}
self.cursor += want as u64;
Ok(want)
}
}
pub(crate) fn patch_inode_record<F>(records: &mut [(Vec<u8>, Vec<u8>)], target_oid: u64, patcher: F)
where
F: FnOnce(&mut Vec<u8>),
{
for (k, v) in records.iter_mut() {
if k.len() < 8 {
continue;
}
let hdr = u64::from_le_bytes(k[0..8].try_into().unwrap());
let oid = hdr & OBJ_ID_MASK;
let kind = (hdr >> OBJ_TYPE_SHIFT) as u8;
if oid == target_oid && kind == APFS_TYPE_INODE {
patcher(v);
return;
}
}
}
pub(crate) fn find_drec(
records: &[(Vec<u8>, Vec<u8>)],
parent_oid: u64,
name: &str,
) -> Option<(u64, u16)> {
for (k, v) in records {
if k.len() < 10 {
continue;
}
let hdr = u64::from_le_bytes(k[0..8].try_into().unwrap());
let oid = hdr & OBJ_ID_MASK;
let kind = (hdr >> OBJ_TYPE_SHIFT) as u8;
if oid != parent_oid || kind != APFS_TYPE_DIR_REC {
continue;
}
let nlen = u16::from_le_bytes(k[8..10].try_into().unwrap()) as usize;
if k.len() < 10 + nlen || nlen == 0 {
continue;
}
let stored = &k[10..10 + nlen - 1];
if stored != name.as_bytes() {
continue;
}
if v.len() < 18 {
return None;
}
let target_oid = u64::from_le_bytes(v[0..8].try_into().unwrap());
let dtype = u16::from_le_bytes(v[16..18].try_into().unwrap());
return Some((target_oid, dtype));
}
None
}
pub(crate) fn remove_drec(records: &mut Vec<(Vec<u8>, Vec<u8>)>, parent_oid: u64, name: &str) {
records.retain(|(k, _)| {
if k.len() < 10 {
return true;
}
let hdr = u64::from_le_bytes(k[0..8].try_into().unwrap());
let oid = hdr & OBJ_ID_MASK;
let kind = (hdr >> OBJ_TYPE_SHIFT) as u8;
if oid != parent_oid || kind != APFS_TYPE_DIR_REC {
return true;
}
let nlen = u16::from_le_bytes(k[8..10].try_into().unwrap()) as usize;
if k.len() < 10 + nlen || nlen == 0 {
return true;
}
let stored = &k[10..10 + nlen - 1];
stored != name.as_bytes()
});
}
pub(crate) fn push_drec(
records: &mut Vec<(Vec<u8>, Vec<u8>)>,
parent_oid: u64,
name: &str,
target_oid: u64,
dtype: u16,
) {
let nlen = name.len() + 1;
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());
records.push((key, val));
}
pub(crate) fn drec_count_for(records: &[(Vec<u8>, Vec<u8>)], parent_oid: u64) -> usize {
let mut n = 0usize;
for (k, _) in records {
if k.len() < 8 {
continue;
}
let hdr = u64::from_le_bytes(k[0..8].try_into().unwrap());
let oid = hdr & OBJ_ID_MASK;
let kind = (hdr >> OBJ_TYPE_SHIFT) as u8;
if oid == parent_oid && kind == APFS_TYPE_DIR_REC {
n += 1;
}
}
n
}
pub(crate) fn remove_all_records_for_oid(records: &mut Vec<(Vec<u8>, Vec<u8>)>, target_oid: u64) {
records.retain(|(k, _)| {
if k.len() < 8 {
return true;
}
let hdr = u64::from_le_bytes(k[0..8].try_into().unwrap());
let oid = hdr & OBJ_ID_MASK;
oid != target_oid
});
}
pub fn probe(dev: &mut dyn BlockDevice) -> Result<bool> {
if dev.total_size() < 64 {
return Ok(false);
}
let mut head = [0u8; 64];
dev.read_at(0, &mut head)?;
Ok(&head[32..36] == b"NXSB")
}
fn load_container(dev: &mut dyn BlockDevice) -> Result<ContainerCtx> {
let mut block0 = vec![0u8; 4096];
dev.read_at(0, &mut block0)?;
let label_sb = NxSuperblock::decode(&block0)?;
let block_size = label_sb.block_size;
if block_size == 0 || block_size > 65_536 || !block_size.is_power_of_two() {
return Err(crate::Error::InvalidImage(format!(
"apfs: nx_block_size {block_size} is not a sensible power of two"
)));
}
let mut block0 = vec![0u8; block_size as usize];
dev.read_at(0, &mut block0)?;
let label_sb = NxSuperblock::decode(&block0)?;
let live_sb = find_live_nxsb(dev, &label_sb, block_size)?.unwrap_or(label_sb.clone());
let total_bytes = live_sb.block_count.saturating_mul(block_size as u64);
let omap_phys = read_object::<OmapPhys>(dev, live_sb.omap_oid, block_size, OmapPhys::decode)?;
let mut omap_root_block = vec![0u8; block_size as usize];
dev.read_at(
omap_phys.tree_oid.saturating_mul(block_size as u64),
&mut omap_root_block,
)?;
Ok(ContainerCtx {
live_sb,
omap_root: omap_root_block,
block_size,
total_bytes,
})
}
fn find_volume_paddr(dev: &mut dyn BlockDevice, ctx: &ContainerCtx) -> Result<(usize, u64)> {
let vol_index = ctx
.live_sb
.fs_oid
.iter()
.position(|&o| o != 0)
.ok_or_else(|| crate::Error::InvalidImage("apfs: container has no volumes".into()))?;
let vol_oid = ctx.live_sb.fs_oid[vol_index];
let target_xid = ctx.live_sb.obj.xid;
let mut dev_reader = DevReader {
dev,
block_size: ctx.block_size,
};
let val = omap_lookup(&ctx.omap_root, vol_oid, target_xid, &mut |paddr, buf| {
dev_reader.read(paddr, buf)
})?
.ok_or_else(|| {
crate::Error::InvalidImage(format!(
"apfs: container omap has no entry for volume oid {vol_oid:#x}"
))
})?;
Ok((vol_index, val.paddr))
}
fn read_spaceman_high_water(dev: &mut dyn BlockDevice, ctx: &ContainerCtx) -> Option<u64> {
let mut block = vec![0u8; ctx.block_size as usize];
let off = write::SPACEMAN_PADDR * ctx.block_size as u64;
if dev.read_at(off, &mut block).is_err() {
return None;
}
let otype = u32::from_le_bytes(block[24..28].try_into().ok()?);
if otype & 0x0000_ffff != spaceman::OBJECT_TYPE_SPACEMAN {
return None;
}
let main_block_count = u64::from_le_bytes(block[48..56].try_into().ok()?);
let main_free_count = u64::from_le_bytes(block[72..80].try_into().ok()?);
Some(main_block_count - main_free_count)
}
fn find_live_nxsb(
dev: &mut dyn BlockDevice,
label: &NxSuperblock,
block_size: u32,
) -> Result<Option<NxSuperblock>> {
let n = label.xp_desc_blocks as u64;
let base = label.xp_desc_base;
let mut best: Option<NxSuperblock> = None;
let mut buf = vec![0u8; block_size as usize];
for i in 0..n {
let paddr = base.saturating_add(i);
let off = paddr.saturating_mul(block_size as u64);
if off + block_size as u64 > dev.total_size() {
continue;
}
dev.read_at(off, &mut buf)?;
if buf.len() < 36 || &buf[32..36] != b"NXSB".as_slice() {
let mw = u32::from_le_bytes(buf[32..36].try_into().unwrap_or([0; 4]));
if mw != NX_MAGIC {
continue;
}
}
let sb = match NxSuperblock::decode(&buf) {
Ok(s) => s,
Err(_) => continue,
};
let better = match &best {
None => sb.obj.xid >= label.obj.xid,
Some(b) => sb.obj.xid > b.obj.xid,
};
if better {
best = Some(sb);
}
}
Ok(best)
}
fn read_object<T>(
dev: &mut dyn BlockDevice,
paddr: u64,
block_size: u32,
decode: impl Fn(&[u8]) -> Result<T>,
) -> Result<T> {
let mut buf = vec![0u8; block_size as usize];
let off = paddr.checked_mul(block_size as u64).ok_or_else(|| {
crate::Error::InvalidImage(format!("apfs: paddr {paddr} overflows when multiplied"))
})?;
let end = off.checked_add(block_size as u64).ok_or_else(|| {
crate::Error::InvalidImage(format!("apfs: paddr {paddr} +block size overflows"))
})?;
if end > dev.total_size() {
return Err(crate::Error::InvalidImage(format!(
"apfs: object paddr {paddr} out of device bounds"
)));
}
dev.read_at(off, &mut buf)?;
decode(&buf)
}
struct DevReader<'a> {
dev: &'a mut dyn BlockDevice,
block_size: u32,
}
impl<'a> DevReader<'a> {
fn read(&mut self, paddr: u64, buf: &mut [u8]) -> Result<()> {
let off = paddr.saturating_mul(self.block_size as u64);
self.dev.read_at(off, buf)
}
}
fn split_path(path: &str) -> Vec<&str> {
path.split('/')
.filter(|p| !p.is_empty() && *p != ".")
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::MemoryBackend;
use crate::fs::apfs::obj::OBJECT_TYPE_NX_SUPERBLOCK;
#[test]
fn probe_blank_device_false() {
let mut dev = MemoryBackend::new(8192);
assert!(!probe(&mut dev).unwrap());
}
#[test]
fn probe_with_magic() {
let mut dev = MemoryBackend::new(8192);
dev.write_at(32, b"NXSB").unwrap();
assert!(probe(&mut dev).unwrap());
}
#[test]
fn open_blank_fails() {
let mut dev = MemoryBackend::new(64 * 1024);
let e = Apfs::open(&mut dev).unwrap_err();
match e {
crate::Error::InvalidImage(_) => {}
other => panic!("expected InvalidImage, got {other:?}"),
}
}
#[test]
fn split_path_basics() {
assert!(split_path("/").is_empty());
assert!(split_path("").is_empty());
assert!(split_path(".").is_empty());
assert_eq!(split_path("/foo/bar"), vec!["foo", "bar"]);
assert_eq!(split_path("foo/./bar/"), vec!["foo", "bar"]);
}
#[test]
fn open_with_nxsb_only_errors_cleanly() {
let mut dev = MemoryBackend::new(64 * 4096);
let mut buf = vec![0u8; 4096];
buf[24..28].copy_from_slice(&OBJECT_TYPE_NX_SUPERBLOCK.to_le_bytes());
buf[32..36].copy_from_slice(&NX_MAGIC.to_le_bytes());
buf[36..40].copy_from_slice(&4096u32.to_le_bytes()); buf[40..48].copy_from_slice(&64u64.to_le_bytes()); buf[104..108].copy_from_slice(&0u32.to_le_bytes());
buf[112..120].copy_from_slice(&0u64.to_le_bytes());
buf[160..168].copy_from_slice(&u64::MAX.to_le_bytes());
buf[180..184].copy_from_slice(&1u32.to_le_bytes());
buf[184..192].copy_from_slice(&7u64.to_le_bytes());
dev.write_at(0, &buf).unwrap();
let e = Apfs::open(&mut dev).unwrap_err();
assert!(matches!(
e,
crate::Error::InvalidImage(_) | crate::Error::OutOfBounds { .. }
));
}
#[test]
fn list_volumes_blank_fails() {
let mut dev = MemoryBackend::new(64 * 1024);
let e = Apfs::list_volumes(&mut dev).unwrap_err();
assert!(matches!(e, crate::Error::InvalidImage(_)));
}
#[test]
fn trait_round_trip_format_create_flush_read() {
use crate::fs::{FileMeta, FileSource, Filesystem};
use std::io::Read;
let total_blocks = 128u64;
let bs = 4096u32;
let mut dev = MemoryBackend::new(total_blocks * bs as u64);
let mut apfs = Apfs::format(&mut dev, total_blocks, bs, "TraitVol").unwrap();
assert!(matches!(
apfs.mutation_capability(),
crate::fs::MutationCapability::WholeFileOnly
));
apfs.create_dir(
&mut dev,
std::path::Path::new("/sub"),
FileMeta::with_mode(0o755),
)
.unwrap();
let payload: Vec<u8> = b"hello via trait".to_vec();
apfs.create_file(
&mut dev,
std::path::Path::new("/sub/hello.txt"),
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(payload.clone())),
len: payload.len() as u64,
},
FileMeta::with_mode(0o644),
)
.unwrap();
apfs.create_file(
&mut dev,
std::path::Path::new("/note"),
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(b"top-level".to_vec())),
len: 9,
},
FileMeta::with_mode(0o600),
)
.unwrap();
apfs.flush(&mut dev).unwrap();
assert!(matches!(
apfs.mutation_capability(),
crate::fs::MutationCapability::Immutable
));
let root = apfs.list(&mut dev, std::path::Path::new("/")).unwrap();
let names: Vec<&str> = root.iter().map(|e| e.name.as_str()).collect();
assert!(names.contains(&"sub"), "root listing: {names:?}");
assert!(names.contains(&"note"), "root listing: {names:?}");
let sub = apfs.list(&mut dev, std::path::Path::new("/sub")).unwrap();
let sub_names: Vec<&str> = sub.iter().map(|e| e.name.as_str()).collect();
assert!(
sub_names.contains(&"hello.txt"),
"/sub listing: {sub_names:?}"
);
let mut buf = Vec::new();
apfs.read_file(&mut dev, std::path::Path::new("/sub/hello.txt"))
.unwrap()
.read_to_end(&mut buf)
.unwrap();
assert_eq!(buf, payload);
buf.clear();
apfs.read_file(&mut dev, std::path::Path::new("/note"))
.unwrap()
.read_to_end(&mut buf)
.unwrap();
assert_eq!(buf, b"top-level");
}
#[test]
fn open_file_ro_random_seek_round_trip() {
use crate::fs::{FileMeta, FileSource, Filesystem};
use std::io::{Read, Seek, SeekFrom};
let total_blocks = 128u64;
let bs = 4096u32;
let mut dev = MemoryBackend::new(total_blocks * bs as u64);
let body: Vec<u8> = (0..4096u32).map(|i| (i & 0xff) as u8).collect();
let mut apfs = Apfs::format(&mut dev, total_blocks, bs, "Vol").unwrap();
apfs.create_file(
&mut dev,
std::path::Path::new("/data.bin"),
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(body.clone())),
len: body.len() as u64,
},
FileMeta::with_mode(0o644),
)
.unwrap();
apfs.flush(&mut dev).unwrap();
let mut h = apfs
.open_file_ro(&mut dev, std::path::Path::new("/data.bin"))
.unwrap();
assert_eq!(h.len(), body.len() as u64);
h.seek(SeekFrom::Start(1000)).unwrap();
let mut chunk = [0u8; 32];
h.read_exact(&mut chunk).unwrap();
assert_eq!(&chunk[..], &body[1000..1032]);
let where_ = h.seek(SeekFrom::End(100)).unwrap();
assert_eq!(where_, body.len() as u64);
let n = h.read(&mut chunk).unwrap();
assert_eq!(n, 0);
}
#[test]
fn open_file_ro_refused_pre_flush() {
use crate::fs::Filesystem;
let total_blocks = 64u64;
let bs = 4096u32;
let mut dev = MemoryBackend::new(total_blocks * bs as u64);
let mut apfs = Apfs::format(&mut dev, total_blocks, bs, "Vol").unwrap();
let err = match apfs.open_file_ro(&mut dev, std::path::Path::new("/x")) {
Ok(_) => panic!("open_file_ro must refuse in pending-write mode"),
Err(e) => e,
};
assert!(matches!(err, crate::Error::Unsupported(_)));
}
#[test]
fn trait_create_after_flush_is_unsupported() {
use crate::fs::{FileMeta, FileSource, Filesystem};
let total_blocks = 64u64;
let bs = 4096u32;
let mut dev = MemoryBackend::new(total_blocks * bs as u64);
let mut apfs = Apfs::format(&mut dev, total_blocks, bs, "Vol").unwrap();
apfs.flush(&mut dev).unwrap();
let e = apfs
.create_file(
&mut dev,
std::path::Path::new("/late"),
FileSource::Zero(0),
FileMeta::default(),
)
.unwrap_err();
assert!(matches!(e, crate::Error::Unsupported(_)));
}
#[test]
fn trait_create_file_requires_existing_parent() {
use crate::fs::{FileMeta, FileSource, Filesystem};
let total_blocks = 64u64;
let bs = 4096u32;
let mut dev = MemoryBackend::new(total_blocks * bs as u64);
let mut apfs = Apfs::format(&mut dev, total_blocks, bs, "Vol").unwrap();
let e = apfs
.create_file(
&mut dev,
std::path::Path::new("/nope/file"),
FileSource::Zero(0),
FileMeta::default(),
)
.unwrap_err();
assert!(matches!(e, crate::Error::InvalidArgument(_)));
}
#[test]
fn read_before_flush_is_unsupported() {
use crate::fs::Filesystem;
let total_blocks = 64u64;
let bs = 4096u32;
let mut dev = MemoryBackend::new(total_blocks * bs as u64);
let mut apfs = Apfs::format(&mut dev, total_blocks, bs, "Vol").unwrap();
let e = apfs.list(&mut dev, std::path::Path::new("/")).unwrap_err();
assert!(matches!(e, crate::Error::Unsupported(_)));
}
#[test]
fn open_writable_round_trip_basic() {
use crate::fs::{FileMeta, FileSource, Filesystem};
let total_blocks = 256u64;
let bs = 4096u32;
let mut dev = MemoryBackend::new(total_blocks * bs as u64);
let mut apfs = Apfs::format(&mut dev, total_blocks, bs, "Vol").unwrap();
apfs.create_file(
&mut dev,
std::path::Path::new("/note.txt"),
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(b"hello".to_vec())),
len: 5,
},
FileMeta::with_mode(0o644),
)
.unwrap();
apfs.flush(&mut dev).unwrap();
drop(apfs);
let apfs = Apfs::open_writable(&mut dev).unwrap();
assert!(matches!(apfs.state, ApfsState::Write(_)));
assert!(matches!(
apfs.mutation_capability(),
crate::fs::MutationCapability::Mutable
));
}
#[test]
fn rw_round_trip_overwrite_existing_file() {
use crate::fs::{FileMeta, FileSource, Filesystem, OpenFlags};
use std::io::{Read, Write};
let total_blocks = 256u64;
let bs = 4096u32;
let mut dev = MemoryBackend::new(total_blocks * bs as u64);
let mut apfs = Apfs::format(&mut dev, total_blocks, bs, "Vol").unwrap();
let original: Vec<u8> = b"original-payload".to_vec();
apfs.create_file(
&mut dev,
std::path::Path::new("/note.txt"),
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(original.clone())),
len: original.len() as u64,
},
FileMeta::with_mode(0o644),
)
.unwrap();
apfs.flush(&mut dev).unwrap();
drop(apfs);
let mut apfs = Apfs::open_writable(&mut dev).unwrap();
let new_payload: Vec<u8> = b"REWRITTEN-PAYLOAD-DATA".to_vec();
{
let mut h = apfs
.open_file_rw(
&mut dev,
std::path::Path::new("/note.txt"),
OpenFlags {
truncate: true,
..OpenFlags::default()
},
None,
)
.unwrap();
h.write_all(&new_payload).unwrap();
h.sync().unwrap();
}
drop(apfs);
let mut apfs = Apfs::open(&mut dev).unwrap();
let mut buf = Vec::new();
apfs.read_file(&mut dev, std::path::Path::new("/note.txt"))
.unwrap()
.read_to_end(&mut buf)
.unwrap();
assert_eq!(buf, new_payload);
}
#[test]
fn rw_round_trip_two_consecutive_sessions() {
use crate::fs::{FileMeta, FileSource, Filesystem, OpenFlags};
use std::io::{Read, Write};
let total_blocks = 256u64;
let bs = 4096u32;
let mut dev = MemoryBackend::new(total_blocks * bs as u64);
let mut apfs = Apfs::format(&mut dev, total_blocks, bs, "Vol").unwrap();
apfs.create_file(
&mut dev,
std::path::Path::new("/log"),
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(b"v0".to_vec())),
len: 2,
},
FileMeta::with_mode(0o644),
)
.unwrap();
apfs.flush(&mut dev).unwrap();
drop(apfs);
let mut apfs = Apfs::open_writable(&mut dev).unwrap();
{
let mut h = apfs
.open_file_rw(
&mut dev,
std::path::Path::new("/log"),
OpenFlags {
truncate: true,
..OpenFlags::default()
},
None,
)
.unwrap();
h.write_all(b"v1-after-first-commit").unwrap();
h.sync().unwrap();
}
drop(apfs);
let mut apfs = Apfs::open_writable(&mut dev).unwrap();
{
let mut h = apfs
.open_file_rw(
&mut dev,
std::path::Path::new("/log"),
OpenFlags {
truncate: true,
..OpenFlags::default()
},
None,
)
.unwrap();
h.write_all(b"v2-final-payload-here").unwrap();
h.sync().unwrap();
}
drop(apfs);
let mut apfs = Apfs::open(&mut dev).unwrap();
let mut buf = Vec::new();
apfs.read_file(&mut dev, std::path::Path::new("/log"))
.unwrap()
.read_to_end(&mut buf)
.unwrap();
assert_eq!(buf, b"v2-final-payload-here");
}
}