#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
use std::collections::HashSet;
use state_history_forensic::epoch::EpochTag;
use forensic_vfs::{
ArchiveContents, Confidence, CredentialSource, DynSource, FileId, FileSystem, FsMeta, Layer,
Locator, NoCredentials, NodeAddr, NodeKind, Openers, 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: Locator,
pub fs: Option<forensic_vfs::DynFs>,
}
pub struct Resolved {
pub fs: forensic_vfs::DynFs,
pub spec: Locator,
pub source: DynSource,
pub source_spec: Locator,
}
pub struct ResolvedSource {
pub source: DynSource,
pub spec: Locator,
}
pub trait SourceOpen {
fn open(&self, source: DynSource, spec: Locator, depth: usize) -> VfsResult<Option<Resolved>> {
self.open_with_credentials(source, spec, depth, &NoCredentials)
}
fn open_with_credentials(
&self,
source: DynSource,
spec: Locator,
depth: usize,
creds: &dyn CredentialSource,
) -> VfsResult<Option<Resolved>>;
fn resolve_to_source(
&self,
source: DynSource,
spec: Locator,
depth: usize,
) -> VfsResult<Option<ResolvedSource>>;
}
impl SourceOpen for Openers {
fn open_with_credentials(
&self,
source: DynSource,
spec: Locator,
depth: usize,
creds: &dyn CredentialSource,
) -> VfsResult<Option<Resolved>> {
if depth > MAX_DEPTH {
return Ok(None);
}
let (head, n, tail, tn, total) = read_sniff_buffers(&source)?;
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_with_credentials(sub, child, depth + 1, creds)? {
return Ok(Some(found));
}
}
}
}
let mut credential_attempt: Vec<usize> = Vec::new();
if let Some(found) = descend_signature_encryption(
self,
&window,
&source,
&spec,
depth,
creds,
&mut credential_attempt,
)? {
return Ok(Some(found));
}
if let Some(found) =
descend_packaging(self, &window, &source, &spec, depth, &mut |src, sp, d| {
self.open_with_credentials(src, sp, d, creds)
})?
{
return Ok(Some(found));
}
descend_credential_attempt_encryption(
self,
&source,
&spec,
depth,
creds,
&credential_attempt,
)
}
fn resolve_to_source(
&self,
source: DynSource,
spec: Locator,
depth: usize,
) -> VfsResult<Option<ResolvedSource>> {
if depth > MAX_DEPTH {
return Ok(None);
}
let (head, n, tail, tn, total) = read_sniff_buffers(&source)?;
let window = SniffWindow::with_tail(
0,
head.get(..n).unwrap_or(&[]),
total,
tail.get(..tn).unwrap_or(&[]),
);
if let Some(found) =
descend_packaging(self, &window, &source, &spec, depth, &mut |src, sp, d| {
self.resolve_to_source(src, sp, d)
})?
{
return Ok(Some(found));
}
Ok(Some(ResolvedSource { source, spec }))
}
}
fn descend_packaging<T>(
openers: &Openers,
window: &SniffWindow,
source: &DynSource,
spec: &Locator,
depth: usize,
recurse: &mut dyn FnMut(DynSource, Locator, usize) -> VfsResult<Option<T>>,
) -> VfsResult<Option<T>> {
for cd in openers.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) = recurse(decoded, child, depth + 1)? {
return Ok(Some(found));
}
}
}
for ar in openers.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) = recurse(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) = recurse(member.source, child, depth + 1)? {
return Ok(Some(found));
}
}
}
_ => {} }
}
}
Ok(None)
}
type SniffBuffers = (Vec<u8>, usize, Vec<u8>, usize, u64);
fn read_sniff_buffers(source: &DynSource) -> VfsResult<SniffBuffers> {
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)?;
Ok((head, n, tail, tn, total))
}
fn descend_signature_encryption(
openers: &Openers,
window: &SniffWindow,
source: &DynSource,
spec: &Locator,
depth: usize,
creds: &dyn CredentialSource,
credential_attempt: &mut Vec<usize>,
) -> VfsResult<Option<Resolved>> {
for (i, enc) in openers.encryption_layers().iter().enumerate() {
match enc.probe(window) {
Confidence::Yes { .. } => {
let layer = enc.open(source.clone())?;
let decrypted = layer.open(creds)?;
let child = spec.clone().push(Layer::Encryption {
scheme: enc.scheme(),
});
if let Some(found) =
openers.open_with_credentials(decrypted, child, depth + 1, creds)?
{
return Ok(Some(found));
}
}
Confidence::Maybe => credential_attempt.push(i),
Confidence::No => {}
}
}
Ok(None)
}
fn descend_credential_attempt_encryption(
openers: &Openers,
source: &DynSource,
spec: &Locator,
depth: usize,
creds: &dyn CredentialSource,
credential_attempt: &[usize],
) -> VfsResult<Option<Resolved>> {
for &i in credential_attempt {
let Some(enc) = openers.encryption_layers().get(i) else {
continue; };
let layer = enc.open(source.clone())?;
let Ok(decrypted) = layer.open(creds) else {
continue;
};
let child = spec.clone().push(Layer::Encryption {
scheme: enc.scheme(),
});
if let Some(found) = openers.open_with_credentials(decrypted, child, depth + 1, creds)? {
return Ok(Some(found));
}
}
Ok(None)
}
#[derive(Debug, Clone)]
pub struct SnapshotView {
pub epoch: EpochTag,
pub xid: u64,
pub name: String,
pub locator: Locator,
}
#[must_use]
pub fn snapshot_view(
source_spec: &Locator,
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)
}