1use std::collections::HashMap;
7use std::fs;
8use std::io::{Read, Seek, SeekFrom};
9use std::path::{Path, PathBuf};
10
11use thiserror::Error;
12
13use rakata_core::{ResRef, ResourceId, ResourceTypeCode};
14use rakata_formats::{
15 read_bif_from_bytes, read_key_from_bytes, Bif, BifBinaryError, Key, KeyBinaryError,
16 KeyResourceEntry,
17};
18
19use crate::util::cmp_ascii_case_insensitive;
20
21#[derive(Debug, Clone)]
23pub struct KeyFile {
24 key_path: PathBuf,
25 bif_base_path: PathBuf,
26 bif_path_index: BifPathIndex,
27 key: Key,
28 resource_index: HashMap<(ResRef, ResourceTypeCode), usize>,
33}
34
35impl KeyFile {
36 #[cfg_attr(
38 feature = "tracing",
39 tracing::instrument(level = "debug", skip(key_path))
40 )]
41 pub fn read_from_file(key_path: impl AsRef<Path>) -> Result<Self, KeyFileError> {
42 let key_path = key_path.as_ref();
43 let bif_base_path = key_path
44 .parent()
45 .map_or_else(|| PathBuf::from("."), Path::to_path_buf);
46 Self::read_from_file_with_base(key_path, bif_base_path)
47 }
48
49 #[cfg_attr(
51 feature = "tracing",
52 tracing::instrument(level = "debug", skip(key_path, bif_base_path))
53 )]
54 pub fn read_from_file_with_base(
55 key_path: impl AsRef<Path>,
56 bif_base_path: impl AsRef<Path>,
57 ) -> Result<Self, KeyFileError> {
58 let key_path = key_path.as_ref();
59 let bif_base_path = bif_base_path.as_ref();
60 let bytes = fs::read(key_path).map_err(|source| KeyFileError::Io {
61 path: key_path.to_path_buf(),
62 source,
63 })?;
64 let key = read_key_from_bytes(&bytes)?;
65 let resource_index = build_key_resource_index(&key);
66 Ok(Self {
67 key_path: key_path.to_path_buf(),
68 bif_base_path: bif_base_path.to_path_buf(),
69 bif_path_index: BifPathIndex::build(bif_base_path),
70 key,
71 resource_index,
72 })
73 }
74
75 pub fn key_path(&self) -> &Path {
77 &self.key_path
78 }
79
80 pub fn bif_base_path(&self) -> &Path {
82 &self.bif_base_path
83 }
84
85 pub fn key(&self) -> &Key {
87 &self.key
88 }
89
90 #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self), fields(resref = %resref, resource_type = resource_type.raw_id())))]
92 pub fn resource_entry(
93 &self,
94 resref: &ResRef,
95 resource_type: ResourceTypeCode,
96 ) -> Option<&KeyResourceEntry> {
97 let &index = self.resource_index.get(&(*resref, resource_type))?;
98 self.key.resources.get(index)
99 }
100
101 #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self)))]
109 pub fn bif_path_by_index(&self, bif_index: u32) -> Option<PathBuf> {
110 let index = usize::try_from(bif_index).ok()?;
111 let filename = self.key.bif_entries.get(index)?.filename.as_str();
112 self.resolve_bif_path(filename)
113 }
114
115 fn resolve_bif_path(&self, key_filename: &str) -> Option<PathBuf> {
116 let normalized = normalize_key_bif_name(key_filename);
117 if normalized.is_empty() {
118 return None;
119 }
120
121 let relative = parse_key_relative_path(&normalized);
122 let direct = self.bif_base_path.join(&relative);
123 if direct.exists() {
124 return Some(direct);
125 }
126
127 let direct_data = self.bif_base_path.join("data").join(&relative);
128 if direct_data.exists() {
129 return Some(direct_data);
130 }
131
132 let relative_key = normalized.to_ascii_lowercase();
133 if let Some(path) = self.bif_path_index.by_relposix.get(&relative_key) {
134 if path.exists() {
135 return Some(path.clone());
136 }
137 }
138
139 let basename = Path::new(&normalized)
140 .file_name()
141 .and_then(|name| name.to_str())
142 .map(str::to_ascii_lowercase)?;
143 self.bif_path_index
144 .by_basename
145 .get(&basename)
146 .filter(|path| path.exists())
147 .cloned()
148 }
149
150 #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self)))]
152 pub fn read_bif_by_index(&self, bif_index: u32) -> Result<Bif, KeyFileError> {
153 let path = self
154 .bif_path_by_index(bif_index)
155 .ok_or(KeyFileError::MissingBifIndex { bif_index })?;
156 let bytes = fs::read(&path).map_err(|source| KeyFileError::Io {
157 path: path.clone(),
158 source,
159 })?;
160 read_bif_from_bytes(&bytes).map_err(KeyFileError::from)
161 }
162
163 #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self), fields(resref = %resref, resource_type = resource_type.raw_id())))]
171 pub fn resource(
172 &self,
173 resref: &ResRef,
174 resource_type: ResourceTypeCode,
175 ) -> Result<Option<Vec<u8>>, KeyFileError> {
176 let Some(entry) = self.resource_entry(resref, resource_type) else {
177 return Ok(None);
178 };
179
180 let bif_index = entry.resource_id.bif_index();
181 let data = self.read_resource_by_seek(bif_index, entry.resource_id)?;
182 Ok(Some(data))
183 }
184
185 #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self), fields(resource_id = resource_id.raw())))]
192 pub fn read_resource_by_seek(
193 &self,
194 bif_index: u32,
195 resource_id: ResourceId,
196 ) -> Result<Vec<u8>, KeyFileError> {
197 let path = self
198 .bif_path_by_index(bif_index)
199 .ok_or(KeyFileError::MissingBifIndex { bif_index })?;
200
201 let mut file = fs::File::open(&path).map_err(|source| KeyFileError::Io {
202 path: path.clone(),
203 source,
204 })?;
205
206 let mut header = [0u8; 20];
208 file.read_exact(&mut header)
209 .map_err(|source| KeyFileError::Io {
210 path: path.clone(),
211 source,
212 })?;
213
214 if &header[0..4] != b"BIFF" {
215 let mut signature = [0u8; 4];
216 signature.copy_from_slice(&header[0..4]);
217 return Err(KeyFileError::BifSignatureMismatch {
218 path: path.clone(),
219 signature,
220 });
221 }
222
223 let var_count =
224 u32::from_le_bytes(header[8..12].try_into().expect("4-byte slice is [u8; 4]"));
225 let var_table_offset =
226 u32::from_le_bytes(header[16..20].try_into().expect("4-byte slice is [u8; 4]"));
227
228 file.seek(SeekFrom::Start(u64::from(var_table_offset)))
230 .map_err(|source| KeyFileError::Io {
231 path: path.clone(),
232 source,
233 })?;
234
235 let target_id = resource_id.raw();
236 let mut entry_buf = [0u8; 16];
237 let mut found: Option<(u32, u32)> = None;
238
239 for _ in 0..var_count {
240 file.read_exact(&mut entry_buf)
241 .map_err(|source| KeyFileError::Io {
242 path: path.clone(),
243 source,
244 })?;
245
246 let entry_id = u32::from_le_bytes(
247 entry_buf[0..4]
248 .try_into()
249 .expect("4-byte slice of 12-byte buffer"),
250 );
251 if entry_id == target_id {
252 let data_offset = u32::from_le_bytes(
253 entry_buf[4..8]
254 .try_into()
255 .expect("4-byte slice of 12-byte buffer"),
256 );
257 let data_size = u32::from_le_bytes(
258 entry_buf[8..12]
259 .try_into()
260 .expect("4-byte slice of 12-byte buffer"),
261 );
262 found = Some((data_offset, data_size));
263 break;
264 }
265 }
266
267 let (data_offset, data_size) = found.ok_or(KeyFileError::MissingBifResource {
268 bif_index,
269 resource_id,
270 })?;
271
272 file.seek(SeekFrom::Start(u64::from(data_offset)))
274 .map_err(|source| KeyFileError::Io {
275 path: path.clone(),
276 source,
277 })?;
278
279 let data_len =
280 usize::try_from(data_size).map_err(|_| KeyFileError::MissingBifResource {
281 bif_index,
282 resource_id,
283 })?;
284 let mut data = vec![0u8; data_len];
285 file.read_exact(&mut data)
286 .map_err(|source| KeyFileError::Io {
287 path: path.clone(),
288 source,
289 })?;
290
291 Ok(data)
292 }
293}
294
295fn parse_key_relative_path(filename: &str) -> PathBuf {
296 let mut path = PathBuf::new();
297 for component in filename.split(['\\', '/']) {
298 if !component.is_empty() {
299 path.push(component);
300 }
301 }
302 path
303}
304
305fn normalize_key_bif_name(filename: &str) -> String {
306 filename
307 .replace('\\', "/")
308 .trim_start_matches('/')
309 .trim_start_matches("./")
310 .to_string()
311}
312
313fn build_key_resource_index(key: &Key) -> HashMap<(ResRef, ResourceTypeCode), usize> {
318 let mut index = HashMap::with_capacity(key.resources.len());
319 for (i, entry) in key.resources.iter().enumerate() {
320 index
321 .entry((entry.resref, entry.resource_type))
322 .or_insert(i);
323 }
324 index
325}
326
327#[derive(Debug, Clone, Default, PartialEq, Eq)]
328struct BifPathIndex {
329 by_basename: HashMap<String, PathBuf>,
330 by_relposix: HashMap<String, PathBuf>,
331}
332
333impl BifPathIndex {
334 fn build(base_path: &Path) -> Self {
335 let mut files = Vec::new();
336 collect_bif_files_recursive(base_path, &mut files);
337
338 files.sort_by(|a, b| {
339 let a_rel = a.strip_prefix(base_path).unwrap_or(a).to_string_lossy();
340 let b_rel = b.strip_prefix(base_path).unwrap_or(b).to_string_lossy();
341 cmp_ascii_case_insensitive(&a_rel, &b_rel).then(a.cmp(b))
342 });
343
344 let mut index = Self::default();
345 for path in files {
346 let Ok(relative) = path.strip_prefix(base_path) else {
347 continue;
348 };
349 let rel_key = relative
350 .to_string_lossy()
351 .replace('\\', "/")
352 .to_ascii_lowercase();
353 index
354 .by_relposix
355 .entry(rel_key)
356 .or_insert_with(|| path.clone());
357
358 let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
359 continue;
360 };
361 index
362 .by_basename
363 .entry(file_name.to_ascii_lowercase())
364 .or_insert(path);
365 }
366 index
367 }
368}
369
370fn collect_bif_files_recursive(directory: &Path, output: &mut Vec<PathBuf>) {
371 let Ok(entries) = fs::read_dir(directory) else {
372 return;
373 };
374 for entry in entries.filter_map(Result::ok) {
375 let path = entry.path();
376 if path.is_dir() {
377 collect_bif_files_recursive(&path, output);
378 continue;
379 }
380 if !path.is_file() {
381 continue;
382 }
383 let is_bif = path
384 .extension()
385 .and_then(|ext| ext.to_str())
386 .is_some_and(|ext| ext.eq_ignore_ascii_case("bif"));
387 if is_bif {
388 output.push(path);
389 }
390 }
391}
392
393#[derive(Debug, Error)]
395pub enum KeyFileError {
396 #[error("I/O failure for `{path}`: {source}")]
398 Io {
399 path: PathBuf,
401 #[source]
403 source: std::io::Error,
404 },
405 #[error(transparent)]
407 Key(#[from] KeyBinaryError),
408 #[error(transparent)]
410 Bif(#[from] BifBinaryError),
411 #[error("KEY entry references missing BIF index {bif_index}")]
413 MissingBifIndex {
414 bif_index: u32,
416 },
417 #[error("BIF index {bif_index} missing KEY resource id {resource_id:#x}")]
419 MissingBifResource {
420 bif_index: u32,
422 resource_id: ResourceId,
424 },
425 #[error("BIF signature mismatch in `{path}`: expected BIFF, got {signature:?}")]
427 BifSignatureMismatch {
428 path: PathBuf,
430 signature: [u8; 4],
432 },
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438
439 use rakata_core::{ResourceId, ResourceType};
440 use rakata_formats::{write_bif_to_vec, write_key_to_vec};
441 use tempfile::TempDir;
442
443 #[test]
444 fn resolves_resource_through_key_and_bif() {
445 let resource_id = ResourceId::from_parts(0, 0).expect("valid resource id");
446 let resource_type = ResourceTypeCode::from(ResourceType::Utc);
447
448 let mut bif = Bif::new();
449 bif.push_resource(resource_id, resource_type, b"npc".to_vec());
450 let bif_bytes = write_bif_to_vec(&bif).expect("write bif");
451
452 let mut key = Key::new();
453 key.push_bif_entry(
454 "data\\templates.bif",
455 u32::try_from(bif_bytes.len()).expect("test BIF size fits in u32"),
456 0,
457 );
458 key.push_resource(
459 ResRef::new("p_bastila").expect("valid resref"),
460 resource_type,
461 resource_id,
462 );
463 let key_bytes = write_key_to_vec(&key).expect("write key");
464
465 let temp = TempDir::new().expect("create tempdir");
466 let root = temp.path();
467 let data_dir = root.join("data");
468 fs::create_dir_all(&data_dir).expect("create data dir");
469 let bif_path = data_dir.join("templates.bif");
470 let key_path = root.join("chitin.key");
471 fs::write(&bif_path, bif_bytes).expect("write bif");
472 fs::write(&key_path, key_bytes).expect("write key");
473
474 let key_file = KeyFile::read_from_file_with_base(&key_path, root).expect("load keyfile");
475 let resref = ResRef::new("p_bastila").expect("valid resref");
476 let data = key_file
477 .resource(&resref, resource_type)
478 .expect("resolve resource")
479 .expect("resource exists");
480 assert_eq!(data, b"npc");
481 assert_eq!(key_file.bif_path_by_index(0), Some(bif_path));
482 }
483
484 #[test]
485 fn missing_resource_returns_none() {
486 let resource_type = ResourceTypeCode::from(ResourceType::Utc);
487 let mut key = Key::new();
488 key.push_bif_entry("data\\templates.bif", 0, 0);
489 let key_bytes = write_key_to_vec(&key).expect("write key");
490
491 let temp = TempDir::new().expect("create tempdir");
492 let root = temp.path();
493 let key_path = root.join("chitin.key");
494 fs::write(&key_path, key_bytes).expect("write key");
495
496 let key_file = KeyFile::read_from_file_with_base(&key_path, root).expect("load keyfile");
497 let resref = ResRef::new("missing").expect("valid resref");
498 let resolved = key_file
499 .resource(&resref, resource_type)
500 .expect("lookup should succeed");
501 assert_eq!(resolved, None);
502 }
503
504 #[test]
505 fn resolves_bif_from_data_directory_when_key_stores_basename() {
506 let resource_id = ResourceId::from_parts(0, 0).expect("valid resource id");
507 let resource_type = ResourceTypeCode::from(ResourceType::Utc);
508
509 let mut bif = Bif::new();
510 bif.push_resource(resource_id, resource_type, b"npc".to_vec());
511 let bif_bytes = write_bif_to_vec(&bif).expect("write bif");
512
513 let mut key = Key::new();
514 key.push_bif_entry(
515 "templates.bif",
516 u32::try_from(bif_bytes.len()).expect("test BIF size fits in u32"),
517 0,
518 );
519 key.push_resource(
520 ResRef::new("p_bastila").expect("valid resref"),
521 resource_type,
522 resource_id,
523 );
524 let key_bytes = write_key_to_vec(&key).expect("write key");
525
526 let temp = TempDir::new().expect("create tempdir");
527 let root = temp.path();
528 let data_dir = root.join("data");
529 fs::create_dir_all(&data_dir).expect("create data dir");
530 let bif_path = data_dir.join("templates.bif");
531 let key_path = root.join("chitin.key");
532 fs::write(&bif_path, bif_bytes).expect("write bif");
533 fs::write(&key_path, key_bytes).expect("write key");
534
535 let key_file = KeyFile::read_from_file_with_base(&key_path, root).expect("load keyfile");
536 assert_eq!(key_file.bif_path_by_index(0), Some(bif_path));
537 }
538
539 #[test]
540 fn resolves_bif_case_insensitively_from_path_index() {
541 let resource_id = ResourceId::from_parts(0, 0).expect("valid resource id");
542 let resource_type = ResourceTypeCode::from(ResourceType::Utc);
543
544 let mut bif = Bif::new();
545 bif.push_resource(resource_id, resource_type, b"npc".to_vec());
546 let bif_bytes = write_bif_to_vec(&bif).expect("write bif");
547
548 let mut key = Key::new();
549 key.push_bif_entry(
550 "data\\templates.bif",
551 u32::try_from(bif_bytes.len()).expect("test BIF size fits in u32"),
552 0,
553 );
554 key.push_resource(
555 ResRef::new("p_bastila").expect("valid resref"),
556 resource_type,
557 resource_id,
558 );
559 let key_bytes = write_key_to_vec(&key).expect("write key");
560
561 let temp = TempDir::new().expect("create tempdir");
562 let root = temp.path();
563 let data_dir = root.join("Data");
564 fs::create_dir_all(&data_dir).expect("create data dir");
565 let bif_path = data_dir.join("Templates.BIF");
566 let key_path = root.join("chitin.key");
567 fs::write(&bif_path, bif_bytes).expect("write bif");
568 fs::write(&key_path, key_bytes).expect("write key");
569
570 let key_file = KeyFile::read_from_file_with_base(&key_path, root).expect("load keyfile");
571 assert_eq!(key_file.bif_path_by_index(0), Some(bif_path));
572 }
573}