pub mod btree;
pub mod checksum;
pub mod fstree;
pub mod jrec;
pub mod obj;
pub mod omap;
pub mod snap;
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,
};
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,
snap_meta_tree_oid: u64,
volume_index: usize,
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)
.field("volume_index", &self.volume_index)
.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 {
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 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,
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>> {
if self.snap_meta_tree_oid == 0 {
return Ok(Vec::new());
}
let mut root = vec![0u8; self.block_size as usize];
let off = self
.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 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,
self.volume_index,
Some((snap.sblock_paddr, snap.xid)),
)
}
pub fn open_snapshot_by_name(&self, dev: &mut dyn BlockDevice, name: &str) -> Result<Self> {
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,
self.volume_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 target = FsKeyTarget {
oid: target_oid,
kind: APFS_TYPE_XATTR,
tail: &[],
drec_layout: self.drec_layout,
};
let block_size = self.block_size;
let mut ctx = self.fs_ctx.borrow_mut();
let mut out = std::collections::HashMap::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)
})? {
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 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;
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 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 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_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(_)));
}
}