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}