forensic_mount/
fs_exfat.rs1#![forbid(unsafe_code)]
2
3use crate::archive_tree::ArchiveTree;
17use crate::{not_supported, ForensicFs, FsDirEntry, FsError, FsMetadata, FsResult, FsTimestamp};
18use exfat_fs::dir::entry::fs::FsElement;
19use exfat_fs::dir::Root;
20use exfat_fs::disk::{PartitionError, ReadOffset};
21use std::io::{Read, Seek, SeekFrom};
22use std::sync::Mutex;
23
24const MAX_NODES: usize = 5_000_000;
26
27#[derive(Debug)]
29enum ExfatErr {
30 UnexpectedEop,
31 ClusterNotFound(u32),
32 Io(String),
33}
34
35impl PartitionError for ExfatErr {
36 fn unexpected_eop() -> Self {
37 ExfatErr::UnexpectedEop
38 }
39 fn cluster_not_found(cluster: u32) -> Self {
40 ExfatErr::ClusterNotFound(cluster)
41 }
42}
43
44impl std::fmt::Display for ExfatErr {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 ExfatErr::UnexpectedEop => f.write_str("unexpected end of partition"),
48 ExfatErr::ClusterNotFound(c) => write!(f, "cluster {c} not found"),
49 ExfatErr::Io(msg) => write!(f, "I/O error: {msg}"),
50 }
51 }
52}
53
54impl From<ExfatErr> for std::io::Error {
55 fn from(e: ExfatErr) -> Self {
56 std::io::Error::other(e.to_string())
57 }
58}
59
60struct OffsetReader<R> {
63 inner: Mutex<R>,
64}
65
66impl<R> std::fmt::Debug for OffsetReader<R> {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 f.write_str("OffsetReader")
69 }
70}
71
72impl<R: Read + Seek> ReadOffset for OffsetReader<R> {
73 type Err = ExfatErr;
74
75 fn read_at(&self, offset: u64, buffer: &mut [u8]) -> Result<usize, ExfatErr> {
76 let mut guard = self
77 .inner
78 .lock()
79 .map_err(|_| ExfatErr::Io("reader mutex poisoned".to_string()))?;
80 guard
81 .seek(SeekFrom::Start(offset))
82 .map_err(|e| ExfatErr::Io(e.to_string()))?;
83 guard.read(buffer).map_err(|e| ExfatErr::Io(e.to_string()))
84 }
85}
86
87pub struct ExFatForensicFs {
89 tree: ArchiveTree,
90 data: Vec<Vec<u8>>,
92}
93
94fn join(prefix: &str, name: &str) -> String {
95 if prefix.is_empty() {
96 name.to_string()
97 } else {
98 format!("{prefix}/{name}")
99 }
100}
101
102impl ExFatForensicFs {
103 pub fn new<R: Read + Seek>(source: R) -> Result<Self, FsError> {
109 let device = OffsetReader {
110 inner: Mutex::new(source),
111 };
112 let mut root =
113 Root::open(device).map_err(|e| FsError::Corrupt(format!("not exFAT: {e:?}")))?;
114
115 let mut tree = ArchiveTree::new();
116 let mut data: Vec<Vec<u8>> = Vec::new();
117 let mut stack: Vec<(Vec<FsElement<OffsetReader<R>>>, String)> = Vec::new();
118
119 for element in root.items() {
120 ingest(element, "", &mut tree, &mut data, &mut stack)?;
121 }
122 while let Some((mut children, prefix)) = stack.pop() {
123 if tree.len() >= MAX_NODES {
124 break;
125 }
126 for element in &mut children {
127 ingest(element, &prefix, &mut tree, &mut data, &mut stack)?;
128 }
129 }
130
131 Ok(Self { tree, data })
132 }
133}
134
135fn ingest<R: Read + Seek>(
138 element: &mut FsElement<OffsetReader<R>>,
139 prefix: &str,
140 tree: &mut ArchiveTree,
141 data: &mut Vec<Vec<u8>>,
142 stack: &mut Vec<(Vec<FsElement<OffsetReader<R>>>, String)>,
143) -> Result<(), FsError> {
144 match element {
145 FsElement::F(file) => {
146 let path = join(prefix, file.name());
147 let mut buf = Vec::new();
148 file.read_to_end(&mut buf).map_err(FsError::Io)?;
149 let id = data.len();
150 if tree
151 .insert(
152 &path,
153 false,
154 buf.len() as u64,
155 FsTimestamp::default(),
156 Some(id),
157 )
158 .is_some()
159 {
160 data.push(buf);
161 }
162 }
163 FsElement::D(dir) => {
164 let path = join(prefix, dir.name());
165 tree.insert(&path, true, 0, FsTimestamp::default(), None);
166 let children = dir
167 .open()
168 .map_err(|e| FsError::Corrupt(format!("exFAT dir: {e:?}")))?;
169 stack.push((children, path));
170 }
171 }
172 Ok(())
173}
174
175impl ForensicFs for ExFatForensicFs {
176 fn root_ino(&self) -> u64 {
177 self.tree.root_ino()
178 }
179
180 fn read_dir(&mut self, ino: u64) -> FsResult<Vec<FsDirEntry>> {
181 self.tree.read_dir(ino)
182 }
183
184 fn lookup(&mut self, parent_ino: u64, name: &[u8]) -> FsResult<Option<u64>> {
185 self.tree.lookup(parent_ino, name)
186 }
187
188 fn metadata(&mut self, ino: u64) -> FsResult<FsMetadata> {
189 self.tree.metadata(ino)
190 }
191
192 fn read_file(&mut self, ino: u64) -> FsResult<Vec<u8>> {
193 let id = self
194 .tree
195 .payload_id(ino)
196 .ok_or_else(|| FsError::NotFound(format!("inode {ino} is not a file")))?;
197 self.data
198 .get(id)
199 .cloned()
200 .ok_or_else(|| FsError::NotFound(format!("payload {id}")))
201 }
202
203 fn read_file_range(&mut self, ino: u64, offset: u64, len: u64) -> FsResult<Vec<u8>> {
204 let data = self.read_file(ino)?;
205 let start = (offset as usize).min(data.len());
206 let end = start.saturating_add(len as usize).min(data.len());
207 Ok(data[start..end].to_vec())
208 }
209
210 fn read_link(&mut self, _ino: u64) -> FsResult<Vec<u8>> {
211 Err(not_supported("exFAT has no symlinks"))
212 }
213
214 fn fs_info(&self) -> FsResult<serde_json::Value> {
215 Ok(serde_json::json!({
216 "type": "exfat",
217 "entries": self.data.len(),
218 }))
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use std::io::Cursor;
226
227 const IMG: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/exfat.img");
230
231 fn open() -> Option<ExFatForensicFs> {
232 let data = std::fs::read(IMG).ok()?;
233 ExFatForensicFs::new(Cursor::new(data)).ok()
234 }
235
236 #[test]
237 fn root_ino_is_2() {
238 let Some(fs) = open() else {
239 eprintln!("skip: exfat.img unavailable");
240 return;
241 };
242 assert_eq!(fs.root_ino(), 2);
243 }
244
245 #[test]
246 fn root_lists_hello_and_sub() {
247 let Some(mut fs) = open() else {
248 eprintln!("skip");
249 return;
250 };
251 let names: Vec<String> = fs
252 .read_dir(2)
253 .unwrap()
254 .iter()
255 .map(FsDirEntry::name_str)
256 .collect();
257 assert!(names.contains(&"hello.txt".to_string()), "got {names:?}");
258 assert!(names.contains(&"sub".to_string()), "got {names:?}");
259 }
260
261 #[test]
262 fn read_hello_matches_icat() {
263 let Some(mut fs) = open() else {
264 eprintln!("skip");
265 return;
266 };
267 let ino = fs.lookup(2, b"hello.txt").unwrap().unwrap();
268 assert_eq!(fs.read_file(ino).unwrap(), b"hello from exfat\n");
269 }
270
271 #[test]
272 fn nested_file_reachable() {
273 let Some(mut fs) = open() else {
274 eprintln!("skip");
275 return;
276 };
277 let sub = fs.lookup(2, b"sub").unwrap().unwrap();
278 let deep = fs.lookup(sub, b"deep.txt").unwrap().unwrap();
279 assert_eq!(fs.read_file(deep).unwrap(), b"deep exfat content\n");
280 }
281
282 #[test]
283 fn metadata_size_matches() {
284 let Some(mut fs) = open() else {
285 eprintln!("skip");
286 return;
287 };
288 let ino = fs.lookup(2, b"hello.txt").unwrap().unwrap();
289 assert_eq!(fs.metadata(ino).unwrap().size, 17); }
291
292 #[test]
293 fn fs_info_reports_exfat() {
294 let Some(fs) = open() else {
295 eprintln!("skip");
296 return;
297 };
298 assert_eq!(fs.fs_info().unwrap()["type"], "exfat");
299 }
300}