#![forbid(unsafe_code)]
#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
pub mod detect;
pub mod filter;
#[cfg(unix)]
pub mod fusefs;
pub mod inode_map;
pub mod marking;
pub mod session;
pub mod win_map;
#[cfg(unix)]
pub mod fuse_unix;
#[cfg(windows)]
pub mod fuse_windows;
#[cfg(feature = "memory")]
pub mod mem;
pub mod engine_fs;
pub mod types;
pub use engine_fs::{open_image, open_image_all, EngineFs, MultiPartitionFs};
pub use types::*;
use std::io;
use std::path::Path;
pub trait ForensicFs {
fn root_ino(&self) -> u64;
fn read_dir(&mut self, ino: u64) -> FsResult<Vec<FsDirEntry>>;
fn lookup(&mut self, parent_ino: u64, name: &[u8]) -> FsResult<Option<u64>>;
fn metadata(&mut self, ino: u64) -> FsResult<FsMetadata>;
fn read_file(&mut self, ino: u64) -> FsResult<Vec<u8>>;
fn read_file_range(&mut self, ino: u64, offset: u64, len: u64) -> FsResult<Vec<u8>>;
fn read_link(&mut self, ino: u64) -> FsResult<Vec<u8>>;
fn deleted_inodes(&mut self) -> FsResult<Vec<FsDeletedInode>> {
Ok(vec![])
}
fn deleted_nodes(&mut self) -> FsResult<Vec<FsDeletedNode>> {
Ok(vec![])
}
fn recover_file(&mut self, _ino: u64) -> FsResult<FsRecoveryResult> {
Err(not_supported("recover_file"))
}
fn timeline(&mut self) -> FsResult<Vec<FsTimelineEvent>> {
Ok(vec![])
}
fn unallocated_blocks(&mut self) -> FsResult<Vec<FsBlockRange>> {
Ok(vec![])
}
fn read_unallocated(&mut self, _range: &FsBlockRange) -> FsResult<Vec<u8>> {
Err(not_supported("read_unallocated"))
}
fn journal_transactions(&mut self) -> FsResult<Vec<FsTransaction>> {
Ok(vec![])
}
fn fs_info(&self) -> FsResult<serde_json::Value> {
Ok(serde_json::Value::Null)
}
fn block_size(&self) -> u64 {
4096
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MountLayout {
#[default]
DiskOverlay,
Raw,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)]
pub enum DeletedMode {
#[default]
Latest,
All,
Off,
}
pub struct MountOptions {
pub read_only: bool,
pub daemon: bool,
pub fs_name: String,
pub layout: MountLayout,
pub deleted_mode: DeletedMode,
}
impl Default for MountOptions {
fn default() -> Self {
Self {
read_only: false,
daemon: false,
fs_name: "4n6mount".to_string(),
layout: MountLayout::DiskOverlay,
deleted_mode: DeletedMode::default(),
}
}
}
#[cfg(feature = "memory")]
struct DynSourceDumpReader {
src: forensic_vfs::DynSource,
pos: u64,
len: u64,
}
#[cfg(feature = "memory")]
impl DynSourceDumpReader {
fn new(src: forensic_vfs::DynSource) -> Self {
let len = src.len();
Self { src, pos: 0, len }
}
}
#[cfg(feature = "memory")]
impl io::Read for DynSourceDumpReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let n = self
.src
.read_at(self.pos, buf)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
self.pos += n as u64;
Ok(n)
}
}
#[cfg(feature = "memory")]
impl io::Seek for DynSourceDumpReader {
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
let new = match pos {
io::SeekFrom::Start(o) => Some(o),
io::SeekFrom::End(d) => self.len.checked_add_signed(d),
io::SeekFrom::Current(d) => self.pos.checked_add_signed(d),
};
match new {
Some(p) => {
self.pos = p;
Ok(p)
}
None => Err(io::Error::new(
io::ErrorKind::InvalidInput,
"seek to a negative or overflowing offset",
)),
}
}
}
#[cfg(feature = "memory")]
pub fn open_memory_provider(
image: &Path,
) -> io::Result<Box<dyn memf_format::PhysicalMemoryProvider>> {
use forensic_vfs_resolver::SourceOpen as _;
let bad = |msg: String| io::Error::new(io::ErrorKind::InvalidData, msg);
let base: forensic_vfs::DynSource = std::sync::Arc::new(
forensic_vfs::adapters::FileSource::open(image)
.map_err(|e| bad(format!("cannot open memory dump {}: {e}", image.display())))?,
);
let base_spec = forensic_vfs::Locator::file(image);
let openers = forensic_vfs::Openers::new().archive(archive_core::ArchiveOpener);
let resolved = openers
.resolve_to_source(base, base_spec, 0)
.map_err(|e| bad(format!("unwrapping memory dump {}: {e}", image.display())))?
.ok_or_else(|| {
bad(format!(
"could not unwrap memory dump {} (packaging recursion cap hit)",
image.display()
))
})?;
let reader = DynSourceDumpReader::new(resolved.source);
memf_format::open_source_with_raw_fallback(Box::new(reader))
.map_err(|e| bad(format!("cannot open memory dump {}: {e}", image.display())))
}
#[cfg(feature = "memory")]
pub fn build_memory_fs(
image: &Path,
symbols: Option<&Path>,
) -> io::Result<Box<dyn ForensicFs + Send>> {
let bad = |msg: String| io::Error::new(io::ErrorKind::InvalidData, msg);
let provider = open_memory_provider(image)?;
let resolver: Box<dyn memf_symbols::SymbolResolver> = match symbols {
Some(p) => Box::new(
memf_symbols::isf::IsfResolver::from_path(p)
.map_err(|e| bad(format!("cannot load symbols {}: {e}", p.display())))?,
),
None => Box::new(
memf_symbols::isf::IsfResolver::from_value(&serde_json::json!({}))
.map_err(|e| bad(format!("empty symbol resolver: {e}")))?,
),
};
let metadata = provider.metadata();
let ctx = memf_session::build_analysis_context(
metadata.as_ref(),
resolver.as_ref(),
provider.as_ref(),
)
.map_err(|e| bad(format!("memory analysis bootstrap failed: {e}")))?;
Ok(Box::new(mem::memoryfs::MemoryFs::new(
provider, ctx, resolver,
)))
}
pub fn mount(
fs: Box<dyn ForensicFs + Send>,
mountpoint: &Path,
session: Option<session::Session>,
options: &MountOptions,
) -> io::Result<()> {
#[cfg(unix)]
{
fuse_unix::mount_unix(fs, mountpoint, session, options)
}
#[cfg(windows)]
{
fuse_windows::mount_windows(fs, mountpoint, session, options)
}
#[cfg(not(any(unix, windows)))]
{
let _ = (fs, mountpoint, session, options);
Err(io::Error::new(
io::ErrorKind::Unsupported,
"no FUSE support on this platform",
))
}
}
#[cfg(all(test, feature = "memory"))]
mod memory_tests {
use super::*;
#[test]
fn build_memory_fs_bootstraps_crashdump() {
use memf_format::test_builders::CrashDumpBuilder;
let bytes = CrashDumpBuilder::new().cr3(0x1a_b000).build();
let dir = std::env::temp_dir().join(format!("4n6mem_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("crash.dmp");
std::fs::write(&path, &bytes).unwrap();
let mut fs = build_memory_fs(&path, None).expect("crash dump must bootstrap");
let sys = fs
.lookup(mem::inode::ROOT_INO, b"sys")
.unwrap()
.expect("sys");
let oi = fs
.lookup(sys, b"os-info.txt")
.unwrap()
.expect("os-info.txt");
let text = String::from_utf8(fs.read_file(oi).unwrap()).unwrap();
assert!(text.contains("OS: Windows"), "got: {text}");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn wrapped_memory_dump_reads_same_pages_as_raw() {
use memf_format::PhysicalMemoryProvider;
use std::io::Write as _;
let page: Vec<u8> = (0..4096u32).map(|i| (i % 251) as u8).collect();
let dir = std::env::temp_dir().join(format!("4n6mem_wrap_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let raw_path = dir.join("memory.raw");
std::fs::write(&raw_path, &page).unwrap();
let mut zip_bytes = Vec::new();
{
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(&mut zip_bytes));
let opts = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Stored);
zw.start_file("memory.raw", opts).unwrap();
zw.write_all(&page).unwrap();
zw.finish().unwrap();
}
let zip_path = dir.join("memory.zip");
std::fs::write(&zip_path, &zip_bytes).unwrap();
let gz_path = dir.join("memory.zip.gz");
{
let mut enc = flate2::write::GzEncoder::new(
std::fs::File::create(&gz_path).unwrap(),
flate2::Compression::default(),
);
enc.write_all(&zip_bytes).unwrap();
enc.finish().unwrap();
}
let read_first_page = |p: &std::path::Path| -> Vec<u8> {
let provider = open_memory_provider(p).expect("open wrapped/raw memory provider");
assert_eq!(provider.format_name(), "Raw");
assert_eq!(provider.total_size(), page.len() as u64);
let mut got = vec![0u8; page.len()];
let n = provider.read_phys(0, &mut got).unwrap();
got.truncate(n);
got
};
assert_eq!(read_first_page(&raw_path), page, "raw baseline");
assert_eq!(
read_first_page(&zip_path),
page,
".zip-wrapped dump reads the same pages as raw"
);
assert_eq!(
read_first_page(&gz_path),
page,
"nested .zip.gz dump reads the same pages as raw"
);
std::fs::remove_dir_all(&dir).ok();
}
}