#![allow(clippy::unwrap_used, clippy::expect_used)]
use forensic_mount::{open_image_all, FsFileType};
#[test]
fn open_image_all_on_non_image_errors() {
let dir = std::env::temp_dir().join(format!("4n6mount_mp_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("not-an-image.bin");
std::fs::write(&path, vec![0x5Au8; 64 * 1024]).unwrap();
assert!(
open_image_all(&path).is_err(),
"open_image_all must fail loud on a non-image file, got Ok"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn open_image_all_on_missing_path_errors() {
let path = std::env::temp_dir().join("4n6mount_mp_definitely_absent_path.img");
assert!(open_image_all(&path).is_err(), "missing path must Err");
}
#[test]
fn open_image_all_bare_volume_renders_single_root_dir() {
let img = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data/hfsplus.img");
let mut fs = open_image_all(&img).expect("open_image_all must mount the bare HFS+ fixture");
let root = fs.root_ino();
let entries = fs.read_dir(root).expect("read_dir(root) must succeed");
let names: Vec<String> = entries
.iter()
.map(forensic_mount::FsDirEntry::name_str)
.collect();
assert_eq!(
names,
vec!["root".to_string()],
"a bare unpartitioned filesystem must render exactly one `root` volume, saw {names:?}"
);
let vol = &entries[0];
assert_eq!(
vol.file_type,
FsFileType::Directory,
"the `root` volume entry must be a directory"
);
let mut queue: std::collections::VecDeque<u64> = std::collections::VecDeque::new();
let mut visited: std::collections::HashSet<u64> = std::collections::HashSet::new();
queue.push_back(vol.inode);
visited.insert(vol.inode);
let mut read_a_file = false;
'outer: while let Some(ino) = queue.pop_front() {
let Ok(children) = fs.read_dir(ino) else {
continue;
};
for e in children {
if e.name == b"." || e.name == b".." {
continue;
}
match e.file_type {
FsFileType::Directory => {
if visited.insert(e.inode) {
queue.push_back(e.inode);
}
}
FsFileType::RegularFile => {
let Ok(meta) = fs.metadata(e.inode) else {
continue;
};
if meta.size == 0 {
continue;
}
let bytes = fs
.read_file(e.inode)
.expect("read_file on a real bare-volume file must succeed");
assert_eq!(
bytes.len() as u64,
meta.size,
"read_file byte count for {:?} must equal metadata size",
e.name_str()
);
assert!(!bytes.is_empty());
read_a_file = true;
break 'outer;
}
_ => {}
}
}
}
assert!(
read_a_file,
"must read at least one real file through <root>/… on the bare exFAT fixture"
);
}
const NTFS_MARKERS: &[&[u8]] = &[
b"$MFT",
b"$LogFile",
b"$Extend",
b"$Boot",
b"$Bitmap",
b"Windows",
b"Users",
];
const MAX_NODES: usize = 20_000;
const MAX_DEPTH: u32 = 8;
const MAX_READ_SIZE: u64 = 16 * 1024 * 1024;
#[test]
fn e2e_multipartition_surfaces_ntfs() {
let Some(img) = std::env::var_os("FN_E2E_IMAGE") else {
eprintln!("SKIP e2e_multipartition_surfaces_ntfs: set FN_E2E_IMAGE=<path/to/image.E01>");
return;
};
let img = std::path::PathBuf::from(img);
if !img.is_file() {
eprintln!(
"SKIP e2e_multipartition_surfaces_ntfs: {} is not a file",
img.display()
);
return;
}
let mut fs = open_image_all(&img).expect("open_image_all must mount a real disk image");
let root = fs.root_ino();
let parts = fs.read_dir(root).expect("read_dir(root) must succeed");
let part_names: Vec<String> = parts
.iter()
.map(forensic_mount::FsDirEntry::name_str)
.collect();
eprintln!(
"e2e: {} partitions under the synthetic root: {:?}",
parts.len(),
part_names
);
assert!(
parts.len() >= 2,
"a multi-partition disk must surface >= 2 partitions, saw {part_names:?}"
);
for n in &part_names {
assert!(
!n.is_empty(),
"a volume dir name must never be empty, saw {part_names:?}"
);
}
if let Some(expect) = std::env::var_os("FN_E2E_EXPECT_PARTS") {
let expected: Vec<String> = expect
.to_string_lossy()
.split(',')
.map(|s| s.trim().to_string())
.collect();
assert_eq!(
part_names, expected,
"volume dir names must match the TSK-oracle-derived expectation"
);
}
for e in &parts {
assert_eq!(
e.file_type,
FsFileType::Directory,
"each partition entry must be a directory: {:?}",
e.name_str()
);
}
let mut ntfs_root: Option<(u64, String)> = None;
for e in &parts {
let Ok(entries) = fs.read_dir(e.inode) else {
continue;
};
if entries
.iter()
.any(|c| NTFS_MARKERS.contains(&c.name.as_slice()))
{
ntfs_root = Some((e.inode, e.name_str()));
break;
}
}
let (ntfs_ino, ntfs_label) =
ntfs_root.expect("one partition's root must carry NTFS markers ($MFT/Windows/Users)");
eprintln!("e2e: NTFS partition surfaced as {ntfs_label:?}");
let mut queue: std::collections::VecDeque<(u64, String, u32)> =
std::collections::VecDeque::new();
let mut visited: std::collections::HashSet<u64> = std::collections::HashSet::new();
queue.push_back((ntfs_ino, ntfs_label.clone(), 0));
visited.insert(ntfs_ino);
let mut nodes = 0usize;
while let Some((ino, path, depth)) = queue.pop_front() {
nodes += 1;
if nodes > MAX_NODES {
break;
}
let Ok(entries) = fs.read_dir(ino) else {
continue;
};
for e in entries {
if e.name == b"." || e.name == b".." {
continue;
}
let child_path = format!("{path}/{}", e.name_str());
match e.file_type {
FsFileType::Directory => {
if depth < MAX_DEPTH && visited.insert(e.inode) {
queue.push_back((e.inode, child_path, depth + 1));
}
}
FsFileType::RegularFile => {
let Ok(meta) = fs.metadata(e.inode) else {
continue;
};
if meta.size == 0 || meta.size > MAX_READ_SIZE {
continue;
}
let bytes = fs
.read_file(e.inode)
.expect("read_file on a real NTFS regular file must succeed");
assert_eq!(
bytes.len() as u64,
meta.size,
"read_file byte count for {child_path:?} must equal metadata size",
);
assert!(
!bytes.is_empty(),
"read_file returned empty for {child_path:?}"
);
eprintln!(
"e2e: VERIFIED NTFS file {child_path:?} through <volume>/ — read {} bytes \
== metadata size (first bytes: {:02x?})",
bytes.len(),
&bytes[..bytes.len().min(16)],
);
return;
}
_ => {}
}
}
}
panic!(
"no readable regular file (0 < size <= {MAX_READ_SIZE}) found in the NTFS partition \
within {nodes} nodes / depth {MAX_DEPTH}"
);
}