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 = "ad1")]
34pub mod fs_ad1;
35
36#[cfg(feature = "aff4")]
37pub mod fs_aff4;
38
39#[cfg(feature = "ntfs")]
40pub mod fs_ntfs;
41
42#[cfg(feature = "hfsplus")]
43pub mod fs_hfsplus;
44
45#[cfg(feature = "exfat")]
46pub mod fs_exfat;
47
48#[cfg(feature = "apfs")]
49pub mod fs_apfs;
50
51#[cfg(feature = "memory")]
52pub mod mem;
53
54pub mod fs_raw;
55
56pub use types::*;
57
58use std::io;
59use std::path::Path;
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
69pub enum MountLayout {
70 #[default]
72 DiskOverlay,
73 Raw,
75}
76
77pub struct MountOptions {
82 pub read_only: bool,
83 pub daemon: bool,
84 pub fs_name: String,
85 pub layout: MountLayout,
86}
87
88impl Default for MountOptions {
89 fn default() -> Self {
90 Self {
91 read_only: false,
92 daemon: false,
93 fs_name: "4n6mount".to_string(),
94 layout: MountLayout::DiskOverlay,
95 }
96 }
97}
98
99pub trait ForensicFs {
104 fn root_ino(&self) -> u64;
108
109 fn read_dir(&mut self, ino: u64) -> FsResult<Vec<FsDirEntry>>;
111
112 fn lookup(&mut self, parent_ino: u64, name: &[u8]) -> FsResult<Option<u64>>;
114
115 fn metadata(&mut self, ino: u64) -> FsResult<FsMetadata>;
117
118 fn read_file(&mut self, ino: u64) -> FsResult<Vec<u8>>;
120
121 fn read_file_range(&mut self, ino: u64, offset: u64, len: u64) -> FsResult<Vec<u8>>;
123
124 fn read_link(&mut self, ino: u64) -> FsResult<Vec<u8>>;
126
127 fn deleted_inodes(&mut self) -> FsResult<Vec<FsDeletedInode>> {
131 Ok(vec![])
132 }
133
134 fn recover_file(&mut self, _ino: u64) -> FsResult<FsRecoveryResult> {
136 Err(not_supported("recover_file"))
137 }
138
139 fn timeline(&mut self) -> FsResult<Vec<FsTimelineEvent>> {
141 Ok(vec![])
142 }
143
144 fn unallocated_blocks(&mut self) -> FsResult<Vec<FsBlockRange>> {
146 Ok(vec![])
147 }
148
149 fn read_unallocated(&mut self, _range: &FsBlockRange) -> FsResult<Vec<u8>> {
151 Err(not_supported("read_unallocated"))
152 }
153
154 fn journal_transactions(&mut self) -> FsResult<Vec<FsTransaction>> {
156 Ok(vec![])
157 }
158
159 fn fs_info(&self) -> FsResult<serde_json::Value> {
161 Ok(serde_json::Value::Null)
162 }
163
164 fn block_size(&self) -> u64 {
166 4096
167 }
168}
169
170pub fn build_filesystem<R: io::Read + io::Seek + Send + 'static>(
186 reader: R,
187 fs_type: detect::FsType,
188 name: &str,
189) -> io::Result<Box<dyn ForensicFs + Send>> {
190 use detect::FsType;
191 let bad = |e: FsError| io::Error::new(io::ErrorKind::InvalidData, e.to_string());
192 match fs_type {
193 #[cfg(feature = "ext4")]
194 FsType::Ext4 => Ok(Box::new(fs_ext4::Ext4ForensicFs::new(reader).map_err(bad)?)),
195 #[cfg(feature = "iso")]
196 FsType::Iso => Ok(Box::new(fs_iso::IsoForensicFs::new(reader).map_err(bad)?)),
197 #[cfg(feature = "ntfs")]
198 FsType::Ntfs => Ok(Box::new(fs_ntfs::NtfsForensicFs::new(reader).map_err(bad)?)),
199 #[cfg(feature = "hfsplus")]
200 FsType::Hfsplus => Ok(Box::new(
201 fs_hfsplus::HfsPlusForensicFs::new(reader).map_err(bad)?,
202 )),
203 #[cfg(feature = "exfat")]
204 FsType::ExFat => Ok(Box::new(
205 fs_exfat::ExFatForensicFs::new(reader).map_err(bad)?,
206 )),
207 #[cfg(feature = "tarball")]
208 FsType::TarGz => Ok(Box::new(
209 fs_tar::TarballForensicFs::from_gz(reader).map_err(bad)?,
210 )),
211 #[cfg(feature = "tarball")]
212 FsType::TarBz2 => Ok(Box::new(
213 fs_tar::TarballForensicFs::from_bz2(reader).map_err(bad)?,
214 )),
215 #[cfg(feature = "zip")]
216 FsType::Zip => Ok(Box::new(fs_zip::ZipForensicFs::new(reader).map_err(bad)?)),
217 #[cfg(feature = "sevenz")]
218 FsType::SevenZ => Ok(Box::new(
219 fs_sevenz::SevenZForensicFs::new(reader).map_err(bad)?,
220 )),
221 FsType::Unknown => Ok(Box::new(
222 fs_raw::RawForensicFs::new(reader, name.to_string()).map_err(bad)?,
223 )),
224 #[cfg(feature = "apfs")]
225 FsType::Apfs => Ok(Box::new(fs_apfs::ApfsForensicFs::new(reader).map_err(bad)?)),
226 other => Err(io::Error::new(
227 io::ErrorKind::Unsupported,
228 format!(
229 "filesystem '{other}' cannot be built here \
230 (a container type, or its feature was not compiled in)"
231 ),
232 )),
233 }
234}
235
236#[cfg(feature = "memory")]
251pub fn build_memory_fs(
252 image: &Path,
253 symbols: Option<&Path>,
254) -> io::Result<Box<dyn ForensicFs + Send>> {
255 let bad = |msg: String| io::Error::new(io::ErrorKind::InvalidData, msg);
256
257 let provider = memf_format::open_dump(image)
258 .map_err(|e| bad(format!("cannot open memory dump {}: {e}", image.display())))?;
259
260 let resolver: Box<dyn memf_symbols::SymbolResolver> = match symbols {
263 Some(p) => Box::new(
264 memf_symbols::isf::IsfResolver::from_path(p)
265 .map_err(|e| bad(format!("cannot load symbols {}: {e}", p.display())))?,
266 ),
267 None => Box::new(
268 memf_symbols::isf::IsfResolver::from_value(&serde_json::json!({}))
269 .map_err(|e| bad(format!("empty symbol resolver: {e}")))?,
270 ),
271 };
272
273 let metadata = provider.metadata();
274 let ctx = memf_session::build_analysis_context(
275 metadata.as_ref(),
276 resolver.as_ref(),
277 provider.as_ref(),
278 )
279 .map_err(|e| bad(format!("memory analysis bootstrap failed: {e}")))?;
280
281 Ok(Box::new(mem::memoryfs::MemoryFs::new(
282 provider, ctx, resolver,
283 )))
284}
285
286pub fn mount(
295 fs: Box<dyn ForensicFs + Send>,
296 mountpoint: &Path,
297 session: Option<session::Session>,
298 options: &MountOptions,
299) -> io::Result<()> {
300 #[cfg(unix)]
301 {
302 fuse_unix::mount_unix(fs, mountpoint, session, options)
303 }
304 #[cfg(windows)]
305 {
306 fuse_windows::mount_windows(fs, mountpoint, session, options)
307 }
308 #[cfg(not(any(unix, windows)))]
309 {
310 let _ = (fs, mountpoint, session, options);
311 Err(io::Error::new(
312 io::ErrorKind::Unsupported,
313 "no FUSE support on this platform",
314 ))
315 }
316}
317
318#[cfg(test)]
319mod dispatch_tests {
320 use super::*;
321 use std::io::Cursor;
322
323 #[test]
324 fn apfs_garbage_errors_loud_not_silent() {
325 match build_filesystem(Cursor::new(vec![0u8; 64]), detect::FsType::Apfs, "x") {
327 Err(e) => assert_eq!(e.kind(), io::ErrorKind::InvalidData),
328 Ok(_) => panic!("garbage must error, not mount"),
329 }
330 }
331
332 #[cfg(feature = "apfs")]
333 #[test]
334 fn apfs_dispatches_to_module() {
335 let img = "/Users/4n6h4x0r/src/apfs-forensic/tests/data/apfs_fstree.bin";
336 let Ok(data) = std::fs::read(img) else {
337 eprintln!("skip: apfs_fstree.bin unavailable");
338 return;
339 };
340 let fs = build_filesystem(Cursor::new(data), detect::FsType::Apfs, "x").unwrap();
341 assert_eq!(fs.fs_info().unwrap()["type"], "apfs");
342 }
343
344 #[test]
345 fn unknown_builds_raw() {
346 let fs = build_filesystem(
347 Cursor::new(b"hello".to_vec()),
348 detect::FsType::Unknown,
349 "evidence.bin",
350 )
351 .unwrap();
352 assert_eq!(fs.fs_info().unwrap()["filesystem"], "raw");
353 }
354
355 #[cfg(feature = "hfsplus")]
356 #[test]
357 fn hfsplus_dispatches_to_module() {
358 let img = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/hfsplus.img");
359 let Ok(data) = std::fs::read(img) else {
360 eprintln!("skip: hfsplus.img unavailable");
361 return;
362 };
363 let fs = build_filesystem(Cursor::new(data), detect::FsType::Hfsplus, "x").unwrap();
364 assert_eq!(fs.fs_info().unwrap()["type"], "hfsplus");
365 }
366
367 #[cfg(feature = "exfat")]
368 #[test]
369 fn exfat_dispatches_to_module() {
370 let img = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/exfat.img");
371 let Ok(data) = std::fs::read(img) else {
372 eprintln!("skip: exfat.img unavailable");
373 return;
374 };
375 let fs = build_filesystem(Cursor::new(data), detect::FsType::ExFat, "x").unwrap();
376 assert_eq!(fs.fs_info().unwrap()["type"], "exfat");
377 }
378}
379
380#[cfg(all(test, feature = "memory"))]
381mod memory_tests {
382 use super::*;
383
384 #[test]
387 fn build_memory_fs_bootstraps_crashdump() {
388 use memf_format::test_builders::CrashDumpBuilder;
389 let bytes = CrashDumpBuilder::new().cr3(0x1ab000).build();
390
391 let dir = std::env::temp_dir().join(format!("4n6mem_{}", std::process::id()));
392 std::fs::create_dir_all(&dir).unwrap();
393 let path = dir.join("crash.dmp");
394 std::fs::write(&path, &bytes).unwrap();
395
396 let mut fs = build_memory_fs(&path, None).expect("crash dump must bootstrap");
397 let sys = fs
399 .lookup(mem::inode::ROOT_INO, b"sys")
400 .unwrap()
401 .expect("sys");
402 let oi = fs
403 .lookup(sys, b"os-info.txt")
404 .unwrap()
405 .expect("os-info.txt");
406 let text = String::from_utf8(fs.read_file(oi).unwrap()).unwrap();
407 assert!(text.contains("OS: Windows"), "got: {text}");
408
409 std::fs::remove_dir_all(&dir).ok();
410 }
411}