#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum FileType {
Unknown,
Pe,
Elf,
MachO,
Pdf,
Png,
Jpeg,
Gif,
Webp,
Zip,
Gzip,
Bzip2,
Xz,
Mp4,
Mp3,
Sqlite3,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct FileHashEvent {
pub algorithm: &'static str,
pub hash_hex: String,
pub bytes: u64,
pub file_type: FileType,
}
pub(super) fn classify(probe: &[u8]) -> FileType {
if probe.len() >= 4 {
if probe.starts_with(b"\x89PNG\r\n\x1a\n") {
return FileType::Png;
}
if probe.starts_with(b"GIF87a") || probe.starts_with(b"GIF89a") {
return FileType::Gif;
}
if probe.starts_with(b"\xFF\xD8\xFF") {
return FileType::Jpeg;
}
if probe.len() >= 12 && probe.starts_with(b"RIFF") && &probe[8..12] == b"WEBP" {
return FileType::Webp;
}
if probe.starts_with(b"PK\x03\x04")
|| probe.starts_with(b"PK\x05\x06")
|| probe.starts_with(b"PK\x07\x08")
{
return FileType::Zip;
}
if probe.starts_with(b"\x1F\x8B") {
return FileType::Gzip;
}
if probe.starts_with(b"%PDF") {
return FileType::Pdf;
}
if probe.starts_with(b"\x7FELF") {
return FileType::Elf;
}
if probe.starts_with(b"MZ") {
return FileType::Pe;
}
if probe.starts_with(b"\xFE\xED\xFA\xCE")
|| probe.starts_with(b"\xCE\xFA\xED\xFE")
|| probe.starts_with(b"\xFE\xED\xFA\xCF")
|| probe.starts_with(b"\xCF\xFA\xED\xFE")
{
return FileType::MachO;
}
if probe.starts_with(b"BZh") {
return FileType::Bzip2;
}
if probe.starts_with(b"\xFD7zXZ\x00") {
return FileType::Xz;
}
if probe.starts_with(b"ID3")
|| probe.starts_with(b"\xFF\xFB")
|| probe.starts_with(b"\xFF\xF3")
{
return FileType::Mp3;
}
if probe.starts_with(b"SQLite format 3\0") {
return FileType::Sqlite3;
}
}
if probe.len() >= 12 {
if &probe[4..8] == b"ftyp" {
return FileType::Mp4;
}
}
FileType::Unknown
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classifies_png() {
let payload = b"\x89PNG\r\n\x1a\n\x00\x00\x00\x0D";
assert_eq!(classify(payload), FileType::Png);
}
#[test]
fn classifies_pdf() {
let payload = b"%PDF-1.7\n...";
assert_eq!(classify(payload), FileType::Pdf);
}
#[test]
fn classifies_zip() {
let payload = b"PK\x03\x04\x14\x00\x00\x00\x08\x00";
assert_eq!(classify(payload), FileType::Zip);
}
#[test]
fn classifies_elf() {
let payload = b"\x7FELF\x02\x01\x01\x00";
assert_eq!(classify(payload), FileType::Elf);
}
#[test]
fn classifies_pe() {
let payload = b"MZ\x90\x00\x03\x00\x00\x00";
assert_eq!(classify(payload), FileType::Pe);
}
#[test]
fn classifies_macho_64bit_le() {
let payload = b"\xCF\xFA\xED\xFE\x07\x00\x00\x01";
assert_eq!(classify(payload), FileType::MachO);
}
#[test]
fn classifies_mp4_via_ftyp() {
let payload = b"\x00\x00\x00\x20ftypisom\x00\x00\x02\x00";
assert_eq!(classify(payload), FileType::Mp4);
}
#[test]
fn classifies_gzip() {
let payload = b"\x1F\x8B\x08\x00\x00\x00\x00\x00";
assert_eq!(classify(payload), FileType::Gzip);
}
#[test]
fn returns_unknown_for_random_bytes() {
let payload = b"this is just some random text data";
assert_eq!(classify(payload), FileType::Unknown);
}
#[test]
fn short_input_returns_unknown() {
let payload = b"x";
assert_eq!(classify(payload), FileType::Unknown);
}
}