Skip to main content

rakata_extract/
keyfile.rs

1//! KEY/BIF extraction primitives.
2//!
3//! This module binds one parsed KEY index to a filesystem base path and
4//! provides convenience lookup helpers that load BIF payload bytes.
5
6use std::collections::HashMap;
7use std::fs;
8use std::io::{Read, Seek, SeekFrom};
9use std::path::{Path, PathBuf};
10
11use thiserror::Error;
12
13use rakata_core::{ResRef, ResourceId, ResourceTypeCode};
14use rakata_formats::{
15    read_bif_from_bytes, read_key_from_bytes, Bif, BifBinaryError, Key, KeyBinaryError,
16    KeyResourceEntry,
17};
18
19use crate::util::cmp_ascii_case_insensitive;
20
21/// Read-only KEY/BIF lookup wrapper.
22#[derive(Debug, Clone)]
23pub struct KeyFile {
24    key_path: PathBuf,
25    bif_base_path: PathBuf,
26    bif_path_index: BifPathIndex,
27    key: Key,
28    /// O(1) lookup index: `(lowercase_resref, type) -> Vec index`.
29    ///
30    /// Keeps the first entry for each key, matching the engine's first-match
31    /// semantics for duplicate KEY entries.
32    resource_index: HashMap<(ResRef, ResourceTypeCode), usize>,
33}
34
35impl KeyFile {
36    /// Reads a KEY file and binds BIF lookups to the KEY parent directory.
37    #[cfg_attr(
38        feature = "tracing",
39        tracing::instrument(level = "debug", skip(key_path))
40    )]
41    pub fn read_from_file(key_path: impl AsRef<Path>) -> Result<Self, KeyFileError> {
42        let key_path = key_path.as_ref();
43        let bif_base_path = key_path
44            .parent()
45            .map_or_else(|| PathBuf::from("."), Path::to_path_buf);
46        Self::read_from_file_with_base(key_path, bif_base_path)
47    }
48
49    /// Reads a KEY file and binds BIF lookups to an explicit base directory.
50    #[cfg_attr(
51        feature = "tracing",
52        tracing::instrument(level = "debug", skip(key_path, bif_base_path))
53    )]
54    pub fn read_from_file_with_base(
55        key_path: impl AsRef<Path>,
56        bif_base_path: impl AsRef<Path>,
57    ) -> Result<Self, KeyFileError> {
58        let key_path = key_path.as_ref();
59        let bif_base_path = bif_base_path.as_ref();
60        let bytes = fs::read(key_path).map_err(|source| KeyFileError::Io {
61            path: key_path.to_path_buf(),
62            source,
63        })?;
64        let key = read_key_from_bytes(&bytes)?;
65        let resource_index = build_key_resource_index(&key);
66        Ok(Self {
67            key_path: key_path.to_path_buf(),
68            bif_base_path: bif_base_path.to_path_buf(),
69            bif_path_index: BifPathIndex::build(bif_base_path),
70            key,
71            resource_index,
72        })
73    }
74
75    /// Returns the KEY path.
76    pub fn key_path(&self) -> &Path {
77        &self.key_path
78    }
79
80    /// Returns the BIF base path used for KEY filename resolution.
81    pub fn bif_base_path(&self) -> &Path {
82        &self.bif_base_path
83    }
84
85    /// Returns the parsed KEY value.
86    pub fn key(&self) -> &Key {
87        &self.key
88    }
89
90    /// Returns the first matching KEY entry for `(resref, type)`.
91    #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self), fields(resref = %resref, resource_type = resource_type.raw_id())))]
92    pub fn resource_entry(
93        &self,
94        resref: &ResRef,
95        resource_type: ResourceTypeCode,
96    ) -> Option<&KeyResourceEntry> {
97        let &index = self.resource_index.get(&(*resref, resource_type))?;
98        self.key.resources.get(index)
99    }
100
101    /// Resolves one BIF path by KEY BIF index.
102    ///
103    /// Resolution order:
104    /// 1. direct `<base>/<key-relative-path>`
105    /// 2. `<base>/data/<key-relative-path>`
106    /// 3. case-insensitive indexed relative path match
107    /// 4. case-insensitive indexed basename match
108    #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self)))]
109    pub fn bif_path_by_index(&self, bif_index: u32) -> Option<PathBuf> {
110        let index = usize::try_from(bif_index).ok()?;
111        let filename = self.key.bif_entries.get(index)?.filename.as_str();
112        self.resolve_bif_path(filename)
113    }
114
115    fn resolve_bif_path(&self, key_filename: &str) -> Option<PathBuf> {
116        let normalized = normalize_key_bif_name(key_filename);
117        if normalized.is_empty() {
118            return None;
119        }
120
121        let relative = parse_key_relative_path(&normalized);
122        let direct = self.bif_base_path.join(&relative);
123        if direct.exists() {
124            return Some(direct);
125        }
126
127        let direct_data = self.bif_base_path.join("data").join(&relative);
128        if direct_data.exists() {
129            return Some(direct_data);
130        }
131
132        let relative_key = normalized.to_ascii_lowercase();
133        if let Some(path) = self.bif_path_index.by_relposix.get(&relative_key) {
134            if path.exists() {
135                return Some(path.clone());
136            }
137        }
138
139        let basename = Path::new(&normalized)
140            .file_name()
141            .and_then(|name| name.to_str())
142            .map(str::to_ascii_lowercase)?;
143        self.bif_path_index
144            .by_basename
145            .get(&basename)
146            .filter(|path| path.exists())
147            .cloned()
148    }
149
150    /// Reads one BIF archive by KEY BIF index.
151    #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self)))]
152    pub fn read_bif_by_index(&self, bif_index: u32) -> Result<Bif, KeyFileError> {
153        let path = self
154            .bif_path_by_index(bif_index)
155            .ok_or(KeyFileError::MissingBifIndex { bif_index })?;
156        let bytes = fs::read(&path).map_err(|source| KeyFileError::Io {
157            path: path.clone(),
158            source,
159        })?;
160        read_bif_from_bytes(&bytes).map_err(KeyFileError::from)
161    }
162
163    /// Resolves one payload by `(resref, type)`.
164    ///
165    /// Returns `Ok(None)` when the KEY has no matching entry.
166    ///
167    /// This uses seek-based I/O to read only the requested resource from the
168    /// BIF file, avoiding loading the entire (often hundreds of MB) BIF into
169    /// memory.
170    #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self), fields(resref = %resref, resource_type = resource_type.raw_id())))]
171    pub fn resource(
172        &self,
173        resref: &ResRef,
174        resource_type: ResourceTypeCode,
175    ) -> Result<Option<Vec<u8>>, KeyFileError> {
176        let Some(entry) = self.resource_entry(resref, resource_type) else {
177            return Ok(None);
178        };
179
180        let bif_index = entry.resource_id.bif_index();
181        let data = self.read_resource_by_seek(bif_index, entry.resource_id)?;
182        Ok(Some(data))
183    }
184
185    /// Reads a single resource from a BIF file using seek-based I/O.
186    ///
187    /// Instead of loading the entire BIF into memory, this reads only the BIF
188    /// variable table header (a few KB) and then seeks directly to the
189    /// requested resource's data. This is dramatically more memory-efficient
190    /// for large BIF files.
191    #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self), fields(resource_id = resource_id.raw())))]
192    pub fn read_resource_by_seek(
193        &self,
194        bif_index: u32,
195        resource_id: ResourceId,
196    ) -> Result<Vec<u8>, KeyFileError> {
197        let path = self
198            .bif_path_by_index(bif_index)
199            .ok_or(KeyFileError::MissingBifIndex { bif_index })?;
200
201        let mut file = fs::File::open(&path).map_err(|source| KeyFileError::Io {
202            path: path.clone(),
203            source,
204        })?;
205
206        // Read BIF header: signature (8) + var_count (4) + fixed_count (4) + var_table_offset (4)
207        let mut header = [0u8; 20];
208        file.read_exact(&mut header)
209            .map_err(|source| KeyFileError::Io {
210                path: path.clone(),
211                source,
212            })?;
213
214        if &header[0..4] != b"BIFF" {
215            let mut signature = [0u8; 4];
216            signature.copy_from_slice(&header[0..4]);
217            return Err(KeyFileError::BifSignatureMismatch {
218                path: path.clone(),
219                signature,
220            });
221        }
222
223        let var_count =
224            u32::from_le_bytes(header[8..12].try_into().expect("4-byte slice is [u8; 4]"));
225        let var_table_offset =
226            u32::from_le_bytes(header[16..20].try_into().expect("4-byte slice is [u8; 4]"));
227
228        // Seek to variable table and scan for matching resource_id
229        file.seek(SeekFrom::Start(u64::from(var_table_offset)))
230            .map_err(|source| KeyFileError::Io {
231                path: path.clone(),
232                source,
233            })?;
234
235        let target_id = resource_id.raw();
236        let mut entry_buf = [0u8; 16];
237        let mut found: Option<(u32, u32)> = None;
238
239        for _ in 0..var_count {
240            file.read_exact(&mut entry_buf)
241                .map_err(|source| KeyFileError::Io {
242                    path: path.clone(),
243                    source,
244                })?;
245
246            let entry_id = u32::from_le_bytes(
247                entry_buf[0..4]
248                    .try_into()
249                    .expect("4-byte slice of 12-byte buffer"),
250            );
251            if entry_id == target_id {
252                let data_offset = u32::from_le_bytes(
253                    entry_buf[4..8]
254                        .try_into()
255                        .expect("4-byte slice of 12-byte buffer"),
256                );
257                let data_size = u32::from_le_bytes(
258                    entry_buf[8..12]
259                        .try_into()
260                        .expect("4-byte slice of 12-byte buffer"),
261                );
262                found = Some((data_offset, data_size));
263                break;
264            }
265        }
266
267        let (data_offset, data_size) = found.ok_or(KeyFileError::MissingBifResource {
268            bif_index,
269            resource_id,
270        })?;
271
272        // Seek to data and read
273        file.seek(SeekFrom::Start(u64::from(data_offset)))
274            .map_err(|source| KeyFileError::Io {
275                path: path.clone(),
276                source,
277            })?;
278
279        let data_len =
280            usize::try_from(data_size).map_err(|_| KeyFileError::MissingBifResource {
281                bif_index,
282                resource_id,
283            })?;
284        let mut data = vec![0u8; data_len];
285        file.read_exact(&mut data)
286            .map_err(|source| KeyFileError::Io {
287                path: path.clone(),
288                source,
289            })?;
290
291        Ok(data)
292    }
293}
294
295fn parse_key_relative_path(filename: &str) -> PathBuf {
296    let mut path = PathBuf::new();
297    for component in filename.split(['\\', '/']) {
298        if !component.is_empty() {
299            path.push(component);
300        }
301    }
302    path
303}
304
305fn normalize_key_bif_name(filename: &str) -> String {
306    filename
307        .replace('\\', "/")
308        .trim_start_matches('/')
309        .trim_start_matches("./")
310        .to_string()
311}
312
313/// Builds a `(ResRef, ResourceTypeCode) -> index` HashMap from KEY resources.
314///
315/// First-match wins when duplicate keys exist, matching the engine's
316/// linear-scan semantics.
317fn build_key_resource_index(key: &Key) -> HashMap<(ResRef, ResourceTypeCode), usize> {
318    let mut index = HashMap::with_capacity(key.resources.len());
319    for (i, entry) in key.resources.iter().enumerate() {
320        index
321            .entry((entry.resref, entry.resource_type))
322            .or_insert(i);
323    }
324    index
325}
326
327#[derive(Debug, Clone, Default, PartialEq, Eq)]
328struct BifPathIndex {
329    by_basename: HashMap<String, PathBuf>,
330    by_relposix: HashMap<String, PathBuf>,
331}
332
333impl BifPathIndex {
334    fn build(base_path: &Path) -> Self {
335        let mut files = Vec::new();
336        collect_bif_files_recursive(base_path, &mut files);
337
338        files.sort_by(|a, b| {
339            let a_rel = a.strip_prefix(base_path).unwrap_or(a).to_string_lossy();
340            let b_rel = b.strip_prefix(base_path).unwrap_or(b).to_string_lossy();
341            cmp_ascii_case_insensitive(&a_rel, &b_rel).then(a.cmp(b))
342        });
343
344        let mut index = Self::default();
345        for path in files {
346            let Ok(relative) = path.strip_prefix(base_path) else {
347                continue;
348            };
349            let rel_key = relative
350                .to_string_lossy()
351                .replace('\\', "/")
352                .to_ascii_lowercase();
353            index
354                .by_relposix
355                .entry(rel_key)
356                .or_insert_with(|| path.clone());
357
358            let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
359                continue;
360            };
361            index
362                .by_basename
363                .entry(file_name.to_ascii_lowercase())
364                .or_insert(path);
365        }
366        index
367    }
368}
369
370fn collect_bif_files_recursive(directory: &Path, output: &mut Vec<PathBuf>) {
371    let Ok(entries) = fs::read_dir(directory) else {
372        return;
373    };
374    for entry in entries.filter_map(Result::ok) {
375        let path = entry.path();
376        if path.is_dir() {
377            collect_bif_files_recursive(&path, output);
378            continue;
379        }
380        if !path.is_file() {
381            continue;
382        }
383        let is_bif = path
384            .extension()
385            .and_then(|ext| ext.to_str())
386            .is_some_and(|ext| ext.eq_ignore_ascii_case("bif"));
387        if is_bif {
388            output.push(path);
389        }
390    }
391}
392
393/// Errors produced by KEY/BIF primitives.
394#[derive(Debug, Error)]
395pub enum KeyFileError {
396    /// I/O failure while reading KEY/BIF files.
397    #[error("I/O failure for `{path}`: {source}")]
398    Io {
399        /// Path being read.
400        path: PathBuf,
401        /// Underlying OS error.
402        #[source]
403        source: std::io::Error,
404    },
405    /// KEY parse failure.
406    #[error(transparent)]
407    Key(#[from] KeyBinaryError),
408    /// BIF parse failure.
409    #[error(transparent)]
410    Bif(#[from] BifBinaryError),
411    /// KEY entry references a BIF index not present in the BIF table.
412    #[error("KEY entry references missing BIF index {bif_index}")]
413    MissingBifIndex {
414        /// BIF index.
415        bif_index: u32,
416    },
417    /// KEY entry exists but referenced BIF does not contain the expected resource.
418    #[error("BIF index {bif_index} missing KEY resource id {resource_id:#x}")]
419    MissingBifResource {
420        /// BIF index from KEY `resource_id`.
421        bif_index: u32,
422        /// Typed KEY resource ID.
423        resource_id: ResourceId,
424    },
425    /// BIF file has an invalid signature (expected `BIFF`).
426    #[error("BIF signature mismatch in `{path}`: expected BIFF, got {signature:?}")]
427    BifSignatureMismatch {
428        /// Path to the BIF file.
429        path: PathBuf,
430        /// Actual first four bytes.
431        signature: [u8; 4],
432    },
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438
439    use rakata_core::{ResourceId, ResourceType};
440    use rakata_formats::{write_bif_to_vec, write_key_to_vec};
441    use tempfile::TempDir;
442
443    #[test]
444    fn resolves_resource_through_key_and_bif() {
445        let resource_id = ResourceId::from_parts(0, 0).expect("valid resource id");
446        let resource_type = ResourceTypeCode::from(ResourceType::Utc);
447
448        let mut bif = Bif::new();
449        bif.push_resource(resource_id, resource_type, b"npc".to_vec());
450        let bif_bytes = write_bif_to_vec(&bif).expect("write bif");
451
452        let mut key = Key::new();
453        key.push_bif_entry(
454            "data\\templates.bif",
455            u32::try_from(bif_bytes.len()).expect("test BIF size fits in u32"),
456            0,
457        );
458        key.push_resource(
459            ResRef::new("p_bastila").expect("valid resref"),
460            resource_type,
461            resource_id,
462        );
463        let key_bytes = write_key_to_vec(&key).expect("write key");
464
465        let temp = TempDir::new().expect("create tempdir");
466        let root = temp.path();
467        let data_dir = root.join("data");
468        fs::create_dir_all(&data_dir).expect("create data dir");
469        let bif_path = data_dir.join("templates.bif");
470        let key_path = root.join("chitin.key");
471        fs::write(&bif_path, bif_bytes).expect("write bif");
472        fs::write(&key_path, key_bytes).expect("write key");
473
474        let key_file = KeyFile::read_from_file_with_base(&key_path, root).expect("load keyfile");
475        let resref = ResRef::new("p_bastila").expect("valid resref");
476        let data = key_file
477            .resource(&resref, resource_type)
478            .expect("resolve resource")
479            .expect("resource exists");
480        assert_eq!(data, b"npc");
481        assert_eq!(key_file.bif_path_by_index(0), Some(bif_path));
482    }
483
484    #[test]
485    fn missing_resource_returns_none() {
486        let resource_type = ResourceTypeCode::from(ResourceType::Utc);
487        let mut key = Key::new();
488        key.push_bif_entry("data\\templates.bif", 0, 0);
489        let key_bytes = write_key_to_vec(&key).expect("write key");
490
491        let temp = TempDir::new().expect("create tempdir");
492        let root = temp.path();
493        let key_path = root.join("chitin.key");
494        fs::write(&key_path, key_bytes).expect("write key");
495
496        let key_file = KeyFile::read_from_file_with_base(&key_path, root).expect("load keyfile");
497        let resref = ResRef::new("missing").expect("valid resref");
498        let resolved = key_file
499            .resource(&resref, resource_type)
500            .expect("lookup should succeed");
501        assert_eq!(resolved, None);
502    }
503
504    #[test]
505    fn resolves_bif_from_data_directory_when_key_stores_basename() {
506        let resource_id = ResourceId::from_parts(0, 0).expect("valid resource id");
507        let resource_type = ResourceTypeCode::from(ResourceType::Utc);
508
509        let mut bif = Bif::new();
510        bif.push_resource(resource_id, resource_type, b"npc".to_vec());
511        let bif_bytes = write_bif_to_vec(&bif).expect("write bif");
512
513        let mut key = Key::new();
514        key.push_bif_entry(
515            "templates.bif",
516            u32::try_from(bif_bytes.len()).expect("test BIF size fits in u32"),
517            0,
518        );
519        key.push_resource(
520            ResRef::new("p_bastila").expect("valid resref"),
521            resource_type,
522            resource_id,
523        );
524        let key_bytes = write_key_to_vec(&key).expect("write key");
525
526        let temp = TempDir::new().expect("create tempdir");
527        let root = temp.path();
528        let data_dir = root.join("data");
529        fs::create_dir_all(&data_dir).expect("create data dir");
530        let bif_path = data_dir.join("templates.bif");
531        let key_path = root.join("chitin.key");
532        fs::write(&bif_path, bif_bytes).expect("write bif");
533        fs::write(&key_path, key_bytes).expect("write key");
534
535        let key_file = KeyFile::read_from_file_with_base(&key_path, root).expect("load keyfile");
536        assert_eq!(key_file.bif_path_by_index(0), Some(bif_path));
537    }
538
539    #[test]
540    fn resolves_bif_case_insensitively_from_path_index() {
541        let resource_id = ResourceId::from_parts(0, 0).expect("valid resource id");
542        let resource_type = ResourceTypeCode::from(ResourceType::Utc);
543
544        let mut bif = Bif::new();
545        bif.push_resource(resource_id, resource_type, b"npc".to_vec());
546        let bif_bytes = write_bif_to_vec(&bif).expect("write bif");
547
548        let mut key = Key::new();
549        key.push_bif_entry(
550            "data\\templates.bif",
551            u32::try_from(bif_bytes.len()).expect("test BIF size fits in u32"),
552            0,
553        );
554        key.push_resource(
555            ResRef::new("p_bastila").expect("valid resref"),
556            resource_type,
557            resource_id,
558        );
559        let key_bytes = write_key_to_vec(&key).expect("write key");
560
561        let temp = TempDir::new().expect("create tempdir");
562        let root = temp.path();
563        let data_dir = root.join("Data");
564        fs::create_dir_all(&data_dir).expect("create data dir");
565        let bif_path = data_dir.join("Templates.BIF");
566        let key_path = root.join("chitin.key");
567        fs::write(&bif_path, bif_bytes).expect("write bif");
568        fs::write(&key_path, key_bytes).expect("write key");
569
570        let key_file = KeyFile::read_from_file_with_base(&key_path, root).expect("load keyfile");
571        assert_eq!(key_file.bif_path_by_index(0), Some(bif_path));
572    }
573}