#![forbid(unsafe_code)]
use std::io::{self, Read, Seek, SeekFrom};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FsType {
Ext4,
Ntfs,
ExFat,
Hfsplus,
Apfs,
Ewf,
Iso,
Vmdk,
Zip,
SevenZ,
TarGz,
TarBz2,
Unknown,
}
impl std::fmt::Display for FsType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FsType::Ext4 => write!(f, "ext4"),
FsType::Ntfs => write!(f, "ntfs"),
FsType::ExFat => write!(f, "exfat"),
FsType::Hfsplus => write!(f, "hfsplus"),
FsType::Apfs => write!(f, "apfs"),
FsType::Ewf => write!(f, "ewf"),
FsType::Iso => write!(f, "iso9660"),
FsType::Vmdk => write!(f, "vmdk"),
FsType::Zip => write!(f, "zip"),
FsType::SevenZ => write!(f, "7z"),
FsType::TarGz => write!(f, "tar.gz"),
FsType::TarBz2 => write!(f, "tar.bz2"),
FsType::Unknown => write!(f, "unknown"),
}
}
}
impl std::str::FromStr for FsType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"ext4" => Ok(FsType::Ext4),
"ntfs" => Ok(FsType::Ntfs),
"exfat" => Ok(FsType::ExFat),
"hfsplus" | "hfs+" | "hfsx" => Ok(FsType::Hfsplus),
"apfs" => Ok(FsType::Apfs),
"ewf" | "e01" => Ok(FsType::Ewf),
"vmdk" => Ok(FsType::Vmdk),
"iso" | "iso9660" | "cd" | "udf" => Ok(FsType::Iso),
"zip" => Ok(FsType::Zip),
"7z" | "sevenz" | "7zip" => Ok(FsType::SevenZ),
"targz" | "tar.gz" | "tgz" | "gz" | "gzip" => Ok(FsType::TarGz),
"tarbz2" | "tar.bz2" | "tbz2" | "tbz" | "bz2" | "bzip2" => Ok(FsType::TarBz2),
_ => Err(format!("unknown filesystem type: {s}")),
}
}
}
pub fn detect_filesystem<R: Read + Seek>(source: &mut R) -> io::Result<FsType> {
source.seek(SeekFrom::Start(0))?;
let mut buf = vec![0u8; 37_640];
let bytes_read = read_fill(source, &mut buf);
source.seek(SeekFrom::Start(0))?;
if bytes_read >= 8 && buf[0..3] == [0x45, 0x56, 0x46] && buf[3] == 0x09 {
return Ok(FsType::Ewf);
}
if bytes_read >= 4 && buf[0..4] == [0x4B, 0x44, 0x4D, 0x56] {
return Ok(FsType::Vmdk);
}
if bytes_read >= 21 && buf[0..21] == *b"# Disk DescriptorFile" {
return Ok(FsType::Vmdk);
}
if bytes_read >= 2 && buf[0] == 0x1F && buf[1] == 0x8B {
return Ok(FsType::TarGz);
}
if bytes_read >= 3 && &buf[0..3] == b"BZh" {
return Ok(FsType::TarBz2);
}
if bytes_read >= 4
&& buf[0..2] == [0x50, 0x4B]
&& matches!(buf[2..4], [0x03, 0x04] | [0x05, 0x06] | [0x07, 0x08])
{
return Ok(FsType::Zip);
}
if bytes_read >= 6 && buf[0..6] == [0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C] {
return Ok(FsType::SevenZ);
}
if bytes_read >= 36 && &buf[32..36] == b"NXSB" {
return Ok(FsType::Apfs);
}
if bytes_read >= 7 && &buf[3..7] == b"NTFS" {
return Ok(FsType::Ntfs);
}
if bytes_read >= 8 && &buf[3..8] == b"EXFAT" {
return Ok(FsType::ExFat);
}
if bytes_read >= 1026 && buf[1024] == 0x48 && (buf[1025] == 0x2B || buf[1025] == 0x58) {
return Ok(FsType::Hfsplus);
}
if bytes_read >= 1082 {
let magic = u16::from_le_bytes([buf[1080], buf[1081]]);
if magic == 0xEF53 {
return Ok(FsType::Ext4);
}
}
if bytes_read >= 32_774 && &buf[32_769..32_774] == b"CD001" {
return Ok(FsType::Iso);
}
if bytes_read >= 37_638 && &buf[37_633..37_638] == b"CD001" {
return Ok(FsType::Iso);
}
Ok(FsType::Unknown)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MemDumpFormat {
Lime,
Avml,
ElfCore,
WinCrashDump,
}
impl std::fmt::Display for MemDumpFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MemDumpFormat::Lime => write!(f, "lime"),
MemDumpFormat::Avml => write!(f, "avml"),
MemDumpFormat::ElfCore => write!(f, "elf-core"),
MemDumpFormat::WinCrashDump => write!(f, "win-crashdump"),
}
}
}
pub fn detect_memory_dump<R: Read + Seek>(source: &mut R) -> io::Result<Option<MemDumpFormat>> {
source.seek(SeekFrom::Start(0))?;
let mut buf = [0u8; 18];
let n = read_fill(source, &mut buf);
source.seek(SeekFrom::Start(0))?;
if n >= 4 && &buf[0..4] == b"EMiL" {
return Ok(Some(MemDumpFormat::Lime));
}
if n >= 4 && &buf[0..4] == b"AVML" {
return Ok(Some(MemDumpFormat::Avml));
}
if n >= 8 && &buf[0..8] == b"PAGEDU64" {
return Ok(Some(MemDumpFormat::WinCrashDump));
}
if n >= 18 && buf[0..4] == [0x7F, b'E', b'L', b'F'] {
let e_type = u16::from_le_bytes([buf[16], buf[17]]);
if e_type == 4 {
return Ok(Some(MemDumpFormat::ElfCore));
}
}
Ok(None)
}
fn read_fill<R: Read>(source: &mut R, buf: &mut [u8]) -> usize {
let mut total = 0;
while total < buf.len() {
match source.read(&mut buf[total..]) {
Ok(0) | Err(_) => break,
Ok(n) => total += n,
}
}
total
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn detects_vmdk_sparse_magic() {
let mut data = vec![0u8; 2048];
data[0..4].copy_from_slice(b"KDMV");
assert_eq!(
detect_filesystem(&mut Cursor::new(data)).unwrap(),
FsType::Vmdk
);
}
#[test]
fn detects_vmdk_text_descriptor() {
let data = b"# Disk DescriptorFile\nversion=1\n".to_vec();
assert_eq!(
detect_filesystem(&mut Cursor::new(data)).unwrap(),
FsType::Vmdk
);
}
fn make_ext4_image() -> Vec<u8> {
let mut data = vec![0u8; 2048];
data[1080] = 0x53; data[1081] = 0xEF; data
}
fn make_ntfs_image() -> Vec<u8> {
let mut data = vec![0u8; 512];
data[3..7].copy_from_slice(b"NTFS");
data
}
fn make_exfat_image() -> Vec<u8> {
let mut data = vec![0u8; 512];
data[3..8].copy_from_slice(b"EXFAT");
data
}
#[test]
fn detect_ext4() {
let data = make_ext4_image();
let mut cursor = Cursor::new(data);
assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Ext4);
}
#[test]
fn detect_ntfs() {
let data = make_ntfs_image();
let mut cursor = Cursor::new(data);
assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Ntfs);
}
#[test]
fn detect_exfat() {
let data = make_exfat_image();
let mut cursor = Cursor::new(data);
assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::ExFat);
}
#[test]
fn detect_unknown() {
let data = vec![0u8; 2048];
let mut cursor = Cursor::new(data);
assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Unknown);
}
fn make_iso_image() -> Vec<u8> {
let mut data = vec![0u8; 18 * 2048];
let pvd = 16 * 2048;
data[pvd] = 0x01;
data[pvd + 1..pvd + 6].copy_from_slice(b"CD001");
data[pvd + 6] = 0x01;
data
}
#[test]
fn detect_iso() {
let data = make_iso_image();
let mut cursor = Cursor::new(data);
assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Iso);
}
#[test]
fn iso_fstype_parses_from_str() {
assert_eq!("iso".parse::<FsType>().unwrap(), FsType::Iso);
assert_eq!("iso9660".parse::<FsType>().unwrap(), FsType::Iso);
}
#[test]
fn detect_too_short() {
let data = vec![0u8; 10];
let mut cursor = Cursor::new(data);
let result = detect_filesystem(&mut cursor);
assert!(result.is_ok());
assert_eq!(result.unwrap(), FsType::Unknown);
}
#[test]
fn detect_real_ext4_image() {
let path = "/Users/4n6h4x0r/src/ext4fs-forensic/tests/data/forensic.img";
let Ok(data) = std::fs::read(path) else {
eprintln!("skip: forensic.img not found");
return;
};
let mut cursor = Cursor::new(data);
assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Ext4);
}
#[test]
fn fstype_from_str() {
assert_eq!("ext4".parse::<FsType>().unwrap(), FsType::Ext4);
assert_eq!("NTFS".parse::<FsType>().unwrap(), FsType::Ntfs);
assert_eq!("ExFat".parse::<FsType>().unwrap(), FsType::ExFat);
assert!("btrfs".parse::<FsType>().is_err());
}
#[test]
fn fstype_display() {
assert_eq!(FsType::Ext4.to_string(), "ext4");
assert_eq!(FsType::Ntfs.to_string(), "ntfs");
assert_eq!(FsType::ExFat.to_string(), "exfat");
assert_eq!(FsType::Unknown.to_string(), "unknown");
}
#[test]
fn detect_ewf_image() {
let mut data = vec![0u8; 2048];
data[0..8].copy_from_slice(&[0x45, 0x56, 0x46, 0x09, 0x0D, 0x0A, 0xFF, 0x00]);
let mut cursor = Cursor::new(data);
assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Ewf);
}
#[test]
fn fstype_ewf_display() {
assert_eq!(FsType::Ewf.to_string(), "ewf");
}
#[test]
fn fstype_ewf_from_str() {
assert_eq!("ewf".parse::<FsType>().unwrap(), FsType::Ewf);
assert_eq!("e01".parse::<FsType>().unwrap(), FsType::Ewf);
}
#[test]
fn detect_resets_seek_position() {
let data = make_ext4_image();
let mut cursor = Cursor::new(data);
cursor.seek(SeekFrom::Start(500)).unwrap();
assert_eq!(detect_filesystem(&mut cursor).unwrap(), FsType::Ext4);
assert_eq!(cursor.stream_position().unwrap(), 0);
}
#[test]
fn detect_gzip_as_targz() {
let mut data = vec![0u8; 64];
data[0] = 0x1F;
data[1] = 0x8B;
data[2] = 0x08; assert_eq!(
detect_filesystem(&mut Cursor::new(data)).unwrap(),
FsType::TarGz
);
}
#[test]
fn detect_mem_lime() {
let mut data = vec![0u8; 64];
data[0..4].copy_from_slice(b"EMiL"); assert_eq!(
detect_memory_dump(&mut Cursor::new(data)).unwrap(),
Some(MemDumpFormat::Lime)
);
}
#[test]
fn detect_mem_avml() {
let mut data = vec![0u8; 64];
data[0..4].copy_from_slice(b"AVML");
assert_eq!(
detect_memory_dump(&mut Cursor::new(data)).unwrap(),
Some(MemDumpFormat::Avml)
);
}
#[test]
fn detect_mem_elf_core() {
let mut data = vec![0u8; 64];
data[0..4].copy_from_slice(&[0x7F, b'E', b'L', b'F']);
data[16..18].copy_from_slice(&4u16.to_le_bytes()); assert_eq!(
detect_memory_dump(&mut Cursor::new(data)).unwrap(),
Some(MemDumpFormat::ElfCore)
);
}
#[test]
fn detect_mem_elf_exec_is_not_a_dump() {
let mut data = vec![0u8; 64];
data[0..4].copy_from_slice(&[0x7F, b'E', b'L', b'F']);
data[16..18].copy_from_slice(&2u16.to_le_bytes()); assert_eq!(detect_memory_dump(&mut Cursor::new(data)).unwrap(), None);
}
#[test]
fn detect_mem_win_crashdump() {
let mut data = vec![0u8; 64];
data[0..8].copy_from_slice(b"PAGEDU64");
assert_eq!(
detect_memory_dump(&mut Cursor::new(data)).unwrap(),
Some(MemDumpFormat::WinCrashDump)
);
}
#[test]
fn detect_mem_none_for_non_dump() {
let data = vec![0u8; 64];
assert_eq!(detect_memory_dump(&mut Cursor::new(data)).unwrap(), None);
}
#[test]
fn detect_bzip2_as_tarbz2() {
let mut data = vec![0u8; 64];
data[0..3].copy_from_slice(b"BZh");
data[3] = b'9'; assert_eq!(
detect_filesystem(&mut Cursor::new(data)).unwrap(),
FsType::TarBz2
);
}
#[test]
fn detect_zip_local_file_header() {
let mut data = vec![0u8; 64];
data[0..4].copy_from_slice(b"PK\x03\x04");
assert_eq!(
detect_filesystem(&mut Cursor::new(data)).unwrap(),
FsType::Zip
);
}
#[test]
fn detect_zip_empty_archive() {
let mut data = vec![0u8; 64];
data[0..4].copy_from_slice(b"PK\x05\x06");
assert_eq!(
detect_filesystem(&mut Cursor::new(data)).unwrap(),
FsType::Zip
);
}
#[test]
fn detect_7z_signature() {
let mut data = vec![0u8; 64];
data[0..6].copy_from_slice(&[0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]);
assert_eq!(
detect_filesystem(&mut Cursor::new(data)).unwrap(),
FsType::SevenZ
);
}
#[test]
fn detect_hfsplus_signature() {
let mut data = vec![0u8; 2048];
data[1024] = 0x48; data[1025] = 0x2B; assert_eq!(
detect_filesystem(&mut Cursor::new(data)).unwrap(),
FsType::Hfsplus
);
}
#[test]
fn detect_hfsx_signature() {
let mut data = vec![0u8; 2048];
data[1024] = 0x48; data[1025] = 0x58; assert_eq!(
detect_filesystem(&mut Cursor::new(data)).unwrap(),
FsType::Hfsplus
);
}
#[test]
fn detect_apfs_nxsb() {
let mut data = vec![0u8; 4096];
data[32..36].copy_from_slice(b"NXSB");
assert_eq!(
detect_filesystem(&mut Cursor::new(data)).unwrap(),
FsType::Apfs
);
}
#[test]
fn new_fstypes_parse_and_display() {
assert_eq!("hfsplus".parse::<FsType>().unwrap(), FsType::Hfsplus);
assert_eq!("apfs".parse::<FsType>().unwrap(), FsType::Apfs);
assert_eq!("zip".parse::<FsType>().unwrap(), FsType::Zip);
assert_eq!("7z".parse::<FsType>().unwrap(), FsType::SevenZ);
assert_eq!("tar.gz".parse::<FsType>().unwrap(), FsType::TarGz);
assert_eq!(FsType::Hfsplus.to_string(), "hfsplus");
assert_eq!(FsType::SevenZ.to_string(), "7z");
assert_eq!(FsType::TarGz.to_string(), "tar.gz");
}
}