pub mod btree;
pub mod checksum;
pub mod fstree;
pub mod jrec;
pub mod obj;
pub mod omap;
pub mod superblock;
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, DT_DIR, DT_LNK, DT_REG, DrecKey,
DrecVal, FileExtentVal, InodeVal,
};
use obj::{OBJECT_TYPE_MASK, ObjPhys};
use omap::{OmapPhys, lookup as omap_lookup};
use superblock::{ApfsSuperblock, NX_MAGIC, NxSuperblock};
const ROOT_DIR_INO: u64 = 2;
pub struct Apfs {
block_size: u32,
total_bytes: u64,
volume_name: String,
fsroot_block: Vec<u8>,
fs_ctx: std::cell::RefCell<FsTreeCtx>,
drec_layout: DrecKeyLayout,
}
impl std::fmt::Debug for Apfs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Apfs")
.field("block_size", &self.block_size)
.field("total_bytes", &self.total_bytes)
.field("volume_name", &self.volume_name)
.field("drec_layout", &self.drec_layout)
.finish_non_exhaustive()
}
}
const APFS_INCOMPAT_NORMALIZATION_INSENSITIVE: u64 = 0x0000_0008;
impl Apfs {
pub fn open(dev: &mut dyn BlockDevice) -> Result<Self> {
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,
)?;
let vol_oid = live_sb
.fs_oid
.iter()
.copied()
.find(|&o| o != 0)
.ok_or_else(|| {
crate::Error::InvalidImage("apfs: container has no volumes in nx_fs_oid".into())
})?;
let target_xid = live_sb.obj.xid;
let mut dev_reader = DevReader { dev, block_size };
let vol_loc = omap_lookup(&omap_root_block, 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}"
))
})?;
let mut apsb_block = vec![0u8; block_size as usize];
dev_reader.read(vol_loc.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(),
));
}
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 fsroot_loc = omap_lookup(
&vol_omap_root,
apsb.root_tree_oid,
apsb.obj.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}",
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, apsb.obj.xid, block_size as usize);
Ok(Self {
block_size,
total_bytes,
volume_name: apsb.volname,
fsroot_block,
fs_ctx: std::cell::RefCell::new(fs_ctx),
drec_layout,
})
}
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 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 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_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 layout = self.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 = self.fs_ctx.borrow_mut();
let mut scan =
RangeScan::start(&self.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 layout = self.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 = self.fs_ctx.borrow_mut();
let mut out = Vec::new();
let mut scan =
RangeScan::start(&self.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,
});
}
Ok(out)
}
fn lookup_inode_size(&self, dev: &mut dyn BlockDevice, oid: u64) -> Result<(u64, Option<u64>)> {
let target = FsKeyTarget {
oid,
kind: APFS_TYPE_INODE,
tail: &[],
drec_layout: self.drec_layout,
};
let block_size = self.block_size;
let mut ctx = self.fs_ctx.borrow_mut();
let mut scan =
RangeScan::start(&self.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;
if ino.mode & S_IFMT != S_IFREG {
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 target = FsKeyTarget {
oid: dstream_oid,
kind: APFS_TYPE_FILE_EXTENT,
tail: &[],
drec_layout: self.drec_layout,
};
let block_size = self.block_size;
let mut ctx = self.fs_ctx.borrow_mut();
let mut out = Vec::new();
let mut scan =
RangeScan::start(&self.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)
}
}
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::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 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 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 { .. }
));
}
}