Skip to main content

rakata_formats/bif/
mod.rs

1//! BIF binary reader and writer.
2//!
3//! BIF (`BIFF`) files are archive containers referenced by KEY indexes.
4//! Each entry stores only `(resource_id, type_id, payload)`; names/resrefs are
5//! resolved through KEY metadata.
6//!
7//! ## Shape of the container
8//!
9//! A fixed 20-byte header, a variable resource table located by
10//! `variable_table_offset`, an optional fixed resource table directly after it,
11//! and the payload addressed per entry.
12//!
13//! Nothing here carries a name. An entry knows its id, type, offset and size;
14//! the resref lives in the [`KEY`](crate::key), which reaches in by position.
15//! That is why a BIF cannot be read on its own.
16//!
17//! The fixed table is a format feature nothing uses: `fixed_count` is zero in
18//! all 26 archives of a vanilla install, and the engine ignores the scalar
19//! anyway. The table is parsed when present so such a file round-trips.
20//!
21//! Compressed `.bzf` archives are gated behind the crate feature `bzf` and read
22//! through [`BifIndex`](crate::bif::BifIndex), which takes the container kind
23//! from the caller rather than guessing it from content -- nothing inside a
24//! compressed file distinguishes it.
25//!
26//! Byte-level field maps for the header and both entry records, the compressed
27//! variant and its LZMA payload framing, and the engine's own load sequence
28//! live in `docs/src/formats/archives/bif.md`.
29
30mod index;
31mod layout;
32mod reader;
33mod writer;
34
35pub use index::{BifIndex, BifIndexEntry};
36pub use reader::{
37    read_bif, read_bif_from_bytes, read_bif_from_bytes_with_options, read_bif_with_options,
38};
39pub use writer::{write_bif, write_bif_to_vec};
40
41use thiserror::Error;
42
43use rakata_core::{ResourceId, ResourceTypeCode};
44
45use crate::binary::{self, DecodeBinary, EncodeBinary};
46
47/// BIF header size in bytes.
48const FILE_HEADER_SIZE: usize = 20;
49/// Variable resource-table entry size.
50const VARIABLE_ENTRY_SIZE: usize = 16;
51/// Fixed resource-table entry size.
52const FIXED_ENTRY_SIZE: usize = 20;
53/// BIF file signature.
54const BIF_MAGIC: [u8; 4] = *b"BIFF";
55/// KotOR BIF version.
56const BIF_VERSION_V10: [u8; 4] = *b"V1  ";
57/// Alternate BIF version accepted by some tooling.
58const BIF_VERSION_V11: [u8; 4] = *b"V1.1";
59/// Size of the LZMA-alone header prefixing each compressed payload:
60/// one properties byte plus a little-endian dictionary size.
61#[cfg(feature = "bzf")]
62const LZMA_ALONE_HEADER_SIZE: usize = 5;
63
64/// BIF container kind.
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
66pub enum BifContainer {
67    /// Uncompressed `BIFF` container.
68    #[default]
69    Biff,
70    /// Compressed container: per-resource LZMA-alone payloads, table sizes
71    /// still recording uncompressed lengths.
72    Bzf,
73}
74
75/// One resource entry stored in a BIF archive.
76#[derive(Debug, Clone, Eq)]
77pub struct BifResource {
78    /// Resource ID used by KEY table lookup.
79    pub resource_id: ResourceId,
80    /// Resource type code from the variable table.
81    ///
82    /// Unknown IDs are preserved losslessly.
83    pub resource_type: ResourceTypeCode,
84    /// Source table metadata for this resource entry.
85    pub storage: BifResourceStorage,
86    /// Resource payload bytes.
87    pub data: Vec<u8>,
88    /// Original byte offset of this resource in its source file.
89    ///
90    /// `Some` when read from a file; the writer uses this offset verbatim (filling any
91    /// preceding gap with zero bytes) to preserve the exact on-disk layout.
92    /// `None` for programmatically-constructed resources; the writer then uses its
93    /// default 4-byte-aligned offset calculation.
94    pub source_data_offset: Option<u32>,
95}
96
97impl PartialEq for BifResource {
98    fn eq(&self, other: &Self) -> bool {
99        self.resource_id == other.resource_id
100            && self.resource_type == other.resource_type
101            && self.storage == other.storage
102            && self.data == other.data
103        // source_data_offset is layout metadata, not semantic content; excluded from equality.
104    }
105}
106
107/// Storage table kind for a [`BifResource`].
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
109pub enum BifResourceStorage {
110    /// Entry comes from the variable resource table.
111    Variable,
112    /// Entry comes from the fixed resource table.
113    Fixed {
114        /// Declared fixed part count from the fixed table entry.
115        part_count: u32,
116    },
117}
118
119/// In-memory BIF archive.
120#[derive(Debug, Clone, PartialEq, Eq, Default)]
121pub struct Bif {
122    /// On-disk container kind.
123    pub container: BifContainer,
124    /// Ordered resource entries.
125    pub resources: Vec<BifResource>,
126}
127
128/// BIF reader option set.
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
130pub struct BifReadOptions {
131    /// Input policy for accepted source variants.
132    pub input: BifReadMode,
133}
134
135impl Default for BifReadOptions {
136    fn default() -> Self {
137        Self {
138            input: BifReadMode::CanonicalK1,
139        }
140    }
141}
142
143/// BIF reader input policy.
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
145pub enum BifReadMode {
146    /// Accept only canonical vanilla K1 BIF variants.
147    ///
148    /// This mode requires `V1  ` and follows K1 runtime behavior by loading only
149    /// variable-table entries.
150    CanonicalK1,
151    /// Accept broader Aurora-family BIF variants.
152    ///
153    /// This mode accepts `V1.1`.
154    CompatibilityAurora,
155}
156
157impl Bif {
158    /// Creates an empty BIF archive.
159    pub fn new() -> Self {
160        Self::default()
161    }
162
163    /// Appends one resource entry.
164    pub fn push_resource(
165        &mut self,
166        resource_id: ResourceId,
167        resource_type: ResourceTypeCode,
168        data: Vec<u8>,
169    ) {
170        self.resources.push(BifResource {
171            resource_id,
172            resource_type,
173            storage: BifResourceStorage::Variable,
174            data,
175            source_data_offset: None,
176        });
177    }
178
179    /// Appends one fixed-table resource entry.
180    pub fn push_fixed_resource(
181        &mut self,
182        resource_id: ResourceId,
183        resource_type: ResourceTypeCode,
184        part_count: u32,
185        data: Vec<u8>,
186    ) {
187        self.resources.push(BifResource {
188            resource_id,
189            resource_type,
190            storage: BifResourceStorage::Fixed { part_count },
191            data,
192            source_data_offset: None,
193        });
194    }
195
196    /// Returns the first matching resource payload by resource ID.
197    pub fn resource_by_id(&self, resource_id: ResourceId) -> Option<&[u8]> {
198        self.resources
199            .iter()
200            .find(|resource| resource.resource_id == resource_id)
201            .map(|resource| resource.data.as_slice())
202    }
203}
204
205impl DecodeBinary for Bif {
206    type Error = BifBinaryError;
207
208    fn decode_binary(bytes: &[u8]) -> Result<Self, Self::Error> {
209        read_bif_from_bytes(bytes)
210    }
211}
212
213impl EncodeBinary for Bif {
214    type Error = BifBinaryError;
215
216    fn encode_binary(&self) -> Result<Vec<u8>, Self::Error> {
217        write_bif_to_vec(self)
218    }
219}
220
221/// Errors produced while parsing or serializing BIF binary data.
222#[derive(Debug, Error)]
223pub enum BifBinaryError {
224    /// I/O read/write failure.
225    #[error(transparent)]
226    Io(#[from] std::io::Error),
227    /// Header signature is unsupported.
228    #[error("invalid BIF magic: {0:?}")]
229    InvalidMagic([u8; 4]),
230    /// Header version is unsupported.
231    #[error("invalid BIF version: {0:?}")]
232    InvalidVersion([u8; 4]),
233    /// BZF support is unavailable because the `bzf` crate feature is disabled.
234    #[error("BZF support requires enabling the `bzf` feature on `rakata-formats`")]
235    BzfFeatureDisabled,
236    /// Header/body layout is invalid or truncated.
237    #[error("invalid BIF header: {0}")]
238    InvalidHeader(String),
239    /// Archive content is structurally invalid.
240    #[error("invalid BIF data: {0}")]
241    InvalidData(String),
242    /// Value cannot fit on-disk integer width.
243    #[error("value overflow while writing field `{0}`")]
244    ValueOverflow(&'static str),
245}
246
247impl From<binary::BinaryLayoutError> for BifBinaryError {
248    fn from(error: binary::BinaryLayoutError) -> Self {
249        Self::InvalidHeader(error.to_string())
250    }
251}