1use std::collections::HashMap;
25use std::fs::File;
26use std::io::{Read, Seek};
27use std::path::Path;
28use std::sync::{Arc, Mutex};
29
30use rakata_core::{ResourceId, ResourceTypeCode};
31
32use super::layout::{self, BifHeader};
33#[cfg(feature = "bzf")]
34use super::LZMA_ALONE_HEADER_SIZE;
35use super::{BifBinaryError, BifContainer, BifReadOptions, BifResourceStorage, FILE_HEADER_SIZE};
36
37const COMPRESSED_EXTENSION: &str = "bzf";
39use crate::archive::EntryDefect;
40use crate::section_reader::SectionReader;
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct BifIndexEntry {
45 pub resource_id: ResourceId,
47 pub resource_type: ResourceTypeCode,
49 pub storage: BifResourceStorage,
51 pub offset: u64,
54 pub size: u64,
59 pub packed_size: u64,
63 pub defect: Option<EntryDefect>,
66}
67
68impl BifIndexEntry {
69 pub fn is_readable(&self) -> bool {
71 self.defect.is_none()
72 }
73}
74
75#[derive(Debug)]
85pub struct BifIndex<R = File> {
86 section: SectionReader<R>,
87 container: BifContainer,
88 entries: Vec<BifIndexEntry>,
89 by_resource_id: HashMap<ResourceId, usize>,
90}
91
92impl BifIndex<File> {
93 pub fn open(path: impl AsRef<Path>) -> Result<Self, BifBinaryError> {
104 Self::open_with_options(path, BifReadOptions::default())
105 }
106
107 pub fn open_with_options(
111 path: impl AsRef<Path>,
112 options: BifReadOptions,
113 ) -> Result<Self, BifBinaryError> {
114 let path = path.as_ref();
115 let container = container_for_path(path);
116 let file = File::open(path)?;
117 let len = file.metadata()?.len();
118 Self::new_with_options(
119 SectionReader::whole(Arc::new(Mutex::new(file)), len),
120 container,
121 options,
122 )
123 }
124}
125
126fn container_for_path(path: &Path) -> BifContainer {
128 let compressed = path
129 .extension()
130 .and_then(|extension| extension.to_str())
131 .is_some_and(|extension| extension.eq_ignore_ascii_case(COMPRESSED_EXTENSION));
132 if compressed {
133 BifContainer::Bzf
134 } else {
135 BifContainer::Biff
136 }
137}
138
139impl<R: Read + Seek> BifIndex<R> {
140 pub fn new(section: SectionReader<R>) -> Result<Self, BifBinaryError> {
142 Self::new_with_options(section, BifContainer::Biff, BifReadOptions::default())
143 }
144
145 pub fn new_with_container(
150 section: SectionReader<R>,
151 container: BifContainer,
152 ) -> Result<Self, BifBinaryError> {
153 Self::new_with_options(section, container, BifReadOptions::default())
154 }
155
156 pub fn new_with_options(
163 section: SectionReader<R>,
164 container: BifContainer,
165 options: BifReadOptions,
166 ) -> Result<Self, BifBinaryError> {
167 if container == BifContainer::Bzf && !cfg!(feature = "bzf") {
168 return Err(BifBinaryError::BzfFeatureDisabled);
169 }
170 let header_bytes = read_region(§ion, 0, u64_from(FILE_HEADER_SIZE)?, "BIF header")?;
171 let header = BifHeader::parse(&header_bytes, options)?;
172
173 let variable_table = read_region(
174 §ion,
175 u64_from(header.variable_table_offset)?,
176 u64_from(header.variable_table_size()?)?,
177 "variable resource table",
178 )?;
179
180 let mut entries = Vec::with_capacity(header.variable_count);
181 for index in 0..header.variable_count {
182 entries.push(layout::parse_variable_entry(&variable_table, index)?);
183 }
184
185 if header.parse_fixed_entries {
186 let fixed_table = read_region(
187 §ion,
188 u64_from(header.fixed_table_offset)?,
189 u64_from(header.fixed_table_size()?)?,
190 "fixed resource table",
191 )?;
192 for index in 0..header.fixed_count {
193 entries.push(layout::parse_fixed_entry(&fixed_table, index)?);
194 }
195 }
196
197 let file_len = usize::try_from(section.len()).map_err(|_| {
200 BifBinaryError::InvalidHeader("archive length exceeds addressable range".into())
201 })?;
202 let packed = if container == BifContainer::Bzf {
203 Some(layout::packed_sizes(&entries, file_len)?)
204 } else {
205 None
206 };
207
208 let mut indexed = Vec::with_capacity(entries.len());
209 let mut by_resource_id = HashMap::with_capacity(entries.len());
210 for (position, entry) in entries.into_iter().enumerate() {
211 let offset = u64_from(entry.data_offset)?;
212 let size = u64_from(entry.data_size)?;
213 let packed_size = match &packed {
214 Some(packed) => u64_from(packed[position])?,
215 None => size,
216 };
217 let defect = EntryDefect::check_bounds(offset, packed_size, section.len());
222
223 let resource_id = ResourceId::from_raw(entry.resource_id);
224 by_resource_id.entry(resource_id).or_insert(position);
227 indexed.push(BifIndexEntry {
228 resource_id,
229 resource_type: entry.resource_type,
230 storage: entry.storage,
231 offset,
232 size,
233 packed_size,
234 defect,
235 });
236 }
237
238 Ok(Self {
239 section,
240 container,
241 entries: indexed,
242 by_resource_id,
243 })
244 }
245
246 pub fn read_entry(&self, position: usize) -> Result<Vec<u8>, BifBinaryError> {
251 let entry = self.entries.get(position).ok_or_else(|| {
252 BifBinaryError::InvalidData(format!("resource index {position} is out of range"))
253 })?;
254 if let Some(defect) = &entry.defect {
255 return Err(BifBinaryError::InvalidHeader(format!(
256 "resource data[{position}] (id {}) is unreadable: {defect}",
257 entry.resource_id.raw()
258 )));
259 }
260 let raw = self
261 .entry_section(position)
262 .ok_or_else(|| {
263 BifBinaryError::InvalidHeader(format!(
264 "resource data[{position}] exceeds file bounds"
265 ))
266 })?
267 .read_all()
268 .map_err(|source| {
269 BifBinaryError::InvalidData(format!(
270 "failed reading {} bytes for resource id {}: {source}",
271 entry.packed_size,
272 entry.resource_id.raw()
273 ))
274 })?;
275
276 match self.container {
277 BifContainer::Biff => Ok(raw),
278 BifContainer::Bzf => {
279 let expected = usize::try_from(entry.size).map_err(|_| {
280 BifBinaryError::InvalidData(format!(
281 "resource data[{position}] declares a length beyond addressable memory"
282 ))
283 })?;
284 decompress_payload(&raw, expected)
285 }
286 }
287 }
288
289 pub fn container(&self) -> BifContainer {
291 self.container
292 }
293
294 pub fn resolve_by_id(
299 &self,
300 resource_id: ResourceId,
301 ) -> Result<Option<Vec<u8>>, BifBinaryError> {
302 let Some(position) = self.by_resource_id.get(&resource_id) else {
303 return Ok(None);
304 };
305 self.read_entry(*position).map(Some)
306 }
307
308 pub fn entry_section(&self, position: usize) -> Option<SectionReader<R>> {
313 let entry = self.entries.get(position)?;
314 if entry.defect.is_some() {
315 return None;
316 }
317 self.section.section(entry.offset, entry.packed_size)
318 }
319
320 pub fn iter_resources(
325 &self,
326 ) -> impl Iterator<Item = Result<(&BifIndexEntry, Vec<u8>), BifBinaryError>> + '_ {
327 self.entries
328 .iter()
329 .enumerate()
330 .map(move |(position, entry)| self.read_entry(position).map(|bytes| (entry, bytes)))
331 }
332}
333
334impl<R> BifIndex<R> {
335 pub fn entries(&self) -> &[BifIndexEntry] {
337 &self.entries
338 }
339
340 pub fn entry(&self, position: usize) -> Option<&BifIndexEntry> {
342 self.entries.get(position)
343 }
344
345 pub fn len(&self) -> usize {
347 self.entries.len()
348 }
349
350 pub fn is_empty(&self) -> bool {
352 self.entries.is_empty()
353 }
354
355 pub fn contains_id(&self, resource_id: ResourceId) -> bool {
360 self.by_resource_id.contains_key(&resource_id)
361 }
362
363 pub fn defects(&self) -> impl Iterator<Item = (usize, &BifIndexEntry, &EntryDefect)> + '_ {
368 self.entries
369 .iter()
370 .enumerate()
371 .filter_map(|(index, entry)| entry.defect.as_ref().map(|d| (index, entry, d)))
372 }
373
374 pub fn has_defects(&self) -> bool {
376 self.entries.iter().any(|entry| entry.defect.is_some())
377 }
378}
379
380#[cfg(feature = "bzf")]
391fn decompress_payload(payload: &[u8], expected: usize) -> Result<Vec<u8>, BifBinaryError> {
392 use std::io::Cursor;
393
394 use lzma_rust2::LzmaReader;
395
396 if expected == 0 {
397 return Ok(Vec::new());
398 }
399 if payload.len() < LZMA_ALONE_HEADER_SIZE {
400 return Err(BifBinaryError::InvalidData(format!(
401 "compressed payload is {} bytes, too short to carry an LZMA header",
402 payload.len()
403 )));
404 }
405
406 let properties = payload[0];
407 let dictionary_size = u32::from_le_bytes(
408 payload[1..5]
409 .try_into()
410 .expect("4-byte slice of a 5-byte header"),
411 );
412 let mut packed = u32::from(properties);
415 let literal_context = packed % 9;
416 packed /= 9;
417 let literal_position = packed % 5;
418 let position = packed / 5;
419
420 let mut reader = LzmaReader::new(
421 Cursor::new(&payload[LZMA_ALONE_HEADER_SIZE..]),
422 u64::try_from(expected).expect("usize fits in u64 on supported targets"),
423 literal_context,
424 literal_position,
425 position,
426 dictionary_size,
427 None,
428 )
429 .map_err(|error| {
430 BifBinaryError::InvalidData(format!("failed to initialize the payload decoder: {error}"))
431 })?;
432
433 let mut out = Vec::with_capacity(expected);
434 reader.read_to_end(&mut out).map_err(|error| {
435 BifBinaryError::InvalidData(format!("failed to decode a compressed payload: {error}"))
436 })?;
437 if out.len() != expected {
438 return Err(BifBinaryError::InvalidData(format!(
439 "decoded payload length mismatch (expected {expected}, got {})",
440 out.len()
441 )));
442 }
443 Ok(out)
444}
445
446#[cfg(not(feature = "bzf"))]
451fn decompress_payload(_payload: &[u8], _expected: usize) -> Result<Vec<u8>, BifBinaryError> {
452 Err(BifBinaryError::BzfFeatureDisabled)
453}
454
455fn read_region<R: Read + Seek>(
460 section: &SectionReader<R>,
461 offset: u64,
462 len: u64,
463 name: &str,
464) -> Result<Vec<u8>, BifBinaryError> {
465 section
466 .section(offset, len)
467 .ok_or_else(|| BifBinaryError::InvalidHeader(format!("{name} exceeds file bounds")))?
468 .read_all()
469 .map_err(BifBinaryError::Io)
470}
471
472fn u64_from(value: usize) -> Result<u64, BifBinaryError> {
474 u64::try_from(value)
475 .map_err(|_| BifBinaryError::InvalidHeader("offset exceeds addressable range".into()))
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481 use crate::bif::read_bif_from_bytes;
482 use std::io::Cursor;
483
484 const K1_PLAYER_BIF: &[u8] = include_bytes!(concat!(
485 env!("CARGO_MANIFEST_DIR"),
486 "/../../fixtures/k1_player.bif"
487 ));
488
489 fn index_over(bytes: &[u8]) -> BifIndex<Cursor<Vec<u8>>> {
490 let len = u64::try_from(bytes.len()).expect("fixture fits in u64");
491 let source = Arc::new(Mutex::new(Cursor::new(bytes.to_vec())));
492 BifIndex::new(SectionReader::whole(source, len)).expect("fixture indexes")
493 }
494
495 #[test]
496 fn indexes_the_same_resources_the_eager_reader_finds() {
497 let eager = read_bif_from_bytes(K1_PLAYER_BIF).expect("fixture parses eagerly");
498 let index = index_over(K1_PLAYER_BIF);
499
500 assert_eq!(index.len(), eager.resources.len());
501
502 for (position, expected) in eager.resources.iter().enumerate() {
503 let entry = index.entry(position).expect("entry exists");
504 assert_eq!(entry.resource_id, expected.resource_id);
505 assert_eq!(entry.resource_type, expected.resource_type);
506 assert_eq!(entry.storage, expected.storage);
507
508 let bytes = index.read_entry(position).expect("read succeeds");
509 assert_eq!(bytes, expected.data, "bytes differ at position {position}");
510 }
511 }
512
513 #[test]
514 fn resolves_by_resource_id() {
515 let eager = read_bif_from_bytes(K1_PLAYER_BIF).expect("fixture parses eagerly");
516 let index = index_over(K1_PLAYER_BIF);
517 let first = eager.resources.first().expect("fixture has resources");
518
519 let bytes = index
520 .resolve_by_id(first.resource_id)
521 .expect("resolve succeeds")
522 .expect("resource is present");
523
524 assert_eq!(bytes, first.data);
525 assert!(index.contains_id(first.resource_id));
526 }
527
528 #[test]
529 fn an_unknown_resource_id_is_a_clean_miss_not_an_error() {
530 let index = index_over(K1_PLAYER_BIF);
531 let absent = ResourceId::from_raw(u32::MAX);
532
533 let found = index.resolve_by_id(absent).expect("a miss is not an error");
534
535 assert!(found.is_none());
536 assert!(!index.contains_id(absent));
537 }
538
539 #[test]
540 fn iterating_yields_every_resource_with_its_bytes() {
541 let eager = read_bif_from_bytes(K1_PLAYER_BIF).expect("fixture parses eagerly");
542 let index = index_over(K1_PLAYER_BIF);
543
544 let collected = index
545 .iter_resources()
546 .collect::<Result<Vec<_>, _>>()
547 .expect("iteration succeeds");
548
549 assert_eq!(collected.len(), eager.resources.len());
550 for ((entry, bytes), expected) in collected.iter().zip(&eager.resources) {
551 assert_eq!(entry.resource_id, expected.resource_id);
552 assert_eq!(bytes, &expected.data);
553 }
554 }
555
556 #[test]
557 fn one_out_of_bounds_entry_does_not_cost_the_whole_archive() {
558 let mut bytes = K1_PLAYER_BIF.to_vec();
560 let header = crate::bif::layout::BifHeader::parse(&bytes, BifReadOptions::default())
561 .expect("fixture header parses");
562 let data_offset_field = header.variable_table_offset + 4;
563 bytes[data_offset_field..data_offset_field + 4].copy_from_slice(&u32::MAX.to_le_bytes());
564
565 let index = index_over(&bytes);
566 let eager_count = read_bif_from_bytes(K1_PLAYER_BIF)
567 .expect("fixture parses eagerly")
568 .resources
569 .len();
570
571 assert_eq!(index.len(), eager_count);
572 assert_eq!(index.defects().count(), 1);
573
574 let (position, entry, _) = index.defects().next().expect("one defect");
575 assert!(!entry.is_readable());
576 assert!(index.read_entry(position).is_err());
577 assert!(index.entry_section(position).is_none());
578
579 for other in 0..index.len() {
580 if other != position {
581 assert!(index.read_entry(other).is_ok(), "entry {other} should read");
582 }
583 }
584 }
585
586 #[cfg(feature = "bzf")]
587 #[test]
588 fn a_synthesized_compressed_archive_round_trips() {
589 use crate::bif::{write_bif_to_vec, Bif, BifContainer};
590
591 let mut bif = Bif::new();
592 bif.container = BifContainer::Bzf;
593 let payload = b"MAXLAYOUT ".repeat(64);
596 bif.push_resource(
597 ResourceId::from_raw(0x0010_0000),
598 ResourceTypeCode::from_raw_id(3000),
599 payload.clone(),
600 );
601 bif.push_resource(
602 ResourceId::from_raw(0x0010_0001),
603 ResourceTypeCode::from_raw_id(3000),
604 b"short".to_vec(),
605 );
606 let bytes = write_bif_to_vec(&bif).expect("compressed archive writes");
607
608 let len = u64::try_from(bytes.len()).expect("fits in u64");
609 let source = Arc::new(Mutex::new(Cursor::new(bytes)));
610 let index =
611 BifIndex::new_with_container(SectionReader::whole(source, len), BifContainer::Bzf)
612 .expect("compressed archive indexes");
613
614 assert_eq!(index.container(), BifContainer::Bzf);
615 assert_eq!(index.len(), 2);
616
617 let first = index.entry(0).expect("entry present");
618 assert_eq!(usize::try_from(first.size).expect("fits"), payload.len());
619 assert!(
620 first.packed_size < first.size,
621 "the payload should be smaller on disk than decompressed"
622 );
623
624 assert_eq!(index.read_entry(0).expect("first decodes"), payload);
625 assert_eq!(index.read_entry(1).expect("second decodes"), b"short");
626 }
627
628 #[cfg(feature = "bzf")]
635 #[test]
636 fn a_real_compressed_archive_decodes_when_one_is_provided() {
637 let Ok(path) = std::env::var("RAKATA_BZF_FIXTURE") else {
638 return;
639 };
640
641 let index = BifIndex::open(&path).expect("real archive indexes");
642 assert_eq!(
643 index.container(),
644 BifContainer::Bzf,
645 "a .bzf path must be inferred as compressed"
646 );
647 assert!(!index.is_empty(), "real archive should hold resources");
648 assert!(
649 !index.has_defects(),
650 "a healthy archive must not report defects; \
651 every entry unreadable would mean the container was misread"
652 );
653
654 let declared: u64 = index.entries().iter().map(|entry| entry.size).sum();
656 let packed: u64 = index.entries().iter().map(|entry| entry.packed_size).sum();
657 assert!(
658 declared > packed,
659 "declared {declared} should exceed packed {packed} in a compressed archive"
660 );
661
662 for position in 0..index.len() {
663 let entry = index.entry(position).expect("entry present");
664 let data = index
665 .read_entry(position)
666 .unwrap_or_else(|error| panic!("entry {position} failed to decode: {error}"));
667 assert_eq!(
668 u64::try_from(data.len()).expect("fits"),
669 entry.size,
670 "entry {position} decoded to the wrong length"
671 );
672 }
673 }
674
675 #[test]
676 fn a_truncated_archive_either_fails_to_index_or_reports_defects() {
677 let truncated = &K1_PLAYER_BIF[..K1_PLAYER_BIF.len() / 2];
681 let len = u64::try_from(truncated.len()).expect("fits in u64");
682 let source = Arc::new(Mutex::new(Cursor::new(truncated.to_vec())));
683
684 match BifIndex::new(SectionReader::whole(source, len)) {
685 Err(_) => {}
686 Ok(index) => assert!(
687 index.has_defects(),
688 "a truncated archive must not index clean"
689 ),
690 }
691 }
692}