1#![forbid(unsafe_code)]
2
3use crate::{
7 ForensicFs, FsBlockRange, FsDeletedInode, FsDirEntry, FsError, FsEventType, FsFileType,
8 FsMetadata, FsRecoveryResult, FsResult, FsTimelineEvent, FsTimestamp, FsTransaction,
9};
10use ext4fs::Ext4Fs;
11use std::io::{Read, Seek};
12
13pub struct Ext4ForensicFs<R: Read + Seek> {
15 fs: Ext4Fs<R>,
16}
17
18impl<R: Read + Seek> Ext4ForensicFs<R> {
19 pub fn new(source: R) -> Result<Self, FsError> {
20 let fs = Ext4Fs::open(source).map_err(map_err)?;
21 Ok(Self { fs })
22 }
23}
24
25impl<R: Read + Seek> ForensicFs for Ext4ForensicFs<R> {
26 fn root_ino(&self) -> u64 {
27 2 }
29
30 fn read_dir(&mut self, ino: u64) -> FsResult<Vec<FsDirEntry>> {
31 let entries = self.fs.read_dir_by_ino(ino).map_err(map_err)?;
32 Ok(entries
33 .iter()
34 .map(|e| FsDirEntry {
35 inode: u64::from(e.inode),
36 name: e.name.clone(),
37 file_type: map_dir_entry_type(e.file_type),
38 })
39 .collect())
40 }
41
42 fn lookup(&mut self, parent_ino: u64, name: &[u8]) -> FsResult<Option<u64>> {
43 self.fs.lookup_by_ino(parent_ino, name).map_err(map_err)
44 }
45
46 fn metadata(&mut self, ino: u64) -> FsResult<FsMetadata> {
47 let inode = self.fs.inode(ino).map_err(map_err)?;
48 let allocated = self.fs.is_inode_allocated(ino).unwrap_or(false);
49 Ok(FsMetadata {
50 ino,
51 file_type: map_file_type(inode.file_type()),
52 mode: inode.mode,
53 uid: inode.uid,
54 gid: inode.gid,
55 size: inode.size,
56 links_count: inode.links_count,
57 atime: map_ts(&inode.atime),
58 mtime: map_ts(&inode.mtime),
59 ctime: map_ts(&inode.ctime),
60 crtime: map_ts(&inode.crtime),
61 allocated,
62 })
63 }
64
65 fn read_file(&mut self, ino: u64) -> FsResult<Vec<u8>> {
66 self.fs.read_inode_data(ino).map_err(map_err)
67 }
68
69 fn read_file_range(&mut self, ino: u64, offset: u64, len: u64) -> FsResult<Vec<u8>> {
70 self.fs
71 .read_inode_data_range(ino, offset, len as usize)
72 .map_err(map_err)
73 }
74
75 fn read_link(&mut self, ino: u64) -> FsResult<Vec<u8>> {
76 self.fs.read_link_by_ino(ino).map_err(map_err)
77 }
78
79 fn deleted_inodes(&mut self) -> FsResult<Vec<FsDeletedInode>> {
80 let deleted = self.fs.deleted_inodes().map_err(map_err)?;
81 Ok(deleted
82 .into_iter()
83 .map(|d| FsDeletedInode {
84 ino: d.ino,
85 file_type: map_file_type(d.file_type),
86 size: d.size,
87 dtime: d.dtime,
88 recoverability: d.recoverability,
89 })
90 .collect())
91 }
92
93 fn recover_file(&mut self, ino: u64) -> FsResult<FsRecoveryResult> {
94 let r = self.fs.recover_file(ino).map_err(map_err)?;
95 let pct = r.recovery_percentage();
96 Ok(FsRecoveryResult {
97 ino,
98 data: r.data,
99 expected_size: r.expected_size,
100 recovered_bytes: r.recovered_size,
101 recovery_percentage: pct,
102 })
103 }
104
105 fn timeline(&mut self) -> FsResult<Vec<FsTimelineEvent>> {
106 let events = self.fs.timeline().map_err(map_err)?;
107 Ok(events
108 .into_iter()
109 .map(|e| FsTimelineEvent {
110 timestamp: map_ts(&e.timestamp),
111 event_type: match e.event_type {
112 ext4fs::forensic::EventType::Created => FsEventType::Created,
113 ext4fs::forensic::EventType::Modified => FsEventType::Modified,
114 ext4fs::forensic::EventType::Accessed => FsEventType::Accessed,
115 ext4fs::forensic::EventType::Changed => FsEventType::Changed,
116 ext4fs::forensic::EventType::Deleted => FsEventType::Deleted,
117 ext4fs::forensic::EventType::Mounted => FsEventType::Mounted,
118 },
119 inode: e.inode,
120 size: e.size,
121 uid: e.uid,
122 gid: e.gid,
123 })
124 .collect())
125 }
126
127 fn unallocated_blocks(&mut self) -> FsResult<Vec<FsBlockRange>> {
128 let blocks = self.fs.unallocated_blocks().map_err(map_err)?;
129 Ok(blocks
130 .into_iter()
131 .map(|b| FsBlockRange {
132 start: b.start,
133 length: b.length,
134 })
135 .collect())
136 }
137
138 fn read_unallocated(&mut self, range: &FsBlockRange) -> FsResult<Vec<u8>> {
139 let ext4_range = ext4fs::forensic::BlockRange {
140 start: range.start,
141 length: range.length,
142 };
143 self.fs.read_unallocated(&ext4_range).map_err(map_err)
144 }
145
146 fn journal_transactions(&mut self) -> FsResult<Vec<FsTransaction>> {
147 let journal = self.fs.journal().map_err(map_err)?;
148 Ok(journal
149 .transactions
150 .into_iter()
151 .map(|t| FsTransaction {
152 sequence: u64::from(t.sequence),
153 commit_seconds: t.commit_seconds as u64,
154 commit_nanoseconds: t.commit_nanoseconds,
155 })
156 .collect())
157 }
158
159 fn fs_info(&self) -> FsResult<serde_json::Value> {
160 let sb = self.fs.superblock();
161 Ok(serde_json::json!({
162 "filesystem": "ext4",
163 "label": sb.label(),
164 "uuid": sb.uuid_string(),
165 "block_size": sb.block_size,
166 "blocks_count": sb.blocks_count,
167 "inodes_count": sb.inodes_count,
168 }))
169 }
170
171 fn block_size(&self) -> u64 {
172 u64::from(self.fs.superblock().block_size)
173 }
174}
175
176fn map_err(e: ext4fs::error::Ext4Error) -> FsError {
179 FsError::Other(e.to_string())
180}
181
182fn map_file_type(ft: ext4fs::ondisk::FileType) -> FsFileType {
183 match ft {
184 ext4fs::ondisk::FileType::RegularFile => FsFileType::RegularFile,
185 ext4fs::ondisk::FileType::Directory => FsFileType::Directory,
186 ext4fs::ondisk::FileType::Symlink => FsFileType::Symlink,
187 ext4fs::ondisk::FileType::CharDevice => FsFileType::CharDevice,
188 ext4fs::ondisk::FileType::BlockDevice => FsFileType::BlockDevice,
189 ext4fs::ondisk::FileType::Fifo => FsFileType::Fifo,
190 ext4fs::ondisk::FileType::Socket => FsFileType::Socket,
191 ext4fs::ondisk::FileType::Unknown => FsFileType::Unknown,
192 }
193}
194
195fn map_dir_entry_type(dt: ext4fs::ondisk::DirEntryType) -> FsFileType {
196 match dt {
197 ext4fs::ondisk::DirEntryType::RegularFile => FsFileType::RegularFile,
198 ext4fs::ondisk::DirEntryType::Directory => FsFileType::Directory,
199 ext4fs::ondisk::DirEntryType::Symlink => FsFileType::Symlink,
200 ext4fs::ondisk::DirEntryType::CharDevice => FsFileType::CharDevice,
201 ext4fs::ondisk::DirEntryType::BlockDevice => FsFileType::BlockDevice,
202 ext4fs::ondisk::DirEntryType::Fifo => FsFileType::Fifo,
203 ext4fs::ondisk::DirEntryType::Socket => FsFileType::Socket,
204 ext4fs::ondisk::DirEntryType::Unknown => FsFileType::Unknown,
205 }
206}
207
208fn map_ts(ts: &ext4fs::ondisk::Timestamp) -> FsTimestamp {
209 FsTimestamp {
210 seconds: ts.seconds,
211 nanoseconds: ts.nanoseconds,
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use std::io::Cursor;
219
220 fn open_forensic() -> Option<Ext4ForensicFs<Cursor<Vec<u8>>>> {
221 let path = "/Users/4n6h4x0r/src/ext4fs-forensic/tests/data/forensic.img";
222 let data = std::fs::read(path).ok()?;
223 Ext4ForensicFs::new(Cursor::new(data)).ok()
224 }
225
226 #[test]
227 fn root_ino_is_2() {
228 let Some(fs) = open_forensic() else {
229 eprintln!("skip");
230 return;
231 };
232 assert_eq!(fs.root_ino(), 2);
233 }
234
235 #[test]
236 fn read_dir_root_has_entries() {
237 let Some(mut fs) = open_forensic() else {
238 eprintln!("skip");
239 return;
240 };
241 let entries = fs.read_dir(2).unwrap();
242 let names: Vec<String> = entries
243 .iter()
244 .map(super::super::types::FsDirEntry::name_str)
245 .collect();
246 assert!(names.contains(&"hello.txt".to_string()));
247 }
248
249 #[test]
250 fn lookup_finds_file() {
251 let Some(mut fs) = open_forensic() else {
252 eprintln!("skip");
253 return;
254 };
255 let ino = fs.lookup(2, b"hello.txt").unwrap();
256 assert!(ino.is_some());
257 }
258
259 #[test]
260 fn metadata_root_is_directory() {
261 let Some(mut fs) = open_forensic() else {
262 eprintln!("skip");
263 return;
264 };
265 let meta = fs.metadata(2).unwrap();
266 assert_eq!(meta.file_type, FsFileType::Directory);
267 assert!(meta.allocated);
268 }
269
270 #[test]
271 fn read_file_returns_content() {
272 let Some(mut fs) = open_forensic() else {
273 eprintln!("skip");
274 return;
275 };
276 let ino = fs.lookup(2, b"hello.txt").unwrap().unwrap();
277 let data = fs.read_file(ino).unwrap();
278 assert!(String::from_utf8_lossy(&data).contains("Hello"));
279 }
280
281 #[test]
282 fn read_file_range_returns_prefix() {
283 let Some(mut fs) = open_forensic() else {
284 eprintln!("skip");
285 return;
286 };
287 let ino = fs.lookup(2, b"hello.txt").unwrap().unwrap();
288 let data = fs.read_file_range(ino, 0, 5).unwrap();
289 assert_eq!(&data, b"Hello");
290 }
291
292 #[test]
293 fn read_link_returns_target() {
294 let Some(mut fs) = open_forensic() else {
295 eprintln!("skip");
296 return;
297 };
298 let ino = fs.lookup(2, b"abs-link").unwrap().unwrap();
299 let target = fs.read_link(ino).unwrap();
300 assert_eq!(String::from_utf8_lossy(&target), "/hello.txt");
301 }
302
303 #[test]
304 fn deleted_inodes_found() {
305 let Some(mut fs) = open_forensic() else {
306 eprintln!("skip");
307 return;
308 };
309 let deleted = fs.deleted_inodes().unwrap();
310 assert!(deleted.len() >= 2);
311 }
312
313 #[test]
314 fn timeline_has_events() {
315 let Some(mut fs) = open_forensic() else {
316 eprintln!("skip");
317 return;
318 };
319 let events = fs.timeline().unwrap();
320 assert!(!events.is_empty());
321 }
322
323 #[test]
324 fn fs_info_is_ext4() {
325 let Some(fs) = open_forensic() else {
326 eprintln!("skip");
327 return;
328 };
329 let info = fs.fs_info().unwrap();
330 assert_eq!(info["filesystem"], "ext4");
331 }
332
333 #[test]
334 fn block_size_is_4096() {
335 let Some(fs) = open_forensic() else {
336 eprintln!("skip");
337 return;
338 };
339 assert_eq!(fs.block_size(), 4096);
340 }
341}