#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::sync::{Arc, Mutex};
use forensic_vfs::adapters::SubRange;
use forensic_vfs::{
Allocation, ArchiveContents, ArchiveOpen, Confidence, ContainerFormat, ContainerOpen,
CredentialSource, DirEntry, DirStream, DynFs, DynSource, EncryptionLayer, EncryptionOpen,
EncryptionScheme, FileId, FileSystem, FileSystemOpen, FsKind, FsMeta, ImageSource, Layer,
Locator, MacbTimes, Member, NoCredentials, NodeAddr, NodeKind, Openers, ResidencyKind,
SectorSizes, SnapshotRef, SniffWindow, TimeZonePolicy, VfsError, VfsResult, VolumeDesc,
VolumeKind, VolumeScheme, VolumeSystem, VolumeSystemOpen,
};
use forensic_vfs_resolver::{
epoch_from_create_time, snapshot_view, walk, Evidence, ResolvedSource, SourceOpen,
};
type Children = Vec<(Vec<u8>, u64, NodeKind)>;
type Tree = Vec<(u64, Children)>;
struct MemSource(Vec<u8>);
impl ImageSource for MemSource {
fn len(&self) -> u64 {
self.0.len() as u64
}
fn read_at(&self, offset: u64, buf: &mut [u8]) -> VfsResult<usize> {
let off = usize::try_from(offset).unwrap_or(usize::MAX);
let Some(src) = self.0.get(off..) else {
return Ok(0);
};
let n = src.len().min(buf.len());
buf[..n].copy_from_slice(&src[..n]);
Ok(n)
}
}
fn mem(bytes: Vec<u8>) -> DynSource {
Arc::new(MemSource(bytes))
}
struct TreeFs {
tree: Tree,
}
impl TreeFs {
fn children(&self, id: u64) -> &[(Vec<u8>, u64, NodeKind)] {
self.tree
.iter()
.find(|(k, _)| *k == id)
.map_or(&[], |(_, v)| v.as_slice())
}
}
impl FileSystem for TreeFs {
fn kind(&self) -> FsKind {
FsKind::NTFS
}
fn root(&self) -> FileId {
FileId::Opaque(0)
}
fn sector_sizes(&self) -> SectorSizes {
SectorSizes {
logical: 512,
physical: 512,
cluster_or_block: 512,
}
}
fn timestamp_zone(&self) -> TimeZonePolicy {
TimeZonePolicy::Utc
}
fn read_dir(&self, ino: FileId) -> VfsResult<DirStream> {
let id = match ino {
FileId::Opaque(n) => n,
_ => 0,
};
let kids: Vec<VfsResult<DirEntry>> = self
.children(id)
.iter()
.map(|(name, cid, kind)| {
Ok(DirEntry {
name: name.clone(),
id: FileId::Opaque(*cid),
kind: *kind,
})
})
.collect();
Ok(DirStream::new(kids.into_iter()))
}
fn extents(
&self,
_ino: FileId,
_stream: forensic_vfs::StreamId,
) -> VfsResult<forensic_vfs::ExtentStream> {
Ok(forensic_vfs::ExtentStream::empty())
}
fn lookup(&self, _parent: FileId, _name: &[u8]) -> VfsResult<Option<FileId>> {
Ok(None)
}
fn meta(&self, ino: FileId) -> VfsResult<FsMeta> {
let id = match ino {
FileId::Opaque(n) => n,
_ => 0,
};
let kind = if id == 0 {
NodeKind::Dir
} else {
self.tree
.iter()
.flat_map(|(_, v)| v.iter())
.find(|(_, cid, _)| *cid == id)
.map_or(NodeKind::File, |(_, _, k)| *k)
};
Ok(FsMeta {
ino: id,
kind,
allocated: Allocation::Allocated,
size: 0,
nlink: 1,
uid: None,
gid: None,
mode: None,
times: MacbTimes::default(),
streams: Vec::new(),
residency: ResidencyKind::Resident { inline_len: 0 },
link_target: None,
})
}
fn read_at(
&self,
_ino: FileId,
_stream: forensic_vfs::StreamId,
_off: u64,
_buf: &mut [u8],
) -> VfsResult<usize> {
Ok(0)
}
fn read_link(&self, _ino: FileId, _cap: usize) -> VfsResult<Vec<u8>> {
Ok(Vec::new())
}
fn deleted(&self) -> VfsResult<forensic_vfs::NodeStream> {
Ok(forensic_vfs::NodeStream::empty())
}
fn unallocated(&self) -> VfsResult<forensic_vfs::ExtentStream> {
Ok(forensic_vfs::ExtentStream::empty())
}
}
struct FakeFsProbe {
magic: &'static [u8],
fail: bool,
}
impl FileSystemOpen for FakeFsProbe {
fn kind(&self) -> FsKind {
FsKind::NTFS
}
fn probe(&self, w: &SniffWindow) -> Confidence {
if w.has_magic(0, self.magic) {
Confidence::Yes { how: "fake fs" }
} else {
Confidence::No
}
}
fn open(&self, _src: DynSource) -> VfsResult<DynFs> {
if self.fail {
return Err(VfsError::Decode {
layer: "fakefs",
offset: 0,
detail: "garbage body".to_string(),
bytes: forensic_vfs::SmallHex::new(&[]),
});
}
Ok(Arc::new(TreeFs {
tree: vec![
(
0,
vec![
(b"dir".to_vec(), 1, NodeKind::Dir),
(b"a.txt".to_vec(), 2, NodeKind::File),
],
),
(1, vec![(b"b.txt".to_vec(), 3, NodeKind::File)]),
],
}))
}
}
struct FakeVs {
parent: DynSource,
descs: Vec<VolumeDesc>,
}
impl VolumeSystem for FakeVs {
fn scheme(&self) -> VolumeScheme {
VolumeScheme::Gpt
}
fn volumes(&self) -> &[VolumeDesc] {
&self.descs
}
fn open_volume(&self, index: usize) -> VfsResult<DynSource> {
let d = self.descs.get(index).ok_or(VfsError::OutOfRange {
what: "fake volume",
offset: index as u64,
len: 1,
bound: self.descs.len() as u64,
})?;
Ok(Arc::new(SubRange::new(self.parent.clone(), d.start, d.len)))
}
}
struct FakeVsProbe {
magic: &'static [u8],
windows: Vec<(u64, u64)>,
}
impl VolumeSystemOpen for FakeVsProbe {
fn scheme(&self) -> VolumeScheme {
VolumeScheme::Gpt
}
fn probe(&self, w: &SniffWindow) -> Confidence {
if w.has_magic(0, self.magic) {
Confidence::Yes { how: "fake vs" }
} else {
Confidence::No
}
}
fn open(&self, src: DynSource) -> VfsResult<Box<dyn VolumeSystem>> {
let descs = self
.windows
.iter()
.enumerate()
.map(|(i, (start, len))| VolumeDesc {
index: i,
kind: VolumeKind::Partition,
start: *start,
len: *len,
type_hint: None,
label: None,
})
.collect();
Ok(Box::new(FakeVs { parent: src, descs }))
}
}
struct FakeContainer {
magic: &'static [u8],
inner: (u64, u64),
tail: bool,
}
impl ContainerOpen for FakeContainer {
fn format(&self) -> ContainerFormat {
ContainerFormat::Raw
}
fn probe(&self, w: &SniffWindow) -> Confidence {
let hit = if self.tail {
w.has_magic_from_end(self.magic.len(), self.magic)
} else {
w.has_magic(0, self.magic)
};
if hit {
Confidence::Yes {
how: "fake container",
}
} else {
Confidence::No
}
}
fn open(&self, src: DynSource) -> VfsResult<DynSource> {
Ok(Arc::new(SubRange::new(src, self.inner.0, self.inner.1)))
}
}
struct FakeArchive {
magic: &'static [u8],
members: bool,
inner: (u64, u64),
}
impl ArchiveOpen for FakeArchive {
fn probe(&self, w: &SniffWindow) -> Confidence {
if w.has_magic(0, self.magic) {
Confidence::Yes {
how: "fake archive",
}
} else {
Confidence::No
}
}
fn open(&self, src: DynSource) -> VfsResult<ArchiveContents> {
let inner: DynSource = Arc::new(SubRange::new(src, self.inner.0, self.inner.1));
if self.members {
Ok(ArchiveContents::Members(vec![Member {
name: b"case.img".to_vec(),
source: inner,
}]))
} else {
Ok(ArchiveContents::Stream(inner))
}
}
}
enum FakeVerdict {
Signature(&'static [u8]),
CredentialAttempt,
}
struct FakeEncLayer {
scheme: EncryptionScheme,
src: DynSource,
inner: Option<(u64, u64)>,
attempts: Arc<Mutex<usize>>,
}
impl EncryptionLayer for FakeEncLayer {
fn scheme(&self) -> EncryptionScheme {
self.scheme
}
fn open(&self, _creds: &dyn CredentialSource) -> VfsResult<DynSource> {
*self.attempts.lock().unwrap() += 1;
match self.inner {
Some((start, len)) => Ok(Arc::new(SubRange::new(self.src.clone(), start, len))),
None => Err(VfsError::Decode {
layer: "fake-enc",
offset: 0,
detail: "bad key".to_string(),
bytes: forensic_vfs::SmallHex::new(&[]),
}),
}
}
}
struct FakeEncProbe {
scheme: EncryptionScheme,
verdict: FakeVerdict,
inner: Option<(u64, u64)>,
attempts: Arc<Mutex<usize>>,
}
impl EncryptionOpen for FakeEncProbe {
fn scheme(&self) -> EncryptionScheme {
self.scheme
}
fn probe(&self, w: &SniffWindow) -> Confidence {
match self.verdict {
FakeVerdict::Signature(magic) => {
if w.has_magic(0, magic) {
Confidence::Yes { how: "fake enc" }
} else {
Confidence::No
}
}
FakeVerdict::CredentialAttempt => Confidence::Maybe,
}
}
fn open(&self, src: DynSource) -> VfsResult<Box<dyn EncryptionLayer>> {
Ok(Box::new(FakeEncLayer {
scheme: self.scheme,
src,
inner: self.inner,
attempts: self.attempts.clone(),
}))
}
}
#[test]
fn resolves_a_filesystem_at_the_top_layer() {
let reg = Openers::new().filesystem(FakeFsProbe {
magic: b"FSFS",
fail: false,
});
let mut data = b"FSFS".to_vec();
data.resize(8192, 0);
let base = Locator::root(Layer::Range {
start: 0,
len: data.len() as u64,
});
let out = reg.open(mem(data), base, 0).unwrap().expect("mounts fs");
assert!(matches!(
out.spec.layer,
Layer::Fs {
kind: FsKind::NTFS,
at: NodeAddr::Path(_)
}
));
assert_eq!(out.fs.kind(), FsKind::NTFS);
}
#[test]
fn a_matching_probe_whose_open_fails_propagates_loud() {
let reg = Openers::new().filesystem(FakeFsProbe {
magic: b"FSFS",
fail: true,
});
let mut data = b"FSFS".to_vec();
data.resize(8192, 0);
let base = Locator::root(Layer::Range {
start: 0,
len: data.len() as u64,
});
assert!(reg.open(mem(data), base, 0).is_err());
}
#[test]
fn descends_a_volume_system_into_its_filesystem() {
let reg = Openers::new()
.filesystem(FakeFsProbe {
magic: b"FSFS",
fail: false,
})
.volume_system(FakeVsProbe {
magic: b"VOLS",
windows: vec![(512, 4096)],
});
let mut data = b"VOLS".to_vec();
data.resize(512, 0);
data.extend_from_slice(b"FSFS");
data.resize(8192, 0);
let base = Locator::root(Layer::Range {
start: 0,
len: data.len() as u64,
});
let out = reg
.open(mem(data), base, 0)
.unwrap()
.expect("mounts fs inside a volume");
let uri = out.spec.to_uri();
assert!(uri.contains("volume:") && uri.contains("fs:"), "{uri}");
}
#[test]
fn descends_a_container_into_its_filesystem() {
let reg = Openers::new()
.filesystem(FakeFsProbe {
magic: b"FSFS",
fail: false,
})
.container(FakeContainer {
magic: b"CONT",
inner: (1024, 7168),
tail: false,
});
let mut data = b"CONT".to_vec();
data.resize(1024, 0);
data.extend_from_slice(b"FSFS");
data.resize(8192, 0);
let base = Locator::root(Layer::Range {
start: 0,
len: data.len() as u64,
});
let out = reg
.open(mem(data), base, 0)
.unwrap()
.expect("mounts fs inside a container");
assert!(out.spec.to_uri().contains("fs:"));
}
#[test]
fn a_tail_probed_container_matches_a_trailer_magic() {
let reg = Openers::new()
.filesystem(FakeFsProbe {
magic: b"FSFS",
fail: false,
})
.container(FakeContainer {
magic: b"KOLY",
inner: (16, 8192),
tail: true,
});
let mut data = vec![0u8; 16];
data.extend_from_slice(b"FSFS"); data.resize(8188, 0);
data.extend_from_slice(b"KOLY"); let base = Locator::root(Layer::Range {
start: 0,
len: data.len() as u64,
});
let out = reg
.open(mem(data), base, 0)
.unwrap()
.expect("tail-probed container decodes to a mountable fs");
assert!(out.spec.to_uri().contains("fs:"));
}
#[test]
fn unrecognized_source_resolves_to_none() {
let reg = Openers::new().filesystem(FakeFsProbe {
magic: b"FSFS",
fail: false,
});
let base = Locator::root(Layer::Range {
start: 0,
len: 4096,
});
assert!(reg.open(mem(vec![0u8; 4096]), base, 0).unwrap().is_none());
}
#[test]
fn a_volume_system_whose_volumes_hold_no_fs_falls_through_to_none() {
let reg = Openers::new()
.filesystem(FakeFsProbe {
magic: b"FSFS",
fail: false,
})
.volume_system(FakeVsProbe {
magic: b"VOLS",
windows: vec![(512, 512)],
});
let mut data = b"VOLS".to_vec();
data.resize(4096, 0); let base = Locator::root(Layer::Range {
start: 0,
len: data.len() as u64,
});
assert!(reg.open(mem(data), base, 0).unwrap().is_none());
}
#[test]
fn a_container_whose_payload_holds_no_fs_falls_through_to_none() {
let reg = Openers::new()
.filesystem(FakeFsProbe {
magic: b"FSFS",
fail: false,
})
.container(FakeContainer {
magic: b"CONT",
inner: (16, 4096),
tail: false,
});
let mut data = b"CONT".to_vec();
data.resize(4096, 0); let base = Locator::root(Layer::Range {
start: 0,
len: data.len() as u64,
});
assert!(reg.open(mem(data), base, 0).unwrap().is_none());
}
#[test]
fn descends_an_archive_stream_into_its_filesystem() {
let reg = Openers::new()
.filesystem(FakeFsProbe {
magic: b"FSFS",
fail: false,
})
.archive(FakeArchive {
magic: b"GZIP",
members: false,
inner: (16, 8192),
});
let mut data = b"GZIP".to_vec();
data.resize(16, 0);
data.extend_from_slice(b"FSFS"); data.resize(16 + 8192, 0);
let base = Locator::root(Layer::Range {
start: 0,
len: data.len() as u64,
});
let out = reg
.open(mem(data), base, 0)
.unwrap()
.expect("mounts fs inside an archive stream");
let uri = out.spec.to_uri();
assert!(uri.contains("archive:") && uri.contains("fs:"), "{uri}");
assert!(
out.spec
.layers()
.iter()
.any(|l| matches!(l, Layer::Archive { member: None })),
"{uri}"
);
}
#[test]
fn descends_an_archive_member_into_its_filesystem() {
let reg = Openers::new()
.filesystem(FakeFsProbe {
magic: b"FSFS",
fail: false,
})
.archive(FakeArchive {
magic: b"ZIPM",
members: true,
inner: (16, 8192),
});
let mut data = b"ZIPM".to_vec();
data.resize(16, 0);
data.extend_from_slice(b"FSFS");
data.resize(16 + 8192, 0);
let base = Locator::root(Layer::Range {
start: 0,
len: data.len() as u64,
});
let out = reg
.open(mem(data), base, 0)
.unwrap()
.expect("mounts fs inside an archive member");
let uri = out.spec.to_uri();
assert!(
out.spec
.layers()
.iter()
.any(|l| matches!(l, Layer::Archive { member: Some(0) })),
"{uri}"
);
}
#[test]
fn an_archive_stream_holding_no_fs_falls_through_to_none() {
let reg = Openers::new()
.filesystem(FakeFsProbe {
magic: b"FSFS",
fail: false,
})
.archive(FakeArchive {
magic: b"GZIP",
members: false,
inner: (16, 4096),
});
let mut data = b"GZIP".to_vec();
data.resize(4096, 0); let base = Locator::root(Layer::Range {
start: 0,
len: data.len() as u64,
});
assert!(reg.open(mem(data), base, 0).unwrap().is_none());
}
#[test]
fn an_archive_member_holding_no_fs_falls_through_to_none() {
let reg = Openers::new()
.filesystem(FakeFsProbe {
magic: b"FSFS",
fail: false,
})
.archive(FakeArchive {
magic: b"ZIPM",
members: true,
inner: (16, 4096),
});
let mut data = b"ZIPM".to_vec();
data.resize(4096, 0); let base = Locator::root(Layer::Range {
start: 0,
len: data.len() as u64,
});
assert!(reg.open(mem(data), base, 0).unwrap().is_none());
}
#[test]
fn resolve_to_source_returns_a_bare_source_as_its_own_terminal() {
let reg = Openers::new();
let data = b"RAWDUMP0".to_vec();
let base = Locator::root(Layer::Range {
start: 0,
len: data.len() as u64,
});
let resolved: ResolvedSource = reg
.resolve_to_source(mem(data), base, 0)
.unwrap()
.expect("a bare source is its own raw terminal");
let mut buf = [0u8; 8];
let n = resolved.source.read_at(0, &mut buf).unwrap();
assert_eq!(n, 8);
assert_eq!(&buf, b"RAWDUMP0");
}
#[test]
fn resolve_to_source_returns_the_innermost_stream_peel_not_none() {
let reg = Openers::new().archive(FakeArchive {
magic: b"GZIP",
members: false,
inner: (16, 256),
});
let mut data = b"GZIP".to_vec();
data.resize(16, 0);
data.extend_from_slice(b"RAWDUMP0"); data.resize(16 + 256, 0);
let base = Locator::root(Layer::Range {
start: 0,
len: data.len() as u64,
});
assert!(reg
.open(mem(data.clone()), base.clone(), 0)
.unwrap()
.is_none());
let resolved = reg
.resolve_to_source(mem(data), base, 0)
.unwrap()
.expect("the peeled stream is returned, not dropped");
let mut buf = [0u8; 8];
let n = resolved.source.read_at(0, &mut buf).unwrap();
assert_eq!(n, 8);
assert_eq!(
&buf, b"RAWDUMP0",
"returned source is the decoded inner stream"
);
assert!(
resolved
.spec
.layers()
.iter()
.any(|l| matches!(l, Layer::Archive { member: None })),
"{}",
resolved.spec.to_uri()
);
}
#[test]
fn resolve_to_source_returns_a_single_member_source() {
let reg = Openers::new().archive(FakeArchive {
magic: b"ZIPM",
members: true,
inner: (16, 256),
});
let mut data = b"ZIPM".to_vec();
data.resize(16, 0);
data.extend_from_slice(b"RAWDUMP0");
data.resize(16 + 256, 0);
let base = Locator::root(Layer::Range {
start: 0,
len: data.len() as u64,
});
let resolved = reg
.resolve_to_source(mem(data), base, 0)
.unwrap()
.expect("the member source is returned");
let mut buf = [0u8; 8];
resolved.source.read_at(0, &mut buf).unwrap();
assert_eq!(&buf, b"RAWDUMP0");
assert!(
resolved
.spec
.layers()
.iter()
.any(|l| matches!(l, Layer::Archive { member: Some(0) })),
"{}",
resolved.spec.to_uri()
);
}
#[test]
fn resolve_to_source_peels_a_container_then_archive_nesting() {
let reg = Openers::new()
.container(FakeContainer {
magic: b"CONT",
inner: (32, 512),
tail: false,
})
.archive(FakeArchive {
magic: b"GZIP",
members: false,
inner: (16, 256),
});
let mut data = b"CONT".to_vec();
data.resize(32, 0);
data.extend_from_slice(b"GZIP"); data.resize(48, 0);
data.extend_from_slice(b"RAWDUMP0"); data.resize(544, 0);
let base = Locator::root(Layer::Range {
start: 0,
len: data.len() as u64,
});
let resolved = reg
.resolve_to_source(mem(data), base, 0)
.unwrap()
.expect("container→archive→raw peels to the raw terminal");
let mut buf = [0u8; 8];
resolved.source.read_at(0, &mut buf).unwrap();
assert_eq!(&buf, b"RAWDUMP0");
let uri = resolved.spec.to_uri();
assert!(
uri.contains("container:") && uri.contains("archive:"),
"{uri}"
);
assert!(
!resolved
.spec
.layers()
.iter()
.any(|l| matches!(l, Layer::Fs { .. })),
"the source terminal never mounts a filesystem: {uri}"
);
}
#[test]
fn resolve_to_source_is_depth_capped() {
let reg = Openers::new();
let base = Locator::root(Layer::Range { start: 0, len: 16 });
assert!(reg
.resolve_to_source(mem(vec![0u8; 16]), base, 100)
.unwrap()
.is_none());
}
#[test]
fn descends_a_signature_encryption_layer_into_its_filesystem() {
let reg = Openers::new()
.filesystem(FakeFsProbe {
magic: b"FSFS",
fail: false,
})
.encryption(FakeEncProbe {
scheme: EncryptionScheme::Bitlocker,
verdict: FakeVerdict::Signature(b"BDE!"),
inner: Some((512, 8192)),
attempts: Arc::new(Mutex::new(0)),
});
let mut data = b"BDE!".to_vec();
data.resize(512, 0);
data.extend_from_slice(b"FSFS"); data.resize(512 + 8192, 0);
let base = Locator::root(Layer::Range {
start: 0,
len: data.len() as u64,
});
let out = reg
.open_with_credentials(mem(data), base, 0, &NoCredentials)
.unwrap()
.expect("mounts fs behind a signature encryption layer");
let uri = out.spec.to_uri();
assert!(
uri.contains("encryption:bitlocker") && uri.contains("fs:"),
"{uri}"
);
assert!(
out.spec.layers().iter().any(|l| matches!(
l,
Layer::Encryption {
scheme: EncryptionScheme::Bitlocker
}
)),
"{uri}"
);
}
#[test]
fn a_signature_encryption_whose_decrypt_fails_propagates_loud() {
let reg = Openers::new()
.filesystem(FakeFsProbe {
magic: b"FSFS",
fail: false,
})
.encryption(FakeEncProbe {
scheme: EncryptionScheme::Luks1,
verdict: FakeVerdict::Signature(b"LUKS"),
inner: None, attempts: Arc::new(Mutex::new(0)),
});
let mut data = b"LUKS".to_vec();
data.resize(8192, 0);
let base = Locator::root(Layer::Range {
start: 0,
len: data.len() as u64,
});
assert!(reg
.open_with_credentials(mem(data), base, 0, &NoCredentials)
.is_err());
}
#[test]
fn a_credential_attempt_scheme_never_shadows_a_real_filesystem() {
let attempts = Arc::new(Mutex::new(0usize));
let reg = Openers::new()
.filesystem(FakeFsProbe {
magic: b"FSFS",
fail: false,
})
.encryption(FakeEncProbe {
scheme: EncryptionScheme::VeraCrypt,
verdict: FakeVerdict::CredentialAttempt,
inner: Some((0, 8192)), attempts: attempts.clone(),
});
let mut data = b"FSFS".to_vec();
data.resize(8192, 0);
let base = Locator::root(Layer::Range {
start: 0,
len: data.len() as u64,
});
let out = reg
.open_with_credentials(mem(data), base, 0, &NoCredentials)
.unwrap()
.expect("plaintext fs mounts directly");
assert_eq!(
*attempts.lock().unwrap(),
0,
"a credential-attempt scheme must not be tried while a real fs claims the source"
);
assert!(
!out.spec
.layers()
.iter()
.any(|l| matches!(l, Layer::Encryption { .. })),
"no encryption layer belongs in a plaintext stack"
);
}
#[test]
fn a_credential_attempt_scheme_descends_as_a_last_resort() {
let attempts = Arc::new(Mutex::new(0usize));
let reg = Openers::new()
.filesystem(FakeFsProbe {
magic: b"FSFS",
fail: false,
})
.encryption(FakeEncProbe {
scheme: EncryptionScheme::VeraCrypt,
verdict: FakeVerdict::CredentialAttempt,
inner: Some((512, 8192)),
attempts: attempts.clone(),
});
let mut data = vec![0u8; 512];
data.extend_from_slice(b"FSFS");
data.resize(512 + 8192, 0);
let base = Locator::root(Layer::Range {
start: 0,
len: data.len() as u64,
});
let out = reg
.open_with_credentials(mem(data), base, 0, &NoCredentials)
.unwrap()
.expect("veracrypt decrypts to a mountable fs");
assert_eq!(
*attempts.lock().unwrap(),
1,
"the last-resort decrypt ran once"
);
assert!(
out.spec.layers().iter().any(|l| matches!(
l,
Layer::Encryption {
scheme: EncryptionScheme::VeraCrypt
}
)),
"{}",
out.spec.to_uri()
);
}
#[test]
fn a_credential_attempt_whose_decrypt_fails_falls_through_to_none() {
let reg = Openers::new()
.filesystem(FakeFsProbe {
magic: b"FSFS",
fail: false,
})
.encryption(FakeEncProbe {
scheme: EncryptionScheme::VeraCrypt,
verdict: FakeVerdict::CredentialAttempt,
inner: None, attempts: Arc::new(Mutex::new(0)),
});
let base = Locator::root(Layer::Range {
start: 0,
len: 4096,
});
assert!(reg
.open_with_credentials(mem(vec![0u8; 4096]), base, 0, &NoCredentials)
.unwrap()
.is_none());
}
#[test]
fn a_signature_scheme_that_does_not_match_yields_a_no_verdict() {
let reg = Openers::new()
.filesystem(FakeFsProbe {
magic: b"FSFS",
fail: false,
})
.encryption(FakeEncProbe {
scheme: EncryptionScheme::Bitlocker,
verdict: FakeVerdict::Signature(b"BDE!"),
inner: Some((0, 4096)),
attempts: Arc::new(Mutex::new(0)),
});
let base = Locator::root(Layer::Range {
start: 0,
len: 4096,
});
assert!(reg
.open_with_credentials(mem(vec![0u8; 4096]), base, 0, &NoCredentials)
.unwrap()
.is_none());
}
#[test]
fn a_signature_scheme_whose_plaintext_holds_no_fs_falls_through_to_none() {
let reg = Openers::new()
.filesystem(FakeFsProbe {
magic: b"FSFS",
fail: false,
})
.encryption(FakeEncProbe {
scheme: EncryptionScheme::Bitlocker,
verdict: FakeVerdict::Signature(b"BDE!"),
inner: Some((512, 4096)), attempts: Arc::new(Mutex::new(0)),
});
let mut data = b"BDE!".to_vec();
data.resize(512 + 4096, 0);
let base = Locator::root(Layer::Range {
start: 0,
len: data.len() as u64,
});
assert!(reg
.open_with_credentials(mem(data), base, 0, &NoCredentials)
.unwrap()
.is_none());
}
#[test]
fn a_credential_attempt_whose_plaintext_holds_no_fs_falls_through_to_none() {
let attempts = Arc::new(Mutex::new(0usize));
let reg = Openers::new()
.filesystem(FakeFsProbe {
magic: b"FSFS",
fail: false,
})
.encryption(FakeEncProbe {
scheme: EncryptionScheme::VeraCrypt,
verdict: FakeVerdict::CredentialAttempt,
inner: Some((0, 4096)), attempts: attempts.clone(),
});
let base = Locator::root(Layer::Range {
start: 0,
len: 4096,
});
assert!(reg
.open_with_credentials(mem(vec![0u8; 4096]), base, 0, &NoCredentials)
.unwrap()
.is_none());
assert!(
*attempts.lock().unwrap() >= 1,
"the last-resort decrypt was driven"
);
}
#[test]
fn plain_open_delegates_through_no_credentials_and_still_descends_encryption() {
let reg = Openers::new()
.filesystem(FakeFsProbe {
magic: b"FSFS",
fail: false,
})
.encryption(FakeEncProbe {
scheme: EncryptionScheme::FileVault,
verdict: FakeVerdict::Signature(b"CS!!"),
inner: Some((16, 8192)),
attempts: Arc::new(Mutex::new(0)),
});
let mut data = b"CS!!".to_vec();
data.resize(16, 0);
data.extend_from_slice(b"FSFS");
data.resize(16 + 8192, 0);
let base = Locator::root(Layer::Range {
start: 0,
len: data.len() as u64,
});
let out = reg
.open(mem(data), base, 0)
.unwrap()
.expect("descends encryption via the retained open()");
assert!(
out.spec.layers().iter().any(|l| matches!(
l,
Layer::Encryption {
scheme: EncryptionScheme::FileVault
}
)),
"{}",
out.spec.to_uri()
);
}
#[test]
fn recursion_is_depth_capped_on_a_self_referential_container() {
let reg = Openers::new().container(FakeContainer {
magic: b"LOOP",
inner: (0, 4096),
tail: false,
});
let mut data = b"LOOP".to_vec();
data.resize(4096, 0);
let base = Locator::root(Layer::Range {
start: 0,
len: data.len() as u64,
});
assert!(reg.open(mem(data), base, 0).unwrap().is_none());
}
#[test]
fn an_empty_source_reads_a_single_byte_window_and_resolves_to_none() {
let reg = Openers::new().filesystem(FakeFsProbe {
magic: b"FSFS",
fail: false,
});
let base = Locator::root(Layer::Range { start: 0, len: 0 });
assert!(reg.open(mem(Vec::new()), base, 0).unwrap().is_none());
}
fn fs_with(tree: Tree) -> TreeFs {
TreeFs { tree }
}
#[test]
fn walk_enumerates_the_tree_and_skips_dot_entries() {
let fs = fs_with(vec![
(
0,
vec![
(b".".to_vec(), 0, NodeKind::Dir),
(b"..".to_vec(), 0, NodeKind::Dir),
(b"dir".to_vec(), 1, NodeKind::Dir),
(b"a.txt".to_vec(), 2, NodeKind::File),
],
),
(1, vec![(b"b.txt".to_vec(), 3, NodeKind::File)]),
]);
let entries = walk(&fs).unwrap();
let names: Vec<String> = entries
.iter()
.filter_map(|e| {
e.path
.last()
.map(|n| String::from_utf8_lossy(n).to_string())
})
.collect();
assert!(names.contains(&"dir".to_string()));
assert!(names.contains(&"a.txt".to_string()));
assert!(names.contains(&"b.txt".to_string()));
assert!(!names.iter().any(|n| n == "." || n == ".."));
let b = entries
.iter()
.find(|e| e.path.last().map(Vec::as_slice) == Some(b"b.txt".as_slice()))
.unwrap();
assert_eq!(b.path.len(), 2);
}
#[test]
fn walk_is_loop_guarded_against_a_self_referential_directory() {
let fs = fs_with(vec![(0, vec![(b"self".to_vec(), 0, NodeKind::Dir)])]);
let entries = walk(&fs).unwrap();
assert_eq!(entries.len(), 1);
}
#[test]
fn walk_propagates_a_read_dir_error() {
struct Boom;
impl FileSystem for Boom {
fn kind(&self) -> FsKind {
FsKind::NTFS
}
fn root(&self) -> FileId {
FileId::Opaque(0)
}
fn sector_sizes(&self) -> SectorSizes {
SectorSizes {
logical: 512,
physical: 512,
cluster_or_block: 512,
}
}
fn timestamp_zone(&self) -> TimeZonePolicy {
TimeZonePolicy::Utc
}
fn read_dir(&self, _ino: FileId) -> VfsResult<DirStream> {
Err(VfsError::Decode {
layer: "boom",
offset: 0,
detail: "read_dir failed".to_string(),
bytes: forensic_vfs::SmallHex::new(&[]),
})
}
fn extents(
&self,
_ino: FileId,
_stream: forensic_vfs::StreamId,
) -> VfsResult<forensic_vfs::ExtentStream> {
Ok(forensic_vfs::ExtentStream::empty())
}
fn lookup(&self, _parent: FileId, _name: &[u8]) -> VfsResult<Option<FileId>> {
Ok(None)
}
fn meta(&self, _ino: FileId) -> VfsResult<FsMeta> {
Err(VfsError::Decode {
layer: "boom",
offset: 0,
detail: "meta failed".to_string(),
bytes: forensic_vfs::SmallHex::new(&[]),
})
}
fn read_at(
&self,
_ino: FileId,
_stream: forensic_vfs::StreamId,
_off: u64,
_buf: &mut [u8],
) -> VfsResult<usize> {
Ok(0)
}
fn read_link(&self, _ino: FileId, _cap: usize) -> VfsResult<Vec<u8>> {
Ok(Vec::new())
}
fn deleted(&self) -> VfsResult<forensic_vfs::NodeStream> {
Ok(forensic_vfs::NodeStream::empty())
}
fn unallocated(&self) -> VfsResult<forensic_vfs::ExtentStream> {
Ok(forensic_vfs::ExtentStream::empty())
}
}
assert!(walk(&Boom).is_err());
}
#[test]
fn walk_propagates_a_meta_error() {
struct MetaBoom;
impl FileSystem for MetaBoom {
fn kind(&self) -> FsKind {
FsKind::NTFS
}
fn root(&self) -> FileId {
FileId::Opaque(0)
}
fn sector_sizes(&self) -> SectorSizes {
SectorSizes {
logical: 512,
physical: 512,
cluster_or_block: 512,
}
}
fn timestamp_zone(&self) -> TimeZonePolicy {
TimeZonePolicy::Utc
}
fn read_dir(&self, _ino: FileId) -> VfsResult<DirStream> {
Ok(DirStream::new(std::iter::once(Ok(DirEntry {
name: b"x".to_vec(),
id: FileId::Opaque(1),
kind: NodeKind::File,
}))))
}
fn extents(
&self,
_ino: FileId,
_stream: forensic_vfs::StreamId,
) -> VfsResult<forensic_vfs::ExtentStream> {
Ok(forensic_vfs::ExtentStream::empty())
}
fn lookup(&self, _parent: FileId, _name: &[u8]) -> VfsResult<Option<FileId>> {
Ok(None)
}
fn meta(&self, _ino: FileId) -> VfsResult<FsMeta> {
Err(VfsError::Decode {
layer: "boom",
offset: 0,
detail: "meta failed".to_string(),
bytes: forensic_vfs::SmallHex::new(&[]),
})
}
fn read_at(
&self,
_ino: FileId,
_stream: forensic_vfs::StreamId,
_off: u64,
_buf: &mut [u8],
) -> VfsResult<usize> {
Ok(0)
}
fn read_link(&self, _ino: FileId, _cap: usize) -> VfsResult<Vec<u8>> {
Ok(Vec::new())
}
fn deleted(&self) -> VfsResult<forensic_vfs::NodeStream> {
Ok(forensic_vfs::NodeStream::empty())
}
fn unallocated(&self) -> VfsResult<forensic_vfs::ExtentStream> {
Ok(forensic_vfs::ExtentStream::empty())
}
}
assert!(walk(&MetaBoom).is_err());
}
#[test]
fn walk_propagates_a_dir_entry_stream_error() {
struct EntryBoom;
impl FileSystem for EntryBoom {
fn kind(&self) -> FsKind {
FsKind::NTFS
}
fn root(&self) -> FileId {
FileId::Opaque(0)
}
fn sector_sizes(&self) -> SectorSizes {
SectorSizes {
logical: 512,
physical: 512,
cluster_or_block: 512,
}
}
fn timestamp_zone(&self) -> TimeZonePolicy {
TimeZonePolicy::Utc
}
fn read_dir(&self, _ino: FileId) -> VfsResult<DirStream> {
Ok(DirStream::new(std::iter::once(Err(VfsError::Decode {
layer: "boom",
offset: 0,
detail: "entry failed".to_string(),
bytes: forensic_vfs::SmallHex::new(&[]),
}))))
}
fn extents(
&self,
_ino: FileId,
_stream: forensic_vfs::StreamId,
) -> VfsResult<forensic_vfs::ExtentStream> {
Ok(forensic_vfs::ExtentStream::empty())
}
fn lookup(&self, _parent: FileId, _name: &[u8]) -> VfsResult<Option<FileId>> {
Ok(None)
}
fn meta(&self, _ino: FileId) -> VfsResult<FsMeta> {
Err(VfsError::Decode {
layer: "boom",
offset: 0,
detail: "meta".to_string(),
bytes: forensic_vfs::SmallHex::new(&[]),
})
}
fn read_at(
&self,
_ino: FileId,
_stream: forensic_vfs::StreamId,
_off: u64,
_buf: &mut [u8],
) -> VfsResult<usize> {
Ok(0)
}
fn read_link(&self, _ino: FileId, _cap: usize) -> VfsResult<Vec<u8>> {
Ok(Vec::new())
}
fn deleted(&self) -> VfsResult<forensic_vfs::NodeStream> {
Ok(forensic_vfs::NodeStream::empty())
}
fn unallocated(&self) -> VfsResult<forensic_vfs::ExtentStream> {
Ok(forensic_vfs::ExtentStream::empty())
}
}
assert!(walk(&EntryBoom).is_err());
}
#[test]
fn walk_is_depth_capped() {
let depth = 300u64;
let tree: Tree = (0..depth)
.map(|n| {
(
n,
vec![(format!("d{n}").into_bytes(), n + 1, NodeKind::Dir)],
)
})
.collect();
assert!(walk(&fs_with(tree)).is_ok());
}
#[test]
fn evidence_carries_a_root_locator_and_optional_fs() {
let ev = Evidence {
root: Locator::file("/img.raw"),
fs: None,
};
assert!(ev.fs.is_none());
assert_eq!(ev.root, Locator::file("/img.raw"));
}
#[test]
fn epoch_from_create_time_round_trips_and_orders() {
let t = 0x0123_4567_89ab_cdefu64;
let tag = epoch_from_create_time(t);
assert_eq!(&tag.0[0..24], &[0u8; 24], "high 24 bytes are zero");
assert_eq!(
u64::from_be_bytes(tag.0[24..32].try_into().unwrap()),
t,
"create_time round-trips out of the low 8 bytes"
);
assert!(
epoch_from_create_time(t + 1).0 > tag.0,
"a later create_time yields a greater tag"
);
}
#[test]
fn snapshot_view_carries_epoch_and_snapshot_locator() {
let base = Locator::file("/ev.dmg");
let v = snapshot_view(&base, 42, "daily".to_string(), 1000);
assert_eq!(v.xid, 42);
assert_eq!(v.name, "daily");
assert_eq!(v.epoch, epoch_from_create_time(1000));
assert!(matches!(
v.locator.layer,
Layer::Snapshot {
store: SnapshotRef::ApfsXid(42)
}
));
}
#[test]
fn resolve_keeps_the_base_source_shared_across_layers() {
struct Counting {
inner: DynSource,
seen: Arc<Mutex<usize>>,
}
impl ImageSource for Counting {
fn len(&self) -> u64 {
self.inner.len()
}
fn read_at(&self, offset: u64, buf: &mut [u8]) -> VfsResult<usize> {
*self.seen.lock().unwrap() += 1;
self.inner.read_at(offset, buf)
}
}
let seen = Arc::new(Mutex::new(0usize));
let reg = Openers::new().filesystem(FakeFsProbe {
magic: b"FSFS",
fail: false,
});
let mut data = b"FSFS".to_vec();
data.resize(8192, 0);
let src: DynSource = Arc::new(Counting {
inner: mem(data.clone()),
seen: seen.clone(),
});
let base = Locator::root(Layer::Range {
start: 0,
len: data.len() as u64,
});
assert!(reg.open(src, base, 0).unwrap().is_some());
assert!(*seen.lock().unwrap() >= 1, "the base source was read");
}