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 Ad1,
21 Aff4Disk,
23 Aff4Logical,
25 Unknown,
26}
27
28impl std::fmt::Display for FsType {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 match self {
31 FsType::Ext4 => write!(f, "ext4"),
32 FsType::Ntfs => write!(f, "ntfs"),
33 FsType::ExFat => write!(f, "exfat"),
34 FsType::Hfsplus => write!(f, "hfsplus"),
35 FsType::Apfs => write!(f, "apfs"),
36 FsType::Ewf => write!(f, "ewf"),
37 FsType::Iso => write!(f, "iso9660"),
38 FsType::Vmdk => write!(f, "vmdk"),
39 FsType::Zip => write!(f, "zip"),
40 FsType::SevenZ => write!(f, "7z"),
41 FsType::TarGz => write!(f, "tar.gz"),
42 FsType::TarBz2 => write!(f, "tar.bz2"),
43 FsType::Ad1 => write!(f, "ad1"),
44 FsType::Aff4Disk => write!(f, "aff4-disk"),
45 FsType::Aff4Logical => write!(f, "aff4-logical"),
46 FsType::Unknown => write!(f, "unknown"),
47 }
48 }
49}
50
51impl std::str::FromStr for FsType {
52 type Err = String;
53 fn from_str(s: &str) -> Result<Self, Self::Err> {
54 match s.to_lowercase().as_str() {
55 "ext4" => Ok(FsType::Ext4),
56 "ntfs" => Ok(FsType::Ntfs),
57 "exfat" => Ok(FsType::ExFat),
58 "hfsplus" | "hfs+" | "hfsx" => Ok(FsType::Hfsplus),
59 "apfs" => Ok(FsType::Apfs),
60 "ewf" | "e01" => Ok(FsType::Ewf),
61 "vmdk" => Ok(FsType::Vmdk),
62 "iso" | "iso9660" | "cd" | "udf" => Ok(FsType::Iso),
63 "zip" => Ok(FsType::Zip),
64 "7z" | "sevenz" | "7zip" => Ok(FsType::SevenZ),
65 "targz" | "tar.gz" | "tgz" | "gz" | "gzip" => Ok(FsType::TarGz),
66 "tarbz2" | "tar.bz2" | "tbz2" | "tbz" | "bz2" | "bzip2" => Ok(FsType::TarBz2),
67 "ad1" | "adsegmentedfile" => Ok(FsType::Ad1),
68 "aff4" | "aff4-disk" | "aff4disk" => Ok(FsType::Aff4Disk),
69 "aff4-logical" | "aff4logical" | "aff4l" => Ok(FsType::Aff4Logical),
70 _ => Err(format!("unknown filesystem type: {s}")),
71 }
72 }
73}
74
75pub fn detect_filesystem<R: Read + Seek>(source: &mut R) -> io::Result<FsType> {
81 source.seek(SeekFrom::Start(0))?;
83
84 let mut buf = vec![0u8; 37_640];
88 let bytes_read = read_fill(source, &mut buf);
89
90 source.seek(SeekFrom::Start(0))?;
92
93 if bytes_read >= 8 && buf[0..3] == [0x45, 0x56, 0x46] && buf[3] == 0x09 {
95 return Ok(FsType::Ewf);
96 }
97
98 if bytes_read >= 4 && buf[0..4] == [0x4B, 0x44, 0x4D, 0x56] {
101 return Ok(FsType::Vmdk);
102 }
103 if bytes_read >= 21 && buf[0..21] == *b"# Disk DescriptorFile" {
104 return Ok(FsType::Vmdk);
105 }
106
107 if bytes_read >= 2 && buf[0] == 0x1F && buf[1] == 0x8B {
113 return Ok(FsType::TarGz);
114 }
115 if bytes_read >= 3 && &buf[0..3] == b"BZh" {
117 return Ok(FsType::TarBz2);
118 }
119 if bytes_read >= 4
120 && buf[0..2] == [0x50, 0x4B]
121 && matches!(buf[2..4], [0x03, 0x04] | [0x05, 0x06] | [0x07, 0x08])
122 {
123 return Ok(FsType::Zip);
124 }
125 if bytes_read >= 6 && buf[0..6] == [0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C] {
126 return Ok(FsType::SevenZ);
127 }
128 if bytes_read >= 15 && buf[0..15] == *b"ADSEGMENTEDFILE" {
132 return Ok(FsType::Ad1);
133 }
134 if bytes_read >= 7 && buf[0..7] == *b"ADCRYPT" {
135 return Ok(FsType::Ad1);
136 }
137
138 if bytes_read >= 36 && &buf[32..36] == b"NXSB" {
141 return Ok(FsType::Apfs);
142 }
143
144 if bytes_read >= 7 && &buf[3..7] == b"NTFS" {
146 return Ok(FsType::Ntfs);
147 }
148
149 if bytes_read >= 8 && &buf[3..8] == b"EXFAT" {
151 return Ok(FsType::ExFat);
152 }
153
154 if bytes_read >= 1026 && buf[1024] == 0x48 && (buf[1025] == 0x2B || buf[1025] == 0x58) {
157 return Ok(FsType::Hfsplus);
158 }
159
160 if bytes_read >= 1082 {
162 let magic = u16::from_le_bytes([buf[1080], buf[1081]]);
163 if magic == 0xEF53 {
164 return Ok(FsType::Ext4);
165 }
166 }
167
168 if bytes_read >= 32_774 && &buf[32_769..32_774] == b"CD001" {
172 return Ok(FsType::Iso);
173 }
174 if bytes_read >= 37_638 && &buf[37_633..37_638] == b"CD001" {
175 return Ok(FsType::Iso);
176 }
177
178 Ok(FsType::Unknown)
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub enum MemDumpFormat {
185 Lime,
187 Avml,
189 ElfCore,
191 WinCrashDump,
193}
194
195impl std::fmt::Display for MemDumpFormat {
196 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197 match self {
198 MemDumpFormat::Lime => write!(f, "lime"),
199 MemDumpFormat::Avml => write!(f, "avml"),
200 MemDumpFormat::ElfCore => write!(f, "elf-core"),
201 MemDumpFormat::WinCrashDump => write!(f, "win-crashdump"),
202 }
203 }
204}
205
206#[cfg(feature = "aff4")]
219pub fn detect_aff4(path: &std::path::Path) -> Option<FsType> {
220 match aff4::container_kind(path) {
221 Ok(aff4::ContainerKind::Disk | aff4::ContainerKind::Encrypted) => Some(FsType::Aff4Disk),
224 Ok(aff4::ContainerKind::Logical) => Some(FsType::Aff4Logical),
225 Err(_) => None,
226 }
227}
228
229pub fn detect_memory_dump<R: Read + Seek>(source: &mut R) -> io::Result<Option<MemDumpFormat>> {
230 source.seek(SeekFrom::Start(0))?;
231 let mut buf = [0u8; 18];
232 let n = read_fill(source, &mut buf);
233 source.seek(SeekFrom::Start(0))?;
234
235 if n >= 4 && &buf[0..4] == b"EMiL" {
237 return Ok(Some(MemDumpFormat::Lime));
238 }
239 if n >= 4 && &buf[0..4] == b"AVML" {
241 return Ok(Some(MemDumpFormat::Avml));
242 }
243 if n >= 8 && &buf[0..8] == b"PAGEDU64" {
245 return Ok(Some(MemDumpFormat::WinCrashDump));
246 }
247 if n >= 18 && buf[0..4] == [0x7F, b'E', b'L', b'F'] {
249 let e_type = u16::from_le_bytes([buf[16], buf[17]]);
250 if e_type == 4 {
251 return Ok(Some(MemDumpFormat::ElfCore));
252 }
253 }
254 Ok(None)
255}
256
257fn read_fill<R: Read>(source: &mut R, buf: &mut [u8]) -> usize {
259 let mut total = 0;
260 while total < buf.len() {
261 match source.read(&mut buf[total..]) {
262 Ok(0) | Err(_) => break,
263 Ok(n) => total += n,
264 }
265 }
266 total
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272 use std::io::Cursor;
273
274 #[cfg(feature = "aff4")]
275 mod aff4 {
276 use super::*;
277 use std::io::Write as _;
278
279 fn write_tmp(bytes: &[u8]) -> tempfile::NamedTempFile {
280 let mut f = tempfile::NamedTempFile::new().unwrap();
281 f.write_all(bytes).unwrap();
282 f
283 }
284
285 #[test]
286 fn detects_aff4_disk_image() {
287 let f = write_tmp(&::aff4::testutil::test_aff4(&[0u8; 512]));
288 assert_eq!(detect_aff4(f.path()), Some(FsType::Aff4Disk));
289 }
290
291 #[test]
292 fn detects_aff4_logical() {
293 let img = ::aff4::testutil::test_aff4_logical(
295 "a.txt",
296 b"hi",
297 "00000000000000000000000000000000",
298 );
299 let f = write_tmp(&img);
300 assert_eq!(detect_aff4(f.path()), Some(FsType::Aff4Logical));
301 }
302
303 #[test]
304 fn non_aff4_file_is_none() {
305 let f = write_tmp(b"not an aff4 container at all");
306 assert_eq!(detect_aff4(f.path()), None);
307 }
308 }
309
310 #[test]
311 fn detects_vmdk_sparse_magic() {
312 let mut data = vec![0u8; 2048];
314 data[0..4].copy_from_slice(b"KDMV");
315 assert_eq!(
316 detect_filesystem(&mut Cursor::new(data)).unwrap(),
317 FsType::Vmdk
318 );
319 }
320
321 #[test]
322 fn detects_ad1_segmented_magic() {
323 let mut data = vec![0u8; 512];
325 data[0..16].copy_from_slice(b"ADSEGMENTEDFILE\0");
326 assert_eq!(
327 detect_filesystem(&mut Cursor::new(data)).unwrap(),
328 FsType::Ad1
329 );
330 }
331
332 #[test]
333 fn detects_ad1_encrypted_magic() {
334 let mut data = vec![0u8; 512];
337 data[0..7].copy_from_slice(b"ADCRYPT");
338 assert_eq!(
339 detect_filesystem(&mut Cursor::new(data)).unwrap(),
340 FsType::Ad1
341 );
342 }
343
344 #[test]
345 fn detects_vmdk_text_descriptor() {
346 let data = b"# Disk DescriptorFile\nversion=1\n".to_vec();
347 assert_eq!(
348 detect_filesystem(&mut Cursor::new(data)).unwrap(),
349 FsType::Vmdk
350 );
351 }
352
353 fn make_ext4_image() -> Vec<u8> {
354 let mut data = vec![0u8; 2048];
357 data[1080] = 0x53; data[1081] = 0xEF; data
360 }
361
362 fn make_ntfs_image() -> Vec<u8> {
363 let mut data = vec![0u8; 512];
365 data[3..7].copy_from_slice(b"NTFS");
366 data
367 }
368
369 fn make_exfat_image() -> Vec<u8> {
370 let mut data = vec![0u8; 512];
372 data[3..8].copy_from_slice(b"EXFAT");
373 data
374 }
375
376 #[test]
377 fn detect_ext4() {
378 let data = make_ext4_image();
379 let mut cursor = Cursor::new(data);
380 assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Ext4);
381 }
382
383 #[test]
384 fn detect_ntfs() {
385 let data = make_ntfs_image();
386 let mut cursor = Cursor::new(data);
387 assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Ntfs);
388 }
389
390 #[test]
391 fn detect_exfat() {
392 let data = make_exfat_image();
393 let mut cursor = Cursor::new(data);
394 assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::ExFat);
395 }
396
397 #[test]
398 fn detect_unknown() {
399 let data = vec![0u8; 2048];
400 let mut cursor = Cursor::new(data);
401 assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Unknown);
402 }
403
404 fn make_iso_image() -> Vec<u8> {
406 let mut data = vec![0u8; 18 * 2048];
407 let pvd = 16 * 2048;
408 data[pvd] = 0x01;
409 data[pvd + 1..pvd + 6].copy_from_slice(b"CD001");
410 data[pvd + 6] = 0x01;
411 data
412 }
413
414 #[test]
415 fn detect_iso() {
416 let data = make_iso_image();
417 let mut cursor = Cursor::new(data);
418 assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Iso);
419 }
420
421 #[test]
422 fn iso_fstype_parses_from_str() {
423 assert_eq!("iso".parse::<FsType>().unwrap(), FsType::Iso);
424 assert_eq!("iso9660".parse::<FsType>().unwrap(), FsType::Iso);
425 }
426
427 #[test]
428 fn detect_too_short() {
429 let data = vec![0u8; 10];
430 let mut cursor = Cursor::new(data);
431 let result = detect_filesystem(&mut cursor);
433 assert!(result.is_ok());
434 assert_eq!(result.unwrap(), FsType::Unknown);
435 }
436
437 #[test]
438 fn detect_real_ext4_image() {
439 let path = "/Users/4n6h4x0r/src/ext4fs-forensic/tests/data/forensic.img";
440 let Ok(data) = std::fs::read(path) else {
441 eprintln!("skip: forensic.img not found");
442 return;
443 };
444 let mut cursor = Cursor::new(data);
445 assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Ext4);
446 }
447
448 #[test]
449 fn fstype_from_str() {
450 assert_eq!("ext4".parse::<FsType>().unwrap(), FsType::Ext4);
451 assert_eq!("NTFS".parse::<FsType>().unwrap(), FsType::Ntfs);
452 assert_eq!("ExFat".parse::<FsType>().unwrap(), FsType::ExFat);
453 assert!("btrfs".parse::<FsType>().is_err());
454 }
455
456 #[test]
457 fn fstype_display() {
458 assert_eq!(FsType::Ext4.to_string(), "ext4");
459 assert_eq!(FsType::Ntfs.to_string(), "ntfs");
460 assert_eq!(FsType::ExFat.to_string(), "exfat");
461 assert_eq!(FsType::Unknown.to_string(), "unknown");
462 }
463
464 #[test]
465 fn detect_ewf_image() {
466 let mut data = vec![0u8; 2048];
468 data[0..8].copy_from_slice(&[0x45, 0x56, 0x46, 0x09, 0x0D, 0x0A, 0xFF, 0x00]);
469 let mut cursor = Cursor::new(data);
470 assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Ewf);
471 }
472
473 #[test]
474 fn fstype_ewf_display() {
475 assert_eq!(FsType::Ewf.to_string(), "ewf");
476 }
477
478 #[test]
479 fn fstype_ewf_from_str() {
480 assert_eq!("ewf".parse::<FsType>().unwrap(), FsType::Ewf);
481 assert_eq!("e01".parse::<FsType>().unwrap(), FsType::Ewf);
482 }
483
484 #[test]
485 fn detect_resets_seek_position() {
486 let data = make_ext4_image();
487 let mut cursor = Cursor::new(data);
488 cursor.seek(SeekFrom::Start(500)).unwrap();
489 assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Ext4);
490 assert_eq!(cursor.stream_position().unwrap(), 0);
492 }
493
494 #[test]
495 fn detect_gzip_as_targz() {
496 let mut data = vec![0u8; 64];
498 data[0] = 0x1F;
499 data[1] = 0x8B;
500 data[2] = 0x08; assert_eq!(
502 detect_filesystem(&mut Cursor::new(data)).unwrap(),
503 FsType::TarGz
504 );
505 }
506
507 #[test]
508 fn detect_mem_lime() {
509 let mut data = vec![0u8; 64];
510 data[0..4].copy_from_slice(b"EMiL"); assert_eq!(
512 detect_memory_dump(&mut Cursor::new(data)).unwrap(),
513 Some(MemDumpFormat::Lime)
514 );
515 }
516
517 #[test]
518 fn detect_mem_avml() {
519 let mut data = vec![0u8; 64];
520 data[0..4].copy_from_slice(b"AVML");
521 assert_eq!(
522 detect_memory_dump(&mut Cursor::new(data)).unwrap(),
523 Some(MemDumpFormat::Avml)
524 );
525 }
526
527 #[test]
528 fn detect_mem_elf_core() {
529 let mut data = vec![0u8; 64];
530 data[0..4].copy_from_slice(&[0x7F, b'E', b'L', b'F']);
531 data[16..18].copy_from_slice(&4u16.to_le_bytes()); assert_eq!(
533 detect_memory_dump(&mut Cursor::new(data)).unwrap(),
534 Some(MemDumpFormat::ElfCore)
535 );
536 }
537
538 #[test]
539 fn detect_mem_elf_exec_is_not_a_dump() {
540 let mut data = vec![0u8; 64];
542 data[0..4].copy_from_slice(&[0x7F, b'E', b'L', b'F']);
543 data[16..18].copy_from_slice(&2u16.to_le_bytes()); assert_eq!(detect_memory_dump(&mut Cursor::new(data)).unwrap(), None);
545 }
546
547 #[test]
548 fn detect_mem_win_crashdump() {
549 let mut data = vec![0u8; 64];
550 data[0..8].copy_from_slice(b"PAGEDU64");
551 assert_eq!(
552 detect_memory_dump(&mut Cursor::new(data)).unwrap(),
553 Some(MemDumpFormat::WinCrashDump)
554 );
555 }
556
557 #[test]
558 fn detect_mem_none_for_non_dump() {
559 let data = vec![0u8; 64];
560 assert_eq!(detect_memory_dump(&mut Cursor::new(data)).unwrap(), None);
561 }
562
563 #[test]
564 fn detect_bzip2_as_tarbz2() {
565 let mut data = vec![0u8; 64];
567 data[0..3].copy_from_slice(b"BZh");
568 data[3] = b'9'; assert_eq!(
570 detect_filesystem(&mut Cursor::new(data)).unwrap(),
571 FsType::TarBz2
572 );
573 }
574
575 #[test]
576 fn detect_zip_local_file_header() {
577 let mut data = vec![0u8; 64];
578 data[0..4].copy_from_slice(b"PK\x03\x04");
579 assert_eq!(
580 detect_filesystem(&mut Cursor::new(data)).unwrap(),
581 FsType::Zip
582 );
583 }
584
585 #[test]
586 fn detect_zip_empty_archive() {
587 let mut data = vec![0u8; 64];
589 data[0..4].copy_from_slice(b"PK\x05\x06");
590 assert_eq!(
591 detect_filesystem(&mut Cursor::new(data)).unwrap(),
592 FsType::Zip
593 );
594 }
595
596 #[test]
597 fn detect_7z_signature() {
598 let mut data = vec![0u8; 64];
599 data[0..6].copy_from_slice(&[0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]);
600 assert_eq!(
601 detect_filesystem(&mut Cursor::new(data)).unwrap(),
602 FsType::SevenZ
603 );
604 }
605
606 #[test]
607 fn detect_hfsplus_signature() {
608 let mut data = vec![0u8; 2048];
610 data[1024] = 0x48; data[1025] = 0x2B; assert_eq!(
613 detect_filesystem(&mut Cursor::new(data)).unwrap(),
614 FsType::Hfsplus
615 );
616 }
617
618 #[test]
619 fn detect_hfsx_signature() {
620 let mut data = vec![0u8; 2048];
622 data[1024] = 0x48; data[1025] = 0x58; assert_eq!(
625 detect_filesystem(&mut Cursor::new(data)).unwrap(),
626 FsType::Hfsplus
627 );
628 }
629
630 #[test]
631 fn detect_apfs_nxsb() {
632 let mut data = vec![0u8; 4096];
634 data[32..36].copy_from_slice(b"NXSB");
635 assert_eq!(
636 detect_filesystem(&mut Cursor::new(data)).unwrap(),
637 FsType::Apfs
638 );
639 }
640
641 #[test]
642 fn new_fstypes_parse_and_display() {
643 assert_eq!("hfsplus".parse::<FsType>().unwrap(), FsType::Hfsplus);
644 assert_eq!("apfs".parse::<FsType>().unwrap(), FsType::Apfs);
645 assert_eq!("zip".parse::<FsType>().unwrap(), FsType::Zip);
646 assert_eq!("7z".parse::<FsType>().unwrap(), FsType::SevenZ);
647 assert_eq!("tar.gz".parse::<FsType>().unwrap(), FsType::TarGz);
648 assert_eq!(FsType::Hfsplus.to_string(), "hfsplus");
649 assert_eq!(FsType::SevenZ.to_string(), "7z");
650 assert_eq!(FsType::TarGz.to_string(), "tar.gz");
651 }
652}