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}