rakata_formats/key/
mod.rs

1//! KEY binary reader and writer.
2//!
3//! KEY files are global resource indexes that map `(resref, type)` pairs to a
4//! packed `resource_id` that identifies both:
5//! - which BIF file contains the resource, and
6//! - which entry inside that BIF file to load.
7//!
8//! ## Format Layout
9//! ```text
10//! +------------------------------+ 0x0000
11//! | Header (64 bytes)            |
12//! +------------------------------+ file_table_offset
13//! | BIF file table               |
14//! | 12 bytes * bif_count         |
15//! +------------------------------+ filename table (from file entries)
16//! | BIF filenames                |
17//! | variable-size strings        |
18//! +------------------------------+ key_table_offset
19//! | Resource key table           |
20//! | 22 bytes * key_count         |
21//! +------------------------------+
22//! ```
23//!
24//! ## BIF File Entry (12 bytes)
25//! ```text
26//! 0x00..0x04  file_size       (u32)
27//! 0x04..0x08  filename_offset (u32)
28//! 0x08..0x0A  filename_size   (u16)
29//! 0x0A..0x0C  drives          (u16)
30//! ```
31//!
32//! ## Key Entry (22 bytes)
33//! ```text
34//! 0x00..0x10  resref[16]      (null padded)
35//! 0x10..0x12  type_id         (u16)
36//! 0x12..0x16  resource_id     (u32)
37//! ```
38//!
39//! ## Header (64 bytes)
40//! ```text
41//! 0x00..0x04  magic             ("KEY ")
42//! 0x04..0x08  version           ("V1  " or "V1.1")
43//! 0x08..0x0C  bif_count         (u32)
44//! 0x0C..0x10  key_count         (u32)
45//! 0x10..0x14  file_table_offset (u32)
46//! 0x14..0x18  key_table_offset  (u32)
47//! 0x18..0x1C  build_year        (u32, years since 1900)
48//! 0x1C..0x20  build_day         (u32, day of year)
49//! 0x20..0x40  reserved          (32 bytes, preserved verbatim)
50//! ```
51//!
52//! `resource_id` bit layout:
53//! - bits 31..20: BIF index
54//! - bits 19..0: resource index within that BIF
55
56mod reader;
57mod writer;
58
59pub use reader::{
60    read_key, read_key_from_bytes, read_key_from_bytes_with_options, read_key_with_options,
61};
62pub use writer::{write_key, write_key_to_vec};
63
64use thiserror::Error;
65
66use rakata_core::{DecodeTextError, EncodeTextError, ResRef, ResRefError, TextEncoding};
67use rakata_core::{ResourceId, ResourceIdError, ResourceTypeCode};
68
69use crate::binary::{self, DecodeBinary, EncodeBinary};
70
71/// KEY header size in bytes.
72const FILE_HEADER_SIZE: usize = 64;
73/// BIF file-table entry size.
74const FILE_ENTRY_SIZE: usize = 12;
75/// Resource key-table entry size.
76const KEY_ENTRY_SIZE: usize = 22;
77/// KEY file signature.
78const KEY_MAGIC: [u8; 4] = *b"KEY ";
79/// KotOR KEY version.
80const KEY_VERSION_V10: [u8; 4] = *b"V1  ";
81/// Alternate KEY version accepted by some tooling.
82const KEY_VERSION_V11: [u8; 4] = *b"V1.1";
83/// KEY filename/resref text encoding.
84const KEY_TEXT_ENCODING: TextEncoding = TextEncoding::Windows1252;
85
86/// One BIF file-table entry in a KEY index.
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct KeyBifEntry {
89    /// Relative path to the BIF file.
90    pub filename: String,
91    /// BIF file size in bytes.
92    pub file_size: u32,
93    /// Legacy drive-location bit flags.
94    pub drives: u16,
95}
96
97/// One resource entry in the KEY key table.
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct KeyResourceEntry {
100    /// Resource name (up to 16 bytes on disk).
101    pub resref: ResRef,
102    /// Resource type code used by archive tables.
103    ///
104    /// Unknown IDs are preserved losslessly.
105    pub resource_type: ResourceTypeCode,
106    /// Packed resource identifier.
107    ///
108    /// High 12 bits encode BIF index and low 20 bits encode resource index.
109    pub resource_id: ResourceId,
110}
111
112/// KEY reader option set.
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
114pub struct KeyReadOptions {
115    /// Input policy for accepted source variants.
116    pub input: KeyReadMode,
117}
118
119impl Default for KeyReadOptions {
120    fn default() -> Self {
121        Self {
122            input: KeyReadMode::CanonicalK1,
123        }
124    }
125}
126
127/// KEY reader input policy.
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
129pub enum KeyReadMode {
130    /// Accept only canonical vanilla K1 KEY variants (`V1  `).
131    CanonicalK1,
132    /// Accept broader Aurora-family KEY variants (`V1  ` and `V1.1`).
133    CompatibilityAurora,
134}
135
136impl KeyResourceEntry {
137    /// Returns BIF index encoded in [`Self::resource_id`].
138    pub const fn bif_index(&self) -> u32 {
139        self.resource_id.bif_index()
140    }
141
142    /// Returns resource index encoded in [`Self::resource_id`].
143    pub const fn resource_index(&self) -> u32 {
144        self.resource_id.resource_index()
145    }
146
147    /// Constructs an entry from explicit BIF/resource indexes.
148    pub fn from_indices(
149        resref: ResRef,
150        resource_type: ResourceTypeCode,
151        bif_index: u32,
152        resource_index: u32,
153    ) -> Result<Self, KeyBinaryError> {
154        let resource_id = ResourceId::from_parts(bif_index, resource_index).map_err(
155            |ResourceIdError::InvalidParts {
156                 bif_index,
157                 resource_index,
158             }| KeyBinaryError::InvalidResourceIdParts {
159                bif_index,
160                resource_index,
161            },
162        )?;
163        Ok(Self {
164            resref,
165            resource_type,
166            resource_id,
167        })
168    }
169}
170
171/// In-memory KEY container.
172#[derive(Debug, Clone, PartialEq, Eq, Default)]
173pub struct Key {
174    /// Build year (`years since 1900`).
175    pub build_year: u32,
176    /// Build day of year (`1..=366`).
177    pub build_day: u32,
178    /// Ordered BIF file entries.
179    pub bif_entries: Vec<KeyBifEntry>,
180    /// Ordered resource key entries.
181    pub resources: Vec<KeyResourceEntry>,
182    /// Header bytes 0x20..0x40 (32-byte reserved block).
183    ///
184    /// Not accessed by the K1 engine loader (`CExoKeyTable::AddKeyTableContents`
185    /// confirmed by Ghidra -- see `docs/notes/archive_formats.md`).
186    /// New files should write zeros; roundtrips preserve whatever was read.
187    pub reserved: [u8; 32],
188}
189
190impl Key {
191    /// Creates an empty KEY index.
192    pub fn new() -> Self {
193        Self::default()
194    }
195
196    /// Appends one BIF file-table entry.
197    pub fn push_bif_entry(&mut self, filename: impl Into<String>, file_size: u32, drives: u16) {
198        self.bif_entries.push(KeyBifEntry {
199            filename: filename.into(),
200            file_size,
201            drives,
202        });
203    }
204
205    /// Appends one resource entry.
206    pub fn push_resource(
207        &mut self,
208        resref: ResRef,
209        resource_type: ResourceTypeCode,
210        resource_id: ResourceId,
211    ) {
212        self.resources.push(KeyResourceEntry {
213            resref,
214            resource_type,
215            resource_id,
216        });
217    }
218
219    /// Returns the first matching resource entry.
220    pub fn resource(
221        &self,
222        resref: &ResRef,
223        resource_type: ResourceTypeCode,
224    ) -> Option<&KeyResourceEntry> {
225        self.resources
226            .iter()
227            .find(|entry| entry.resref == *resref && entry.resource_type == resource_type)
228    }
229
230    /// Returns the first matching resource entry by packed resource ID.
231    pub fn resource_by_id(&self, resource_id: ResourceId) -> Option<&KeyResourceEntry> {
232        self.resources
233            .iter()
234            .find(|entry| entry.resource_id == resource_id)
235    }
236}
237
238impl DecodeBinary for Key {
239    type Error = KeyBinaryError;
240
241    fn decode_binary(bytes: &[u8]) -> Result<Self, Self::Error> {
242        read_key_from_bytes(bytes)
243    }
244}
245
246impl EncodeBinary for Key {
247    type Error = KeyBinaryError;
248
249    fn encode_binary(&self) -> Result<Vec<u8>, Self::Error> {
250        write_key_to_vec(self)
251    }
252}
253
254/// Errors produced while parsing or serializing KEY binary data.
255#[derive(Debug, Error)]
256pub enum KeyBinaryError {
257    /// I/O read/write failure.
258    #[error(transparent)]
259    Io(#[from] std::io::Error),
260    /// Header signature is not `KEY `.
261    #[error("invalid KEY magic: {0:?}")]
262    InvalidMagic([u8; 4]),
263    /// Header version is unsupported.
264    #[error("invalid KEY version: {0:?}")]
265    InvalidVersion([u8; 4]),
266    /// Header/body layout is invalid or truncated.
267    #[error("invalid KEY header: {0}")]
268    InvalidHeader(String),
269    /// KEY content is structurally invalid.
270    #[error("invalid KEY data: {0}")]
271    InvalidData(String),
272    /// Value cannot fit on-disk integer width.
273    #[error("value overflow while writing field `{0}`")]
274    ValueOverflow(&'static str),
275    /// Resource name validation failed during read.
276    #[error("invalid resref at {context}: {source}")]
277    InvalidResRef {
278        /// Field context.
279        context: String,
280        /// Validation error details.
281        #[source]
282        source: ResRefError,
283    },
284    /// BIF filename cannot fit in 16-bit length field.
285    #[error("filename `{filename}` has encoded length {len} (max {max})")]
286    FilenameTooLong {
287        /// BIF filename.
288        filename: String,
289        /// Encoded byte length including trailing NUL.
290        len: usize,
291        /// Maximum allowed length.
292        max: usize,
293    },
294    /// BIF filename contains an embedded NUL byte.
295    #[error("filename `{filename}` contains NUL byte")]
296    FilenameContainsNul {
297        /// BIF filename.
298        filename: String,
299    },
300    /// BIF/resource indexes do not fit packed `resource_id` layout.
301    #[error(
302        "resource id parts out of range (bif_index={bif_index}, resource_index={resource_index})"
303    )]
304    InvalidResourceIdParts {
305        /// BIF index (must be `<= 0xFFF`).
306        bif_index: u32,
307        /// Resource index (must be `<= 0xFFFFF`).
308        resource_index: u32,
309    },
310    /// Text cannot be represented as KEY encoding.
311    #[error("KEY text encoding failed for {context}: {source}")]
312    TextEncoding {
313        /// Value context.
314        context: String,
315        /// Encoding error details.
316        #[source]
317        source: EncodeTextError,
318    },
319    /// Text bytes cannot be decoded as KEY encoding.
320    #[error("KEY text decoding failed for {context}: {source}")]
321    TextDecoding {
322        /// Value context.
323        context: String,
324        /// Decoding error details.
325        #[source]
326        source: DecodeTextError,
327    },
328}
329
330impl From<binary::BinaryLayoutError> for KeyBinaryError {
331    fn from(error: binary::BinaryLayoutError) -> Self {
332        Self::InvalidHeader(error.to_string())
333    }
334}
335
336/// Packs `(bif_index, resource_index)` into KEY `resource_id` format.
337pub fn pack_resource_id(bif_index: u32, resource_index: u32) -> Result<u32, KeyBinaryError> {
338    ResourceId::from_parts(bif_index, resource_index)
339        .map(|resource_id| resource_id.raw())
340        .map_err(
341            |ResourceIdError::InvalidParts {
342                 bif_index,
343                 resource_index,
344             }| KeyBinaryError::InvalidResourceIdParts {
345                bif_index,
346                resource_index,
347            },
348        )
349}