Skip to main content

mmd_anim_format/
format.rs

1use serde::Serialize;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
4#[serde(rename_all = "camelCase")]
5pub enum MmdFormatKind {
6    Pmd,
7    Pmx,
8    Vmd,
9    Vpd,
10    Pmm,
11    Nmd,
12    X,
13    Vac,
14    Unknown,
15}
16
17pub fn sniff(data: &[u8]) -> Option<MmdFormatKind> {
18    if data.starts_with(b"PMX ") {
19        return Some(MmdFormatKind::Pmx);
20    }
21    if data.starts_with(b"Pmd") {
22        return Some(MmdFormatKind::Pmd);
23    }
24    if data.starts_with(b"Vocaloid Motion Data") {
25        return Some(MmdFormatKind::Vmd);
26    }
27    if data.starts_with(b"Polygon Movie maker ") {
28        return Some(MmdFormatKind::Pmm);
29    }
30    None
31}
32
33pub fn detect_mmd_format(data: &[u8], file_name: Option<&str>) -> MmdFormatKind {
34    if let Some(kind) = sniff(data) {
35        return kind;
36    }
37    if data.starts_with(b"Vocaloid Pose Data") {
38        return MmdFormatKind::Vpd;
39    }
40    if data.starts_with(b"xof ") {
41        return MmdFormatKind::X;
42    }
43    if looks_like_nmd(data, file_name) {
44        return MmdFormatKind::Nmd;
45    }
46    match extension(file_name).as_deref() {
47        Some("x") => MmdFormatKind::X,
48        Some("vac") => MmdFormatKind::Vac,
49        Some("nmd") => MmdFormatKind::Nmd,
50        Some("pmd") => MmdFormatKind::Pmd,
51        Some("pmx") => MmdFormatKind::Pmx,
52        Some("vmd") => MmdFormatKind::Vmd,
53        Some("vpd") => MmdFormatKind::Vpd,
54        Some("pmm") => MmdFormatKind::Pmm,
55        _ => MmdFormatKind::Unknown,
56    }
57}
58
59fn extension(file_name: Option<&str>) -> Option<String> {
60    file_name?
61        .rsplit_once('.')
62        .map(|(_, ext)| ext.trim().to_ascii_lowercase())
63}
64
65fn looks_like_nmd(data: &[u8], file_name: Option<&str>) -> bool {
66    matches!(extension(file_name).as_deref(), Some("nmd"))
67        || data.starts_with(b"NMD")
68        || data.starts_with(b"Nanoem Motion Data")
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn detects_core_mmd_formats_from_magic_bytes() {
77        assert_eq!(detect_mmd_format(b"PMX test", None), MmdFormatKind::Pmx);
78        assert_eq!(detect_mmd_format(b"Pmd\x00", None), MmdFormatKind::Pmd);
79        assert_eq!(
80            detect_mmd_format(b"Vocaloid Motion Data 0002", None),
81            MmdFormatKind::Vmd
82        );
83        assert_eq!(
84            detect_mmd_format(b"Vocaloid Pose Data file", None),
85            MmdFormatKind::Vpd
86        );
87        assert_eq!(
88            detect_mmd_format(b"Polygon Movie maker 0002", None),
89            MmdFormatKind::Pmm
90        );
91        assert_eq!(
92            detect_mmd_format(b"xof 0303txt 0032", None),
93            MmdFormatKind::X
94        );
95        assert_eq!(
96            detect_mmd_format(b"Nanoem Motion Data", None),
97            MmdFormatKind::Nmd
98        );
99    }
100
101    #[test]
102    fn sniffs_core_binary_formats_from_magic_bytes() {
103        assert_eq!(sniff(b"PMX test"), Some(MmdFormatKind::Pmx));
104        assert_eq!(sniff(b"Pmd\x00"), Some(MmdFormatKind::Pmd));
105        assert_eq!(
106            sniff(b"Vocaloid Motion Data 0002\0\0\0\0\0model"),
107            Some(MmdFormatKind::Vmd)
108        );
109        assert_eq!(
110            sniff(b"Polygon Movie maker 0002\0"),
111            Some(MmdFormatKind::Pmm)
112        );
113        assert_eq!(sniff(b"Vocaloid Pose Data file"), None);
114        assert_eq!(sniff(b""), None);
115    }
116
117    #[test]
118    fn falls_back_to_case_insensitive_extension() {
119        assert_eq!(
120            detect_mmd_format(b"", Some("motion.NMD")),
121            MmdFormatKind::Nmd
122        );
123        assert_eq!(
124            detect_mmd_format(b"", Some("accessory.VAC")),
125            MmdFormatKind::Vac
126        );
127        assert_eq!(
128            detect_mmd_format(b"", Some("model.PMD")),
129            MmdFormatKind::Pmd
130        );
131        assert_eq!(
132            detect_mmd_format(b"", Some("unknown.bin")),
133            MmdFormatKind::Unknown
134        );
135    }
136}