Skip to main content

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        // The GFF magics are not listed here. Each is its type's extension
81        // uppercased, so `ResourceType::GFF_BACKED` is the one declaration and
82        // this consults it rather than repeating seventeen arms that would
83        // then have somewhere to drift from.
84        _ 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        // 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}