use std::io::{Read, Seek, SeekFrom};
use std::sync::Mutex;
use forensic_vfs::{
Allocation, ByteRun, DirEntry, DirStream, ExtentStream, FileId, FileSystem, FsKind, FsMeta,
MacbTimes, NodeKind, NodeStream, ResidencyKind, RunAlloc, RunFlags, RunInfo, SectorSizes,
SmallHex, StreamId, TimeResolution, TimeSource, TimeStamp, TimeZonePolicy, VfsError, VfsResult,
};
use crate::dir::{self, ROOT_DIR_INO_NUM};
use crate::extent;
use crate::inode::Inode;
use crate::volume::ApfsVolume;
use crate::{ApfsContainer, ApfsError, Result};
const S_IFMT: u16 = 0xF000;
const S_IFDIR: u16 = 0x4000;
const S_IFREG: u16 = 0x8000;
const S_IFLNK: u16 = 0xA000;
const DT_MASK: u16 = 0x0F;
const DT_DIR: u16 = 4;
const DT_REG: u16 = 8;
const DT_LNK: u16 = 10;
pub struct ApfsFs<R: Read + Seek> {
reader: Mutex<R>,
volume: ApfsVolume,
block_size: usize,
root_xid: u64,
}
impl<R: Read + Seek> ApfsFs<R> {
pub fn open(reader: R) -> Result<Self> {
let (reader, volume, block_size) = Self::open_first_volume(reader)?;
let root_xid = volume.xid();
Ok(Self {
reader: Mutex::new(reader),
volume,
block_size,
root_xid,
})
}
fn open_first_volume(reader: R) -> Result<(R, ApfsVolume, usize)> {
let mut container = ApfsContainer::open(reader)?;
let block_size = container.superblock().block_size as usize;
let addrs = container.volume_superblock_addrs()?;
let vaddr = *addrs.first().ok_or(ApfsError::NoValidSuperblock {
checked: 0,
last_magic: 0,
})?;
let mut reader = container.into_reader();
let offset = vaddr.saturating_mul(block_size as u64);
reader.seek(SeekFrom::Start(offset))?;
let mut buf = vec![0u8; block_size];
reader.read_exact(&mut buf)?;
let volume = ApfsVolume::parse(&buf)?;
Ok((reader, volume, block_size))
}
pub fn open_snapshot(reader: R, xid: u64) -> Result<Self> {
let (mut reader, live, block_size) = Self::open_first_volume(reader)?;
if xid == live.xid() {
return Ok(Self {
reader: Mutex::new(reader),
volume: live,
block_size,
root_xid: xid,
});
}
let snaps = crate::snapshot::list_snapshots(&mut reader, &live, block_size)?;
let snapshot = snaps
.iter()
.find(|s| s.xid == xid)
.ok_or(ApfsError::SnapshotNotFound { xid })?;
let volume = crate::snapshot::mount_snapshot(&mut reader, &live, snapshot, block_size)?;
Ok(Self {
reader: Mutex::new(reader),
volume,
block_size,
root_xid: xid,
})
}
pub fn snapshots(reader: R) -> Result<Vec<crate::snapshot::Snapshot>> {
let (mut reader, volume, block_size) = Self::open_first_volume(reader)?;
crate::snapshot::list_snapshots(&mut reader, &volume, block_size)
}
}
fn oid_of(id: FileId) -> VfsResult<u64> {
match id {
FileId::ApfsOid { oid, .. } => Ok(oid),
other => Err(VfsError::Unsupported {
layer: "apfs file-id",
scheme: format!("{other:?}"),
}),
}
}
fn require_default_stream(stream: StreamId) -> VfsResult<()> {
match stream {
StreamId::Default => Ok(()),
other => Err(VfsError::Unsupported {
layer: "apfs stream",
scheme: format!("{other:?}"),
}),
}
}
fn map_err(e: ApfsError) -> VfsError {
match e {
ApfsError::Io(source) => VfsError::Io {
op: "apfs read",
source,
},
other => VfsError::Decode {
layer: "apfs",
offset: 0,
detail: other.to_string(),
bytes: SmallHex::new(&[]),
},
}
}
fn poisoned() -> VfsError {
VfsError::Bootstrap {
stage: "apfs reader lock",
detail: "interior reader mutex poisoned".to_string(),
}
}
fn dt_to_kind(flags: u16) -> NodeKind {
match flags & DT_MASK {
DT_DIR => NodeKind::Dir,
DT_REG => NodeKind::File,
DT_LNK => NodeKind::Symlink,
_ => NodeKind::Other,
}
}
fn mode_to_kind(mode: u16) -> NodeKind {
match mode & S_IFMT {
S_IFDIR => NodeKind::Dir,
S_IFREG => NodeKind::File,
S_IFLNK => NodeKind::Symlink,
_ => NodeKind::Other,
}
}
fn build_meta(inode: &Inode) -> FsMeta {
let ts = |ns: u64| TimeStamp {
unix_nanos: i128::from(ns),
source: TimeSource::InodeTable,
resolution: TimeResolution::Nanos,
};
FsMeta {
ino: inode.oid,
kind: mode_to_kind(inode.mode),
allocated: Allocation::Allocated,
size: inode.size.unwrap_or(0),
nlink: inode.nlink_or_nchildren.max(0) as u32,
uid: Some(inode.uid),
gid: Some(inode.gid),
mode: Some(u32::from(inode.mode)),
times: MacbTimes {
born: Some(ts(inode.create_time)),
modified: Some(ts(inode.mod_time)),
changed: Some(ts(inode.change_time)),
accessed: Some(ts(inode.access_time)),
},
streams: Vec::new(),
residency: ResidencyKind::NonResident,
link_target: None,
}
}
impl<R: Read + Seek + Send> FileSystem for ApfsFs<R> {
fn kind(&self) -> FsKind {
FsKind::APFS
}
fn root(&self) -> FileId {
FileId::ApfsOid {
oid: ROOT_DIR_INO_NUM,
xid: self.root_xid,
}
}
fn sector_sizes(&self) -> SectorSizes {
let bs = self.block_size as u32;
SectorSizes {
logical: bs,
physical: bs,
cluster_or_block: bs,
}
}
fn timestamp_zone(&self) -> TimeZonePolicy {
TimeZonePolicy::Utc
}
fn read_dir(&self, ino: FileId) -> VfsResult<DirStream> {
let parent = oid_of(ino)?;
let xid = self.root_xid;
let mut guard = self.reader.lock().map_err(|_| poisoned())?;
let entries =
dir::list_dir(&mut *guard, &self.volume, parent, self.block_size).map_err(map_err)?;
let out: Vec<VfsResult<DirEntry>> = entries
.into_iter()
.map(|e| {
Ok(DirEntry {
kind: dt_to_kind(e.flags),
name: e.name.into_bytes(),
id: FileId::ApfsOid {
oid: e.file_id,
xid,
},
})
})
.collect();
Ok(DirStream::new(out.into_iter()))
}
fn extents(&self, ino: FileId, stream: StreamId) -> VfsResult<ExtentStream> {
let node = oid_of(ino)?;
require_default_stream(stream)?;
let bs = self.block_size;
let mut guard = self.reader.lock().map_err(|_| poisoned())?;
let inode = dir::load_inode(&mut *guard, &self.volume, node, bs).map_err(map_err)?;
let exts = extent::list_extents(&mut *guard, &self.volume, inode.private_id, bs)
.map_err(map_err)?;
let out: Vec<VfsResult<RunInfo>> = exts
.into_iter()
.map(|x| {
let sparse = x.phys_block_num == 0;
Ok(RunInfo {
run: ByteRun {
image_offset: x.phys_block_num.saturating_mul(bs as u64),
len: x.len,
flags: RunFlags {
sparse,
..RunFlags::default()
},
},
alloc: RunAlloc::Allocated,
})
})
.collect();
Ok(ExtentStream::new(out.into_iter()))
}
fn lookup(&self, parent: FileId, name: &[u8]) -> VfsResult<Option<FileId>> {
let parent = oid_of(parent)?;
let xid = self.root_xid;
let Ok(name) = std::str::from_utf8(name) else {
return Ok(None);
};
let mut guard = self.reader.lock().map_err(|_| poisoned())?;
let found = dir::lookup_child(&mut *guard, &self.volume, parent, name, self.block_size)
.map_err(map_err)?;
Ok(found.map(|oid| FileId::ApfsOid { oid, xid }))
}
fn meta(&self, ino: FileId) -> VfsResult<FsMeta> {
let node = oid_of(ino)?;
let mut guard = self.reader.lock().map_err(|_| poisoned())?;
let inode =
dir::load_inode(&mut *guard, &self.volume, node, self.block_size).map_err(map_err)?;
Ok(build_meta(&inode))
}
fn read_at(&self, ino: FileId, stream: StreamId, off: u64, buf: &mut [u8]) -> VfsResult<usize> {
let node = oid_of(ino)?;
require_default_stream(stream)?;
let bs = self.block_size;
let mut guard = self.reader.lock().map_err(|_| poisoned())?;
let inode = dir::load_inode(&mut *guard, &self.volume, node, bs).map_err(map_err)?;
let data = extent::read_data(&mut *guard, &self.volume, &inode, bs).map_err(map_err)?;
let start = usize::try_from(off).unwrap_or(usize::MAX);
if start >= data.len() {
return Ok(0);
}
let n = buf.len().min(data.len() - start);
if let (Some(dst), Some(src)) = (buf.get_mut(..n), data.get(start..start + n)) {
dst.copy_from_slice(src);
return Ok(n);
}
Ok(0) }
fn read_link(&self, _ino: FileId, _cap: usize) -> VfsResult<Vec<u8>> {
Ok(Vec::new())
}
fn deleted(&self) -> VfsResult<NodeStream> {
Ok(NodeStream::empty())
}
fn unallocated(&self) -> VfsResult<ExtentStream> {
Ok(ExtentStream::empty())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
const CONTENT: &[u8] = include_bytes!("../../tests/data/apfs_content.bin");
fn live_xid() -> u64 {
ApfsFs::open(Cursor::new(CONTENT))
.expect("open live apfs")
.root_xid
}
#[test]
fn p4_fixture_lists_zero_snapshots() {
let snaps = ApfsFs::snapshots(Cursor::new(CONTENT)).expect("list snapshots");
assert!(
snaps.is_empty(),
"P4 fixture has no snapshots; got {snaps:?}"
);
}
#[test]
fn open_snapshot_at_live_xid_reads_plain_txt() {
let fs = ApfsFs::open_snapshot(Cursor::new(CONTENT), live_xid())
.expect("mount live-xid snapshot");
let root = fs.root();
let found = fs
.lookup(root, b"plain.txt")
.expect("lookup plain.txt")
.expect("plain.txt present at root");
let mut buf = [0u8; 64];
let n = fs
.read_at(found, StreamId::Default, 0, &mut buf)
.expect("read plain.txt");
assert!(n > 0, "plain.txt should have content");
}
#[test]
fn open_snapshot_unknown_xid_fails_loud() {
let bogus = live_xid().wrapping_add(0xDEAD_BEEF);
let err = ApfsFs::open_snapshot(Cursor::new(CONTENT), bogus)
.err()
.expect("a bogus xid must fail, not mount");
let ApfsError::SnapshotNotFound { xid } = err else {
unreachable!("bogus xid must be SnapshotNotFound, got {err:?}") };
assert_eq!(xid, bogus);
}
#[test]
fn dt_to_kind_covers_every_arm() {
assert_eq!(dt_to_kind(DT_DIR), NodeKind::Dir);
assert_eq!(dt_to_kind(DT_REG), NodeKind::File);
assert_eq!(dt_to_kind(DT_LNK), NodeKind::Symlink);
assert_eq!(dt_to_kind(1), NodeKind::Other);
}
#[test]
fn mode_to_kind_covers_every_arm() {
assert_eq!(mode_to_kind(S_IFDIR), NodeKind::Dir);
assert_eq!(mode_to_kind(S_IFREG), NodeKind::File);
assert_eq!(mode_to_kind(S_IFLNK), NodeKind::Symlink);
assert_eq!(mode_to_kind(0xC000), NodeKind::Other);
}
#[test]
fn oid_of_rejects_a_non_apfs_file_id() {
let err = oid_of(FileId::Opaque(7)).expect_err("non-APFS id must be rejected");
let VfsError::Unsupported { layer, scheme } = err else {
unreachable!("oid_of(Opaque) must be Unsupported, got {err:?}") };
assert_eq!(layer, "apfs file-id");
assert!(scheme.contains("Opaque"), "scheme should name the variant");
assert_eq!(
oid_of(FileId::ApfsOid { oid: 42, xid: 9 }).expect("apfs id accepted"),
42
);
}
#[test]
fn require_default_stream_rejects_a_named_stream() {
require_default_stream(StreamId::Default).expect("default stream is accepted");
let err = require_default_stream(StreamId::Named(3))
.expect_err("a named stream must be refused loud");
let VfsError::Unsupported { layer, scheme } = err else {
unreachable!("Named stream must be Unsupported, got {err:?}") };
assert_eq!(layer, "apfs stream");
assert!(scheme.contains("Named"), "scheme should name the variant");
}
#[test]
fn map_err_keeps_io_distinct_from_decode() {
let io = ApfsError::Io(std::io::Error::from(std::io::ErrorKind::UnexpectedEof));
let mapped = map_err(io);
let VfsError::Io { op, .. } = mapped else {
unreachable!("Io maps to VfsError::Io, got {mapped:?}") };
assert_eq!(op, "apfs read");
let mapped = map_err(ApfsError::SnapshotNotFound { xid: 5 });
let VfsError::Decode { layer, detail, .. } = mapped else {
unreachable!("non-Io maps to VfsError::Decode, got {mapped:?}") };
assert_eq!(layer, "apfs");
assert!(detail.contains('5'), "detail should carry the message");
}
#[test]
fn poisoned_lock_is_a_named_bootstrap_failure() {
let err = poisoned();
let VfsError::Bootstrap { stage, .. } = err else {
unreachable!("poisoned() is a Bootstrap error, got {err:?}") };
assert_eq!(stage, "apfs reader lock");
}
}