rakata_core/
detect.rs

1use crate::ResourceType;
2
3/// KotOR SFX-obfuscated WAV magic marker.
4const 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/// Detects a resource type from binary signature and optional extension hint.
19///
20/// Detection priority:
21/// 1. Strong 8-byte signatures (`LTR V1.0`, `BWM V1.0`)
22/// 2. KotOR WAV obfuscation marker
23/// 3. 4-byte magic values
24/// 4. Extension hint fallback
25#[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
63/// Maps a four-byte magic header to a known resource type.
64fn 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        // MDL has no magic header (starts with 4 zero bytes, too ambiguous).
150        // MDX is raw vertex data with no header at all.
151        // Both rely on extension fallback from archive metadata or filenames.
152        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        // Without extension hint, these are undetectable.
162        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}