mmd-anim-format 0.3.2

MMD format parser and exporter for mmd-anim
Documentation
use serde::Serialize;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum MmdFormatKind {
    Pmd,
    Pmx,
    Vmd,
    Vpd,
    Pmm,
    Nmd,
    X,
    Vac,
    Unknown,
}

pub fn sniff(data: &[u8]) -> Option<MmdFormatKind> {
    if data.starts_with(b"PMX ") {
        return Some(MmdFormatKind::Pmx);
    }
    if data.starts_with(b"Pmd") {
        return Some(MmdFormatKind::Pmd);
    }
    if data.starts_with(b"Vocaloid Motion Data") {
        return Some(MmdFormatKind::Vmd);
    }
    if data.starts_with(b"Polygon Movie maker ") {
        return Some(MmdFormatKind::Pmm);
    }
    None
}

pub fn detect_mmd_format(data: &[u8], file_name: Option<&str>) -> MmdFormatKind {
    if let Some(kind) = sniff(data) {
        return kind;
    }
    if data.starts_with(b"Vocaloid Pose Data") {
        return MmdFormatKind::Vpd;
    }
    if data.starts_with(b"xof ") {
        return MmdFormatKind::X;
    }
    if looks_like_nmd(data, file_name) {
        return MmdFormatKind::Nmd;
    }
    match extension(file_name).as_deref() {
        Some("x") => MmdFormatKind::X,
        Some("vac") => MmdFormatKind::Vac,
        Some("nmd") => MmdFormatKind::Nmd,
        Some("pmd") => MmdFormatKind::Pmd,
        Some("pmx") => MmdFormatKind::Pmx,
        Some("vmd") => MmdFormatKind::Vmd,
        Some("vpd") => MmdFormatKind::Vpd,
        Some("pmm") => MmdFormatKind::Pmm,
        _ => MmdFormatKind::Unknown,
    }
}

fn extension(file_name: Option<&str>) -> Option<String> {
    file_name?
        .rsplit_once('.')
        .map(|(_, ext)| ext.trim().to_ascii_lowercase())
}

fn looks_like_nmd(data: &[u8], file_name: Option<&str>) -> bool {
    matches!(extension(file_name).as_deref(), Some("nmd"))
        || data.starts_with(b"NMD")
        || data.starts_with(b"Nanoem Motion Data")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn detects_core_mmd_formats_from_magic_bytes() {
        assert_eq!(detect_mmd_format(b"PMX test", None), MmdFormatKind::Pmx);
        assert_eq!(detect_mmd_format(b"Pmd\x00", None), MmdFormatKind::Pmd);
        assert_eq!(
            detect_mmd_format(b"Vocaloid Motion Data 0002", None),
            MmdFormatKind::Vmd
        );
        assert_eq!(
            detect_mmd_format(b"Vocaloid Pose Data file", None),
            MmdFormatKind::Vpd
        );
        assert_eq!(
            detect_mmd_format(b"Polygon Movie maker 0002", None),
            MmdFormatKind::Pmm
        );
        assert_eq!(
            detect_mmd_format(b"xof 0303txt 0032", None),
            MmdFormatKind::X
        );
        assert_eq!(
            detect_mmd_format(b"Nanoem Motion Data", None),
            MmdFormatKind::Nmd
        );
    }

    #[test]
    fn sniffs_core_binary_formats_from_magic_bytes() {
        assert_eq!(sniff(b"PMX test"), Some(MmdFormatKind::Pmx));
        assert_eq!(sniff(b"Pmd\x00"), Some(MmdFormatKind::Pmd));
        assert_eq!(
            sniff(b"Vocaloid Motion Data 0002\0\0\0\0\0model"),
            Some(MmdFormatKind::Vmd)
        );
        assert_eq!(
            sniff(b"Polygon Movie maker 0002\0"),
            Some(MmdFormatKind::Pmm)
        );
        assert_eq!(sniff(b"Vocaloid Pose Data file"), None);
        assert_eq!(sniff(b""), None);
    }

    #[test]
    fn falls_back_to_case_insensitive_extension() {
        assert_eq!(
            detect_mmd_format(b"", Some("motion.NMD")),
            MmdFormatKind::Nmd
        );
        assert_eq!(
            detect_mmd_format(b"", Some("accessory.VAC")),
            MmdFormatKind::Vac
        );
        assert_eq!(
            detect_mmd_format(b"", Some("model.PMD")),
            MmdFormatKind::Pmd
        );
        assert_eq!(
            detect_mmd_format(b"", Some("unknown.bin")),
            MmdFormatKind::Unknown
        );
    }
}