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//! Notes:
41//! - Vanilla KotOR BIFs typically use only variable-resource entries
42//!   (`fixed_count == 0`), but files with fixed-table entries are accepted.
43//! - BZF (`BZF `) compressed variant support is optional and gated behind the
44//!   crate feature `bzf`.
45
46mod reader;
47mod writer;
48
49pub use reader::{
50    read_bif, read_bif_from_bytes, read_bif_from_bytes_with_options, read_bif_with_options,
51};
52pub use writer::{write_bif, write_bif_to_vec};
53
54use thiserror::Error;
55
56use rakata_core::{ResourceId, ResourceTypeCode};
57
58use crate::binary::{self, DecodeBinary, EncodeBinary};
59
60/// BIF header size in bytes.
61const FILE_HEADER_SIZE: usize = 20;
62/// Variable resource-table entry size.
63const VARIABLE_ENTRY_SIZE: usize = 16;
64/// Fixed resource-table entry size.
65const FIXED_ENTRY_SIZE: usize = 20;
66/// BIF file signature.
67const BIF_MAGIC: [u8; 4] = *b"BIFF";
68/// BZF file signature (not currently decoded).
69const BZF_MAGIC: [u8; 4] = *b"BZF ";
70/// KotOR BIF version.
71const BIF_VERSION_V10: [u8; 4] = *b"V1  ";
72/// Alternate BIF version accepted by some tooling.
73const BIF_VERSION_V11: [u8; 4] = *b"V1.1";
74
75/// BIF container kind.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
77pub enum BifContainer {
78    /// Uncompressed `BIFF` container.
79    #[default]
80    Biff,
81    /// Compressed `BZF ` container (LZMA payloads).
82    Bzf,
83}
84
85/// One resource entry stored in a BIF archive.
86#[derive(Debug, Clone, Eq)]
87pub struct BifResource {
88    /// Resource ID used by KEY table lookup.
89    pub resource_id: ResourceId,
90    /// Resource type code from the variable table.
91    ///
92    /// Unknown IDs are preserved losslessly.
93    pub resource_type: ResourceTypeCode,
94    /// Source table metadata for this resource entry.
95    pub storage: BifResourceStorage,
96    /// Resource payload bytes.
97    pub data: Vec<u8>,
98    /// Original byte offset of this resource in its source file.
99    ///
100    /// `Some` when read from a file; the writer uses this offset verbatim (filling any
101    /// preceding gap with zero bytes) to preserve the exact on-disk layout.
102    /// `None` for programmatically-constructed resources; the writer then uses its
103    /// default 4-byte-aligned offset calculation.
104    pub source_data_offset: Option<u32>,
105}
106
107impl PartialEq for BifResource {
108    fn eq(&self, other: &Self) -> bool {
109        self.resource_id == other.resource_id
110            && self.resource_type == other.resource_type
111            && self.storage == other.storage
112            && self.data == other.data
113        // source_data_offset is layout metadata, not semantic content; excluded from equality.
114    }
115}
116
117/// Storage table kind for a [`BifResource`].
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
119pub enum BifResourceStorage {
120    /// Entry comes from the variable resource table.
121    Variable,
122    /// Entry comes from the fixed resource table.
123    Fixed {
124        /// Declared fixed part count from the fixed table entry.
125        part_count: u32,
126    },
127}
128
129/// In-memory BIF archive.
130#[derive(Debug, Clone, PartialEq, Eq, Default)]
131pub struct Bif {
132    /// On-disk container kind.
133    pub container: BifContainer,
134    /// Ordered resource entries.
135    pub resources: Vec<BifResource>,
136}
137
138/// BIF reader option set.
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
140pub struct BifReadOptions {
141    /// Input policy for accepted source variants.
142    pub input: BifReadMode,
143}
144
145impl Default for BifReadOptions {
146    fn default() -> Self {
147        Self {
148            input: BifReadMode::CanonicalK1,
149        }
150    }
151}
152
153/// BIF reader input policy.
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
155pub enum BifReadMode {
156    /// Accept only canonical vanilla K1 BIF variants.
157    ///
158    /// This mode requires `V1  ` and follows K1 runtime behavior by loading only
159    /// variable-table entries.
160    CanonicalK1,
161    /// Accept broader Aurora-family BIF variants.
162    ///
163    /// This mode accepts `V1.1`.
164    CompatibilityAurora,
165}
166
167impl Bif {
168    /// Creates an empty BIF archive.
169    pub fn new() -> Self {
170        Self::default()
171    }
172
173    /// Appends one resource entry.
174    pub fn push_resource(
175        &mut self,
176        resource_id: ResourceId,
177        resource_type: ResourceTypeCode,
178        data: Vec<u8>,
179    ) {
180        self.resources.push(BifResource {
181            resource_id,
182            resource_type,
183            storage: BifResourceStorage::Variable,
184            data,
185            source_data_offset: None,
186        });
187    }
188
189    /// Appends one fixed-table resource entry.
190    pub fn push_fixed_resource(
191        &mut self,
192        resource_id: ResourceId,
193        resource_type: ResourceTypeCode,
194        part_count: u32,
195        data: Vec<u8>,
196    ) {
197        self.resources.push(BifResource {
198            resource_id,
199            resource_type,
200            storage: BifResourceStorage::Fixed { part_count },
201            data,
202            source_data_offset: None,
203        });
204    }
205
206    /// Returns the first matching resource payload by resource ID.
207    pub fn resource_by_id(&self, resource_id: ResourceId) -> Option<&[u8]> {
208        self.resources
209            .iter()
210            .find(|resource| resource.resource_id == resource_id)
211            .map(|resource| resource.data.as_slice())
212    }
213}
214
215impl DecodeBinary for Bif {
216    type Error = BifBinaryError;
217
218    fn decode_binary(bytes: &[u8]) -> Result<Self, Self::Error> {
219        read_bif_from_bytes(bytes)
220    }
221}
222
223impl EncodeBinary for Bif {
224    type Error = BifBinaryError;
225
226    fn encode_binary(&self) -> Result<Vec<u8>, Self::Error> {
227        write_bif_to_vec(self)
228    }
229}
230
231/// Errors produced while parsing or serializing BIF binary data.
232#[derive(Debug, Error)]
233pub enum BifBinaryError {
234    /// I/O read/write failure.
235    #[error(transparent)]
236    Io(#[from] std::io::Error),
237    /// Header signature is unsupported.
238    #[error("invalid BIF magic: {0:?}")]
239    InvalidMagic([u8; 4]),
240    /// Header version is unsupported.
241    #[error("invalid BIF version: {0:?}")]
242    InvalidVersion([u8; 4]),
243    /// BZF support is unavailable because the `bzf` crate feature is disabled.
244    #[error("BZF support requires enabling the `bzf` feature on `rakata-formats`")]
245    BzfFeatureDisabled,
246    /// Header/body layout is invalid or truncated.
247    #[error("invalid BIF header: {0}")]
248    InvalidHeader(String),
249    /// Archive content is structurally invalid.
250    #[error("invalid BIF data: {0}")]
251    InvalidData(String),
252    /// Value cannot fit on-disk integer width.
253    #[error("value overflow while writing field `{0}`")]
254    ValueOverflow(&'static str),
255}
256
257impl From<binary::BinaryLayoutError> for BifBinaryError {
258    fn from(error: binary::BinaryLayoutError) -> Self {
259        Self::InvalidHeader(error.to_string())
260    }
261}