use std::path::Path;
use std::sync::{Mutex, MutexGuard, PoisonError};
use forensic_vfs::{
Allocation, ByteRun, DirEntry as VfsDirEntry, DirStream, ExtentStream, FileId, FileSystem,
FsKind, FsMeta, MacbTimes, NodeKind, NodeStream, ResidencyKind, RunAlloc, RunFlags, RunInfo,
SectorSizes, StreamId, TimeZonePolicy, VfsError, VfsResult,
};
use crate::{Ad1Entry, Ad1Error, Ad1Reader};
const ARCHIVE_BLOCK: u32 = 512;
struct Node {
entry_idx: Option<usize>,
name: Vec<u8>,
kind: NodeKind,
size: u64,
children: Vec<u64>,
}
pub struct Ad1Vfs {
inner: Mutex<Ad1Reader>,
nodes: Vec<Node>,
}
impl Ad1Vfs {
pub fn open(path: &Path) -> VfsResult<Self> {
let reader = Ad1Reader::open(path).map_err(map_err)?;
let nodes = build_tree(reader.entries());
Ok(Self {
inner: Mutex::new(reader),
nodes,
})
}
fn lock(&self) -> MutexGuard<'_, Ad1Reader> {
self.inner.lock().unwrap_or_else(PoisonError::into_inner)
}
fn node_of(&self, id: FileId) -> VfsResult<&Node> {
let idx = index_of(id)?;
self.nodes
.get(usize::try_from(idx).unwrap_or(usize::MAX))
.ok_or(VfsError::Unsupported {
layer: "ad1 file-id",
scheme: format!("Opaque({idx}) out of range"),
})
}
}
fn index_of(id: FileId) -> VfsResult<u64> {
match id {
FileId::Opaque(n) => Ok(n),
other => Err(VfsError::Unsupported {
layer: "ad1 file-id",
scheme: format!("{other:?}"),
}),
}
}
fn require_default_stream(stream: StreamId) -> VfsResult<()> {
match stream {
StreamId::Default => Ok(()),
other => Err(VfsError::Unsupported {
layer: "ad1 stream",
scheme: format!("{other:?}"),
}),
}
}
fn map_err(e: Ad1Error) -> VfsError {
match e {
Ad1Error::Io(source) => VfsError::Io {
op: "ad1 read",
source,
},
Ad1Error::NotAd1(detail) => VfsError::Bootstrap {
stage: "ad1 mount",
detail,
},
Ad1Error::Unsupported(scheme) => VfsError::Unsupported {
layer: "ad1",
scheme,
},
Ad1Error::Malformed(detail) => VfsError::Decode {
layer: "ad1",
offset: 0,
detail,
bytes: forensic_vfs::SmallHex::new(&[]),
},
}
}
fn leaf(path: &str) -> Vec<u8> {
match path.rfind('/') {
Some(pos) => path.get(pos + 1..).unwrap_or("").as_bytes().to_vec(),
None => path.as_bytes().to_vec(),
}
}
fn build_tree(entries: &[Ad1Entry]) -> Vec<Node> {
let mut nodes: Vec<Node> = Vec::with_capacity(entries.len() + 1);
nodes.push(Node {
entry_idx: None,
name: Vec::new(),
kind: NodeKind::Dir,
size: 0,
children: Vec::new(),
});
let mut by_path: std::collections::HashMap<&str, u64> = std::collections::HashMap::new();
for (i, e) in entries.iter().enumerate() {
let id = nodes.len() as u64;
by_path.insert(e.path.as_str(), id);
nodes.push(Node {
entry_idx: Some(i),
name: leaf(&e.path),
kind: if e.is_dir {
NodeKind::Dir
} else {
NodeKind::File
},
size: e.size,
children: Vec::new(),
});
}
for e in entries {
let Some(&child) = by_path.get(e.path.as_str()) else {
continue; };
let parent_id = match e.path.rfind('/') {
Some(pos) => e
.path
.get(..pos)
.and_then(|p| by_path.get(p))
.copied()
.unwrap_or(0),
None => 0,
};
if let Some(parent) = nodes.get_mut(usize::try_from(parent_id).unwrap_or(usize::MAX)) {
parent.children.push(child);
}
}
nodes
}
impl FileSystem for Ad1Vfs {
fn kind(&self) -> FsKind {
FsKind::AD1
}
fn root(&self) -> FileId {
FileId::Opaque(0)
}
fn sector_sizes(&self) -> SectorSizes {
SectorSizes {
logical: ARCHIVE_BLOCK,
physical: ARCHIVE_BLOCK,
cluster_or_block: ARCHIVE_BLOCK,
}
}
fn timestamp_zone(&self) -> TimeZonePolicy {
TimeZonePolicy::LocalUnknown
}
fn read_dir(&self, ino: FileId) -> VfsResult<DirStream> {
let node = self.node_of(ino)?;
if node.kind != NodeKind::Dir {
return Err(VfsError::Decode {
layer: "ad1",
offset: 0,
detail: format!("node {:?} is not a directory", index_of(ino)?),
bytes: forensic_vfs::SmallHex::new(&[]),
});
}
let mut out: Vec<VfsResult<VfsDirEntry>> = Vec::with_capacity(node.children.len());
for &child in &node.children {
let Some(c) = self.nodes.get(usize::try_from(child).unwrap_or(usize::MAX)) else {
continue; };
out.push(Ok(VfsDirEntry {
name: c.name.clone(),
id: FileId::Opaque(child),
kind: c.kind,
}));
}
Ok(DirStream::new(out.into_iter()))
}
fn extents(&self, ino: FileId, stream: StreamId) -> VfsResult<ExtentStream> {
let node = self.node_of(ino)?;
require_default_stream(stream)?;
if node.size == 0 {
return Ok(ExtentStream::empty());
}
let run = RunInfo {
run: ByteRun {
image_offset: 0,
len: node.size,
flags: RunFlags::default(),
},
alloc: RunAlloc::Allocated,
};
Ok(ExtentStream::new(std::iter::once(Ok(run))))
}
fn lookup(&self, parent: FileId, name: &[u8]) -> VfsResult<Option<FileId>> {
let node = self.node_of(parent)?;
if node.kind != NodeKind::Dir {
return Err(VfsError::Decode {
layer: "ad1",
offset: 0,
detail: format!("node {:?} is not a directory", index_of(parent)?),
bytes: forensic_vfs::SmallHex::new(&[]),
});
}
for &child in &node.children {
if let Some(c) = self.nodes.get(usize::try_from(child).unwrap_or(usize::MAX)) {
if c.name == name {
return Ok(Some(FileId::Opaque(child)));
}
}
}
Ok(None)
}
fn meta(&self, ino: FileId) -> VfsResult<FsMeta> {
let idx = index_of(ino)?;
let node = self.node_of(ino)?;
Ok(FsMeta {
ino: idx,
kind: node.kind,
allocated: Allocation::Allocated,
size: node.size,
nlink: 1,
uid: None,
gid: None,
mode: None,
times: MacbTimes::default(),
streams: Vec::new(),
residency: ResidencyKind::NonResident,
link_target: None,
})
}
fn read_at(&self, ino: FileId, stream: StreamId, off: u64, buf: &mut [u8]) -> VfsResult<usize> {
require_default_stream(stream)?;
let (kind, entry_idx) = {
let node = self.node_of(ino)?;
(node.kind, node.entry_idx)
};
if kind != NodeKind::File {
return Ok(0);
}
let Some(entry_idx) = entry_idx else {
return Ok(0);
};
let guard = self.lock();
let Some(entry) = guard.entries().get(entry_idx).cloned() else {
return Ok(0); };
let mut filled = 0usize;
while filled < buf.len() {
let cur = off.saturating_add(filled as u64);
let Some(dst) = buf.get_mut(filled..) else {
break; };
let n = guard.read_at(&entry, cur, dst).map_err(map_err)?;
if n == 0 {
break;
}
filled += n;
}
Ok(filled)
}
fn read_link(&self, ino: FileId, _cap: usize) -> VfsResult<Vec<u8>> {
self.node_of(ino)?;
Ok(Vec::new())
}
fn deleted(&self) -> VfsResult<NodeStream> {
Ok(NodeStream::empty())
}
fn unallocated(&self) -> VfsResult<ExtentStream> {
Ok(ExtentStream::empty())
}
}
#[cfg(all(test, feature = "testfix"))]
mod tests {
use super::*;
use crate::testfix;
use forensic_vfs::{
Allocation, FileId, FileSystem, FsKind, NodeKind, RunAlloc, StreamId, TimeZonePolicy,
};
fn open_sample() -> (tempfile::TempDir, Ad1Vfs, Vec<testfix::Expected>) {
let built = testfix::build(testfix::sample_tree());
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("image.ad1");
std::fs::write(&path, &built.bytes).unwrap();
let fs = Ad1Vfs::open(&path).unwrap();
(dir, fs, built.expected)
}
fn expected_of<'a>(exp: &'a [testfix::Expected], path: &str) -> &'a testfix::Expected {
exp.iter().find(|e| e.path == path).expect("expected entry")
}
fn resolve(fs: &Ad1Vfs, parts: &[&[u8]]) -> FileId {
let mut id = fs.root();
for p in parts {
id = fs.lookup(id, p).unwrap().unwrap();
}
id
}
fn read_all(fs: &Ad1Vfs, id: FileId) -> Vec<u8> {
let mut out = Vec::new();
let mut off = 0u64;
loop {
let mut buf = [0u8; 4096];
let n = fs.read_at(id, StreamId::Default, off, &mut buf).unwrap();
if n == 0 {
break;
}
out.extend_from_slice(&buf[..n]);
off += n as u64;
}
out
}
#[test]
fn kind_root_zone_and_sectors() {
let (_d, fs, _e) = open_sample();
assert_eq!(fs.kind(), FsKind::AD1);
assert!(matches!(fs.root(), FileId::Opaque(0)));
assert_eq!(fs.timestamp_zone(), TimeZonePolicy::LocalUnknown);
let ss = fs.sector_sizes();
assert_eq!(ss.logical, 512);
assert_eq!(ss.cluster_or_block, 512);
assert!(ss.physical >= 512);
assert_eq!(fs.meta(fs.root()).unwrap().kind, NodeKind::Dir);
}
#[test]
fn lists_root_and_reaches_root_dir() {
let (_d, fs, _e) = open_sample();
let names: Vec<Vec<u8>> = fs
.read_dir(fs.root())
.unwrap()
.map(|e| e.unwrap().name)
.collect();
assert!(
names.iter().any(|n| n == b"root"),
"synthetic root should list the 'root' dir, got {names:?}"
);
let root_dir = fs.lookup(fs.root(), b"root").unwrap().unwrap();
assert_eq!(fs.meta(root_dir).unwrap().kind, NodeKind::Dir);
}
#[test]
fn reads_hello_meta_and_content() {
let (_d, fs, exp) = open_sample();
let e = expected_of(&exp, "root/hello.txt");
let id = resolve(&fs, &[b"root", b"hello.txt"]);
let m = fs.meta(id).unwrap();
assert_eq!(m.kind, NodeKind::File);
assert_eq!(m.size, e.size);
assert_eq!(m.allocated, Allocation::Allocated);
assert!(m.times.modified.is_none());
assert!(m.times.accessed.is_none());
assert!(m.times.changed.is_none());
assert!(m.times.born.is_none());
assert_eq!(m.uid, None);
assert_eq!(m.gid, None);
assert_eq!(m.mode, None);
assert_eq!(read_all(&fs, id), *e.data.as_ref().unwrap());
}
#[test]
fn reads_large_file_spanning_chunks() {
let (_d, fs, exp) = open_sample();
let e = expected_of(&exp, "root/sub/a.bin");
let id = resolve(&fs, &[b"root", b"sub", b"a.bin"]);
let m = fs.meta(id).unwrap();
assert_eq!(m.size, e.size);
assert!(m.size > u64::from(testfix::CHUNK_SIZE), "spans >1 chunk");
assert_eq!(&read_all(&fs, id), e.data.as_ref().unwrap());
}
#[test]
fn directory_reports_dir_kind() {
let (_d, fs, _e) = open_sample();
let id = resolve(&fs, &[b"root", b"sub"]);
assert_eq!(fs.meta(id).unwrap().kind, NodeKind::Dir);
assert!(fs.read_dir(id).is_ok());
}
#[test]
fn empty_file_reads_zero_and_no_extents() {
let (_d, fs, _e) = open_sample();
let id = resolve(&fs, &[b"root", b"sub", b"empty.dat"]);
let m = fs.meta(id).unwrap();
assert_eq!(m.size, 0);
assert_eq!(m.kind, NodeKind::File);
let mut buf = [0u8; 8];
assert_eq!(fs.read_at(id, StreamId::Default, 0, &mut buf).unwrap(), 0);
assert_eq!(fs.extents(id, StreamId::Default).unwrap().count(), 0);
}
#[test]
fn extents_hello_single_run_and_root() {
let (_d, fs, exp) = open_sample();
let e = expected_of(&exp, "root/hello.txt");
let id = resolve(&fs, &[b"root", b"hello.txt"]);
let runs: Vec<_> = fs
.extents(id, StreamId::Default)
.unwrap()
.map(|r| r.unwrap())
.collect();
assert_eq!(runs.len(), 1);
assert_eq!(runs[0].run.len, e.size);
assert_eq!(runs[0].alloc, RunAlloc::Allocated);
let root_runs: Vec<_> = fs
.extents(fs.root(), StreamId::Default)
.unwrap()
.map(|r| r.unwrap())
.collect();
assert!(root_runs.len() <= 1);
}
#[test]
fn read_at_offset_and_past_eof() {
let (_d, fs, _e) = open_sample();
let id = resolve(&fs, &[b"root", b"hello.txt"]);
let mut buf = [0u8; 8];
let n = fs.read_at(id, StreamId::Default, 7, &mut buf).unwrap();
assert_eq!(&buf[..n], b"AD1!\n");
assert_eq!(
fs.read_at(id, StreamId::Default, 9999, &mut buf).unwrap(),
0
);
}
#[test]
fn wrong_file_id_and_stream_are_loud() {
let (_d, fs, _e) = open_sample();
let bad = FileId::NtfsRef { entry: 5, seq: 1 };
assert!(fs.meta(bad).is_err());
assert!(fs.read_dir(bad).is_err());
assert!(fs.lookup(bad, b"x").is_err());
assert!(fs.read_link(bad, 8).is_err());
assert!(fs.meta(FileId::Opaque(9_999_999)).is_err());
let id = resolve(&fs, &[b"root", b"hello.txt"]);
assert!(fs
.read_at(id, StreamId::Named(1), 0, &mut [0u8; 4])
.is_err());
assert!(fs.extents(id, StreamId::Named(1)).is_err());
assert!(fs.read_dir(id).is_err());
}
#[test]
fn lookup_missing_is_none() {
let (_d, fs, _e) = open_sample();
assert!(fs.lookup(fs.root(), b"NOPE.NOTPRESENT").unwrap().is_none());
}
#[test]
fn empty_forensic_surfaces() {
let (_d, fs, _e) = open_sample();
assert_eq!(fs.deleted().unwrap().count(), 0);
assert_eq!(fs.unallocated().unwrap().count(), 0);
let id = resolve(&fs, &[b"root", b"hello.txt"]);
assert!(fs.read_link(id, 4096).unwrap().is_empty());
}
#[test]
fn index_of_rejects_non_opaque() {
assert!(super::index_of(FileId::Opaque(42)).is_ok());
assert!(super::index_of(FileId::NtfsRef { entry: 1, seq: 1 }).is_err());
}
#[test]
fn leaf_splits_on_last_separator() {
assert_eq!(super::leaf("root/sub/a.bin"), b"a.bin");
assert_eq!(super::leaf("toplevel"), b"toplevel");
assert_eq!(super::leaf("a/b/c"), b"c");
}
}