#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
use std::collections::HashSet;
use state_history_forensic::epoch::EpochTag;
use forensic_vfs::{
ArchiveContents, DynSource, FileId, FileSystem, FsMeta, Layer, NodeAddr, NodeKind, Openers,
PathSpec, SnapshotRef, SniffWindow, VfsResult,
};
const MAX_DEPTH: usize = 8;
const SNIFF_CAP: u64 = 128 * 1024;
const TAIL_CAP: u64 = 4096;
const WALK_MAX_DEPTH: usize = 256;
pub struct Evidence {
pub root: PathSpec,
pub fs: Option<forensic_vfs::DynFs>,
}
pub struct Resolved {
pub fs: forensic_vfs::DynFs,
pub spec: PathSpec,
pub source: DynSource,
pub source_spec: PathSpec,
}
pub trait SourceOpen {
fn open(&self, source: DynSource, spec: PathSpec, depth: usize) -> VfsResult<Option<Resolved>>;
}
impl SourceOpen for Openers {
fn open(&self, source: DynSource, spec: PathSpec, depth: usize) -> VfsResult<Option<Resolved>> {
if depth > MAX_DEPTH {
return Ok(None);
}
let total = source.len();
let cap = total.clamp(1, SNIFF_CAP) as usize;
let mut head = vec![0u8; cap];
let n = source.read_at(0, &mut head)?;
let tail_cap = total.min(TAIL_CAP);
let tail_start = total - tail_cap;
let mut tail = vec![0u8; tail_cap as usize];
let tn = source.read_at(tail_start, &mut tail)?;
let window = SniffWindow::with_tail(
0,
head.get(..n).unwrap_or(&[]),
total,
tail.get(..tn).unwrap_or(&[]),
);
for probe in self.filesystems() {
if probe.probe(&window).is_candidate() {
let fs = probe.open(source.clone())?;
let fs_spec = spec.clone().push(Layer::Fs {
kind: probe.kind(),
at: NodeAddr::Path(Vec::new()),
});
return Ok(Some(Resolved {
fs,
spec: fs_spec,
source,
source_spec: spec,
}));
}
}
for vsp in self.volume_systems() {
if vsp.probe(&window).is_candidate() {
let vs = vsp.open(source.clone())?;
for index in 0..vs.volumes().len() {
let sub = vs.open_volume(index)?;
let child = spec.clone().push(Layer::Volume {
scheme: vsp.scheme(),
index,
guid: None,
});
if let Some(found) = self.open(sub, child, depth + 1)? {
return Ok(Some(found));
}
}
}
}
for cd in self.containers() {
if cd.probe(&window).is_candidate() {
let decoded = cd.open(source.clone())?;
let child = spec.clone().push(Layer::Container {
format: cd.format(),
});
if let Some(found) = self.open(decoded, child, depth + 1)? {
return Ok(Some(found));
}
}
}
for ar in self.archives() {
if ar.probe(&window).is_candidate() {
match ar.open(source.clone())? {
ArchiveContents::Stream(inner) => {
let child = spec.clone().push(Layer::Archive { member: None });
if let Some(found) = self.open(inner, child, depth + 1)? {
return Ok(Some(found));
}
}
ArchiveContents::Members(members) => {
for (index, member) in members.into_iter().enumerate() {
let child = spec.clone().push(Layer::Archive {
member: Some(index),
});
if let Some(found) = self.open(member.source, child, depth + 1)? {
return Ok(Some(found));
}
}
}
_ => {} }
}
}
Ok(None)
}
}
#[derive(Debug, Clone)]
pub struct SnapshotView {
pub epoch: EpochTag,
pub xid: u64,
pub name: String,
pub locator: PathSpec,
}
#[must_use]
pub fn snapshot_view(
source_spec: &PathSpec,
xid: u64,
name: String,
create_time: u64,
) -> SnapshotView {
SnapshotView {
epoch: epoch_from_create_time(create_time),
xid,
name,
locator: source_spec.clone().push(Layer::Snapshot {
store: SnapshotRef::ApfsXid(xid),
}),
}
}
#[must_use]
pub fn epoch_from_create_time(create_time_ns: u64) -> EpochTag {
let mut bytes = [0u8; 32];
bytes[24..32].copy_from_slice(&create_time_ns.to_be_bytes());
EpochTag::from_bytes(bytes)
}
pub struct WalkEntry {
pub path: Vec<Vec<u8>>,
pub id: FileId,
pub meta: FsMeta,
}
pub fn walk(fs: &dyn FileSystem) -> VfsResult<Vec<WalkEntry>> {
let mut out = Vec::new();
let mut visited: HashSet<FileId> = HashSet::new();
let mut stack: Vec<(Vec<Vec<u8>>, FileId, usize)> = vec![(Vec::new(), fs.root(), 0)];
while let Some((prefix, dir_id, depth)) = stack.pop() {
if depth > WALK_MAX_DEPTH || !visited.insert(dir_id) {
continue;
}
for entry in fs.read_dir(dir_id)? {
let entry = entry?;
if matches!(entry.name.as_slice(), b"." | b"..") {
continue;
}
let mut path = prefix.clone();
path.push(entry.name);
let meta = fs.meta(entry.id)?;
let is_dir = matches!(meta.kind, NodeKind::Dir);
out.push(WalkEntry {
path: path.clone(),
id: entry.id,
meta,
});
if is_dir {
stack.push((path, entry.id, depth + 1));
}
}
}
Ok(out)
}