#![forbid(unsafe_code)]
use std::io::{self, Read, Seek, SeekFrom};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FsType {
Ext4,
Ntfs,
ExFat,
Ewf,
Iso,
Vmdk,
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::Ewf => write!(f, "ewf"),
FsType::Iso => write!(f, "iso9660"),
FsType::Vmdk => write!(f, "vmdk"),
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),
"ewf" | "e01" => Ok(FsType::Ewf),
"iso" | "iso9660" | "cd" | "udf" => Ok(FsType::Iso),
_ => 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 >= 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 >= 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)
}
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);
}
}