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 Hfsplus,
12 Apfs,
13 Ewf,
14 Iso,
15 Vmdk,
16 Zip,
17 SevenZ,
18 TarGz,
19 TarBz2,
20 Unknown,
21}
22
23impl std::fmt::Display for FsType {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 match self {
26 FsType::Ext4 => write!(f, "ext4"),
27 FsType::Ntfs => write!(f, "ntfs"),
28 FsType::ExFat => write!(f, "exfat"),
29 FsType::Hfsplus => write!(f, "hfsplus"),
30 FsType::Apfs => write!(f, "apfs"),
31 FsType::Ewf => write!(f, "ewf"),
32 FsType::Iso => write!(f, "iso9660"),
33 FsType::Vmdk => write!(f, "vmdk"),
34 FsType::Zip => write!(f, "zip"),
35 FsType::SevenZ => write!(f, "7z"),
36 FsType::TarGz => write!(f, "tar.gz"),
37 FsType::TarBz2 => write!(f, "tar.bz2"),
38 FsType::Unknown => write!(f, "unknown"),
39 }
40 }
41}
42
43impl std::str::FromStr for FsType {
44 type Err = String;
45 fn from_str(s: &str) -> Result<Self, Self::Err> {
46 match s.to_lowercase().as_str() {
47 "ext4" => Ok(FsType::Ext4),
48 "ntfs" => Ok(FsType::Ntfs),
49 "exfat" => Ok(FsType::ExFat),
50 "hfsplus" | "hfs+" | "hfsx" => Ok(FsType::Hfsplus),
51 "apfs" => Ok(FsType::Apfs),
52 "ewf" | "e01" => Ok(FsType::Ewf),
53 "iso" | "iso9660" | "cd" | "udf" => Ok(FsType::Iso),
54 "zip" => Ok(FsType::Zip),
55 "7z" | "sevenz" | "7zip" => Ok(FsType::SevenZ),
56 "targz" | "tar.gz" | "tgz" | "gz" | "gzip" => Ok(FsType::TarGz),
57 "tarbz2" | "tar.bz2" | "tbz2" | "tbz" | "bz2" | "bzip2" => Ok(FsType::TarBz2),
58 _ => Err(format!("unknown filesystem type: {s}")),
59 }
60 }
61}
62
63pub fn detect_filesystem<R: Read + Seek>(source: &mut R) -> io::Result<FsType> {
69 source.seek(SeekFrom::Start(0))?;
71
72 let mut buf = vec![0u8; 37_640];
76 let bytes_read = read_fill(source, &mut buf);
77
78 source.seek(SeekFrom::Start(0))?;
80
81 if bytes_read >= 8 && buf[0..3] == [0x45, 0x56, 0x46] && buf[3] == 0x09 {
83 return Ok(FsType::Ewf);
84 }
85
86 if bytes_read >= 4 && buf[0..4] == [0x4B, 0x44, 0x4D, 0x56] {
89 return Ok(FsType::Vmdk);
90 }
91 if bytes_read >= 21 && buf[0..21] == *b"# Disk DescriptorFile" {
92 return Ok(FsType::Vmdk);
93 }
94
95 if bytes_read >= 2 && buf[0] == 0x1F && buf[1] == 0x8B {
101 return Ok(FsType::TarGz);
102 }
103 if bytes_read >= 3 && &buf[0..3] == b"BZh" {
105 return Ok(FsType::TarBz2);
106 }
107 if bytes_read >= 4
108 && buf[0..2] == [0x50, 0x4B]
109 && matches!(buf[2..4], [0x03, 0x04] | [0x05, 0x06] | [0x07, 0x08])
110 {
111 return Ok(FsType::Zip);
112 }
113 if bytes_read >= 6 && buf[0..6] == [0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C] {
114 return Ok(FsType::SevenZ);
115 }
116
117 if bytes_read >= 36 && &buf[32..36] == b"NXSB" {
120 return Ok(FsType::Apfs);
121 }
122
123 if bytes_read >= 7 && &buf[3..7] == b"NTFS" {
125 return Ok(FsType::Ntfs);
126 }
127
128 if bytes_read >= 8 && &buf[3..8] == b"EXFAT" {
130 return Ok(FsType::ExFat);
131 }
132
133 if bytes_read >= 1026 && buf[1024] == 0x48 && (buf[1025] == 0x2B || buf[1025] == 0x58) {
136 return Ok(FsType::Hfsplus);
137 }
138
139 if bytes_read >= 1082 {
141 let magic = u16::from_le_bytes([buf[1080], buf[1081]]);
142 if magic == 0xEF53 {
143 return Ok(FsType::Ext4);
144 }
145 }
146
147 if bytes_read >= 32_774 && &buf[32_769..32_774] == b"CD001" {
151 return Ok(FsType::Iso);
152 }
153 if bytes_read >= 37_638 && &buf[37_633..37_638] == b"CD001" {
154 return Ok(FsType::Iso);
155 }
156
157 Ok(FsType::Unknown)
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub enum MemDumpFormat {
164 Lime,
166 Avml,
168 ElfCore,
170 WinCrashDump,
172}
173
174impl std::fmt::Display for MemDumpFormat {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 match self {
177 MemDumpFormat::Lime => write!(f, "lime"),
178 MemDumpFormat::Avml => write!(f, "avml"),
179 MemDumpFormat::ElfCore => write!(f, "elf-core"),
180 MemDumpFormat::WinCrashDump => write!(f, "win-crashdump"),
181 }
182 }
183}
184
185pub fn detect_memory_dump<R: Read + Seek>(source: &mut R) -> io::Result<Option<MemDumpFormat>> {
192 source.seek(SeekFrom::Start(0))?;
193 let mut buf = [0u8; 18];
194 let n = read_fill(source, &mut buf);
195 source.seek(SeekFrom::Start(0))?;
196
197 if n >= 4 && &buf[0..4] == b"EMiL" {
199 return Ok(Some(MemDumpFormat::Lime));
200 }
201 if n >= 4 && &buf[0..4] == b"AVML" {
203 return Ok(Some(MemDumpFormat::Avml));
204 }
205 if n >= 8 && &buf[0..8] == b"PAGEDU64" {
207 return Ok(Some(MemDumpFormat::WinCrashDump));
208 }
209 if n >= 18 && buf[0..4] == [0x7F, b'E', b'L', b'F'] {
211 let e_type = u16::from_le_bytes([buf[16], buf[17]]);
212 if e_type == 4 {
213 return Ok(Some(MemDumpFormat::ElfCore));
214 }
215 }
216 Ok(None)
217}
218
219fn read_fill<R: Read>(source: &mut R, buf: &mut [u8]) -> usize {
221 let mut total = 0;
222 while total < buf.len() {
223 match source.read(&mut buf[total..]) {
224 Ok(0) | Err(_) => break,
225 Ok(n) => total += n,
226 }
227 }
228 total
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use std::io::Cursor;
235
236 #[test]
237 fn detects_vmdk_sparse_magic() {
238 let mut data = vec![0u8; 2048];
240 data[0..4].copy_from_slice(b"KDMV");
241 assert_eq!(
242 detect_filesystem(&mut Cursor::new(data)).unwrap(),
243 FsType::Vmdk
244 );
245 }
246
247 #[test]
248 fn detects_vmdk_text_descriptor() {
249 let data = b"# Disk DescriptorFile\nversion=1\n".to_vec();
250 assert_eq!(
251 detect_filesystem(&mut Cursor::new(data)).unwrap(),
252 FsType::Vmdk
253 );
254 }
255
256 fn make_ext4_image() -> Vec<u8> {
257 let mut data = vec![0u8; 2048];
260 data[1080] = 0x53; data[1081] = 0xEF; data
263 }
264
265 fn make_ntfs_image() -> Vec<u8> {
266 let mut data = vec![0u8; 512];
268 data[3..7].copy_from_slice(b"NTFS");
269 data
270 }
271
272 fn make_exfat_image() -> Vec<u8> {
273 let mut data = vec![0u8; 512];
275 data[3..8].copy_from_slice(b"EXFAT");
276 data
277 }
278
279 #[test]
280 fn detect_ext4() {
281 let data = make_ext4_image();
282 let mut cursor = Cursor::new(data);
283 assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Ext4);
284 }
285
286 #[test]
287 fn detect_ntfs() {
288 let data = make_ntfs_image();
289 let mut cursor = Cursor::new(data);
290 assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Ntfs);
291 }
292
293 #[test]
294 fn detect_exfat() {
295 let data = make_exfat_image();
296 let mut cursor = Cursor::new(data);
297 assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::ExFat);
298 }
299
300 #[test]
301 fn detect_unknown() {
302 let data = vec![0u8; 2048];
303 let mut cursor = Cursor::new(data);
304 assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Unknown);
305 }
306
307 fn make_iso_image() -> Vec<u8> {
309 let mut data = vec![0u8; 18 * 2048];
310 let pvd = 16 * 2048;
311 data[pvd] = 0x01;
312 data[pvd + 1..pvd + 6].copy_from_slice(b"CD001");
313 data[pvd + 6] = 0x01;
314 data
315 }
316
317 #[test]
318 fn detect_iso() {
319 let data = make_iso_image();
320 let mut cursor = Cursor::new(data);
321 assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Iso);
322 }
323
324 #[test]
325 fn iso_fstype_parses_from_str() {
326 assert_eq!("iso".parse::<FsType>().unwrap(), FsType::Iso);
327 assert_eq!("iso9660".parse::<FsType>().unwrap(), FsType::Iso);
328 }
329
330 #[test]
331 fn detect_too_short() {
332 let data = vec![0u8; 10];
333 let mut cursor = Cursor::new(data);
334 let result = detect_filesystem(&mut cursor);
336 assert!(result.is_ok());
337 assert_eq!(result.unwrap(), FsType::Unknown);
338 }
339
340 #[test]
341 fn detect_real_ext4_image() {
342 let path = "/Users/4n6h4x0r/src/ext4fs-forensic/tests/data/forensic.img";
343 let Ok(data) = std::fs::read(path) else {
344 eprintln!("skip: forensic.img not found");
345 return;
346 };
347 let mut cursor = Cursor::new(data);
348 assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Ext4);
349 }
350
351 #[test]
352 fn fstype_from_str() {
353 assert_eq!("ext4".parse::<FsType>().unwrap(), FsType::Ext4);
354 assert_eq!("NTFS".parse::<FsType>().unwrap(), FsType::Ntfs);
355 assert_eq!("ExFat".parse::<FsType>().unwrap(), FsType::ExFat);
356 assert!("btrfs".parse::<FsType>().is_err());
357 }
358
359 #[test]
360 fn fstype_display() {
361 assert_eq!(FsType::Ext4.to_string(), "ext4");
362 assert_eq!(FsType::Ntfs.to_string(), "ntfs");
363 assert_eq!(FsType::ExFat.to_string(), "exfat");
364 assert_eq!(FsType::Unknown.to_string(), "unknown");
365 }
366
367 #[test]
368 fn detect_ewf_image() {
369 let mut data = vec![0u8; 2048];
371 data[0..8].copy_from_slice(&[0x45, 0x56, 0x46, 0x09, 0x0D, 0x0A, 0xFF, 0x00]);
372 let mut cursor = Cursor::new(data);
373 assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Ewf);
374 }
375
376 #[test]
377 fn fstype_ewf_display() {
378 assert_eq!(FsType::Ewf.to_string(), "ewf");
379 }
380
381 #[test]
382 fn fstype_ewf_from_str() {
383 assert_eq!("ewf".parse::<FsType>().unwrap(), FsType::Ewf);
384 assert_eq!("e01".parse::<FsType>().unwrap(), FsType::Ewf);
385 }
386
387 #[test]
388 fn detect_resets_seek_position() {
389 let data = make_ext4_image();
390 let mut cursor = Cursor::new(data);
391 cursor.seek(SeekFrom::Start(500)).unwrap();
392 assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Ext4);
393 assert_eq!(cursor.stream_position().unwrap(), 0);
395 }
396
397 #[test]
398 fn detect_gzip_as_targz() {
399 let mut data = vec![0u8; 64];
401 data[0] = 0x1F;
402 data[1] = 0x8B;
403 data[2] = 0x08; assert_eq!(
405 detect_filesystem(&mut Cursor::new(data)).unwrap(),
406 FsType::TarGz
407 );
408 }
409
410 #[test]
411 fn detect_mem_lime() {
412 let mut data = vec![0u8; 64];
413 data[0..4].copy_from_slice(b"EMiL"); assert_eq!(
415 detect_memory_dump(&mut Cursor::new(data)).unwrap(),
416 Some(MemDumpFormat::Lime)
417 );
418 }
419
420 #[test]
421 fn detect_mem_avml() {
422 let mut data = vec![0u8; 64];
423 data[0..4].copy_from_slice(b"AVML");
424 assert_eq!(
425 detect_memory_dump(&mut Cursor::new(data)).unwrap(),
426 Some(MemDumpFormat::Avml)
427 );
428 }
429
430 #[test]
431 fn detect_mem_elf_core() {
432 let mut data = vec![0u8; 64];
433 data[0..4].copy_from_slice(&[0x7F, b'E', b'L', b'F']);
434 data[16..18].copy_from_slice(&4u16.to_le_bytes()); assert_eq!(
436 detect_memory_dump(&mut Cursor::new(data)).unwrap(),
437 Some(MemDumpFormat::ElfCore)
438 );
439 }
440
441 #[test]
442 fn detect_mem_elf_exec_is_not_a_dump() {
443 let mut data = vec![0u8; 64];
445 data[0..4].copy_from_slice(&[0x7F, b'E', b'L', b'F']);
446 data[16..18].copy_from_slice(&2u16.to_le_bytes()); assert_eq!(detect_memory_dump(&mut Cursor::new(data)).unwrap(), None);
448 }
449
450 #[test]
451 fn detect_mem_win_crashdump() {
452 let mut data = vec![0u8; 64];
453 data[0..8].copy_from_slice(b"PAGEDU64");
454 assert_eq!(
455 detect_memory_dump(&mut Cursor::new(data)).unwrap(),
456 Some(MemDumpFormat::WinCrashDump)
457 );
458 }
459
460 #[test]
461 fn detect_mem_none_for_non_dump() {
462 let mut data = vec![0u8; 64];
463 assert_eq!(detect_memory_dump(&mut Cursor::new(data)).unwrap(), None);
464 }
465
466 #[test]
467 fn detect_bzip2_as_tarbz2() {
468 let mut data = vec![0u8; 64];
470 data[0..3].copy_from_slice(b"BZh");
471 data[3] = b'9'; assert_eq!(
473 detect_filesystem(&mut Cursor::new(data)).unwrap(),
474 FsType::TarBz2
475 );
476 }
477
478 #[test]
479 fn detect_zip_local_file_header() {
480 let mut data = vec![0u8; 64];
481 data[0..4].copy_from_slice(b"PK\x03\x04");
482 assert_eq!(
483 detect_filesystem(&mut Cursor::new(data)).unwrap(),
484 FsType::Zip
485 );
486 }
487
488 #[test]
489 fn detect_zip_empty_archive() {
490 let mut data = vec![0u8; 64];
492 data[0..4].copy_from_slice(b"PK\x05\x06");
493 assert_eq!(
494 detect_filesystem(&mut Cursor::new(data)).unwrap(),
495 FsType::Zip
496 );
497 }
498
499 #[test]
500 fn detect_7z_signature() {
501 let mut data = vec![0u8; 64];
502 data[0..6].copy_from_slice(&[0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]);
503 assert_eq!(
504 detect_filesystem(&mut Cursor::new(data)).unwrap(),
505 FsType::SevenZ
506 );
507 }
508
509 #[test]
510 fn detect_hfsplus_signature() {
511 let mut data = vec![0u8; 2048];
513 data[1024] = 0x48; data[1025] = 0x2B; assert_eq!(
516 detect_filesystem(&mut Cursor::new(data)).unwrap(),
517 FsType::Hfsplus
518 );
519 }
520
521 #[test]
522 fn detect_hfsx_signature() {
523 let mut data = vec![0u8; 2048];
525 data[1024] = 0x48; data[1025] = 0x58; assert_eq!(
528 detect_filesystem(&mut Cursor::new(data)).unwrap(),
529 FsType::Hfsplus
530 );
531 }
532
533 #[test]
534 fn detect_apfs_nxsb() {
535 let mut data = vec![0u8; 4096];
537 data[32..36].copy_from_slice(b"NXSB");
538 assert_eq!(
539 detect_filesystem(&mut Cursor::new(data)).unwrap(),
540 FsType::Apfs
541 );
542 }
543
544 #[test]
545 fn new_fstypes_parse_and_display() {
546 assert_eq!("hfsplus".parse::<FsType>().unwrap(), FsType::Hfsplus);
547 assert_eq!("apfs".parse::<FsType>().unwrap(), FsType::Apfs);
548 assert_eq!("zip".parse::<FsType>().unwrap(), FsType::Zip);
549 assert_eq!("7z".parse::<FsType>().unwrap(), FsType::SevenZ);
550 assert_eq!("tar.gz".parse::<FsType>().unwrap(), FsType::TarGz);
551 assert_eq!(FsType::Hfsplus.to_string(), "hfsplus");
552 assert_eq!(FsType::SevenZ.to_string(), "7z");
553 assert_eq!(FsType::TarGz.to_string(), "tar.gz");
554 }
555}