forsmost 0.1.1

A Rust Foremost-style file carving command-line tool.
Documentation
const KIBIBYTE: u64 = 1024;
const MEBIBYTE: u64 = 1024 * KIBIBYTE;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FileKind {
    Avi,
    Bmp,
    Config,
    Cpp,
    Doc,
    Docx,
    Elf,
    Exe,
    Gif,
    #[cfg(feature = "gzip")]
    Gzip,
    Html,
    Jpeg,
    Mov,
    Mp4,
    Mpeg,
    Ole,
    Pdf,
    Png,
    Ppt,
    Pptx,
    Rar,
    Reg,
    Riff,
    Sxc,
    Sxi,
    Sxw,
    Wav,
    Wmv,
    Wpd,
    Xls,
    Xlsx,
    Zip,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SearchMode {
    Ascii,
    Forward,
    ForwardNext,
    Reverse,
}

#[derive(Clone, Debug)]
pub struct SearchSpec {
    pub case_sensitive: bool,
    pub footer: Vec<u8>,
    pub found: u64,
    pub header: Vec<u8>,
    pub kind: FileKind,
    pub markers: Vec<Vec<u8>>,
    pub max_len: u64,
    pub search_mode: SearchMode,
    pub suffix: String,
}

impl SearchSpec {
    #[must_use]
    pub fn config(
        suffix: &str,
        case_sensitive: bool,
        max_len: u64,
        header: Vec<u8>,
        footer: Vec<u8>,
        search_mode: SearchMode,
    ) -> Self {
        Self {
            case_sensitive,
            footer,
            found: 0,
            header,
            kind: FileKind::Config,
            markers: Vec::new(),
            max_len,
            search_mode,
            suffix: suffix.to_owned(),
        }
    }

    #[must_use]
    pub const fn directory_name(&self) -> &str {
        if self.suffix.is_empty() {
            "none"
        } else {
            self.suffix.as_str()
        }
    }
}

#[must_use]
pub fn builtins_for_selector(
    selector: &str,
    max_file_size: Option<u64>,
) -> Option<Vec<SearchSpec>> {
    let normalized = selector.to_ascii_lowercase();
    match normalized.as_str() {
        "all" => Some(all_builtins()),
        "avi" => Some(vec![builtin(
            FileKind::Avi,
            "avi",
            b"RIFF",
            b"INFO",
            max_file_size.unwrap_or(20 * MEBIBYTE),
            true,
        )]),
        "bmp" => Some(vec![builtin(
            FileKind::Bmp,
            "bmp",
            b"BM",
            b"",
            max_file_size.unwrap_or(2 * MEBIBYTE),
            true,
        )]),
        "cpp" => Some(vec![builtin_with_markers(
            FileKind::Cpp,
            "cpp",
            b"#include",
            b"char",
            max_file_size.unwrap_or(MEBIBYTE),
            true,
            &[b"int"],
        )]),
        "doc" => Some(vec![ole_family(
            FileKind::Doc,
            "doc",
            max_file_size.unwrap_or(20 * MEBIBYTE),
        )]),
        "docx" => Some(vec![zip_family(
            FileKind::Docx,
            "docx",
            max_file_size.unwrap_or(10 * MEBIBYTE),
        )]),
        "elf" => Some(vec![builtin(
            FileKind::Elf,
            "elf",
            b"\x7fELF",
            b"",
            max_file_size.unwrap_or(MEBIBYTE),
            true,
        )]),
        "exe" => Some(vec![builtin(
            FileKind::Exe,
            "exe",
            b"MZ",
            b"",
            max_file_size.unwrap_or(MEBIBYTE),
            true,
        )]),
        "gif" => Some(vec![builtin_with_markers(
            FileKind::Gif,
            "gif",
            b"GIF8",
            b"\x00\x3b",
            max_file_size.unwrap_or(MEBIBYTE),
            true,
            &[b"\x00\x00\x3b"],
        )]),
        #[cfg(feature = "gzip")]
        "gzip" | "gz" => Some(vec![builtin(
            FileKind::Gzip,
            "gz",
            b"\x1f\x8b",
            b"\x00\x00\x00\x00",
            max_file_size.unwrap_or(100 * MEBIBYTE),
            true,
        )]),
        "html" | "htm" => Some(vec![builtin(
            FileKind::Html,
            "htm",
            b"<html",
            b"</html>",
            max_file_size.unwrap_or(MEBIBYTE),
            false,
        )]),
        "jpg" | "jpeg" => Some(vec![builtin(
            FileKind::Jpeg,
            "jpg",
            b"\xff\xd8\xff",
            b"\xff\xd9",
            max_file_size.unwrap_or(20 * MEBIBYTE),
            true,
        )]),
        "mov" => Some(vec![builtin(
            FileKind::Mov,
            "mov",
            b"moov",
            b"",
            max_file_size.unwrap_or(40 * MEBIBYTE),
            true,
        )]),
        "mp4" => Some(vec![builtin(
            FileKind::Mp4,
            "mp4",
            b"????ftyp",
            b"",
            max_file_size.unwrap_or(600 * MEBIBYTE),
            true,
        )]),
        "mpg" | "mpeg" => Some(vec![builtin_with_markers(
            FileKind::Mpeg,
            "mpg",
            b"\x00\x00\x01\xba",
            b"\x00\x00\x01\xb9",
            max_file_size.unwrap_or(50 * MEBIBYTE),
            true,
            &[b"\x00\x00\x01"],
        )]),
        "ole" | "office" => Some(vec![ole_family(
            FileKind::Ole,
            "ole",
            max_file_size.unwrap_or(10 * MEBIBYTE),
        )]),
        "pdf" => Some(vec![builtin_with_markers(
            FileKind::Pdf,
            "pdf",
            b"%PDF-1.",
            b"%%EOF",
            max_file_size.unwrap_or(20 * MEBIBYTE),
            true,
            &[b"/L ", b"obj", b"/Linearized", b"/Length"],
        )]),
        "png" => Some(vec![builtin(
            FileKind::Png,
            "png",
            b"\x89PNG\r\n\x1a\n",
            b"IEND",
            max_file_size.unwrap_or(MEBIBYTE),
            true,
        )]),
        "ppt" => Some(vec![ole_family(
            FileKind::Ppt,
            "ppt",
            max_file_size.unwrap_or(10 * MEBIBYTE),
        )]),
        "pptx" => Some(vec![zip_family(
            FileKind::Pptx,
            "pptx",
            max_file_size.unwrap_or(10 * MEBIBYTE),
        )]),
        "rar" => Some(vec![builtin(
            FileKind::Rar,
            "rar",
            b"Rar!\x1a\x07\x00",
            b"\x00\x00\x00\x00\x00\x00\x00\x00",
            max_file_size.unwrap_or(100 * MEBIBYTE),
            true,
        )]),
        "reg" => Some(vec![builtin(
            FileKind::Reg,
            "reg",
            b"regf",
            b"",
            max_file_size.unwrap_or(2 * MEBIBYTE),
            true,
        )]),
        "rif" => Some(vec![builtin(
            FileKind::Riff,
            "rif",
            b"RIFF",
            b"INFO",
            max_file_size.unwrap_or(20 * MEBIBYTE),
            true,
        )]),
        "sxc" => Some(vec![zip_family(
            FileKind::Sxc,
            "sxc",
            max_file_size.unwrap_or(10 * MEBIBYTE),
        )]),
        "sxi" => Some(vec![zip_family(
            FileKind::Sxi,
            "sxi",
            max_file_size.unwrap_or(10 * MEBIBYTE),
        )]),
        "sxw" => Some(vec![zip_family(
            FileKind::Sxw,
            "sxw",
            max_file_size.unwrap_or(10 * MEBIBYTE),
        )]),
        "vjpeg" => Some(vec![builtin(
            FileKind::Mov,
            "mov",
            b"pnot",
            b"",
            max_file_size.unwrap_or(40 * MEBIBYTE),
            true,
        )]),
        "wav" => Some(vec![builtin(
            FileKind::Wav,
            "wav",
            b"RIFF",
            b"INFO",
            max_file_size.unwrap_or(20 * MEBIBYTE),
            true,
        )]),
        "wmv" => Some(vec![builtin(
            FileKind::Wmv,
            "wmv",
            b"\x30\x26\xb2\x75\x8e\x66\xcf\x11",
            b"\xa1\xdc\xab\x8c\x47\xa9",
            max_file_size.unwrap_or(20 * MEBIBYTE),
            true,
        )]),
        "wpd" => Some(vec![builtin(
            FileKind::Wpd,
            "wpd",
            b"\xffWPC",
            b"",
            max_file_size.unwrap_or(MEBIBYTE),
            true,
        )]),
        "xls" => Some(vec![ole_family(
            FileKind::Xls,
            "xls",
            max_file_size.unwrap_or(10 * MEBIBYTE),
        )]),
        "xlsx" => Some(vec![zip_family(
            FileKind::Xlsx,
            "xlsx",
            max_file_size.unwrap_or(10 * MEBIBYTE),
        )]),
        "zip" => Some(vec![zip_family(
            FileKind::Zip,
            "zip",
            max_file_size.unwrap_or(100 * MEBIBYTE),
        )]),
        _ => None,
    }
}

#[must_use]
pub fn default_all_builtins() -> Vec<SearchSpec> {
    all_builtins()
}

fn all_builtins() -> Vec<SearchSpec> {
    let mut specs = Vec::new();
    for selector in [
        "jpg", "gif", "bmp", "wmv", "mov", "mp4", "rif", "htm", "ole", "zip", "rar", "exe", "png",
        "mpg", "pdf",
    ] {
        if let Some(mut selected) = builtins_for_selector(selector, None) {
            specs.append(&mut selected);
        }
    }
    specs
}

fn builtin(
    kind: FileKind,
    suffix: &str,
    header: &[u8],
    footer: &[u8],
    max_len: u64,
    case_sensitive: bool,
) -> SearchSpec {
    builtin_with_markers(kind, suffix, header, footer, max_len, case_sensitive, &[])
}

fn builtin_with_markers(
    kind: FileKind,
    suffix: &str,
    header: &[u8],
    footer: &[u8],
    max_len: u64,
    case_sensitive: bool,
    markers: &[&[u8]],
) -> SearchSpec {
    SearchSpec {
        case_sensitive,
        footer: footer.to_vec(),
        found: 0,
        header: header.to_vec(),
        kind,
        markers: markers.iter().map(|marker| marker.to_vec()).collect(),
        max_len,
        search_mode: SearchMode::Forward,
        suffix: suffix.to_owned(),
    }
}

fn ole_family(kind: FileKind, suffix: &str, max_len: u64) -> SearchSpec {
    builtin(
        kind,
        suffix,
        b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1\x00\x00\x00\x00\x00\x00\x00\x00",
        b"",
        max_len,
        true,
    )
}

fn zip_family(kind: FileKind, suffix: &str, max_len: u64) -> SearchSpec {
    builtin(kind, suffix, b"PK\x03\x04", b"PK\x05\x06", max_len, true)
}