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//! The mobile ports ship the same archives LZMA-compressed, one stream per
16//! resource, under a `.bzf` extension. Nothing inside the file says so: the
17//! signature is `BIFF` either way and the KEY still names the file `.bif`.
18//! The extension is the only discriminator shipping data provides, so
19//! compression is stated by the caller rather than guessed — path
20//! constructors read it off the filename, and windowed constructors take it
21//! explicitly. Guessing structurally would be worse than useless now that an
22//! out-of-bounds entry is a tolerated state rather than a contradiction.
23
24use 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
37/// Extension marking an archive whose payloads are LZMA-compressed.
38const COMPRESSED_EXTENSION: &str = "bzf";
39use crate::archive::EntryDefect;
40use crate::section_reader::SectionReader;
41
42/// One indexed resource: its identity and where its bytes live.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct BifIndexEntry {
45    /// Resource id linking this entry to a KEY table row.
46    pub resource_id: ResourceId,
47    /// Resource type code.
48    pub resource_type: ResourceTypeCode,
49    /// Source table this entry came from.
50    pub storage: BifResourceStorage,
51    /// Byte offset of the resource data within the archive, as declared by
52    /// the resource table.
53    pub offset: u64,
54    /// Byte length of the resource data, as declared by the resource table.
55    ///
56    /// For a compressed archive this is the *uncompressed* length; the bytes
57    /// on disk occupy [`Self::packed_size`] instead.
58    pub size: u64,
59    /// Byte length the resource actually occupies on disk.
60    ///
61    /// Equal to `size` for an uncompressed archive.
62    pub packed_size: u64,
63    /// Set when the entry cannot be read, leaving the rest of the archive
64    /// usable. Reading a defective entry returns this as an error.
65    pub defect: Option<EntryDefect>,
66}
67
68impl BifIndexEntry {
69    /// Returns whether this entry's bytes can be read.
70    pub fn is_readable(&self) -> bool {
71        self.defect.is_none()
72    }
73}
74
75/// A lazily-read BIF archive.
76///
77/// Holds both resource tables; payload bytes are read from the backing source
78/// on each request.
79///
80/// Compressed archives are supported behind the `bzf` feature: each entry's
81/// packed extent is derived from offset ordering and decompressed on read, so
82/// the owned-bytes contract is identical either way and callers need not care
83/// which kind they mounted.
84#[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    /// Opens an archive from disk and indexes it.
94    ///
95    /// Compression is inferred from the path: a `.bzf` extension
96    /// (case-insensitive) means LZMA-compressed payloads, anything else means
97    /// plain. That is the only signal shipping data carries — the file
98    /// signature is `BIFF` either way — so it is read here rather than
99    /// guessed from content.
100    ///
101    /// Errors when the file cannot be opened, its tables do not parse, or it
102    /// is compressed and the `bzf` feature is off.
103    pub fn open(path: impl AsRef<Path>) -> Result<Self, BifBinaryError> {
104        Self::open_with_options(path, BifReadOptions::default())
105    }
106
107    /// Opens an archive from disk with explicit read options.
108    ///
109    /// Compression is inferred from the path as in [`Self::open`].
110    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
126/// Returns the container kind a path's extension denotes.
127fn 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    /// Indexes an uncompressed archive occupying `section`.
141    pub fn new(section: SectionReader<R>) -> Result<Self, BifBinaryError> {
142        Self::new_with_options(section, BifContainer::Biff, BifReadOptions::default())
143    }
144
145    /// Indexes an archive occupying `section`, stating its container kind.
146    ///
147    /// Windowed constructors take the kind explicitly because a window has no
148    /// filename to read it from.
149    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    /// Indexes the archive occupying `section` with explicit read options.
157    ///
158    /// Reads both resource tables immediately. A table that will not parse
159    /// fails here, because nothing about the archive is knowable without it.
160    /// An entry whose declared byte range escapes the archive does not: it is
161    /// marked with a defect and the rest stays readable.
162    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(&section, 0, u64_from(FILE_HEADER_SIZE)?, "BIF header")?;
171        let header = BifHeader::parse(&header_bytes, options)?;
172
173        let variable_table = read_region(
174            &section,
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                &section,
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        // A compressed archive stores no packed length, so the on-disk extent
198        // of each entry is derived from where the next one begins.
199        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            // An entry pointing outside the file poisons only itself: the
218            // rest of the archive stays readable, and a caller wanting
219            // strictness inspects the defects. The packed extent is what
220            // occupies the file, so that is what gets bounds-checked.
221            let defect = EntryDefect::check_bounds(offset, packed_size, section.len());
222
223            let resource_id = ResourceId::from_raw(entry.resource_id);
224            // First entry wins on a duplicate id, matching the eager reader's
225            // first-match lookup.
226            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    /// Reads the bytes of the entry at `position` in table order.
247    ///
248    /// This is the lookup a KEY table drives: its rows carry the resource's
249    /// position within the BIF.
250    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    /// Returns the container kind this archive was opened as.
290    pub fn container(&self) -> BifContainer {
291        self.container
292    }
293
294    /// Reads the bytes of the resource carrying `resource_id`.
295    ///
296    /// Returns `Ok(None)` when no entry carries that id, and `Err` only when a
297    /// matching entry exists but its bytes cannot be read.
298    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    /// Returns a window over the entry's bytes without reading them.
309    ///
310    /// Returns `None` when `position` is out of range or the entry carries a
311    /// defect.
312    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    /// Iterates every resource, pairing its entry with freshly-read bytes.
321    ///
322    /// Each item is read on demand, so a caller that stops early pays only for
323    /// what it consumed.
324    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    /// Returns the indexed entries in table order.
336    pub fn entries(&self) -> &[BifIndexEntry] {
337        &self.entries
338    }
339
340    /// Returns the entry at `position` without reading its bytes.
341    pub fn entry(&self, position: usize) -> Option<&BifIndexEntry> {
342        self.entries.get(position)
343    }
344
345    /// Returns the number of indexed resources.
346    pub fn len(&self) -> usize {
347        self.entries.len()
348    }
349
350    /// Returns whether the archive holds no resources.
351    pub fn is_empty(&self) -> bool {
352        self.entries.is_empty()
353    }
354
355    /// Returns whether the archive holds a resource with `resource_id`.
356    ///
357    /// True even when the entry is defective: the archive claims the resource
358    /// exists, it just cannot be read.
359    pub fn contains_id(&self, resource_id: ResourceId) -> bool {
360        self.by_resource_id.contains_key(&resource_id)
361    }
362
363    /// Iterates the entries that cannot be read, with their table positions.
364    ///
365    /// Whatever mounts this archive is expected to surface these rather than
366    /// let a partially-readable archive pass as intact.
367    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    /// Returns whether any entry in the archive is unreadable.
375    pub fn has_defects(&self) -> bool {
376        self.entries.iter().any(|entry| entry.defect.is_some())
377    }
378}
379
380/// Decompresses one LZMA-alone payload to its declared length.
381///
382/// Compressed entries are stored as a bare LZMA-alone stream: a 5-byte header
383/// carrying the properties byte and dictionary size, then the compressed data,
384/// with no trailing length field. The uncompressed length lives in the entry
385/// table instead, which is why it has to be supplied here.
386///
387/// The header is read from the payload rather than assumed. Hardcoding the
388/// observed values would decode the archives we happen to have looked at and
389/// silently mis-decode any encoder that chose differently.
390#[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    // The properties byte packs the three coder parameters as
413    // `(pb * 5 + lp) * 9 + lc`, the standard LZMA encoding.
414    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/// Stand-in used when the `bzf` feature is off.
447///
448/// Unreachable in practice: opening a compressed archive already fails with
449/// [`BifBinaryError::BzfFeatureDisabled`].
450#[cfg(not(feature = "bzf"))]
451fn decompress_payload(_payload: &[u8], _expected: usize) -> Result<Vec<u8>, BifBinaryError> {
452    Err(BifBinaryError::BzfFeatureDisabled)
453}
454
455/// Reads a named region out of the backing section.
456///
457/// Maps an out-of-bounds region onto the same "exceeds file bounds" wording
458/// the eager reader produces for the equivalent failure.
459fn 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
472/// Widens a table-derived `usize` to `u64` for offset arithmetic.
473fn 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        // Point the first resource past the end, leaving both tables intact.
559        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        // Repetitive content so compression actually shortens it, proving the
594        // read went through the decoder rather than past it.
595        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    /// Reads a real compressed archive when one is supplied out of band.
629    ///
630    /// Shipping archives cannot be committed here, so this is opt-in: point
631    /// `RAKATA_BZF_FIXTURE` at an extracted `.bzf` and the test runs against
632    /// ground truth instead of our own encoder's output. Skipped otherwise,
633    /// because a synthesized fixture can only prove we agree with ourselves.
634    #[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        // Declared sizes exceeding the file is the signature of compression.
655        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        // Which of the two depends on where the cut lands: losing the tables
678        // makes the archive unknowable, while losing only payload leaves the
679        // tables describing entries that now point past the end.
680        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}