Skip to main content

rakata_formats/bif/
index.rs

1//! Lazy BIF index: resource tables in memory, entry bytes on demand.
2//!
3//! The third lazy container, and the one that behaves differently. A BIF
4//! stores no resource names: the resref lives in the KEY file, which points
5//! into a BIF by `(bif_index, resource_index)`. So lookups here are by
6//! position or by resource id, never by name, and the KEY layer supplies the
7//! naming.
8//!
9//! This matters more for BIFs than for the other containers because the base
10//! game corpus is a handful of very large archives. Indexing one costs its
11//! table; reading the whole thing costs hundreds of megabytes.
12//!
13//! ## Compressed archives
14//!
15//! Compression is stated by the caller rather than guessed: path constructors
16//! read it off the filename, windowed constructors take it explicitly. That is
17//! forced by the format, which offers no internal discriminator at all -- see
18//! `docs/src/formats/archives/bif.md` for why. Guessing structurally would be
19//! worse than useless here anyway, now that an out-of-bounds entry is a
20//! tolerated state rather than a contradiction.
21
22use 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
35/// Extension marking an archive whose payloads are LZMA-compressed.
36const COMPRESSED_EXTENSION: &str = "bzf";
37use crate::archive::EntryDefect;
38use crate::section_reader::SectionReader;
39
40/// One indexed resource: its identity and where its bytes live.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct BifIndexEntry {
43    /// Resource id linking this entry to a KEY table row.
44    pub resource_id: ResourceId,
45    /// Resource type code.
46    pub resource_type: ResourceTypeCode,
47    /// Source table this entry came from.
48    pub storage: BifResourceStorage,
49    /// Byte offset of the resource data within the archive, as declared by
50    /// the resource table.
51    pub offset: u64,
52    /// Byte length of the resource data, as declared by the resource table.
53    ///
54    /// For a compressed archive this is the *uncompressed* length; the bytes
55    /// on disk occupy [`Self::packed_size`] instead.
56    pub size: u64,
57    /// Byte length the resource actually occupies on disk.
58    ///
59    /// Equal to `size` for an uncompressed archive.
60    pub packed_size: u64,
61    /// Set when the entry cannot be read, leaving the rest of the archive
62    /// usable. Reading a defective entry returns this as an error.
63    pub defect: Option<EntryDefect>,
64}
65
66impl BifIndexEntry {
67    /// Returns whether this entry's bytes can be read.
68    pub fn is_readable(&self) -> bool {
69        self.defect.is_none()
70    }
71}
72
73/// A lazily-read BIF archive.
74///
75/// Holds both resource tables; payload bytes are read from the backing source
76/// on each request.
77///
78/// Compressed archives are supported behind the `bzf` feature: each entry's
79/// packed extent is derived from offset ordering and decompressed on read, so
80/// the owned-bytes contract is identical either way and callers need not care
81/// which kind they mounted.
82#[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    /// Opens an archive from disk and indexes it.
92    ///
93    /// Compression is inferred from the path: a `.bzf` extension
94    /// (case-insensitive) means LZMA-compressed payloads, anything else means
95    /// plain. That is the only signal shipping data carries — the file
96    /// signature is `BIFF` either way — so it is read here rather than
97    /// guessed from content.
98    ///
99    /// # Errors
100    ///
101    /// The same as [`Self::open_with_options`] under the default options.
102    pub fn open(path: impl AsRef<Path>) -> Result<Self, BifBinaryError> {
103        Self::open_with_options(path, BifReadOptions::default())
104    }
105
106    /// Opens an archive from disk with explicit read options.
107    ///
108    /// Compression is inferred from the path as in [`Self::open`].
109    ///
110    /// # Errors
111    ///
112    /// [`BifBinaryError::Io`] when `path` will not open or its length cannot
113    /// be read, and whatever [`Self::new_with_options`] reports for the file
114    /// it opened. A `.bzf` path with the `bzf` feature off fails there, as
115    /// [`BifBinaryError::BzfFeatureDisabled`].
116    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
132/// Returns the container kind a path's extension denotes.
133fn 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    /// Indexes an uncompressed archive occupying `section`.
147    ///
148    /// # Errors
149    ///
150    /// The same as [`Self::new_with_options`] for a
151    /// [`BifContainer::Biff`](crate::bif::BifContainer::Biff) archive under
152    /// the default options.
153    pub fn new(section: SectionReader<R>) -> Result<Self, BifBinaryError> {
154        Self::new_with_options(section, BifContainer::Biff, BifReadOptions::default())
155    }
156
157    /// Indexes an archive occupying `section`, stating its container kind.
158    ///
159    /// Windowed constructors take the kind explicitly because a window has no
160    /// filename to read it from.
161    ///
162    /// # Errors
163    ///
164    /// The same as [`Self::new_with_options`] under the default options.
165    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    /// Indexes the archive occupying `section` with explicit read options.
173    ///
174    /// Reads both resource tables immediately. A table that will not parse
175    /// fails here, because nothing about the archive is knowable without it.
176    /// An entry whose declared byte range escapes the archive does not: it is
177    /// marked with a defect and the rest stays readable.
178    ///
179    /// # Errors
180    ///
181    /// [`BifBinaryError::InvalidHeader`] when the header or either table
182    /// escapes the section's bounds or the two entry counts sum past `u32`,
183    /// and [`BifBinaryError::InvalidMagic`] or
184    /// [`BifBinaryError::InvalidVersion`] for a signature or version
185    /// `options.input` does not accept.
186    ///
187    /// [`BifBinaryError::BzfFeatureDisabled`] when `container` is
188    /// [`Bzf`](crate::bif::BifContainer::Bzf) and the crate was built without
189    /// the `bzf` feature. Indexing does not decompress anything, so this is
190    /// refused up front rather than at the first read.
191    ///
192    /// [`BifBinaryError::Io`] when the backing source fails.
193    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(&section, 0, u64_from(FILE_HEADER_SIZE)?, "BIF header")?;
202        let header = BifHeader::parse(&header_bytes, options)?;
203
204        let variable_table = read_region(
205            &section,
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                &section,
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        // A compressed archive stores no packed length, so the on-disk extent
229        // of each entry is derived from where the next one begins.
230        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            // An entry pointing outside the file poisons only itself: the
249            // rest of the archive stays readable, and a caller wanting
250            // strictness inspects the defects. The packed extent is what
251            // occupies the file, so that is what gets bounds-checked.
252            let defect = EntryDefect::check_bounds(offset, packed_size, section.len());
253
254            let resource_id = ResourceId::from_raw(entry.resource_id);
255            // First entry wins on a duplicate id, matching the eager reader's
256            // first-match lookup.
257            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    /// Reads the bytes of the entry at `position` in table order.
278    ///
279    /// This is the lookup a KEY table drives: its rows carry the resource's
280    /// position within the BIF.
281    ///
282    /// # Errors
283    ///
284    /// [`BifBinaryError::InvalidData`] when `position` is past the end of the
285    /// table or the backing read comes up short, and
286    /// [`BifBinaryError::InvalidHeader`] when the entry carries the defect
287    /// [`Self::new_with_options`] leaves on one whose range escapes the
288    /// archive.
289    ///
290    /// For a `BZF ` archive the payload is decompressed here rather than at
291    /// index time, so [`BifBinaryError::InvalidData`] also covers a declared
292    /// length beyond addressable memory and an LZMA stream that will not
293    /// decode. A plain `BIFF` entry has neither failure.
294    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    /// Returns the container kind this archive was opened as.
334    pub fn container(&self) -> BifContainer {
335        self.container
336    }
337
338    /// Reads the bytes of the resource carrying `resource_id`.
339    ///
340    /// Returns `Ok(None)` when no entry carries that id.
341    ///
342    /// # Errors
343    ///
344    /// Only once the lookup has matched, so an error here means a damaged
345    /// archive rather than a miss: the terms [`Self::read_entry`] gives.
346    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    /// Returns a window over the entry's bytes without reading them.
357    ///
358    /// Returns `None` when `position` is out of range or the entry carries a
359    /// defect.
360    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    /// Iterates every resource, pairing its entry with freshly-read bytes.
369    ///
370    /// Each item is read on demand, so a caller that stops early pays only for
371    /// what it consumed.
372    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    /// Returns the indexed entries in table order.
384    pub fn entries(&self) -> &[BifIndexEntry] {
385        &self.entries
386    }
387
388    /// Returns the entry at `position` without reading its bytes.
389    pub fn entry(&self, position: usize) -> Option<&BifIndexEntry> {
390        self.entries.get(position)
391    }
392
393    /// Returns the number of indexed resources.
394    pub fn len(&self) -> usize {
395        self.entries.len()
396    }
397
398    /// Returns whether the archive holds no resources.
399    pub fn is_empty(&self) -> bool {
400        self.entries.is_empty()
401    }
402
403    /// Returns whether the archive holds a resource with `resource_id`.
404    ///
405    /// True even when the entry is defective: the archive claims the resource
406    /// exists, it just cannot be read.
407    pub fn contains_id(&self, resource_id: ResourceId) -> bool {
408        self.by_resource_id.contains_key(&resource_id)
409    }
410
411    /// Iterates the entries that cannot be read, with their table positions.
412    ///
413    /// Whatever mounts this archive is expected to surface these rather than
414    /// let a partially-readable archive pass as intact.
415    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    /// Returns whether any entry in the archive is unreadable.
423    pub fn has_defects(&self) -> bool {
424        self.entries.iter().any(|entry| entry.defect.is_some())
425    }
426}
427
428/// Decompresses one LZMA-alone payload to its declared length.
429///
430/// Compressed entries are stored as a bare LZMA-alone stream: a 5-byte header
431/// carrying the properties byte and dictionary size, then the compressed data,
432/// with no trailing length field. The uncompressed length lives in the entry
433/// table instead, which is why it has to be supplied here.
434///
435/// The header is read from the payload rather than assumed. Hardcoding the
436/// observed values would decode the archives that have been looked at and
437/// silently mis-decode any encoder that chose differently.
438#[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    // The properties byte packs the three coder parameters as
461    // `(pb * 5 + lp) * 9 + lc`, the standard LZMA encoding.
462    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/// Stand-in used when the `bzf` feature is off.
495///
496/// Unreachable in practice: opening a compressed archive already fails with
497/// [`BifBinaryError::BzfFeatureDisabled`].
498#[cfg(not(feature = "bzf"))]
499fn decompress_payload(_payload: &[u8], _expected: usize) -> Result<Vec<u8>, BifBinaryError> {
500    Err(BifBinaryError::BzfFeatureDisabled)
501}
502
503/// Reads a named region out of the backing section.
504///
505/// Maps an out-of-bounds region onto the same "exceeds file bounds" wording
506/// the eager reader produces for the equivalent failure.
507fn 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
520/// Widens a table-derived `usize` to `u64` for offset arithmetic.
521fn 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        // Point the first resource past the end, leaving both tables intact.
607        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        // Repetitive content so compression actually shortens it, proving the
642        // read went through the decoder rather than past it.
643        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    /// Reads a real compressed archive when one is supplied out of band.
677    ///
678    /// Shipping archives cannot be committed here, so this is opt-in: point
679    /// `RAKATA_BZF_FIXTURE` at an extracted `.bzf` and the test runs against
680    /// ground truth instead of this crate's own encoder output. Skipped
681    /// otherwise: a synthesized fixture only proves the encoder and the
682    /// decoder agree with each other.
683    #[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        // Declared sizes exceeding the file is the signature of compression.
704        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        // Which of the two depends on where the cut lands: losing the tables
727        // makes the archive unknowable, while losing only payload leaves the
728        // tables describing entries that now point past the end.
729        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}