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}