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