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}