1use crate::ResourceType;
2
3const SFX_WAV_MAGIC: [u8; 4] = [0xFF, 0xF3, 0x60, 0xC4];
5
6#[cfg(feature = "tracing")]
7macro_rules! trace_debug {
8 ($($arg:tt)*) => {
9 tracing::debug!($($arg)*);
10 };
11}
12
13#[cfg(not(feature = "tracing"))]
14macro_rules! trace_debug {
15 ($($arg:tt)*) => {};
16}
17
18#[cfg_attr(
26 feature = "tracing",
27 tracing::instrument(
28 level = "debug",
29 skip(bytes),
30 fields(bytes_len = bytes.len(), extension_hint = extension_hint.unwrap_or(""))
31 )
32)]
33pub fn detect_resource_type(bytes: &[u8], extension_hint: Option<&str>) -> ResourceType {
34 let (detected, _reason) = if bytes.len() >= 8 && &bytes[..8] == b"LTR V1.0" {
35 (ResourceType::Ltr, "magic_8_ltr")
36 } else if bytes.len() >= 8 && &bytes[..4] == b"BWM " && &bytes[4..8] == b"V1.0" {
37 (ResourceType::Bwm, "magic_8_bwm")
38 } else if bytes.starts_with(&SFX_WAV_MAGIC) {
39 (ResourceType::Wav, "sfx_wav_magic")
40 } else if bytes.len() >= 4 {
41 let first4 = [bytes[0], bytes[1], bytes[2], bytes[3]];
42 if let Some(resource_type) = detect_by_first4(first4) {
43 (resource_type, "magic_4")
44 } else if let Some(ext) = extension_hint {
45 (ResourceType::from_extension(ext), "extension_hint")
46 } else {
47 (ResourceType::Invalid, "no_match")
48 }
49 } else if let Some(ext) = extension_hint {
50 (ResourceType::from_extension(ext), "extension_hint")
51 } else {
52 (ResourceType::Invalid, "no_match")
53 };
54
55 trace_debug!(
56 detected = ?detected,
57 reason = _reason,
58 "resource detection complete"
59 );
60 detected
61}
62
63fn detect_by_first4(first4: [u8; 4]) -> Option<ResourceType> {
65 let detected = match &first4 {
66 b"2DA " => ResourceType::TwoDa,
67 b"TLK " => ResourceType::Tlk,
68 b"ERF " => ResourceType::Erf,
69 b"MOD " => ResourceType::Mod,
70 b"SAV " => ResourceType::Sav,
71 b"RIM " => ResourceType::Rim,
72 b"BIF " => ResourceType::Bif,
73 b"BZF " => ResourceType::Bzf,
74 b"KEY " => ResourceType::Key,
75 b"LIP " => ResourceType::Lip,
76 b"SSF " => ResourceType::Ssf,
77 b"NCS " => ResourceType::Ncs,
78 b"DDS " => ResourceType::Dds,
79 b"RIFF" => ResourceType::Wav,
80 _ if ResourceType::GFF_BACKED
85 .iter()
86 .any(|kind| kind.gff_magic() == Some(first4)) =>
87 {
88 let detected = ResourceType::GFF_BACKED
89 .iter()
90 .copied()
91 .find(|kind| kind.gff_magic() == Some(first4));
92 trace_debug!(first4 = ?first4, detected = ?detected, "resource type detected");
93 return detected;
94 }
95 _ => {
96 trace_debug!(first4 = ?first4, "resource type not detected by first4 magic");
97 return None;
98 }
99 };
100 trace_debug!(detected = ?detected, "resource type detected by first4 magic");
101 Some(detected)
102}
103
104#[cfg(test)]
105mod tests {
106 use super::detect_resource_type;
107 use crate::ResourceType;
108
109 #[test]
110 fn detects_core_magic_headers() {
111 assert_eq!(detect_resource_type(b"2DA V2.b", None), ResourceType::TwoDa);
112 assert_eq!(detect_resource_type(b"TLK V3.0", None), ResourceType::Tlk);
113 assert_eq!(detect_resource_type(b"RIM V1.0", None), ResourceType::Rim);
114 assert_eq!(detect_resource_type(b"KEY V1 ", None), ResourceType::Key);
115 }
116
117 #[test]
118 fn detects_erf_related_types() {
119 assert_eq!(detect_resource_type(b"ERF V1.0", None), ResourceType::Erf);
120 assert_eq!(detect_resource_type(b"MOD V1.0", None), ResourceType::Mod);
121 assert_eq!(detect_resource_type(b"SAV V1.0", None), ResourceType::Sav);
122 }
123
124 #[test]
125 fn detects_gff_generics_by_magic() {
126 assert_eq!(detect_resource_type(b"UTC \0\0", None), ResourceType::Utc);
127 assert_eq!(detect_resource_type(b"UTI \0\0", None), ResourceType::Uti);
128 assert_eq!(detect_resource_type(b"ARE \0\0", None), ResourceType::Are);
129 assert_eq!(detect_resource_type(b"FAC \0\0", None), ResourceType::Fac);
130 }
131
132 #[test]
133 fn detects_wav_special_magic() {
134 assert_eq!(
135 detect_resource_type(&[0xFF, 0xF3, 0x60, 0xC4, 0, 0, 0, 0], None),
136 ResourceType::Wav
137 );
138 assert_eq!(detect_resource_type(b"RIFFxxxx", None), ResourceType::Wav);
139 }
140
141 #[test]
142 fn detects_ltr_and_bwm_via_strong_8_byte_signatures() {
143 assert_eq!(detect_resource_type(b"LTR V1.0", None), ResourceType::Ltr);
144 assert_eq!(detect_resource_type(b"BWM V1.0", None), ResourceType::Bwm);
145 }
146
147 #[test]
148 fn mdl_mdx_detected_by_extension_only() {
149 let mdl_bytes = [0u8; 16];
153 assert_eq!(
154 detect_resource_type(&mdl_bytes, Some("mdl")),
155 ResourceType::Mdl
156 );
157 assert_eq!(
158 detect_resource_type(&mdl_bytes, Some("mdx")),
159 ResourceType::Mdx
160 );
161 assert_eq!(
163 detect_resource_type(&mdl_bytes, None),
164 ResourceType::Invalid
165 );
166 }
167
168 #[test]
169 fn uses_extension_fallback_for_text_formats() {
170 assert_eq!(
171 detect_resource_type(b"random", Some("txi")),
172 ResourceType::Txi
173 );
174 assert_eq!(
175 detect_resource_type(b"random", Some(".vis")),
176 ResourceType::Vis
177 );
178 assert_eq!(
179 detect_resource_type(b"random", Some("lyt")),
180 ResourceType::Lyt
181 );
182 }
183}