1#![forbid(unsafe_code)]
2
3pub mod archive_tree;
4pub mod detect;
5pub mod filter;
6#[cfg(unix)]
7pub mod fusefs;
8pub mod inode_map;
9pub mod session;
10pub mod types;
11
12#[cfg(unix)]
13pub mod fuse_unix;
14#[cfg(windows)]
15pub mod fuse_windows;
16
17#[cfg(feature = "ext4")]
18pub mod fs_ext4;
19
20#[cfg(feature = "iso")]
21pub mod fs_iso;
22
23#[cfg(feature = "tarball")]
24pub mod fs_tar;
25
26#[cfg(feature = "zip")]
27pub mod fs_zip;
28
29#[cfg(feature = "sevenz")]
30pub mod fs_sevenz;
31
32#[cfg(feature = "ntfs")]
33pub mod fs_ntfs;
34
35#[cfg(feature = "hfsplus")]
36pub mod fs_hfsplus;
37
38#[cfg(feature = "exfat")]
39pub mod fs_exfat;
40
41#[cfg(feature = "apfs")]
42pub mod fs_apfs;
43
44#[cfg(feature = "memory")]
45pub mod mem;
46
47pub mod fs_raw;
48
49pub use types::*;
50
51use std::io;
52use std::path::Path;
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
62pub enum MountLayout {
63 #[default]
65 DiskOverlay,
66 Raw,
68}
69
70pub struct MountOptions {
75 pub read_only: bool,
76 pub daemon: bool,
77 pub fs_name: String,
78 pub layout: MountLayout,
79}
80
81impl Default for MountOptions {
82 fn default() -> Self {
83 Self {
84 read_only: false,
85 daemon: false,
86 fs_name: "4n6mount".to_string(),
87 layout: MountLayout::DiskOverlay,
88 }
89 }
90}
91
92pub trait ForensicFs {
97 fn root_ino(&self) -> u64;
101
102 fn read_dir(&mut self, ino: u64) -> FsResult<Vec<FsDirEntry>>;
104
105 fn lookup(&mut self, parent_ino: u64, name: &[u8]) -> FsResult<Option<u64>>;
107
108 fn metadata(&mut self, ino: u64) -> FsResult<FsMetadata>;
110
111 fn read_file(&mut self, ino: u64) -> FsResult<Vec<u8>>;
113
114 fn read_file_range(&mut self, ino: u64, offset: u64, len: u64) -> FsResult<Vec<u8>>;
116
117 fn read_link(&mut self, ino: u64) -> FsResult<Vec<u8>>;
119
120 fn deleted_inodes(&mut self) -> FsResult<Vec<FsDeletedInode>> {
124 Ok(vec![])
125 }
126
127 fn recover_file(&mut self, _ino: u64) -> FsResult<FsRecoveryResult> {
129 Err(not_supported("recover_file"))
130 }
131
132 fn timeline(&mut self) -> FsResult<Vec<FsTimelineEvent>> {
134 Ok(vec![])
135 }
136
137 fn unallocated_blocks(&mut self) -> FsResult<Vec<FsBlockRange>> {
139 Ok(vec![])
140 }
141
142 fn read_unallocated(&mut self, _range: &FsBlockRange) -> FsResult<Vec<u8>> {
144 Err(not_supported("read_unallocated"))
145 }
146
147 fn journal_transactions(&mut self) -> FsResult<Vec<FsTransaction>> {
149 Ok(vec![])
150 }
151
152 fn fs_info(&self) -> FsResult<serde_json::Value> {
154 Ok(serde_json::Value::Null)
155 }
156
157 fn block_size(&self) -> u64 {
159 4096
160 }
161}
162
163pub fn build_filesystem<R: io::Read + io::Seek + Send + 'static>(
179 reader: R,
180 fs_type: detect::FsType,
181 name: &str,
182) -> io::Result<Box<dyn ForensicFs + Send>> {
183 use detect::FsType;
184 let bad = |e: FsError| io::Error::new(io::ErrorKind::InvalidData, e.to_string());
185 match fs_type {
186 #[cfg(feature = "ext4")]
187 FsType::Ext4 => Ok(Box::new(fs_ext4::Ext4ForensicFs::new(reader).map_err(bad)?)),
188 #[cfg(feature = "iso")]
189 FsType::Iso => Ok(Box::new(fs_iso::IsoForensicFs::new(reader).map_err(bad)?)),
190 #[cfg(feature = "ntfs")]
191 FsType::Ntfs => Ok(Box::new(fs_ntfs::NtfsForensicFs::new(reader).map_err(bad)?)),
192 #[cfg(feature = "hfsplus")]
193 FsType::Hfsplus => Ok(Box::new(
194 fs_hfsplus::HfsPlusForensicFs::new(reader).map_err(bad)?,
195 )),
196 #[cfg(feature = "exfat")]
197 FsType::ExFat => Ok(Box::new(
198 fs_exfat::ExFatForensicFs::new(reader).map_err(bad)?,
199 )),
200 #[cfg(feature = "tarball")]
201 FsType::TarGz => Ok(Box::new(
202 fs_tar::TarballForensicFs::from_gz(reader).map_err(bad)?,
203 )),
204 #[cfg(feature = "tarball")]
205 FsType::TarBz2 => Ok(Box::new(
206 fs_tar::TarballForensicFs::from_bz2(reader).map_err(bad)?,
207 )),
208 #[cfg(feature = "zip")]
209 FsType::Zip => Ok(Box::new(fs_zip::ZipForensicFs::new(reader).map_err(bad)?)),
210 #[cfg(feature = "sevenz")]
211 FsType::SevenZ => Ok(Box::new(
212 fs_sevenz::SevenZForensicFs::new(reader).map_err(bad)?,
213 )),
214 FsType::Unknown => Ok(Box::new(
215 fs_raw::RawForensicFs::new(reader, name.to_string()).map_err(bad)?,
216 )),
217 #[cfg(feature = "apfs")]
218 FsType::Apfs => Ok(Box::new(fs_apfs::ApfsForensicFs::new(reader).map_err(bad)?)),
219 other => Err(io::Error::new(
220 io::ErrorKind::Unsupported,
221 format!(
222 "filesystem '{other}' cannot be built here \
223 (a container type, or its feature was not compiled in)"
224 ),
225 )),
226 }
227}
228
229#[cfg(feature = "memory")]
244pub fn build_memory_fs(
245 image: &Path,
246 symbols: Option<&Path>,
247) -> io::Result<Box<dyn ForensicFs + Send>> {
248 let bad = |msg: String| io::Error::new(io::ErrorKind::InvalidData, msg);
249
250 let provider = memf_format::open_dump(image)
251 .map_err(|e| bad(format!("cannot open memory dump {}: {e}", image.display())))?;
252
253 let resolver: Box<dyn memf_symbols::SymbolResolver> = match symbols {
256 Some(p) => Box::new(
257 memf_symbols::isf::IsfResolver::from_path(p)
258 .map_err(|e| bad(format!("cannot load symbols {}: {e}", p.display())))?,
259 ),
260 None => Box::new(
261 memf_symbols::isf::IsfResolver::from_value(&serde_json::json!({}))
262 .map_err(|e| bad(format!("empty symbol resolver: {e}")))?,
263 ),
264 };
265
266 let metadata = provider.metadata();
267 let ctx = memf_session::build_analysis_context(
268 metadata.as_ref(),
269 resolver.as_ref(),
270 provider.as_ref(),
271 )
272 .map_err(|e| bad(format!("memory analysis bootstrap failed: {e}")))?;
273
274 Ok(Box::new(mem::memoryfs::MemoryFs::new(
275 provider, ctx, resolver,
276 )))
277}
278
279pub fn mount(
289 fs: Box<dyn ForensicFs + Send>,
290 mountpoint: &Path,
291 session: Option<session::Session>,
292 options: &MountOptions,
293) -> io::Result<()> {
294 #[cfg(unix)]
295 {
296 fuse_unix::mount_unix(fs, mountpoint, session, options)
297 }
298 #[cfg(windows)]
299 {
300 fuse_windows::mount_windows(fs, mountpoint, session, options)
301 }
302 #[cfg(not(any(unix, windows)))]
303 {
304 let _ = (fs, mountpoint, session, options);
305 Err(io::Error::new(
306 io::ErrorKind::Unsupported,
307 "no FUSE support on this platform",
308 ))
309 }
310}
311
312#[cfg(test)]
313mod dispatch_tests {
314 use super::*;
315 use std::io::Cursor;
316
317 #[test]
318 fn apfs_garbage_errors_loud_not_silent() {
319 match build_filesystem(Cursor::new(vec![0u8; 64]), detect::FsType::Apfs, "x") {
321 Err(e) => assert_eq!(e.kind(), io::ErrorKind::InvalidData),
322 Ok(_) => panic!("garbage must error, not mount"),
323 }
324 }
325
326 #[cfg(feature = "apfs")]
327 #[test]
328 fn apfs_dispatches_to_module() {
329 let img = "/Users/4n6h4x0r/src/apfs-forensic/tests/data/apfs_fstree.bin";
330 let Ok(data) = std::fs::read(img) else {
331 eprintln!("skip: apfs_fstree.bin unavailable");
332 return;
333 };
334 let fs = build_filesystem(Cursor::new(data), detect::FsType::Apfs, "x").unwrap();
335 assert_eq!(fs.fs_info().unwrap()["type"], "apfs");
336 }
337
338 #[test]
339 fn unknown_builds_raw() {
340 let fs = build_filesystem(
341 Cursor::new(b"hello".to_vec()),
342 detect::FsType::Unknown,
343 "evidence.bin",
344 )
345 .unwrap();
346 assert_eq!(fs.fs_info().unwrap()["filesystem"], "raw");
347 }
348
349 #[cfg(feature = "hfsplus")]
350 #[test]
351 fn hfsplus_dispatches_to_module() {
352 let img = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/hfsplus.img");
353 let Ok(data) = std::fs::read(img) else {
354 eprintln!("skip: hfsplus.img unavailable");
355 return;
356 };
357 let fs = build_filesystem(Cursor::new(data), detect::FsType::Hfsplus, "x").unwrap();
358 assert_eq!(fs.fs_info().unwrap()["type"], "hfsplus");
359 }
360
361 #[cfg(feature = "exfat")]
362 #[test]
363 fn exfat_dispatches_to_module() {
364 let img = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/exfat.img");
365 let Ok(data) = std::fs::read(img) else {
366 eprintln!("skip: exfat.img unavailable");
367 return;
368 };
369 let fs = build_filesystem(Cursor::new(data), detect::FsType::ExFat, "x").unwrap();
370 assert_eq!(fs.fs_info().unwrap()["type"], "exfat");
371 }
372}
373
374#[cfg(all(test, feature = "memory"))]
375mod memory_tests {
376 use super::*;
377
378 #[test]
381 fn build_memory_fs_bootstraps_crashdump() {
382 use memf_format::test_builders::CrashDumpBuilder;
383 let bytes = CrashDumpBuilder::new().cr3(0x1ab000).build();
384
385 let dir = std::env::temp_dir().join(format!("4n6mem_{}", std::process::id()));
386 std::fs::create_dir_all(&dir).unwrap();
387 let path = dir.join("crash.dmp");
388 std::fs::write(&path, &bytes).unwrap();
389
390 let mut fs = build_memory_fs(&path, None).expect("crash dump must bootstrap");
391 let sys = fs
393 .lookup(mem::inode::ROOT_INO, b"sys")
394 .unwrap()
395 .expect("sys");
396 let oi = fs
397 .lookup(sys, b"os-info.txt")
398 .unwrap()
399 .expect("os-info.txt");
400 let text = String::from_utf8(fs.read_file(oi).unwrap()).unwrap();
401 assert!(text.contains("OS: Windows"), "got: {text}");
402
403 std::fs::remove_dir_all(&dir).ok();
404 }
405}