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