1#![forbid(unsafe_code)]
2use std::path::Path;
11
12use ad1::Ad1Reader;
13
14use crate::archive_tree::ArchiveTree;
15use crate::{not_supported, ForensicFs, FsDirEntry, FsError, FsMetadata, FsResult, FsTimestamp};
16
17pub struct Ad1ForensicFs {
19 reader: Ad1Reader,
20 tree: ArchiveTree,
21}
22
23impl Ad1ForensicFs {
24 pub fn open(path: &Path) -> Result<Self, FsError> {
27 let reader = Ad1Reader::open(path).map_err(map_err)?;
28 let mut tree = ArchiveTree::new();
29 for (idx, e) in reader.entries().iter().enumerate() {
30 tree.insert(
34 &e.path,
35 e.is_dir,
36 e.size,
37 FsTimestamp {
38 seconds: 0,
39 nanoseconds: 0,
40 },
41 if e.is_dir { None } else { Some(idx) },
42 );
43 }
44 Ok(Self { reader, tree })
45 }
46
47 fn read_range(&self, ino: u64, offset: u64, len: usize) -> FsResult<Vec<u8>> {
50 let idx = self
51 .tree
52 .payload_id(ino)
53 .ok_or_else(|| FsError::NotFound(format!("inode {ino} is not a readable file")))?;
54 let entry = self.reader.entries()[idx].clone();
57 let want = (entry.size.saturating_sub(offset) as usize).min(len);
58 let mut buf = vec![0u8; want];
59 let mut filled = 0;
60 while filled < want {
61 let n = self
62 .reader
63 .read_at(&entry, offset + filled as u64, &mut buf[filled..])
64 .map_err(map_err)?;
65 if n == 0 {
66 break;
67 }
68 filled += n;
69 }
70 buf.truncate(filled);
71 Ok(buf)
72 }
73}
74
75impl ForensicFs for Ad1ForensicFs {
76 fn root_ino(&self) -> u64 {
77 self.tree.root_ino()
78 }
79 fn read_dir(&mut self, ino: u64) -> FsResult<Vec<FsDirEntry>> {
80 self.tree.read_dir(ino)
81 }
82 fn lookup(&mut self, parent_ino: u64, name: &[u8]) -> FsResult<Option<u64>> {
83 self.tree.lookup(parent_ino, name)
84 }
85 fn metadata(&mut self, ino: u64) -> FsResult<FsMetadata> {
86 self.tree.metadata(ino)
87 }
88 fn read_file(&mut self, ino: u64) -> FsResult<Vec<u8>> {
89 let size = self.metadata(ino)?.size;
90 self.read_range(ino, 0, size as usize)
91 }
92 fn read_file_range(&mut self, ino: u64, offset: u64, len: u64) -> FsResult<Vec<u8>> {
93 self.read_range(ino, offset, len as usize)
94 }
95 fn read_link(&mut self, _ino: u64) -> FsResult<Vec<u8>> {
96 Err(not_supported("ad1: symlinks are not surfaced"))
97 }
98 fn fs_info(&self) -> FsResult<serde_json::Value> {
99 Ok(serde_json::json!({ "type": "ad1", "entries": self.reader.entries().len() }))
100 }
101}
102
103fn map_err(e: ad1::Ad1Error) -> FsError {
105 match e {
106 ad1::Ad1Error::Io(io) => FsError::Io(io),
107 ad1::Ad1Error::Unsupported(m) => FsError::NotSupported(format!("ad1: {m}")),
109 ad1::Ad1Error::NotAd1(m) | ad1::Ad1Error::Malformed(m) => {
110 FsError::Corrupt(format!("ad1: {m}"))
111 }
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use ad1::testfix;
119
120 struct Fixture {
121 _dir: tempfile::TempDir,
122 path: std::path::PathBuf,
123 built: testfix::Built,
124 }
125
126 fn write_fixture() -> Fixture {
127 let built = testfix::build(testfix::sample_tree());
128 let dir = tempfile::tempdir().unwrap();
129 let path = dir.path().join("img.ad1");
130 std::fs::write(&path, &built.bytes).unwrap();
131 Fixture {
132 _dir: dir,
133 path,
134 built,
135 }
136 }
137
138 fn resolve(fs: &mut Ad1ForensicFs, path: &str) -> Option<u64> {
140 let mut ino = fs.root_ino();
141 for comp in path.split('/').filter(|c| !c.is_empty()) {
142 ino = fs.lookup(ino, comp.as_bytes()).ok().flatten()?;
143 }
144 Some(ino)
145 }
146
147 #[test]
148 fn root_lists_entries() {
149 let fx = write_fixture();
150 let mut fs = Ad1ForensicFs::open(&fx.path).unwrap();
151 let root = fs.root_ino();
152 assert!(!fs.read_dir(root).unwrap().is_empty());
153 }
154
155 #[test]
156 fn files_read_back_byte_identical() {
157 let fx = write_fixture();
158 let mut fs = Ad1ForensicFs::open(&fx.path).unwrap();
159 let mut checked = 0;
160 for e in &fx.built.expected {
161 if e.is_dir {
162 continue;
163 }
164 let Some(data) = &e.data else { continue };
165 let ino =
166 resolve(&mut fs, &e.path).unwrap_or_else(|| panic!("path not found: {}", e.path));
167 assert_eq!(fs.metadata(ino).unwrap().size, e.size, "size of {}", e.path);
168 assert_eq!(&fs.read_file(ino).unwrap(), data, "content of {}", e.path);
169 checked += 1;
170 }
171 assert!(checked >= 1, "fixture should contain at least one file");
172 }
173
174 #[test]
175 fn range_read_across_chunk_boundary() {
176 let fx = write_fixture();
177 let mut fs = Ad1ForensicFs::open(&fx.path).unwrap();
178 let big = fx
181 .built
182 .expected
183 .iter()
184 .filter(|e| !e.is_dir && e.data.is_some())
185 .max_by_key(|e| e.size)
186 .expect("fixture should contain a file with data");
187 let data = big.data.as_ref().unwrap();
188 let ino = resolve(&mut fs, &big.path).unwrap();
189 let off = (data.len() / 2) as u64;
190 let len = 4096u64.min(data.len() as u64 - off);
191 let got = fs.read_file_range(ino, off, len).unwrap();
192 assert_eq!(
193 got,
194 &data[off as usize..(off + len) as usize],
195 "mid-file range read of {}",
196 big.path
197 );
198 }
199
200 #[test]
201 fn fs_info_reports_ad1() {
202 let fx = write_fixture();
203 let fs = Ad1ForensicFs::open(&fx.path).unwrap();
204 assert_eq!(fs.fs_info().unwrap()["type"], "ad1");
205 }
206}