Skip to main content

rakata_extract/
chitin.rs

1//! `chitin.key` extraction primitives.
2//!
3//! This module provides a small convenience wrapper that binds the canonical
4//! game-root `chitin.key` location to [`crate::keyfile::KeyFile`].
5
6use std::path::{Path, PathBuf};
7
8use thiserror::Error;
9
10use rakata_core::fs::find_case_insensitive_file;
11use rakata_core::ResRef;
12use rakata_core::ResourceTypeCode;
13use rakata_formats::Key;
14
15use crate::keyfile::{KeyFile, KeyFileError};
16
17/// Canonical `chitin.key` wrapper for one game root.
18#[derive(Debug, Clone)]
19pub struct Chitin {
20    game_root: PathBuf,
21    key_file: KeyFile,
22}
23
24impl Chitin {
25    /// Loads `chitin.key` from `<game_root>/chitin.key`.
26    pub fn read_from_root(game_root: impl AsRef<Path>) -> Result<Self, ChitinError> {
27        let game_root = game_root.as_ref().to_path_buf();
28        let key_path = find_case_insensitive_file(&game_root, "chitin.key")
29            .ok()
30            .flatten()
31            .unwrap_or_else(|| game_root.join("chitin.key"));
32        let key_file = KeyFile::read_from_file_with_base(&key_path, &game_root)?;
33        Ok(Self {
34            game_root,
35            key_file,
36        })
37    }
38
39    /// Returns the game root used for path binding.
40    pub fn game_root(&self) -> &Path {
41        &self.game_root
42    }
43
44    /// Returns the underlying KEY/BIF wrapper.
45    pub fn key_file(&self) -> &KeyFile {
46        &self.key_file
47    }
48
49    /// Returns the parsed KEY value.
50    pub fn key(&self) -> &Key {
51        self.key_file.key()
52    }
53
54    /// Resolves one payload through KEY/BIF lookup.
55    pub fn resource(
56        &self,
57        resref: &ResRef,
58        resource_type: ResourceTypeCode,
59    ) -> Result<Option<Vec<u8>>, ChitinError> {
60        self.key_file
61            .resource(resref, resource_type)
62            .map_err(ChitinError::from)
63    }
64}
65
66/// Errors produced by [`Chitin`] operations.
67#[derive(Debug, Error)]
68pub enum ChitinError {
69    /// KEY/BIF loading/lookup failure.
70    #[error(transparent)]
71    KeyFile(#[from] KeyFileError),
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    use std::fs;
79
80    use rakata_core::{ResourceId, ResourceType};
81    use rakata_formats::{write_bif_to_vec, write_key_to_vec, Bif};
82    use tempfile::TempDir;
83
84    #[test]
85    fn loads_chitin_key_and_resolves_resource() {
86        let resource_id = ResourceId::from_parts(0, 0).expect("valid resource id");
87        let resource_type = ResourceTypeCode::from(ResourceType::Utc);
88
89        let mut bif = Bif::new();
90        bif.push_resource(resource_id, resource_type, b"utc".to_vec());
91        let bif_bytes = write_bif_to_vec(&bif).expect("write bif");
92
93        let mut key = Key::new();
94        key.push_bif_entry(
95            "data\\templates.bif",
96            u32::try_from(bif_bytes.len()).expect("test BIF size fits in u32"),
97            0,
98        );
99        key.push_resource(
100            ResRef::new("p_bastila").expect("valid resref"),
101            resource_type,
102            resource_id,
103        );
104        let key_bytes = write_key_to_vec(&key).expect("write key");
105
106        let temp = TempDir::new().expect("create tempdir");
107        let root = temp.path();
108        let data_dir = root.join("data");
109        fs::create_dir_all(&data_dir).expect("create data dir");
110        fs::write(data_dir.join("templates.bif"), bif_bytes).expect("write bif");
111        fs::write(root.join("chitin.key"), key_bytes).expect("write key");
112
113        let chitin = Chitin::read_from_root(root).expect("load chitin");
114        let resref = ResRef::new("p_bastila").expect("valid resref");
115        let data = chitin
116            .resource(&resref, resource_type)
117            .expect("resolve")
118            .expect("resource exists");
119        assert_eq!(data, b"utc");
120    }
121
122    #[test]
123    fn loads_case_insensitive_chitin_key_name() {
124        let resource_id = ResourceId::from_parts(0, 0).expect("valid resource id");
125        let resource_type = ResourceTypeCode::from(ResourceType::Utc);
126
127        let mut bif = Bif::new();
128        bif.push_resource(resource_id, resource_type, b"utc".to_vec());
129        let bif_bytes = write_bif_to_vec(&bif).expect("write bif");
130
131        let mut key = Key::new();
132        key.push_bif_entry(
133            "data\\templates.bif",
134            u32::try_from(bif_bytes.len()).expect("test BIF size fits in u32"),
135            0,
136        );
137        key.push_resource(
138            ResRef::new("p_bastila").expect("valid resref"),
139            resource_type,
140            resource_id,
141        );
142        let key_bytes = write_key_to_vec(&key).expect("write key");
143
144        let temp = TempDir::new().expect("create tempdir");
145        let root = temp.path();
146        let data_dir = root.join("data");
147        fs::create_dir_all(&data_dir).expect("create data dir");
148        fs::write(data_dir.join("templates.bif"), bif_bytes).expect("write bif");
149        fs::write(root.join("ChItIn.KeY"), key_bytes).expect("write key");
150
151        let chitin = Chitin::read_from_root(root).expect("load chitin");
152        let resref = ResRef::new("p_bastila").expect("valid resref");
153        let data = chitin
154            .resource(&resref, resource_type)
155            .expect("resolve")
156            .expect("resource exists");
157        assert_eq!(data, b"utc");
158    }
159}