Skip to main content

forensic_mount/
fs_raw.rs

1#![forbid(unsafe_code)]
2
3use crate::{ForensicFs, FsDirEntry, FsError, FsFileType, FsMetadata, FsResult, FsTimestamp};
4use std::io::{Read, Seek, SeekFrom};
5
6/// A `ForensicFs` that exposes a single raw data source as one file.
7/// Used for L01 logical evidence or raw binary data.
8pub 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); // ., .., evidence.bin
155        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}