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}