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;
11pub mod win_map;
12
13#[cfg(unix)]
14pub mod fuse_unix;
15#[cfg(windows)]
16pub mod fuse_windows;
17
18#[cfg(feature = "ext4")]
19pub mod fs_ext4;
20
21#[cfg(feature = "iso")]
22pub mod fs_iso;
23
24#[cfg(feature = "tarball")]
25pub mod fs_tar;
26
27#[cfg(feature = "zip")]
28pub mod fs_zip;
29
30#[cfg(feature = "sevenz")]
31pub mod fs_sevenz;
32
33#[cfg(feature = "ntfs")]
34pub mod fs_ntfs;
35
36#[cfg(feature = "hfsplus")]
37pub mod fs_hfsplus;
38
39#[cfg(feature = "exfat")]
40pub mod fs_exfat;
41
42#[cfg(feature = "apfs")]
43pub mod fs_apfs;
44
45#[cfg(feature = "memory")]
46pub mod mem;
47
48pub mod fs_raw;
49
50pub use types::*;
51
52use std::io;
53use std::path::Path;
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
63pub enum MountLayout {
64 #[default]
66 DiskOverlay,
67 Raw,
69}
70
71pub struct MountOptions {
76 pub read_only: bool,
77 pub daemon: bool,
78 pub fs_name: String,
79 pub layout: MountLayout,
80}
81
82impl Default for MountOptions {
83 fn default() -> Self {
84 Self {
85 read_only: false,
86 daemon: false,
87 fs_name: "4n6mount".to_string(),
88 layout: MountLayout::DiskOverlay,
89 }
90 }
91}
92
93pub trait ForensicFs {
98 fn root_ino(&self) -> u64;
102
103 fn read_dir(&mut self, ino: u64) -> FsResult<Vec<FsDirEntry>>;
105
106 fn lookup(&mut self, parent_ino: u64, name: &[u8]) -> FsResult<Option<u64>>;
108
109 fn metadata(&mut self, ino: u64) -> FsResult<FsMetadata>;
111
112 fn read_file(&mut self, ino: u64) -> FsResult<Vec<u8>>;
114
115 fn read_file_range(&mut self, ino: u64, offset: u64, len: u64) -> FsResult<Vec<u8>>;
117
118 fn read_link(&mut self, ino: u64) -> FsResult<Vec<u8>>;
120
121 fn deleted_inodes(&mut self) -> FsResult<Vec<FsDeletedInode>> {
125 Ok(vec![])
126 }
127
128 fn recover_file(&mut self, _ino: u64) -> FsResult<FsRecoveryResult> {
130 Err(not_supported("recover_file"))
131 }
132
133 fn timeline(&mut self) -> FsResult<Vec<FsTimelineEvent>> {
135 Ok(vec![])
136 }
137
138 fn unallocated_blocks(&mut self) -> FsResult<Vec<FsBlockRange>> {
140 Ok(vec![])
141 }
142
143 fn read_unallocated(&mut self, _range: &FsBlockRange) -> FsResult<Vec<u8>> {
145 Err(not_supported("read_unallocated"))
146 }
147
148 fn journal_transactions(&mut self) -> FsResult<Vec<FsTransaction>> {
150 Ok(vec![])
151 }
152
153 fn fs_info(&self) -> FsResult<serde_json::Value> {
155 Ok(serde_json::Value::Null)
156 }
157
158 fn block_size(&self) -> u64 {
160 4096
161 }
162}
163
164pub fn build_filesystem<R: io::Read + io::Seek + Send + 'static>(
180 reader: R,
181 fs_type: detect::FsType,
182 name: &str,
183) -> io::Result<Box<dyn ForensicFs + Send>> {
184 use detect::FsType;
185 let bad = |e: FsError| io::Error::new(io::ErrorKind::InvalidData, e.to_string());
186 match fs_type {
187 #[cfg(feature = "ext4")]
188 FsType::Ext4 => Ok(Box::new(fs_ext4::Ext4ForensicFs::new(reader).map_err(bad)?)),
189 #[cfg(feature = "iso")]
190 FsType::Iso => Ok(Box::new(fs_iso::IsoForensicFs::new(reader).map_err(bad)?)),
191 #[cfg(feature = "ntfs")]
192 FsType::Ntfs => Ok(Box::new(fs_ntfs::NtfsForensicFs::new(reader).map_err(bad)?)),
193 #[cfg(feature = "hfsplus")]
194 FsType::Hfsplus => Ok(Box::new(
195 fs_hfsplus::HfsPlusForensicFs::new(reader).map_err(bad)?,
196 )),
197 #[cfg(feature = "exfat")]
198 FsType::ExFat => Ok(Box::new(
199 fs_exfat::ExFatForensicFs::new(reader).map_err(bad)?,
200 )),
201 #[cfg(feature = "tarball")]
202 FsType::TarGz => Ok(Box::new(
203 fs_tar::TarballForensicFs::from_gz(reader).map_err(bad)?,
204 )),
205 #[cfg(feature = "tarball")]
206 FsType::TarBz2 => Ok(Box::new(
207 fs_tar::TarballForensicFs::from_bz2(reader).map_err(bad)?,
208 )),
209 #[cfg(feature = "zip")]
210 FsType::Zip => Ok(Box::new(fs_zip::ZipForensicFs::new(reader).map_err(bad)?)),
211 #[cfg(feature = "sevenz")]
212 FsType::SevenZ => Ok(Box::new(
213 fs_sevenz::SevenZForensicFs::new(reader).map_err(bad)?,
214 )),
215 FsType::Unknown => Ok(Box::new(
216 fs_raw::RawForensicFs::new(reader, name.to_string()).map_err(bad)?,
217 )),
218 #[cfg(feature = "apfs")]
219 FsType::Apfs => Ok(Box::new(fs_apfs::ApfsForensicFs::new(reader).map_err(bad)?)),
220 other => Err(io::Error::new(
221 io::ErrorKind::Unsupported,
222 format!(
223 "filesystem '{other}' cannot be built here \
224 (a container type, or its feature was not compiled in)"
225 ),
226 )),
227 }
228}
229
230#[cfg(feature = "memory")]
245pub fn build_memory_fs(
246 image: &Path,
247 symbols: Option<&Path>,
248) -> io::Result<Box<dyn ForensicFs + Send>> {
249 let bad = |msg: String| io::Error::new(io::ErrorKind::InvalidData, msg);
250
251 let provider = memf_format::open_dump(image)
252 .map_err(|e| bad(format!("cannot open memory dump {}: {e}", image.display())))?;
253
254 let resolver: Box<dyn memf_symbols::SymbolResolver> = match symbols {
257 Some(p) => Box::new(
258 memf_symbols::isf::IsfResolver::from_path(p)
259 .map_err(|e| bad(format!("cannot load symbols {}: {e}", p.display())))?,
260 ),
261 None => Box::new(
262 memf_symbols::isf::IsfResolver::from_value(&serde_json::json!({}))
263 .map_err(|e| bad(format!("empty symbol resolver: {e}")))?,
264 ),
265 };
266
267 let metadata = provider.metadata();
268 let ctx = memf_session::build_analysis_context(
269 metadata.as_ref(),
270 resolver.as_ref(),
271 provider.as_ref(),
272 )
273 .map_err(|e| bad(format!("memory analysis bootstrap failed: {e}")))?;
274
275 Ok(Box::new(mem::memoryfs::MemoryFs::new(
276 provider, ctx, resolver,
277 )))
278}
279
280pub 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}