#![forbid(unsafe_code)]
use clap::Parser;
#[derive(Parser)]
#[command(
name = "4n6mount",
about = "Universal forensic FUSE mount — auto-detects ext4, NTFS, exFAT",
version
)]
struct Cli {
image: Option<String>,
mountpoint: Option<String>,
#[arg(long)]
fs: Option<String>,
#[arg(long)]
session: Option<String>,
#[arg(long)]
resume: bool,
#[arg(long)]
daemon: bool,
#[arg(long = "filter-db")]
filter_dbs: Vec<String>,
#[arg(long = "export-session")]
export_session: Option<String>,
#[arg(long)]
output: Option<String>,
#[arg(long = "import-session")]
import_session: Option<String>,
}
fn main() {
let cli = Cli::parse();
if let Some(session_dir) = &cli.export_session {
let output = cli.output.as_deref().unwrap_or_else(|| {
eprintln!("--output required with --export-session");
std::process::exit(1);
});
forensic_mount::session::export_session(
std::path::Path::new(session_dir),
std::path::Path::new(output),
)
.unwrap_or_else(|e| {
eprintln!("Export failed: {e}");
std::process::exit(1);
});
eprintln!("Session exported to {output}");
return;
}
if let Some(tarball) = &cli.import_session {
let session_dir = cli.session.as_deref().unwrap_or_else(|| {
eprintln!("--session required with --import-session");
std::process::exit(1);
});
forensic_mount::session::import_session(
std::path::Path::new(tarball),
std::path::Path::new(session_dir),
)
.unwrap_or_else(|e| {
eprintln!("Import failed: {e}");
std::process::exit(1);
});
eprintln!("Session imported to {session_dir}");
return;
}
let image = cli.image.unwrap_or_else(|| {
eprintln!("Usage: 4n6mount <image> <mountpoint>");
std::process::exit(1);
});
let mountpoint = cli.mountpoint.unwrap_or_else(|| {
eprintln!("Usage: 4n6mount <image> <mountpoint>");
std::process::exit(1);
});
let mut file = std::fs::File::open(&image).unwrap_or_else(|e| {
eprintln!("Cannot open {image}: {e}");
std::process::exit(1);
});
let fs_type = if let Some(fs_str) = &cli.fs {
fs_str
.parse::<forensic_mount::detect::FsType>()
.unwrap_or_else(|e| {
eprintln!("{e}");
std::process::exit(1);
})
} else {
forensic_mount::detect::detect_filesystem(&mut file).unwrap_or_else(|e| {
eprintln!("Detection failed: {e}");
std::process::exit(1);
})
};
eprintln!("Detected filesystem: {fs_type}");
let forensic_fs: Box<dyn forensic_mount::ForensicFs + Send> = match fs_type {
#[cfg(feature = "ext4")]
forensic_mount::detect::FsType::Ext4 => Box::new(
forensic_mount::fs_ext4::Ext4ForensicFs::new(file).unwrap_or_else(|e| {
eprintln!("Cannot parse ext4: {e}");
std::process::exit(1);
}),
),
#[cfg(feature = "iso")]
forensic_mount::detect::FsType::Iso => Box::new(
forensic_mount::fs_iso::IsoForensicFs::new(file).unwrap_or_else(|e| {
eprintln!("Cannot parse ISO 9660: {e}");
std::process::exit(1);
}),
),
#[cfg(feature = "vmdk")]
forensic_mount::detect::FsType::Vmdk => {
let mut vmdk_reader = vmdk::VmdkFileReader::open_path(std::path::Path::new(&image))
.unwrap_or_else(|e| {
eprintln!("Cannot open VMDK image: {e}");
std::process::exit(1);
});
let inner_fs_type = forensic_mount::detect::detect_filesystem(&mut vmdk_reader)
.unwrap_or_else(|e| {
eprintln!("Cannot detect filesystem in VMDK: {e}");
std::process::exit(1);
});
eprintln!("VMDK container detected, inner filesystem: {inner_fs_type}");
match inner_fs_type {
#[cfg(feature = "ext4")]
forensic_mount::detect::FsType::Ext4 => Box::new(
forensic_mount::fs_ext4::Ext4ForensicFs::new(vmdk_reader).unwrap_or_else(|e| {
eprintln!("Cannot parse ext4 in VMDK: {e}");
std::process::exit(1);
}),
),
#[cfg(feature = "iso")]
forensic_mount::detect::FsType::Iso => Box::new(
forensic_mount::fs_iso::IsoForensicFs::new(vmdk_reader).unwrap_or_else(|e| {
eprintln!("Cannot parse ISO 9660 in VMDK: {e}");
std::process::exit(1);
}),
),
_ => {
eprintln!("No supported filesystem detected inside VMDK \u{2014} mounting as raw data");
let filename = std::path::Path::new(&image).file_name().map_or_else(
|| "evidence.bin".to_string(),
|n| n.to_string_lossy().to_string(),
);
Box::new(
forensic_mount::fs_raw::RawForensicFs::new(vmdk_reader, filename)
.unwrap_or_else(|e| {
eprintln!("Cannot create raw FS: {e}");
std::process::exit(1);
}),
)
}
}
}
#[cfg(feature = "ewf")]
forensic_mount::detect::FsType::Ewf => {
let mut ewf_reader = ewf::EwfReader::open(&image).unwrap_or_else(|e| {
eprintln!("Cannot open EWF image: {e}");
std::process::exit(1);
});
let inner_fs_type = forensic_mount::detect::detect_filesystem(&mut ewf_reader)
.unwrap_or_else(|e| {
eprintln!("Cannot detect filesystem in EWF: {e}");
std::process::exit(1);
});
eprintln!("EWF container detected, inner filesystem: {inner_fs_type}");
match inner_fs_type {
#[cfg(feature = "ext4")]
forensic_mount::detect::FsType::Ext4 => Box::new(
forensic_mount::fs_ext4::Ext4ForensicFs::new(ewf_reader).unwrap_or_else(|e| {
eprintln!("Cannot parse ext4 in EWF: {e}");
std::process::exit(1);
}),
),
#[cfg(feature = "iso")]
forensic_mount::detect::FsType::Iso => Box::new(
forensic_mount::fs_iso::IsoForensicFs::new(ewf_reader).unwrap_or_else(|e| {
eprintln!("Cannot parse ISO 9660 in EWF: {e}");
std::process::exit(1);
}),
),
forensic_mount::detect::FsType::Unknown => {
eprintln!("No filesystem detected inside EWF — mounting as raw data");
let filename = std::path::Path::new(&image).file_name().map_or_else(
|| "evidence.bin".to_string(),
|n| n.to_string_lossy().to_string(),
);
Box::new(
forensic_mount::fs_raw::RawForensicFs::new(ewf_reader, filename)
.unwrap_or_else(|e| {
eprintln!("Cannot create raw FS: {e}");
std::process::exit(1);
}),
)
}
other => {
eprintln!("Filesystem '{other}' inside EWF is not supported");
std::process::exit(1);
}
}
}
_ => {
eprintln!(
"Filesystem type '{fs_type}' is not supported (compiled features may be missing)"
);
std::process::exit(1);
}
};
let session_mgr = cli.session.map(|dir| {
let session_path = std::path::Path::new(&dir);
if cli.resume {
forensic_mount::session::Session::resume(session_path, std::path::Path::new(&image))
.unwrap_or_else(|e| {
eprintln!("Cannot resume session: {e}");
std::process::exit(1);
})
} else {
forensic_mount::session::Session::create(session_path, std::path::Path::new(&image))
.unwrap_or_else(|e| {
eprintln!("Cannot create session: {e}");
std::process::exit(1);
})
}
});
let options = forensic_mount::MountOptions {
read_only: session_mgr.is_none(),
daemon: cli.daemon,
fs_name: format!("4n6mount-{fs_type}"),
};
eprintln!("Mounting {image} at {mountpoint}");
forensic_mount::mount(
forensic_fs,
std::path::Path::new(&mountpoint),
session_mgr,
&options,
)
.unwrap_or_else(|e| {
eprintln!("Mount failed: {e}");
std::process::exit(1);
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_mount_args() {
let cli = Cli::parse_from(["4n6mount", "image.dd", "/mnt/evidence"]);
assert_eq!(cli.image.unwrap(), "image.dd");
assert_eq!(cli.mountpoint.unwrap(), "/mnt/evidence");
assert!(cli.fs.is_none());
assert!(!cli.daemon);
assert!(!cli.resume);
}
#[test]
fn parse_mount_with_fs_override() {
let cli = Cli::parse_from(["4n6mount", "image.dd", "/mnt", "--fs", "ntfs"]);
assert_eq!(cli.fs.unwrap(), "ntfs");
}
#[test]
fn parse_mount_with_session() {
let cli = Cli::parse_from(["4n6mount", "image.dd", "/mnt", "--session", "./case-001"]);
assert_eq!(cli.session.unwrap(), "./case-001");
}
#[test]
fn parse_mount_with_daemon() {
let cli = Cli::parse_from(["4n6mount", "image.dd", "/mnt", "--daemon"]);
assert!(cli.daemon);
}
#[test]
fn parse_mount_with_resume() {
let cli = Cli::parse_from([
"4n6mount",
"image.dd",
"/mnt",
"--session",
"./case",
"--resume",
]);
assert!(cli.resume);
}
#[test]
fn parse_mount_with_filter_dbs() {
let cli = Cli::parse_from([
"4n6mount",
"image.dd",
"/mnt",
"--filter-db",
"/path/nsrl.db",
"--filter-db",
"/path/custom.txt",
]);
assert_eq!(cli.filter_dbs.len(), 2);
}
#[test]
fn parse_export_session() {
let cli = Cli::parse_from([
"4n6mount",
"--export-session",
"./case-001",
"--output",
"case.tar.gz",
]);
assert_eq!(cli.export_session.unwrap(), "./case-001");
assert_eq!(cli.output.unwrap(), "case.tar.gz");
assert!(cli.image.is_none());
}
#[test]
fn parse_import_session() {
let cli = Cli::parse_from([
"4n6mount",
"--import-session",
"case.tar.gz",
"--session",
"./case-002",
]);
assert_eq!(cli.import_session.unwrap(), "case.tar.gz");
assert_eq!(cli.session.unwrap(), "./case-002");
}
#[test]
fn parse_all_options() {
let cli = Cli::parse_from([
"4n6mount",
"image.E01",
"/mnt/evidence",
"--fs",
"ext4",
"--session",
"./case",
"--resume",
"--daemon",
"--filter-db",
"nsrl.db",
]);
assert_eq!(cli.image.unwrap(), "image.E01");
assert_eq!(cli.mountpoint.unwrap(), "/mnt/evidence");
assert_eq!(cli.fs.unwrap(), "ext4");
assert_eq!(cli.session.unwrap(), "./case");
assert!(cli.resume);
assert!(cli.daemon);
assert_eq!(cli.filter_dbs.len(), 1);
}
}