Skip to main content

forensic_mount/
fs_aff4.rs

1#![forbid(unsafe_code)]
2//! AFF4-Logical (`aff4:FileImage`) mount — `aff4` feature.
3//!
4//! An AFF4-Logical container is a collection of files (not a disk image), so it
5//! mounts like AD1: entries are enumerated at open into a synthetic inode tree
6//! ([`crate::archive_tree::ArchiveTree`]) and bytes are read on FUSE/Dokan
7//! access via [`aff4::LogicalContainer::read_file`]. Read-only; encrypted
8//! containers are refused at open (decryption needs a password).
9//!
10//! AFF4 *disk* images are a different shape — a `Read + Seek` stream whose inner
11//! filesystem is mounted via `build_filesystem` (wired in `main.rs`), not here.
12
13use std::path::Path;
14
15use aff4::LogicalContainer;
16
17use crate::archive_tree::ArchiveTree;
18use crate::{not_supported, ForensicFs, FsDirEntry, FsError, FsMetadata, FsResult, FsTimestamp};
19
20/// A read-only view over an AFF4-Logical file collection.
21pub struct Aff4ForensicFs {
22    container: LogicalContainer,
23    tree: ArchiveTree,
24}
25
26impl Aff4ForensicFs {
27    /// Open an AFF4-Logical container by path. Returns a `NotSupported` error
28    /// for encrypted containers (decryption needs a password).
29    pub fn open(path: &Path) -> Result<Self, FsError> {
30        let container = LogicalContainer::open(path).map_err(map_err)?;
31        let mut tree = ArchiveTree::new();
32        for (idx, e) in container.files().iter().enumerate() {
33            // original_file_name is slash-separated and often prefixed "./".
34            // ArchiveTree synthesises any missing parent directories.
35            let rel = e.original_file_name.trim_start_matches("./");
36            tree.insert(
37                rel,
38                false,
39                e.size,
40                FsTimestamp {
41                    seconds: 0,
42                    nanoseconds: 0,
43                },
44                Some(idx),
45            );
46        }
47        Ok(Self { container, tree })
48    }
49
50    /// Read `len` bytes at `offset` from the file at `ino`. AFF4-Logical has no
51    /// positioned read, so the whole file is inflated and the window sliced.
52    fn read_range(&mut self, ino: u64, offset: u64, len: usize) -> FsResult<Vec<u8>> {
53        let idx = self
54            .tree
55            .payload_id(ino)
56            .ok_or_else(|| FsError::NotFound(format!("inode {ino} is not a readable file")))?;
57        // Clone the small entry to release the immutable borrow before the
58        // mutable read_file call.
59        let entry = self.container.files()[idx].clone();
60        let data = self.container.read_file(&entry).map_err(map_err)?;
61        let start = (offset as usize).min(data.len());
62        let end = start.saturating_add(len).min(data.len());
63        Ok(data[start..end].to_vec())
64    }
65}
66
67impl ForensicFs for Aff4ForensicFs {
68    fn root_ino(&self) -> u64 {
69        self.tree.root_ino()
70    }
71    fn read_dir(&mut self, ino: u64) -> FsResult<Vec<FsDirEntry>> {
72        self.tree.read_dir(ino)
73    }
74    fn lookup(&mut self, parent_ino: u64, name: &[u8]) -> FsResult<Option<u64>> {
75        self.tree.lookup(parent_ino, name)
76    }
77    fn metadata(&mut self, ino: u64) -> FsResult<FsMetadata> {
78        self.tree.metadata(ino)
79    }
80    fn read_file(&mut self, ino: u64) -> FsResult<Vec<u8>> {
81        let size = self.metadata(ino)?.size;
82        self.read_range(ino, 0, size as usize)
83    }
84    fn read_file_range(&mut self, ino: u64, offset: u64, len: u64) -> FsResult<Vec<u8>> {
85        self.read_range(ino, offset, len as usize)
86    }
87    fn read_link(&mut self, _ino: u64) -> FsResult<Vec<u8>> {
88        Err(not_supported("aff4: symlinks are not surfaced"))
89    }
90    fn fs_info(&self) -> FsResult<serde_json::Value> {
91        Ok(serde_json::json!({ "type": "aff4-logical", "entries": self.container.files().len() }))
92    }
93}
94
95/// Map an `aff4::Aff4Error` onto 4n6mount's `FsError`.
96fn map_err(e: aff4::Aff4Error) -> FsError {
97    match e {
98        aff4::Aff4Error::Io(io) => FsError::Io(io),
99        aff4::Aff4Error::Encrypted(m) => FsError::NotSupported(format!("aff4: {m}")),
100        aff4::Aff4Error::BadFormat(m) => FsError::Corrupt(format!("aff4: {m}")),
101        aff4::Aff4Error::Zip(m) => FsError::Corrupt(format!("aff4: zip: {m}")),
102        // Aff4Error is #[non_exhaustive]; surface any future variant loudly.
103        other => FsError::Other(format!("aff4: {other}")),
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use std::io::Write as _;
111
112    const DUMMY_MD5: &str = "00000000000000000000000000000000";
113
114    fn write_tmp(bytes: &[u8]) -> tempfile::NamedTempFile {
115        let mut f = tempfile::NamedTempFile::new().unwrap();
116        f.write_all(bytes).unwrap();
117        f
118    }
119
120    fn resolve(fs: &mut Aff4ForensicFs, path: &str) -> Option<u64> {
121        let mut ino = fs.root_ino();
122        for comp in path.split('/').filter(|c| !c.is_empty()) {
123            ino = fs.lookup(ino, comp.as_bytes()).ok().flatten()?;
124        }
125        Some(ino)
126    }
127
128    #[test]
129    fn lists_and_reads_logical_file() {
130        let content = b"AFF4-Logical file content spanning a chunk boundary...\n";
131        let img = aff4::testutil::test_aff4_logical("dir/dream.txt", content, DUMMY_MD5);
132        let f = write_tmp(&img);
133        let mut fs = Aff4ForensicFs::open(f.path()).unwrap();
134
135        assert!(!fs.read_dir(fs.root_ino()).unwrap().is_empty());
136        let ino = resolve(&mut fs, "dir/dream.txt").expect("dir/dream.txt");
137        assert_eq!(fs.metadata(ino).unwrap().size, content.len() as u64);
138        assert_eq!(&fs.read_file(ino).unwrap(), content);
139    }
140
141    #[test]
142    fn range_read_slices_the_file() {
143        let content = b"0123456789abcdefghijklmnopqrstuvwxyz";
144        let img = aff4::testutil::test_aff4_logical("a.bin", content, DUMMY_MD5);
145        let f = write_tmp(&img);
146        let mut fs = Aff4ForensicFs::open(f.path()).unwrap();
147        let ino = resolve(&mut fs, "a.bin").expect("a.bin");
148        assert_eq!(fs.read_file_range(ino, 10, 6).unwrap(), &content[10..16]);
149    }
150
151    #[test]
152    fn fs_info_reports_aff4_logical() {
153        let img = aff4::testutil::test_aff4_logical("x.txt", b"x", DUMMY_MD5);
154        let f = write_tmp(&img);
155        let fs = Aff4ForensicFs::open(f.path()).unwrap();
156        assert_eq!(fs.fs_info().unwrap()["type"], "aff4-logical");
157    }
158}