Skip to main content

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