1#![forbid(unsafe_code)]
2
3use crate::{ForensicFs, FsDirEntry, FsError, FsFileType, FsMetadata, FsResult, FsTimestamp};
4use std::io::{Read, Seek, SeekFrom};
5
6pub struct RawForensicFs<R: Read + Seek> {
9 source: R,
10 size: u64,
11 filename: String,
12}
13
14impl<R: Read + Seek> RawForensicFs<R> {
15 pub fn new(mut source: R, filename: String) -> Result<Self, FsError> {
16 let size = source.seek(SeekFrom::End(0)).map_err(FsError::Io)?;
17 source.seek(SeekFrom::Start(0)).map_err(FsError::Io)?;
18 Ok(Self {
19 source,
20 size,
21 filename,
22 })
23 }
24}
25
26impl<R: Read + Seek> ForensicFs for RawForensicFs<R> {
27 fn root_ino(&self) -> u64 {
28 1
29 }
30
31 fn read_dir(&mut self, ino: u64) -> FsResult<Vec<FsDirEntry>> {
32 if ino == 1 {
33 Ok(vec![
34 FsDirEntry {
35 inode: 1,
36 name: b".".to_vec(),
37 file_type: FsFileType::Directory,
38 },
39 FsDirEntry {
40 inode: 1,
41 name: b"..".to_vec(),
42 file_type: FsFileType::Directory,
43 },
44 FsDirEntry {
45 inode: 2,
46 name: self.filename.as_bytes().to_vec(),
47 file_type: FsFileType::RegularFile,
48 },
49 ])
50 } else {
51 Err(FsError::NotFound(format!("inode {ino}")))
52 }
53 }
54
55 fn lookup(&mut self, _parent_ino: u64, name: &[u8]) -> FsResult<Option<u64>> {
56 if name == self.filename.as_bytes() {
57 Ok(Some(2))
58 } else if name == b"." || name == b".." {
59 Ok(Some(1))
60 } else {
61 Ok(None)
62 }
63 }
64
65 fn metadata(&mut self, ino: u64) -> FsResult<FsMetadata> {
66 match ino {
67 1 => Ok(FsMetadata {
68 ino: 1,
69 file_type: FsFileType::Directory,
70 mode: 0o40555,
71 uid: 0,
72 gid: 0,
73 size: 0,
74 links_count: 2,
75 atime: FsTimestamp::default(),
76 mtime: FsTimestamp::default(),
77 ctime: FsTimestamp::default(),
78 crtime: FsTimestamp::default(),
79 allocated: true,
80 }),
81 2 => Ok(FsMetadata {
82 ino: 2,
83 file_type: FsFileType::RegularFile,
84 mode: 0o100_444,
85 uid: 0,
86 gid: 0,
87 size: self.size,
88 links_count: 1,
89 atime: FsTimestamp::default(),
90 mtime: FsTimestamp::default(),
91 ctime: FsTimestamp::default(),
92 crtime: FsTimestamp::default(),
93 allocated: true,
94 }),
95 _ => Err(FsError::NotFound(format!("inode {ino}"))),
96 }
97 }
98
99 fn read_file(&mut self, ino: u64) -> FsResult<Vec<u8>> {
100 if ino != 2 {
101 return Err(FsError::NotFound(format!("inode {ino}")));
102 }
103 self.source.seek(SeekFrom::Start(0)).map_err(FsError::Io)?;
104 let mut data = Vec::with_capacity(self.size as usize);
105 self.source.read_to_end(&mut data).map_err(FsError::Io)?;
106 Ok(data)
107 }
108
109 fn read_file_range(&mut self, ino: u64, offset: u64, len: u64) -> FsResult<Vec<u8>> {
110 if ino != 2 {
111 return Err(FsError::NotFound(format!("inode {ino}")));
112 }
113 self.source
114 .seek(SeekFrom::Start(offset))
115 .map_err(FsError::Io)?;
116 let actual_len = len.min(self.size.saturating_sub(offset)) as usize;
117 let mut buf = vec![0u8; actual_len];
118 self.source.read_exact(&mut buf).map_err(FsError::Io)?;
119 Ok(buf)
120 }
121
122 fn read_link(&mut self, _ino: u64) -> FsResult<Vec<u8>> {
123 Err(FsError::NotFound("no symlinks in raw data".to_string()))
124 }
125
126 fn fs_info(&self) -> FsResult<serde_json::Value> {
127 Ok(serde_json::json!({
128 "filesystem": "raw",
129 "size": self.size,
130 "filename": self.filename,
131 }))
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use std::io::Cursor;
139
140 fn make_raw_fs(data: &[u8]) -> RawForensicFs<Cursor<Vec<u8>>> {
141 RawForensicFs::new(Cursor::new(data.to_vec()), "evidence.bin".to_string()).unwrap()
142 }
143
144 #[test]
145 fn root_ino_is_1() {
146 let fs = make_raw_fs(b"hello");
147 assert_eq!(fs.root_ino(), 1);
148 }
149
150 #[test]
151 fn read_dir_root_has_file() {
152 let mut fs = make_raw_fs(b"hello");
153 let entries = fs.read_dir(1).unwrap();
154 assert_eq!(entries.len(), 3); let names: Vec<String> = entries
156 .iter()
157 .map(super::super::types::FsDirEntry::name_str)
158 .collect();
159 assert!(names.contains(&"evidence.bin".to_string()));
160 }
161
162 #[test]
163 fn lookup_file() {
164 let mut fs = make_raw_fs(b"hello");
165 assert_eq!(fs.lookup(1, b"evidence.bin").unwrap(), Some(2));
166 assert_eq!(fs.lookup(1, b"missing").unwrap(), None);
167 }
168
169 #[test]
170 fn metadata_root() {
171 let mut fs = make_raw_fs(b"hello");
172 let meta = fs.metadata(1).unwrap();
173 assert_eq!(meta.file_type, FsFileType::Directory);
174 }
175
176 #[test]
177 fn metadata_file() {
178 let mut fs = make_raw_fs(b"hello");
179 let meta = fs.metadata(2).unwrap();
180 assert_eq!(meta.file_type, FsFileType::RegularFile);
181 assert_eq!(meta.size, 5);
182 }
183
184 #[test]
185 fn read_file_content() {
186 let mut fs = make_raw_fs(b"hello world");
187 let data = fs.read_file(2).unwrap();
188 assert_eq!(data, b"hello world");
189 }
190
191 #[test]
192 fn read_file_range_prefix() {
193 let mut fs = make_raw_fs(b"hello world");
194 let data = fs.read_file_range(2, 0, 5).unwrap();
195 assert_eq!(data, b"hello");
196 }
197
198 #[test]
199 fn read_file_range_middle() {
200 let mut fs = make_raw_fs(b"hello world");
201 let data = fs.read_file_range(2, 6, 5).unwrap();
202 assert_eq!(data, b"world");
203 }
204
205 #[test]
206 fn fs_info_raw() {
207 let fs = make_raw_fs(b"data");
208 let info = fs.fs_info().unwrap();
209 assert_eq!(info["filesystem"], "raw");
210 assert_eq!(info["size"], 4);
211 }
212
213 #[test]
214 fn read_dir_invalid_ino() {
215 let mut fs = make_raw_fs(b"data");
216 assert!(fs.read_dir(99).is_err());
217 }
218}