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//! ## Format Layout
8//! ```text
9//! +------------------------------+ 0x0000
10//! | Header (20 bytes)            |
11//! +------------------------------+ variable_table_offset
12//! | Variable resource table      |
13//! | 16 bytes * resource_count    |
14//! +------------------------------+ (optional)
15//! | Fixed resource table         |
16//! | 20 bytes * fixed_count       |
17//! +------------------------------+ offsets from table
18//! | Resource payload blob        |
19//! | (aligned to 4-byte boundary) |
20//! +------------------------------+
21//! ```
22//!
23//! ## Variable Resource Entry (16 bytes)
24//! ```text
25//! 0x00..0x04  resource_id  (u32)
26//! 0x04..0x08  data_offset  (u32)
27//! 0x08..0x0C  data_size    (u32)
28//! 0x0C..0x10  type_id      (u32; validated to u16 range)
29//! ```
30//!
31//! ## Fixed Resource Entry (20 bytes)
32//! ```text
33//! 0x00..0x04  resource_id  (u32)
34//! 0x04..0x08  data_offset  (u32)
35//! 0x08..0x0C  part_count   (u32)
36//! 0x0C..0x10  data_size    (u32)
37//! 0x10..0x14  type_id      (u32; validated to u16 range)
38//! ```
39//!
40//! ## Compressed Variant (`.bzf`)
41//!
42//! The mobile ports ship the same archives with each payload individually
43//! LZMA-compressed. The container is otherwise identical, which is the
44//! awkward part: the signature is still `BIFF`, the header and tables are
45//! byte-for-byte the same shape, and the KEY still names the file `.bif`.
46//! Only the on-disk extension distinguishes them.
47//!
48//! ```text
49//! +------------------------------+ 0x0000
50//! | Header (20 bytes)            |  identical, signature still `BIFF`
51//! +------------------------------+ variable_table_offset
52//! | Variable resource table      |  data_size = UNCOMPRESSED length
53//! +------------------------------+ offsets from table
54//! | Payload: LZMA-alone stream   |  5-byte header, no length field
55//! |   0x00      properties byte  |    packs (pb * 5 + lp) * 9 + lc
56//! |   0x01..05  dictionary size  |    u32
57//! |   0x05..    compressed data  |    ends with an end-of-stream marker
58//! +------------------------------+
59//! ```
60//!
61//! An entry's *packed* extent is not stored anywhere. It runs from the
62//! entry's offset to wherever the next entry begins, with the last running
63//! to the end of the file. The uncompressed length in the table is what the
64//! decoder is told to produce.
65//!
66//! Notes:
67//! - Vanilla KotOR BIFs typically use only variable-resource entries
68//!   (`fixed_count == 0`), but files with fixed-table entries are accepted.
69//! - Compressed archives are gated behind the crate feature `bzf` and read
70//!   through [`BifIndex`](crate::bif::BifIndex), which takes the container kind from the caller
71//!   rather than guessing it from content.
72
73mod index;
74mod layout;
75mod reader;
76mod writer;
77
78pub use index::{BifIndex, BifIndexEntry};
79pub use reader::{
80    read_bif, read_bif_from_bytes, read_bif_from_bytes_with_options, read_bif_with_options,
81};
82pub use writer::{write_bif, write_bif_to_vec};
83
84use thiserror::Error;
85
86use rakata_core::{ResourceId, ResourceTypeCode};
87
88use crate::binary::{self, DecodeBinary, EncodeBinary};
89
90/// BIF header size in bytes.
91const FILE_HEADER_SIZE: usize = 20;
92/// Variable resource-table entry size.
93const VARIABLE_ENTRY_SIZE: usize = 16;
94/// Fixed resource-table entry size.
95const FIXED_ENTRY_SIZE: usize = 20;
96/// BIF file signature.
97const BIF_MAGIC: [u8; 4] = *b"BIFF";
98/// KotOR BIF version.
99const BIF_VERSION_V10: [u8; 4] = *b"V1  ";
100/// Alternate BIF version accepted by some tooling.
101const BIF_VERSION_V11: [u8; 4] = *b"V1.1";
102/// Size of the LZMA-alone header prefixing each compressed payload:
103/// one properties byte plus a little-endian dictionary size.
104#[cfg(feature = "bzf")]
105const LZMA_ALONE_HEADER_SIZE: usize = 5;
106
107/// BIF container kind.
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
109pub enum BifContainer {
110    /// Uncompressed `BIFF` container.
111    #[default]
112    Biff,
113    /// Compressed container: per-resource LZMA-alone payloads, table sizes
114    /// still recording uncompressed lengths.
115    Bzf,
116}
117
118/// One resource entry stored in a BIF archive.
119#[derive(Debug, Clone, Eq)]
120pub struct BifResource {
121    /// Resource ID used by KEY table lookup.
122    pub resource_id: ResourceId,
123    /// Resource type code from the variable table.
124    ///
125    /// Unknown IDs are preserved losslessly.
126    pub resource_type: ResourceTypeCode,
127    /// Source table metadata for this resource entry.
128    pub storage: BifResourceStorage,
129    /// Resource payload bytes.
130    pub data: Vec<u8>,
131    /// Original byte offset of this resource in its source file.
132    ///
133    /// `Some` when read from a file; the writer uses this offset verbatim (filling any
134    /// preceding gap with zero bytes) to preserve the exact on-disk layout.
135    /// `None` for programmatically-constructed resources; the writer then uses its
136    /// default 4-byte-aligned offset calculation.
137    pub source_data_offset: Option<u32>,
138}
139
140impl PartialEq for BifResource {
141    fn eq(&self, other: &Self) -> bool {
142        self.resource_id == other.resource_id
143            && self.resource_type == other.resource_type
144            && self.storage == other.storage
145            && self.data == other.data
146        // source_data_offset is layout metadata, not semantic content; excluded from equality.
147    }
148}
149
150/// Storage table kind for a [`BifResource`].
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
152pub enum BifResourceStorage {
153    /// Entry comes from the variable resource table.
154    Variable,
155    /// Entry comes from the fixed resource table.
156    Fixed {
157        /// Declared fixed part count from the fixed table entry.
158        part_count: u32,
159    },
160}
161
162/// In-memory BIF archive.
163#[derive(Debug, Clone, PartialEq, Eq, Default)]
164pub struct Bif {
165    /// On-disk container kind.
166    pub container: BifContainer,
167    /// Ordered resource entries.
168    pub resources: Vec<BifResource>,
169}
170
171/// BIF reader option set.
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
173pub struct BifReadOptions {
174    /// Input policy for accepted source variants.
175    pub input: BifReadMode,
176}
177
178impl Default for BifReadOptions {
179    fn default() -> Self {
180        Self {
181            input: BifReadMode::CanonicalK1,
182        }
183    }
184}
185
186/// BIF reader input policy.
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
188pub enum BifReadMode {
189    /// Accept only canonical vanilla K1 BIF variants.
190    ///
191    /// This mode requires `V1  ` and follows K1 runtime behavior by loading only
192    /// variable-table entries.
193    CanonicalK1,
194    /// Accept broader Aurora-family BIF variants.
195    ///
196    /// This mode accepts `V1.1`.
197    CompatibilityAurora,
198}
199
200impl Bif {
201    /// Creates an empty BIF archive.
202    pub fn new() -> Self {
203        Self::default()
204    }
205
206    /// Appends one resource entry.
207    pub fn push_resource(
208        &mut self,
209        resource_id: ResourceId,
210        resource_type: ResourceTypeCode,
211        data: Vec<u8>,
212    ) {
213        self.resources.push(BifResource {
214            resource_id,
215            resource_type,
216            storage: BifResourceStorage::Variable,
217            data,
218            source_data_offset: None,
219        });
220    }
221
222    /// Appends one fixed-table resource entry.
223    pub fn push_fixed_resource(
224        &mut self,
225        resource_id: ResourceId,
226        resource_type: ResourceTypeCode,
227        part_count: u32,
228        data: Vec<u8>,
229    ) {
230        self.resources.push(BifResource {
231            resource_id,
232            resource_type,
233            storage: BifResourceStorage::Fixed { part_count },
234            data,
235            source_data_offset: None,
236        });
237    }
238
239    /// Returns the first matching resource payload by resource ID.
240    pub fn resource_by_id(&self, resource_id: ResourceId) -> Option<&[u8]> {
241        self.resources
242            .iter()
243            .find(|resource| resource.resource_id == resource_id)
244            .map(|resource| resource.data.as_slice())
245    }
246}
247
248impl DecodeBinary for Bif {
249    type Error = BifBinaryError;
250
251    fn decode_binary(bytes: &[u8]) -> Result<Self, Self::Error> {
252        read_bif_from_bytes(bytes)
253    }
254}
255
256impl EncodeBinary for Bif {
257    type Error = BifBinaryError;
258
259    fn encode_binary(&self) -> Result<Vec<u8>, Self::Error> {
260        write_bif_to_vec(self)
261    }
262}
263
264/// Errors produced while parsing or serializing BIF binary data.
265#[derive(Debug, Error)]
266pub enum BifBinaryError {
267    /// I/O read/write failure.
268    #[error(transparent)]
269    Io(#[from] std::io::Error),
270    /// Header signature is unsupported.
271    #[error("invalid BIF magic: {0:?}")]
272    InvalidMagic([u8; 4]),
273    /// Header version is unsupported.
274    #[error("invalid BIF version: {0:?}")]
275    InvalidVersion([u8; 4]),
276    /// BZF support is unavailable because the `bzf` crate feature is disabled.
277    #[error("BZF support requires enabling the `bzf` feature on `rakata-formats`")]
278    BzfFeatureDisabled,
279    /// Header/body layout is invalid or truncated.
280    #[error("invalid BIF header: {0}")]
281    InvalidHeader(String),
282    /// Archive content is structurally invalid.
283    #[error("invalid BIF data: {0}")]
284    InvalidData(String),
285    /// Value cannot fit on-disk integer width.
286    #[error("value overflow while writing field `{0}`")]
287    ValueOverflow(&'static str),
288}
289
290impl From<binary::BinaryLayoutError> for BifBinaryError {
291    fn from(error: binary::BinaryLayoutError) -> Self {
292        Self::InvalidHeader(error.to_string())
293    }
294}