use std::collections::HashMap;
use std::path::Path;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use forensic_vfs::{
Allocation, ByteRun, DirEntry as VfsDirEntry, DirStream, DynFs, DynSource, ExtentStream,
FileId, FileSystem, FsKind, FsMeta, MacbTimes, NodeKind, NodeStream, ResidencyKind, RunAlloc,
RunFlags, RunInfo, SectorSizes, SmallHex, StreamId, TimeZonePolicy, VfsError, VfsResult,
};
const ARCHIVE_BLOCK: u32 = 512;
trait Members: Send {
fn read_member(&mut self, index: usize) -> VfsResult<Vec<u8>>;
}
struct Flat {
name: String,
size: u64,
is_dir: bool,
index: usize,
}
struct Node {
entry_idx: Option<usize>,
name: Vec<u8>,
kind: NodeKind,
size: u64,
children: Vec<u64>,
}
struct Inner<R: Members> {
reader: R,
cache: HashMap<u64, Arc<Vec<u8>>>,
}
struct ContainerFs<R: Members> {
inner: Mutex<Inner<R>>,
nodes: Vec<Node>,
kind: FsKind,
}
impl<R: Members> ContainerFs<R> {
fn new(reader: R, nodes: Vec<Node>, kind: FsKind) -> Self {
Self {
inner: Mutex::new(Inner {
reader,
cache: HashMap::new(),
}),
nodes,
kind,
}
}
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: "container file-id",
scheme: format!("Opaque({idx}) out of range"),
})
}
fn content(&self, node_id: u64, entry_idx: usize) -> VfsResult<Arc<Vec<u8>>> {
let mut inner = self.lock();
if let Some(data) = inner.cache.get(&node_id) {
return Ok(Arc::clone(data));
}
let bytes = inner.reader.read_member(entry_idx)?;
let arc = Arc::new(bytes);
inner.cache.insert(node_id, Arc::clone(&arc));
Ok(arc)
}
}
fn index_of(id: FileId) -> VfsResult<u64> {
match id {
FileId::Opaque(n) => Ok(n),
other => Err(VfsError::Unsupported {
layer: "container file-id",
scheme: format!("{other:?}"),
}),
}
}
fn require_default_stream(stream: StreamId) -> VfsResult<()> {
match stream {
StreamId::Default => Ok(()),
other => Err(VfsError::Unsupported {
layer: "container stream",
scheme: format!("{other:?}"),
}),
}
}
fn build_tree(members: &[Flat]) -> Vec<Node> {
let mut nodes: Vec<Node> = Vec::with_capacity(members.len() + 1);
nodes.push(Node {
entry_idx: None,
name: Vec::new(),
kind: NodeKind::Dir,
size: 0,
children: Vec::new(),
});
let mut by_path: HashMap<String, u64> = HashMap::new();
by_path.insert(String::new(), 0);
for m in members {
let comps: Vec<&str> = m
.name
.split(['/', '\\'])
.filter(|c| !c.is_empty() && *c != ".")
.collect();
let Some(last) = comps.len().checked_sub(1) else {
continue; };
let mut parent_id = 0u64;
let mut acc = String::new();
for (ci, comp) in comps.iter().enumerate() {
if !acc.is_empty() {
acc.push('/');
}
acc.push_str(comp);
if ci == last {
if let Some(&existing) = by_path.get(&acc) {
if let Some(n) = nodes.get_mut(usize::try_from(existing).unwrap_or(usize::MAX))
{
if n.entry_idx.is_none() {
n.entry_idx = Some(m.index);
}
}
} else {
let id = nodes.len() as u64;
nodes.push(Node {
entry_idx: Some(m.index),
name: comp.as_bytes().to_vec(),
kind: if m.is_dir {
NodeKind::Dir
} else {
NodeKind::File
},
size: if m.is_dir { 0 } else { m.size },
children: Vec::new(),
});
by_path.insert(acc.clone(), id);
push_child(&mut nodes, parent_id, id);
}
} else if let Some(&existing) = by_path.get(&acc) {
parent_id = existing;
} else {
let id = nodes.len() as u64;
nodes.push(Node {
entry_idx: None,
name: comp.as_bytes().to_vec(),
kind: NodeKind::Dir,
size: 0,
children: Vec::new(),
});
by_path.insert(acc.clone(), id);
push_child(&mut nodes, parent_id, id);
parent_id = id;
}
}
}
nodes
}
fn push_child(nodes: &mut [Node], parent_id: u64, child: u64) {
if let Some(parent) = nodes.get_mut(usize::try_from(parent_id).unwrap_or(usize::MAX)) {
parent.children.push(child);
}
}
impl<R: Members> FileSystem for ContainerFs<R> {
fn kind(&self) -> FsKind {
self.kind
}
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(not_a_dir(ino)?);
}
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(not_a_dir(parent)?);
}
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> {
let idx = index_of(ino)?;
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 data = self.content(idx, entry_idx)?;
let Ok(start) = usize::try_from(off) else {
return Ok(0);
};
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>> {
self.node_of(ino)?;
Ok(Vec::new())
}
fn deleted(&self) -> VfsResult<NodeStream> {
Ok(NodeStream::empty())
}
fn unallocated(&self) -> VfsResult<ExtentStream> {
Ok(ExtentStream::empty())
}
}
fn not_a_dir(id: FileId) -> VfsResult<VfsError> {
Ok(VfsError::Decode {
layer: "container",
offset: 0,
detail: format!("node {:?} is not a directory", index_of(id)?),
bytes: SmallHex::new(&[]),
})
}
struct ArchiveBackend(archive_core::Archive);
impl Members for ArchiveBackend {
fn read_member(&mut self, index: usize) -> VfsResult<Vec<u8>> {
self.0.read(index).map_err(|e| VfsError::Decode {
layer: "archive",
offset: 0,
detail: e.to_string(),
bytes: SmallHex::new(&[]),
})
}
}
fn archive_kind(format: archive_core::Format) -> FsKind {
match format {
archive_core::Format::Zip => FsKind::ZIP,
archive_core::Format::SevenZip => FsKind::from_name("7z"),
_ => FsKind::from_name("tar"),
}
}
pub(crate) fn open_archive(base: &DynSource, name: Option<&str>) -> VfsResult<Option<DynFs>> {
let len = base.len();
let mut bytes = vec![0u8; usize::try_from(len).unwrap_or(usize::MAX)];
let n = base.read_at(0, &mut bytes)?;
bytes.truncate(n);
let Some(archive) =
archive_core::Archive::open(&bytes, name).map_err(|e| VfsError::Decode {
layer: "archive",
offset: 0,
detail: e.to_string(),
bytes: SmallHex::new(&[]),
})?
else {
return Ok(None);
};
let kind = archive_kind(archive.format());
let members: Vec<Flat> = archive
.entries()
.iter()
.enumerate()
.map(|(index, e)| Flat {
name: e.name.clone(),
size: e.size,
is_dir: e.is_dir,
index,
})
.collect();
let nodes = build_tree(&members);
Ok(Some(Arc::new(ContainerFs::new(
ArchiveBackend(archive),
nodes,
kind,
))))
}
pub(crate) fn open_ad1(path: &Path) -> VfsResult<Option<DynFs>> {
match ad1::Ad1Vfs::open(path) {
Ok(fs) => Ok(Some(Arc::new(fs))),
Err(VfsError::Bootstrap {
stage: "ad1 mount", ..
}) => Ok(None),
Err(e) => Err(e),
}
}
struct DarBackend {
reader: dar::DarReader<std::fs::File>,
paths: Vec<Vec<u8>>,
}
impl Members for DarBackend {
fn read_member(&mut self, index: usize) -> VfsResult<Vec<u8>> {
let key = self
.paths
.get(index)
.ok_or(VfsError::Unsupported {
layer: "dar member",
scheme: format!("index {index} out of range"),
})?
.clone();
self.reader.extract(&key).map_err(|e| VfsError::Decode {
layer: "dar",
offset: 0,
detail: e.to_string(),
bytes: SmallHex::new(&[]),
})
}
}
pub(crate) fn open_dar(path: &Path) -> VfsResult<Option<DynFs>> {
let file = std::fs::File::open(path).map_err(|source| VfsError::Io {
op: "dar open",
source,
})?;
let reader = match dar::DarReader::open(file) {
Ok(r) => r,
Err(dar::DarError::NotADar) => return Ok(None),
Err(e) => {
return Err(VfsError::Decode {
layer: "dar",
offset: 0,
detail: e.to_string(),
bytes: SmallHex::new(&[]),
})
}
};
let entries = reader.entries();
let mut paths: Vec<Vec<u8>> = Vec::with_capacity(entries.len());
let members: Vec<Flat> = entries
.iter()
.enumerate()
.map(|(index, e)| {
paths.push(e.path.clone());
Flat {
name: String::from_utf8_lossy(&e.path).into_owned(),
size: e.size,
is_dir: matches!(e.kind, dar::EntryKind::Directory),
index,
}
})
.collect();
let nodes = build_tree(&members);
Ok(Some(Arc::new(ContainerFs::new(
DarBackend { reader, paths },
nodes,
FsKind::DAR,
))))
}
struct Aff4LogicalBackend(aff4::LogicalContainer);
impl Members for Aff4LogicalBackend {
fn read_member(&mut self, index: usize) -> VfsResult<Vec<u8>> {
let entry = self
.0
.files()
.get(index)
.ok_or(VfsError::Unsupported {
layer: "aff4-logical member",
scheme: format!("index {index} out of range"),
})?
.clone();
self.0.read_file(&entry).map_err(|e| VfsError::Decode {
layer: "aff4-logical",
offset: 0,
detail: e.to_string(),
bytes: SmallHex::new(&[]),
})
}
}
pub(crate) fn open_aff4_logical(path: &Path) -> VfsResult<Option<DynFs>> {
match aff4::container_kind(path) {
Ok(aff4::ContainerKind::Logical) => {}
Ok(_) | Err(_) => return Ok(None),
}
let container = aff4::LogicalContainer::open(path).map_err(|e| VfsError::Decode {
layer: "aff4-logical",
offset: 0,
detail: e.to_string(),
bytes: SmallHex::new(&[]),
})?;
let members: Vec<Flat> = container
.files()
.iter()
.enumerate()
.map(|(index, e)| Flat {
name: e.original_file_name.clone(),
size: e.size,
is_dir: false,
index,
})
.collect();
let nodes = build_tree(&members);
Ok(Some(Arc::new(ContainerFs::new(
Aff4LogicalBackend(container),
nodes,
FsKind::from_name("aff4"),
))))
}