mmd_anim_format/
format.rs1use 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}