1use std::collections::HashMap;
23use std::fs::File;
24use std::io::{Read, Seek};
25use std::path::Path;
26use std::sync::{Arc, Mutex};
27
28use rakata_core::{ResourceId, ResourceTypeCode};
29
30use super::layout::{self, BifHeader};
31#[cfg(feature = "bzf")]
32use super::LZMA_ALONE_HEADER_SIZE;
33use super::{BifBinaryError, BifContainer, BifReadOptions, BifResourceStorage, FILE_HEADER_SIZE};
34
35const COMPRESSED_EXTENSION: &str = "bzf";
37use crate::archive::EntryDefect;
38use crate::section_reader::SectionReader;
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct BifIndexEntry {
43 pub resource_id: ResourceId,
45 pub resource_type: ResourceTypeCode,
47 pub storage: BifResourceStorage,
49 pub offset: u64,
52 pub size: u64,
57 pub packed_size: u64,
61 pub defect: Option<EntryDefect>,
64}
65
66impl BifIndexEntry {
67 pub fn is_readable(&self) -> bool {
69 self.defect.is_none()
70 }
71}
72
73#[derive(Debug)]
83pub struct BifIndex<R = File> {
84 section: SectionReader<R>,
85 container: BifContainer,
86 entries: Vec<BifIndexEntry>,
87 by_resource_id: HashMap<ResourceId, usize>,
88}
89
90impl BifIndex<File> {
91 pub fn open(path: impl AsRef<Path>) -> Result<Self, BifBinaryError> {
103 Self::open_with_options(path, BifReadOptions::default())
104 }
105
106 pub fn open_with_options(
117 path: impl AsRef<Path>,
118 options: BifReadOptions,
119 ) -> Result<Self, BifBinaryError> {
120 let path = path.as_ref();
121 let container = container_for_path(path);
122 let file = File::open(path)?;
123 let len = file.metadata()?.len();
124 Self::new_with_options(
125 SectionReader::whole(Arc::new(Mutex::new(file)), len),
126 container,
127 options,
128 )
129 }
130}
131
132fn container_for_path(path: &Path) -> BifContainer {
134 let compressed = path
135 .extension()
136 .and_then(|extension| extension.to_str())
137 .is_some_and(|extension| extension.eq_ignore_ascii_case(COMPRESSED_EXTENSION));
138 if compressed {
139 BifContainer::Bzf
140 } else {
141 BifContainer::Biff
142 }
143}
144
145impl<R: Read + Seek> BifIndex<R> {
146 pub fn new(section: SectionReader<R>) -> Result<Self, BifBinaryError> {
154 Self::new_with_options(section, BifContainer::Biff, BifReadOptions::default())
155 }
156
157 pub fn new_with_container(
166 section: SectionReader<R>,
167 container: BifContainer,
168 ) -> Result<Self, BifBinaryError> {
169 Self::new_with_options(section, container, BifReadOptions::default())
170 }
171
172 pub fn new_with_options(
194 section: SectionReader<R>,
195 container: BifContainer,
196 options: BifReadOptions,
197 ) -> Result<Self, BifBinaryError> {
198 if container == BifContainer::Bzf && !cfg!(feature = "bzf") {
199 return Err(BifBinaryError::BzfFeatureDisabled);
200 }
201 let header_bytes = read_region(§ion, 0, u64_from(FILE_HEADER_SIZE)?, "BIF header")?;
202 let header = BifHeader::parse(&header_bytes, options)?;
203
204 let variable_table = read_region(
205 §ion,
206 u64_from(header.variable_table_offset)?,
207 u64_from(header.variable_table_size()?)?,
208 "variable resource table",
209 )?;
210
211 let mut entries = Vec::with_capacity(header.variable_count);
212 for index in 0..header.variable_count {
213 entries.push(layout::parse_variable_entry(&variable_table, index)?);
214 }
215
216 if header.parse_fixed_entries {
217 let fixed_table = read_region(
218 §ion,
219 u64_from(header.fixed_table_offset)?,
220 u64_from(header.fixed_table_size()?)?,
221 "fixed resource table",
222 )?;
223 for index in 0..header.fixed_count {
224 entries.push(layout::parse_fixed_entry(&fixed_table, index)?);
225 }
226 }
227
228 let file_len = usize::try_from(section.len()).map_err(|_| {
231 BifBinaryError::InvalidHeader("archive length exceeds addressable range".into())
232 })?;
233 let packed = if container == BifContainer::Bzf {
234 Some(layout::packed_sizes(&entries, file_len)?)
235 } else {
236 None
237 };
238
239 let mut indexed = Vec::with_capacity(entries.len());
240 let mut by_resource_id = HashMap::with_capacity(entries.len());
241 for (position, entry) in entries.into_iter().enumerate() {
242 let offset = u64_from(entry.data_offset)?;
243 let size = u64_from(entry.data_size)?;
244 let packed_size = match &packed {
245 Some(packed) => u64_from(packed[position])?,
246 None => size,
247 };
248 let defect = EntryDefect::check_bounds(offset, packed_size, section.len());
253
254 let resource_id = ResourceId::from_raw(entry.resource_id);
255 by_resource_id.entry(resource_id).or_insert(position);
258 indexed.push(BifIndexEntry {
259 resource_id,
260 resource_type: entry.resource_type,
261 storage: entry.storage,
262 offset,
263 size,
264 packed_size,
265 defect,
266 });
267 }
268
269 Ok(Self {
270 section,
271 container,
272 entries: indexed,
273 by_resource_id,
274 })
275 }
276
277 pub fn read_entry(&self, position: usize) -> Result<Vec<u8>, BifBinaryError> {
295 let entry = self.entries.get(position).ok_or_else(|| {
296 BifBinaryError::InvalidData(format!("resource index {position} is out of range"))
297 })?;
298 if let Some(defect) = &entry.defect {
299 return Err(BifBinaryError::InvalidHeader(format!(
300 "resource data[{position}] (id {}) is unreadable: {defect}",
301 entry.resource_id.raw()
302 )));
303 }
304 let raw = self
305 .entry_section(position)
306 .ok_or_else(|| {
307 BifBinaryError::InvalidHeader(format!(
308 "resource data[{position}] exceeds file bounds"
309 ))
310 })?
311 .read_all()
312 .map_err(|source| {
313 BifBinaryError::InvalidData(format!(
314 "failed reading {} bytes for resource id {}: {source}",
315 entry.packed_size,
316 entry.resource_id.raw()
317 ))
318 })?;
319
320 match self.container {
321 BifContainer::Biff => Ok(raw),
322 BifContainer::Bzf => {
323 let expected = usize::try_from(entry.size).map_err(|_| {
324 BifBinaryError::InvalidData(format!(
325 "resource data[{position}] declares a length beyond addressable memory"
326 ))
327 })?;
328 decompress_payload(&raw, expected)
329 }
330 }
331 }
332
333 pub fn container(&self) -> BifContainer {
335 self.container
336 }
337
338 pub fn resolve_by_id(
347 &self,
348 resource_id: ResourceId,
349 ) -> Result<Option<Vec<u8>>, BifBinaryError> {
350 let Some(position) = self.by_resource_id.get(&resource_id) else {
351 return Ok(None);
352 };
353 self.read_entry(*position).map(Some)
354 }
355
356 pub fn entry_section(&self, position: usize) -> Option<SectionReader<R>> {
361 let entry = self.entries.get(position)?;
362 if entry.defect.is_some() {
363 return None;
364 }
365 self.section.section(entry.offset, entry.packed_size)
366 }
367
368 pub fn iter_resources(
373 &self,
374 ) -> impl Iterator<Item = Result<(&BifIndexEntry, Vec<u8>), BifBinaryError>> + '_ {
375 self.entries
376 .iter()
377 .enumerate()
378 .map(move |(position, entry)| self.read_entry(position).map(|bytes| (entry, bytes)))
379 }
380}
381
382impl<R> BifIndex<R> {
383 pub fn entries(&self) -> &[BifIndexEntry] {
385 &self.entries
386 }
387
388 pub fn entry(&self, position: usize) -> Option<&BifIndexEntry> {
390 self.entries.get(position)
391 }
392
393 pub fn len(&self) -> usize {
395 self.entries.len()
396 }
397
398 pub fn is_empty(&self) -> bool {
400 self.entries.is_empty()
401 }
402
403 pub fn contains_id(&self, resource_id: ResourceId) -> bool {
408 self.by_resource_id.contains_key(&resource_id)
409 }
410
411 pub fn defects(&self) -> impl Iterator<Item = (usize, &BifIndexEntry, &EntryDefect)> + '_ {
416 self.entries
417 .iter()
418 .enumerate()
419 .filter_map(|(index, entry)| entry.defect.as_ref().map(|d| (index, entry, d)))
420 }
421
422 pub fn has_defects(&self) -> bool {
424 self.entries.iter().any(|entry| entry.defect.is_some())
425 }
426}
427
428#[cfg(feature = "bzf")]
439fn decompress_payload(payload: &[u8], expected: usize) -> Result<Vec<u8>, BifBinaryError> {
440 use std::io::Cursor;
441
442 use lzma_rust2::LzmaReader;
443
444 if expected == 0 {
445 return Ok(Vec::new());
446 }
447 if payload.len() < LZMA_ALONE_HEADER_SIZE {
448 return Err(BifBinaryError::InvalidData(format!(
449 "compressed payload is {} bytes, too short to carry an LZMA header",
450 payload.len()
451 )));
452 }
453
454 let properties = payload[0];
455 let dictionary_size = u32::from_le_bytes(
456 payload[1..5]
457 .try_into()
458 .expect("4-byte slice of a 5-byte header"),
459 );
460 let mut packed = u32::from(properties);
463 let literal_context = packed % 9;
464 packed /= 9;
465 let literal_position = packed % 5;
466 let position = packed / 5;
467
468 let mut reader = LzmaReader::new(
469 Cursor::new(&payload[LZMA_ALONE_HEADER_SIZE..]),
470 u64::try_from(expected).expect("usize fits in u64 on supported targets"),
471 literal_context,
472 literal_position,
473 position,
474 dictionary_size,
475 None,
476 )
477 .map_err(|error| {
478 BifBinaryError::InvalidData(format!("failed to initialize the payload decoder: {error}"))
479 })?;
480
481 let mut out = Vec::with_capacity(expected);
482 reader.read_to_end(&mut out).map_err(|error| {
483 BifBinaryError::InvalidData(format!("failed to decode a compressed payload: {error}"))
484 })?;
485 if out.len() != expected {
486 return Err(BifBinaryError::InvalidData(format!(
487 "decoded payload length mismatch (expected {expected}, got {})",
488 out.len()
489 )));
490 }
491 Ok(out)
492}
493
494#[cfg(not(feature = "bzf"))]
499fn decompress_payload(_payload: &[u8], _expected: usize) -> Result<Vec<u8>, BifBinaryError> {
500 Err(BifBinaryError::BzfFeatureDisabled)
501}
502
503fn read_region<R: Read + Seek>(
508 section: &SectionReader<R>,
509 offset: u64,
510 len: u64,
511 name: &str,
512) -> Result<Vec<u8>, BifBinaryError> {
513 section
514 .section(offset, len)
515 .ok_or_else(|| BifBinaryError::InvalidHeader(format!("{name} exceeds file bounds")))?
516 .read_all()
517 .map_err(BifBinaryError::Io)
518}
519
520fn u64_from(value: usize) -> Result<u64, BifBinaryError> {
522 u64::try_from(value)
523 .map_err(|_| BifBinaryError::InvalidHeader("offset exceeds addressable range".into()))
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529 use crate::bif::read_bif_from_bytes;
530 use std::io::Cursor;
531
532 const K1_PLAYER_BIF: &[u8] = include_bytes!(concat!(
533 env!("CARGO_MANIFEST_DIR"),
534 "/../../fixtures/k1_player.bif"
535 ));
536
537 fn index_over(bytes: &[u8]) -> BifIndex<Cursor<Vec<u8>>> {
538 let len = u64::try_from(bytes.len()).expect("fixture fits in u64");
539 let source = Arc::new(Mutex::new(Cursor::new(bytes.to_vec())));
540 BifIndex::new(SectionReader::whole(source, len)).expect("fixture indexes")
541 }
542
543 #[test]
544 fn indexes_the_same_resources_the_eager_reader_finds() {
545 let eager = read_bif_from_bytes(K1_PLAYER_BIF).expect("fixture parses eagerly");
546 let index = index_over(K1_PLAYER_BIF);
547
548 assert_eq!(index.len(), eager.resources.len());
549
550 for (position, expected) in eager.resources.iter().enumerate() {
551 let entry = index.entry(position).expect("entry exists");
552 assert_eq!(entry.resource_id, expected.resource_id);
553 assert_eq!(entry.resource_type, expected.resource_type);
554 assert_eq!(entry.storage, expected.storage);
555
556 let bytes = index.read_entry(position).expect("read succeeds");
557 assert_eq!(bytes, expected.data, "bytes differ at position {position}");
558 }
559 }
560
561 #[test]
562 fn resolves_by_resource_id() {
563 let eager = read_bif_from_bytes(K1_PLAYER_BIF).expect("fixture parses eagerly");
564 let index = index_over(K1_PLAYER_BIF);
565 let first = eager.resources.first().expect("fixture has resources");
566
567 let bytes = index
568 .resolve_by_id(first.resource_id)
569 .expect("resolve succeeds")
570 .expect("resource is present");
571
572 assert_eq!(bytes, first.data);
573 assert!(index.contains_id(first.resource_id));
574 }
575
576 #[test]
577 fn an_unknown_resource_id_is_a_clean_miss_not_an_error() {
578 let index = index_over(K1_PLAYER_BIF);
579 let absent = ResourceId::from_raw(u32::MAX);
580
581 let found = index.resolve_by_id(absent).expect("a miss is not an error");
582
583 assert!(found.is_none());
584 assert!(!index.contains_id(absent));
585 }
586
587 #[test]
588 fn iterating_yields_every_resource_with_its_bytes() {
589 let eager = read_bif_from_bytes(K1_PLAYER_BIF).expect("fixture parses eagerly");
590 let index = index_over(K1_PLAYER_BIF);
591
592 let collected = index
593 .iter_resources()
594 .collect::<Result<Vec<_>, _>>()
595 .expect("iteration succeeds");
596
597 assert_eq!(collected.len(), eager.resources.len());
598 for ((entry, bytes), expected) in collected.iter().zip(&eager.resources) {
599 assert_eq!(entry.resource_id, expected.resource_id);
600 assert_eq!(bytes, &expected.data);
601 }
602 }
603
604 #[test]
605 fn one_out_of_bounds_entry_does_not_cost_the_whole_archive() {
606 let mut bytes = K1_PLAYER_BIF.to_vec();
608 let header = crate::bif::layout::BifHeader::parse(&bytes, BifReadOptions::default())
609 .expect("fixture header parses");
610 let data_offset_field = header.variable_table_offset + 4;
611 bytes[data_offset_field..data_offset_field + 4].copy_from_slice(&u32::MAX.to_le_bytes());
612
613 let index = index_over(&bytes);
614 let eager_count = read_bif_from_bytes(K1_PLAYER_BIF)
615 .expect("fixture parses eagerly")
616 .resources
617 .len();
618
619 assert_eq!(index.len(), eager_count);
620 assert_eq!(index.defects().count(), 1);
621
622 let (position, entry, _) = index.defects().next().expect("one defect");
623 assert!(!entry.is_readable());
624 assert!(index.read_entry(position).is_err());
625 assert!(index.entry_section(position).is_none());
626
627 for other in 0..index.len() {
628 if other != position {
629 assert!(index.read_entry(other).is_ok(), "entry {other} should read");
630 }
631 }
632 }
633
634 #[cfg(feature = "bzf")]
635 #[test]
636 fn a_synthesized_compressed_archive_round_trips() {
637 use crate::bif::{write_bif_to_vec, Bif, BifContainer};
638
639 let mut bif = Bif::new();
640 bif.container = BifContainer::Bzf;
641 let payload = b"MAXLAYOUT ".repeat(64);
644 bif.push_resource(
645 ResourceId::from_raw(0x0010_0000),
646 ResourceTypeCode::from_raw_id(3000),
647 payload.clone(),
648 );
649 bif.push_resource(
650 ResourceId::from_raw(0x0010_0001),
651 ResourceTypeCode::from_raw_id(3000),
652 b"short".to_vec(),
653 );
654 let bytes = write_bif_to_vec(&bif).expect("compressed archive writes");
655
656 let len = u64::try_from(bytes.len()).expect("fits in u64");
657 let source = Arc::new(Mutex::new(Cursor::new(bytes)));
658 let index =
659 BifIndex::new_with_container(SectionReader::whole(source, len), BifContainer::Bzf)
660 .expect("compressed archive indexes");
661
662 assert_eq!(index.container(), BifContainer::Bzf);
663 assert_eq!(index.len(), 2);
664
665 let first = index.entry(0).expect("entry present");
666 assert_eq!(usize::try_from(first.size).expect("fits"), payload.len());
667 assert!(
668 first.packed_size < first.size,
669 "the payload should be smaller on disk than decompressed"
670 );
671
672 assert_eq!(index.read_entry(0).expect("first decodes"), payload);
673 assert_eq!(index.read_entry(1).expect("second decodes"), b"short");
674 }
675
676 #[cfg(feature = "bzf")]
684 #[test]
685 fn a_real_compressed_archive_decodes_when_one_is_provided() {
686 let Ok(path) = std::env::var("RAKATA_BZF_FIXTURE") else {
687 return;
688 };
689
690 let index = BifIndex::open(&path).expect("real archive indexes");
691 assert_eq!(
692 index.container(),
693 BifContainer::Bzf,
694 "a .bzf path must be inferred as compressed"
695 );
696 assert!(!index.is_empty(), "real archive should hold resources");
697 assert!(
698 !index.has_defects(),
699 "a healthy archive must not report defects; \
700 every entry unreadable would mean the container was misread"
701 );
702
703 let declared: u64 = index.entries().iter().map(|entry| entry.size).sum();
705 let packed: u64 = index.entries().iter().map(|entry| entry.packed_size).sum();
706 assert!(
707 declared > packed,
708 "declared {declared} should exceed packed {packed} in a compressed archive"
709 );
710
711 for position in 0..index.len() {
712 let entry = index.entry(position).expect("entry present");
713 let data = index
714 .read_entry(position)
715 .unwrap_or_else(|error| panic!("entry {position} failed to decode: {error}"));
716 assert_eq!(
717 u64::try_from(data.len()).expect("fits"),
718 entry.size,
719 "entry {position} decoded to the wrong length"
720 );
721 }
722 }
723
724 #[test]
725 fn a_truncated_archive_either_fails_to_index_or_reports_defects() {
726 let truncated = &K1_PLAYER_BIF[..K1_PLAYER_BIF.len() / 2];
730 let len = u64::try_from(truncated.len()).expect("fits in u64");
731 let source = Arc::new(Mutex::new(Cursor::new(truncated.to_vec())));
732
733 match BifIndex::new(SectionReader::whole(source, len)) {
734 Err(_) => {}
735 Ok(index) => assert!(
736 index.has_defects(),
737 "a truncated archive must not index clean"
738 ),
739 }
740 }
741}