1#![forbid(unsafe_code)]
2
3use std::io::{self, Read, Seek, SeekFrom};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum FsType {
8 Ext4,
9 Ntfs,
10 ExFat,
11 Ewf,
12 Iso,
13 Vmdk,
14 Unknown,
15}
16
17impl std::fmt::Display for FsType {
18 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 match self {
20 FsType::Ext4 => write!(f, "ext4"),
21 FsType::Ntfs => write!(f, "ntfs"),
22 FsType::ExFat => write!(f, "exfat"),
23 FsType::Ewf => write!(f, "ewf"),
24 FsType::Iso => write!(f, "iso9660"),
25 FsType::Vmdk => write!(f, "vmdk"),
26 FsType::Unknown => write!(f, "unknown"),
27 }
28 }
29}
30
31impl std::str::FromStr for FsType {
32 type Err = String;
33 fn from_str(s: &str) -> Result<Self, Self::Err> {
34 match s.to_lowercase().as_str() {
35 "ext4" => Ok(FsType::Ext4),
36 "ntfs" => Ok(FsType::Ntfs),
37 "exfat" => Ok(FsType::ExFat),
38 "ewf" | "e01" => Ok(FsType::Ewf),
39 "iso" | "iso9660" | "cd" | "udf" => Ok(FsType::Iso),
40 _ => Err(format!("unknown filesystem type: {s}")),
41 }
42 }
43}
44
45pub fn detect_filesystem<R: Read + Seek>(source: &mut R) -> io::Result<FsType> {
50 source.seek(SeekFrom::Start(0))?;
52
53 let mut buf = vec![0u8; 37_640];
57 let bytes_read = read_fill(source, &mut buf);
58
59 source.seek(SeekFrom::Start(0))?;
61
62 if bytes_read >= 8 && buf[0..3] == [0x45, 0x56, 0x46] && buf[3] == 0x09 {
64 return Ok(FsType::Ewf);
65 }
66
67 if bytes_read >= 4 && buf[0..4] == [0x4B, 0x44, 0x4D, 0x56] {
70 return Ok(FsType::Vmdk);
71 }
72 if bytes_read >= 21 && buf[0..21] == *b"# Disk DescriptorFile" {
73 return Ok(FsType::Vmdk);
74 }
75
76 if bytes_read >= 7 && &buf[3..7] == b"NTFS" {
78 return Ok(FsType::Ntfs);
79 }
80
81 if bytes_read >= 8 && &buf[3..8] == b"EXFAT" {
83 return Ok(FsType::ExFat);
84 }
85
86 if bytes_read >= 1082 {
88 let magic = u16::from_le_bytes([buf[1080], buf[1081]]);
89 if magic == 0xEF53 {
90 return Ok(FsType::Ext4);
91 }
92 }
93
94 if bytes_read >= 32_774 && &buf[32_769..32_774] == b"CD001" {
98 return Ok(FsType::Iso);
99 }
100 if bytes_read >= 37_638 && &buf[37_633..37_638] == b"CD001" {
101 return Ok(FsType::Iso);
102 }
103
104 Ok(FsType::Unknown)
105}
106
107fn read_fill<R: Read>(source: &mut R, buf: &mut [u8]) -> usize {
109 let mut total = 0;
110 while total < buf.len() {
111 match source.read(&mut buf[total..]) {
112 Ok(0) | Err(_) => break,
113 Ok(n) => total += n,
114 }
115 }
116 total
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use std::io::Cursor;
123
124 #[test]
125 fn detects_vmdk_sparse_magic() {
126 let mut data = vec![0u8; 2048];
128 data[0..4].copy_from_slice(b"KDMV");
129 assert_eq!(
130 detect_filesystem(&mut Cursor::new(data)).unwrap(),
131 FsType::Vmdk
132 );
133 }
134
135 #[test]
136 fn detects_vmdk_text_descriptor() {
137 let data = b"# Disk DescriptorFile\nversion=1\n".to_vec();
138 assert_eq!(
139 detect_filesystem(&mut Cursor::new(data)).unwrap(),
140 FsType::Vmdk
141 );
142 }
143
144 fn make_ext4_image() -> Vec<u8> {
145 let mut data = vec![0u8; 2048];
148 data[1080] = 0x53; data[1081] = 0xEF; data
151 }
152
153 fn make_ntfs_image() -> Vec<u8> {
154 let mut data = vec![0u8; 512];
156 data[3..7].copy_from_slice(b"NTFS");
157 data
158 }
159
160 fn make_exfat_image() -> Vec<u8> {
161 let mut data = vec![0u8; 512];
163 data[3..8].copy_from_slice(b"EXFAT");
164 data
165 }
166
167 #[test]
168 fn detect_ext4() {
169 let data = make_ext4_image();
170 let mut cursor = Cursor::new(data);
171 assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Ext4);
172 }
173
174 #[test]
175 fn detect_ntfs() {
176 let data = make_ntfs_image();
177 let mut cursor = Cursor::new(data);
178 assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Ntfs);
179 }
180
181 #[test]
182 fn detect_exfat() {
183 let data = make_exfat_image();
184 let mut cursor = Cursor::new(data);
185 assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::ExFat);
186 }
187
188 #[test]
189 fn detect_unknown() {
190 let data = vec![0u8; 2048];
191 let mut cursor = Cursor::new(data);
192 assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Unknown);
193 }
194
195 fn make_iso_image() -> Vec<u8> {
197 let mut data = vec![0u8; 18 * 2048];
198 let pvd = 16 * 2048;
199 data[pvd] = 0x01;
200 data[pvd + 1..pvd + 6].copy_from_slice(b"CD001");
201 data[pvd + 6] = 0x01;
202 data
203 }
204
205 #[test]
206 fn detect_iso() {
207 let data = make_iso_image();
208 let mut cursor = Cursor::new(data);
209 assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Iso);
210 }
211
212 #[test]
213 fn iso_fstype_parses_from_str() {
214 assert_eq!("iso".parse::<FsType>().unwrap(), FsType::Iso);
215 assert_eq!("iso9660".parse::<FsType>().unwrap(), FsType::Iso);
216 }
217
218 #[test]
219 fn detect_too_short() {
220 let data = vec![0u8; 10];
221 let mut cursor = Cursor::new(data);
222 let result = detect_filesystem(&mut cursor);
224 assert!(result.is_ok());
225 assert_eq!(result.unwrap(), FsType::Unknown);
226 }
227
228 #[test]
229 fn detect_real_ext4_image() {
230 let path = "/Users/4n6h4x0r/src/ext4fs-forensic/tests/data/forensic.img";
231 let Ok(data) = std::fs::read(path) else {
232 eprintln!("skip: forensic.img not found");
233 return;
234 };
235 let mut cursor = Cursor::new(data);
236 assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Ext4);
237 }
238
239 #[test]
240 fn fstype_from_str() {
241 assert_eq!("ext4".parse::<FsType>().unwrap(), FsType::Ext4);
242 assert_eq!("NTFS".parse::<FsType>().unwrap(), FsType::Ntfs);
243 assert_eq!("ExFat".parse::<FsType>().unwrap(), FsType::ExFat);
244 assert!("btrfs".parse::<FsType>().is_err());
245 }
246
247 #[test]
248 fn fstype_display() {
249 assert_eq!(FsType::Ext4.to_string(), "ext4");
250 assert_eq!(FsType::Ntfs.to_string(), "ntfs");
251 assert_eq!(FsType::ExFat.to_string(), "exfat");
252 assert_eq!(FsType::Unknown.to_string(), "unknown");
253 }
254
255 #[test]
256 fn detect_ewf_image() {
257 let mut data = vec![0u8; 2048];
259 data[0..8].copy_from_slice(&[0x45, 0x56, 0x46, 0x09, 0x0D, 0x0A, 0xFF, 0x00]);
260 let mut cursor = Cursor::new(data);
261 assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Ewf);
262 }
263
264 #[test]
265 fn fstype_ewf_display() {
266 assert_eq!(FsType::Ewf.to_string(), "ewf");
267 }
268
269 #[test]
270 fn fstype_ewf_from_str() {
271 assert_eq!("ewf".parse::<FsType>().unwrap(), FsType::Ewf);
272 assert_eq!("e01".parse::<FsType>().unwrap(), FsType::Ewf);
273 }
274
275 #[test]
276 fn detect_resets_seek_position() {
277 let data = make_ext4_image();
278 let mut cursor = Cursor::new(data);
279 cursor.seek(SeekFrom::Start(500)).unwrap();
280 assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Ext4);
281 assert_eq!(cursor.stream_position().unwrap(), 0);
283 }
284}