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 b"GFF " => ResourceType::Gff,
81 b"ARE " => ResourceType::Are,
82 b"DLG " => ResourceType::Dlg,
83 b"GIT " => ResourceType::Git,
84 b"IFO " => ResourceType::Ifo,
85 b"JRL " => ResourceType::Jrl,
86 b"PTH " => ResourceType::Pth,
87 b"UTC " => ResourceType::Utc,
88 b"UTD " => ResourceType::Utd,
89 b"UTE " => ResourceType::Ute,
90 b"UTI " => ResourceType::Uti,
91 b"UTM " => ResourceType::Utm,
92 b"UTP " => ResourceType::Utp,
93 b"UTS " => ResourceType::Uts,
94 b"UTT " => ResourceType::Utt,
95 b"UTW " => ResourceType::Utw,
96 _ => {
97 trace_debug!(first4 = ?first4, "resource type not detected by first4 magic");
98 return None;
99 }
100 };
101 trace_debug!(detected = ?detected, "resource type detected by first4 magic");
102 Some(detected)
103}
104
105#[cfg(test)]
106mod tests {
107 use super::detect_resource_type;
108 use crate::ResourceType;
109
110 #[test]
111 fn detects_core_magic_headers() {
112 assert_eq!(detect_resource_type(b"2DA V2.b", None), ResourceType::TwoDa);
113 assert_eq!(detect_resource_type(b"TLK V3.0", None), ResourceType::Tlk);
114 assert_eq!(detect_resource_type(b"RIM V1.0", None), ResourceType::Rim);
115 assert_eq!(detect_resource_type(b"KEY V1 ", None), ResourceType::Key);
116 }
117
118 #[test]
119 fn detects_erf_related_types() {
120 assert_eq!(detect_resource_type(b"ERF V1.0", None), ResourceType::Erf);
121 assert_eq!(detect_resource_type(b"MOD V1.0", None), ResourceType::Mod);
122 assert_eq!(detect_resource_type(b"SAV V1.0", None), ResourceType::Sav);
123 }
124
125 #[test]
126 fn detects_gff_generics_by_magic() {
127 assert_eq!(detect_resource_type(b"UTC \0\0", None), ResourceType::Utc);
128 assert_eq!(detect_resource_type(b"UTI \0\0", None), ResourceType::Uti);
129 assert_eq!(detect_resource_type(b"ARE \0\0", None), ResourceType::Are);
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}