use std::collections::HashMap;
use std::io::{Read, Seek};
use std::sync::{Arc, 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, TimeResolution, TimeSource, TimeStamp, TimeZonePolicy, VfsError,
VfsResult,
};
use crate::{DarEntry, DarError, DarReader, EntryKind};
const ARCHIVE_BLOCK: u32 = 512;
struct Node {
path: Option<Vec<u8>>,
name: Vec<u8>,
kind: NodeKind,
size: u64,
uid: u32,
gid: u32,
mode: u16,
atime: i64,
mtime: i64,
ctime: Option<i64>,
symlink_target: Option<Vec<u8>>,
children: Vec<u64>,
}
struct Inner<R: Read + Seek> {
reader: DarReader<R>,
cache: HashMap<u64, Arc<Vec<u8>>>,
}
pub struct DarVfs<R: Read + Seek> {
inner: Mutex<Inner<R>>,
nodes: Vec<Node>,
}
impl<R: Read + Seek + Send> DarVfs<R> {
pub fn open(reader: R) -> VfsResult<Self> {
let reader = DarReader::open(reader).map_err(map_err)?;
let entries = reader.entries();
let nodes = build_tree(&entries);
Ok(Self {
inner: Mutex::new(Inner {
reader,
cache: HashMap::new(),
}),
nodes,
})
}
fn lock(&self) -> MutexGuard<'_, Inner<R>> {
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: "dar file-id",
scheme: format!("Opaque({idx}) out of range"),
})
}
fn content(&self, idx: u64, path: &[u8]) -> VfsResult<Arc<Vec<u8>>> {
let mut inner = self.lock();
if let Some(data) = inner.cache.get(&idx) {
return Ok(Arc::clone(data));
}
let bytes = inner.reader.extract(path).map_err(map_err)?;
let arc = Arc::new(bytes);
inner.cache.insert(idx, Arc::clone(&arc));
Ok(arc)
}
}
fn index_of(id: FileId) -> VfsResult<u64> {
match id {
FileId::Opaque(n) => Ok(n),
other => Err(VfsError::Unsupported {
layer: "dar file-id",
scheme: format!("{other:?}"),
}),
}
}
fn require_default_stream(stream: StreamId) -> VfsResult<()> {
match stream {
StreamId::Default => Ok(()),
other => Err(VfsError::Unsupported {
layer: "dar stream",
scheme: format!("{other:?}"),
}),
}
}
fn map_err(e: DarError) -> VfsError {
match e {
DarError::Io(source) => VfsError::Io {
op: "dar read",
source,
},
DarError::NotADar => VfsError::Bootstrap {
stage: "dar mount",
detail: "not a DAR archive".to_string(),
},
other => VfsError::Decode {
layer: "dar",
offset: 0,
detail: other.to_string(),
bytes: forensic_vfs::SmallHex::new(&[]),
},
}
}
fn node_kind(kind: EntryKind) -> NodeKind {
match kind {
EntryKind::Directory => NodeKind::Dir,
EntryKind::Symlink => NodeKind::Symlink,
_ => NodeKind::File,
}
}
fn leaf(path: &[u8]) -> Vec<u8> {
match path.iter().rposition(|&b| b == b'/') {
Some(pos) => path.get(pos + 1..).unwrap_or(&[]).to_vec(),
None => path.to_vec(),
}
}
fn to_timestamp(secs: i64) -> TimeStamp {
TimeStamp {
unix_nanos: i128::from(secs) * 1_000_000_000,
source: TimeSource::Unspecified,
resolution: TimeResolution::Seconds,
}
}
fn build_tree(entries: &[DarEntry]) -> Vec<Node> {
let mut nodes: Vec<Node> = Vec::with_capacity(entries.len() + 1);
nodes.push(Node {
path: None,
name: Vec::new(),
kind: NodeKind::Dir,
size: 0,
uid: 0,
gid: 0,
mode: 0,
atime: 0,
mtime: 0,
ctime: None,
symlink_target: None,
children: Vec::new(),
});
let mut by_path: HashMap<Vec<u8>, u64> = HashMap::new();
for e in entries {
let id = nodes.len() as u64;
by_path.insert(e.path.clone(), id);
nodes.push(Node {
path: Some(e.path.clone()),
name: leaf(&e.path),
kind: node_kind(e.kind),
size: e.size,
uid: u32::try_from(e.uid).unwrap_or(u32::MAX),
gid: u32::try_from(e.gid).unwrap_or(u32::MAX),
mode: e.mode,
atime: e.atime,
mtime: e.mtime,
ctime: e.ctime,
symlink_target: e.symlink_target.clone(),
children: Vec::new(),
});
}
for (i, e) in entries.iter().enumerate() {
let child = (i + 1) as u64;
let parent_id = match e.path.iter().rposition(|&b| b == b'/') {
Some(pos) => {
let parent_path = e.path.get(..pos).unwrap_or(&[]);
by_path.get(parent_path).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<R: Read + Seek + Send> FileSystem for DarVfs<R> {
fn kind(&self) -> FsKind {
FsKind::Other
}
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::Utc
}
fn read_dir(&self, ino: FileId) -> VfsResult<DirStream> {
let node = self.node_of(ino)?;
if node.kind != NodeKind::Dir {
return Err(VfsError::Decode {
layer: "dar",
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: "dar",
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: Some(node.uid),
gid: Some(node.gid),
mode: Some(u32::from(node.mode)),
times: MacbTimes {
modified: Some(to_timestamp(node.mtime)),
accessed: Some(to_timestamp(node.atime)),
changed: node.ctime.map(to_timestamp),
born: None,
},
streams: Vec::new(),
residency: ResidencyKind::NonResident,
link_target: node.symlink_target.clone(),
})
}
fn read_at(&self, ino: FileId, stream: StreamId, off: u64, buf: &mut [u8]) -> VfsResult<usize> {
let idx = index_of(ino)?;
require_default_stream(stream)?;
let (kind, path) = {
let node = self.node_of(ino)?;
(node.kind, node.path.clone())
};
if kind != NodeKind::File {
return Ok(0);
}
let Some(path) = path else {
return Ok(0);
};
let data = self.content(idx, &path)?;
let start = usize::try_from(off).unwrap_or(usize::MAX);
if start >= data.len() {
return Ok(0);
}
let n = (data.len() - start).min(buf.len());
if let (Some(dst), Some(src)) = (buf.get_mut(..n), data.get(start..start + n)) {
dst.copy_from_slice(src);
}
Ok(n)
}
fn read_link(&self, ino: FileId, cap: usize) -> VfsResult<Vec<u8>> {
let node = self.node_of(ino)?;
match &node.symlink_target {
Some(target) => {
let n = target.len().min(cap);
Ok(target.get(..n).unwrap_or(&[]).to_vec())
}
None => 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 V11_HELLO: &[u8] = include_bytes!("../../tests/data/v11_hello.dar");
const HELLO_CONTENT: &[u8] = b"hello corpus\n";
fn open_hello() -> DarVfs<Cursor<Vec<u8>>> {
DarVfs::open(Cursor::new(V11_HELLO.to_vec())).expect("open v11_hello.dar")
}
fn hello_id(fs: &DarVfs<Cursor<Vec<u8>>>) -> FileId {
let files = fs
.lookup(fs.root(), b"files")
.expect("lookup files")
.expect("files dir present");
fs.lookup(files, b"hello.txt")
.expect("lookup hello.txt")
.expect("hello.txt present")
}
#[test]
fn kind_root_and_zone() {
let fs = open_hello();
assert_eq!(fs.kind(), FsKind::Other);
assert!(matches!(fs.root(), FileId::Opaque(0)));
assert_eq!(fs.timestamp_zone(), TimeZonePolicy::Utc);
let ss = fs.sector_sizes();
assert_eq!(ss.logical, 512);
assert_eq!(ss.cluster_or_block, 512);
assert!(ss.physical >= 512);
let m = fs.meta(fs.root()).expect("root meta");
assert_eq!(m.kind, NodeKind::Dir);
}
#[test]
fn lists_root_and_reaches_files_dir() {
let fs = open_hello();
let names: Vec<Vec<u8>> = fs
.read_dir(fs.root())
.expect("read_dir root")
.map(|e| e.expect("entry").name)
.collect();
assert!(
names.iter().any(|n| n == b"files"),
"root should list the 'files' directory, got {names:?}"
);
let files = fs
.lookup(fs.root(), b"files")
.expect("lookup")
.expect("files");
assert_eq!(fs.meta(files).expect("meta files").kind, NodeKind::Dir);
}
#[test]
fn reads_hello_content_and_meta() {
let fs = open_hello();
let id = hello_id(&fs);
let m = fs.meta(id).expect("meta hello");
assert_eq!(m.kind, NodeKind::File);
assert_eq!(m.size, HELLO_CONTENT.len() as u64);
assert_eq!(m.allocated, Allocation::Allocated);
assert!(m.times.modified.is_some(), "DAR stores mtime");
assert!(m.times.born.is_none(), "DAR stores no creation time");
let mut buf = [0u8; 64];
let n = fs
.read_at(id, StreamId::Default, 0, &mut buf)
.expect("read_at");
assert_eq!(&buf[..n], HELLO_CONTENT);
}
#[test]
fn read_at_content_matches_extract() {
let fs = open_hello();
let id = hello_id(&fs);
let mut whole = Vec::new();
let mut off = 0u64;
loop {
let mut buf = [0u8; 8];
let n = fs
.read_at(id, StreamId::Default, off, &mut buf)
.expect("read_at");
if n == 0 {
break;
}
whole.extend_from_slice(&buf[..n]);
off += n as u64;
}
let extracted = fs
.lock()
.reader
.extract(b"files/hello.txt")
.expect("extract");
assert_eq!(whole, extracted);
assert_eq!(whole, HELLO_CONTENT);
}
#[test]
fn read_at_with_offset_and_past_eof() {
let fs = open_hello();
let id = hello_id(&fs);
let mut buf = [0u8; 8];
let n = fs
.read_at(id, StreamId::Default, 6, &mut buf)
.expect("read");
assert_eq!(&buf[..n], b"corpus\n");
assert_eq!(
fs.read_at(id, StreamId::Default, 9999, &mut buf)
.expect("eof"),
0
);
}
#[test]
fn root_extents_and_hello_extents() {
let fs = open_hello();
let root_runs: Vec<_> = fs
.extents(fs.root(), StreamId::Default)
.expect("root extents")
.map(|r| r.expect("run"))
.collect();
assert!(root_runs.len() <= 1);
let id = hello_id(&fs);
let runs: Vec<_> = fs
.extents(id, StreamId::Default)
.expect("extents")
.map(|r| r.expect("run"))
.collect();
assert_eq!(runs.len(), 1);
assert_eq!(runs[0].run.len, HELLO_CONTENT.len() as u64);
assert_eq!(runs[0].alloc, RunAlloc::Allocated);
}
#[test]
fn empty_forensic_surfaces() {
let fs = open_hello();
assert_eq!(fs.deleted().unwrap().count(), 0);
assert_eq!(fs.unallocated().unwrap().count(), 0);
let id = hello_id(&fs);
assert!(fs.read_link(id, 4096).unwrap().is_empty());
}
#[test]
fn read_dir_on_a_file_is_loud() {
let fs = open_hello();
let id = hello_id(&fs);
assert!(fs.read_dir(id).is_err());
}
#[test]
fn wrong_file_id_and_stream_are_loud() {
let fs = open_hello();
assert!(fs.meta(FileId::NtfsRef { entry: 5, seq: 1 }).is_err());
assert!(fs.read_dir(FileId::NtfsRef { entry: 5, seq: 1 }).is_err());
assert!(fs
.lookup(FileId::NtfsRef { entry: 5, seq: 1 }, b"x")
.is_err());
assert!(fs
.read_link(FileId::NtfsRef { entry: 5, seq: 1 }, 8)
.is_err());
assert!(fs.meta(FileId::Opaque(9_999_999)).is_err());
let id = hello_id(&fs);
assert!(fs
.read_at(id, StreamId::Named(1), 0, &mut [0u8; 4])
.is_err());
assert!(fs.extents(id, StreamId::Named(1)).is_err());
}
#[test]
fn lookup_missing_is_none() {
let fs = open_hello();
assert!(fs.lookup(fs.root(), b"NOPE.NOTPRESENT").unwrap().is_none());
}
#[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(b"files/hello.txt"), b"hello.txt");
assert_eq!(super::leaf(b"toplevel"), b"toplevel");
assert_eq!(super::leaf(b"a/b/c"), b"c");
}
#[test]
fn map_err_maps_io_bootstrap_and_decode() {
let io = super::map_err(DarError::Io(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"eof",
)));
assert!(matches!(io, VfsError::Io { op: "dar read", .. }));
let boot = super::map_err(DarError::NotADar);
assert!(matches!(
boot,
VfsError::Bootstrap {
stage: "dar mount",
..
}
));
let decode = super::map_err(DarError::Corrupt("boom".into()));
assert!(matches!(decode, VfsError::Decode { layer: "dar", .. }));
}
#[test]
fn node_kind_maps_directory_symlink_and_other() {
assert_eq!(super::node_kind(EntryKind::Directory), NodeKind::Dir);
assert_eq!(super::node_kind(EntryKind::Symlink), NodeKind::Symlink);
assert_eq!(super::node_kind(EntryKind::File), NodeKind::File);
}
#[test]
fn lookup_on_a_file_is_loud() {
let fs = open_hello();
let id = hello_id(&fs);
assert!(fs.lookup(id, b"anything").is_err());
}
#[test]
fn read_at_on_a_directory_reads_zero() {
let fs = open_hello();
let files = fs.lookup(fs.root(), b"files").unwrap().unwrap();
let mut buf = [0u8; 8];
assert_eq!(
fs.read_at(files, StreamId::Default, 0, &mut buf).unwrap(),
0
);
}
fn blank_node(kind: NodeKind) -> Node {
Node {
path: None,
name: Vec::new(),
kind,
size: 0,
uid: 0,
gid: 0,
mode: 0,
atime: 0,
mtime: 0,
ctime: None,
symlink_target: None,
children: Vec::new(),
}
}
#[test]
fn dangling_child_id_is_skipped_not_panicked() {
let mut fs = open_hello();
let bad = fs.nodes.len() as u64;
let mut dir = blank_node(NodeKind::Dir);
dir.path = Some(b"bad".to_vec());
dir.name = b"bad".to_vec();
dir.children = vec![9_999_999];
fs.nodes.push(dir);
let id = FileId::Opaque(bad);
assert_eq!(fs.read_dir(id).unwrap().count(), 0);
assert!(fs.lookup(id, b"whatever").unwrap().is_none());
}
#[test]
fn file_node_without_a_path_reads_zero() {
let mut fs = open_hello();
let ghost = fs.nodes.len() as u64;
let mut node = blank_node(NodeKind::File);
node.size = 8; fs.nodes.push(node);
let mut buf = [0u8; 8];
assert_eq!(
fs.read_at(FileId::Opaque(ghost), StreamId::Default, 0, &mut buf)
.unwrap(),
0
);
}
#[test]
fn read_link_returns_recorded_target_and_honors_cap() {
let mut fs = open_hello();
let sym = fs.nodes.len() as u64;
let mut node = blank_node(NodeKind::Symlink);
node.path = Some(b"link".to_vec());
node.name = b"link".to_vec();
node.symlink_target = Some(b"/etc/passwd".to_vec());
fs.nodes.push(node);
let id = FileId::Opaque(sym);
assert_eq!(fs.read_link(id, 4096).unwrap(), b"/etc/passwd");
assert_eq!(fs.read_link(id, 4).unwrap(), b"/etc");
}
}