BIF (Binary Information File)
A BIF is bulk uncompressed storage: tightly packed resource bytes with no metadata and no filenames. It is built to be read at a known offset, which is what the companion KEY index supplies.
At a Glance
| Property | Value |
|---|---|
| Extension(s) | .bif, .bzf (compressed; mobile ports only) |
| Magic Signatures | BIFF (version V1 ) for both |
| Type | Archive Blob Payload |
| Rust Reference | View rakata_formats::Bif in Rustdocs |
File Layout
A fixed 20-byte header, one or two resource tables, and the payload. The variable table is found by variable_table_offset; the fixed table, when a file has one, follows it directly. Payload bytes are addressed per entry.
| Block | Size | Located by |
|---|---|---|
| Header | 20 bytes | Always at 0x00 |
| Variable resource table | 16 bytes per entry | variable_table_offset |
| Fixed resource table | 20 bytes per entry | Follows the variable table |
| Resource payload | remainder of the file | Each entry’s own data_offset |
There are no names anywhere in here. A BIF entry knows its own id, type, offset and size, and nothing else. The resref lives in the KEY, which reaches in by position. That is what makes a BIF unreadable on its own and why the two formats are always discussed together.
Header (20 bytes)
| Offset | Field | Type | Notes |
|---|---|---|---|
0x00 | magic | fourcc | BIFF. No trailing space on this one. |
0x04 | version | fourcc | V1 , two trailing spaces. |
0x08 | variable_count | u32 | |
0x0C | fixed_count | u32 | Zero in every shipped archive. See below. |
0x10 | variable_table_offset | u32 |
Variable Resource Entry (16 bytes each)
| Offset | Field | Type | Notes |
|---|---|---|---|
0x00 | resource_id | u32 | The KEY’s packed word for this resource, repeated on the BIF side. Derivable from position. See below. |
0x04 | data_offset | u32 | Absolute, from the start of the file. |
0x08 | data_size | u32 | For a .bzf, the uncompressed length. |
0x0C | type_id | u32 | Four bytes holding one of the two-byte resource type codes, as in RIM. Rakata rejects anything that will not fit in a u16; that is our strictness, not traced engine behaviour. |
Fixed Resource Entry (20 bytes each)
| Offset | Field | Type |
|---|---|---|
0x00 | resource_id | u32 |
0x04 | data_offset | u32 |
0x08 | part_count | u32 |
0x0C | data_size | u32 |
0x10 | type_id | u32 |
Note
resource_idis positional, and it is the KEY’s word repeated A variable entry’sresource_idis the same packed value the KEY carries for that resource: the entry’s own index within this archive in the low twenty bits, this archive’s index in the KEY file table in the high twelve. Both halves hold in every variable entry of every archive a retail install’schitin.keynames.The two files agree exactly. Every BIF entry is named by one key, no key points at an entry that is not there, and no two keys carry the same value, so the correspondence is one-to-one in both directions rather than merely consistent where it was checked.
That makes the field carry nothing a reader does not already have from position. A writer, though, has to compute it rather than copy it, because the high half is not a property of the BIF at all: it depends on where the archive lands in the KEY’s file table, which the BIF cannot see.
The fixed table’s
resource_idis untested, since nothing shipped populates that table at all.
Note
The fixed table is a format feature nothing uses
fixed_countis0in all 26 archives of a full vanilla install, without exception. The engine reads the scalar and then ignores it entirely, as the audit below records: files declaring fixed entries are accepted and those entries are never mapped. Rakata parses the table when one is present, so a file carrying it round-trips, but nothing in the game will ever look at what it holds.
The Compressed Variant (.bzf)
Compression did not exist in the original 2003 PC release. Aspyr added it for the modern mobile ports to save storage, and it is the one place the BIF format gets genuinely confusing, because nothing inside the file says it is compressed.
| Property | Uncompressed | Compressed |
|---|---|---|
| Signature | BIFF / V1 | BIFF / V1 (identical) |
| Header and tables | as documented above | byte-for-byte the same shape |
data_size in the table | the resource’s length | the uncompressed length |
| How the KEY names it | data\2da.bif | data\2da.bif (still .bif) |
| On-disk filename | 2da.bif | 2da.bzf |
The extension is the only discriminator that exists in shipping data. Everything else matches. That has a direct consequence for tooling: compression has to be stated, not sniffed. Rakata reads it from the path when opening a file, and requires it as an argument when reading from a window that has no filename attached.
Payload Layout
Each resource is stored as its own LZMA-alone stream:
0x00 properties byte packs (pb * 5 + lp) * 9 + lc
0x01..0x05 dictionary size u32, little endian
0x05.. compressed data terminated by an end-of-stream marker
There is no length field in the stream, because the uncompressed length already lives in the entry table. And there is no packed length recorded anywhere: an entry’s compressed extent runs from its offset to wherever the next entry begins, with the last one running to the end of the file. Trailing alignment inside that span is harmless, since the decoder stops at the end-of-stream marker.
Note
Ground truth and its limits. The facts above are read from a shipping Android bundle (
com.aspyr.swkotor), whose 26 compressed archives every one carry theBIFFsignature. Rakata decodes all 26, each entry to its exact declared length. iOS and Switch are unverified and simply assumed to match.Two things worth flagging as inference rather than fact. Community references describe a
BZFsignature; no file in the bundle uses it, so Rakata does not look for one. And the engine presumably swaps the extension when resolving a KEY entry on mobile, since the KEY says.bifand the disk says.bzf, but that path has not been traced in a mobile binary. The Ghidra project carries the Android builds, so it is auditable whenever someone wants to.
Engine Audits & Decompilation
Read from CExoResFile::LoadHeader (0x0040d910) and CExoResFile::ReadResource (0x0040da20) in swkotor.exe. Provenance: derived, not attested. The rows below have not been separately re-derived, so they sit on the reverse-engineering queue.
Archive Initialization (CExoResFile::LoadHeader)
Mapped from 0x0040d910.
| Pipeline Event | Engine Behavior & Result |
|---|---|
| Signature Check | The engine strictly validates both the BIFF magic and the exact V1 version. It does not actively process any files that deviate from this signature pair. |
| Variable Table Loading | The system extracts the variable_count value from the header and physically reads variable_count * 16 bytes from the variable_table_offset to map the resource keys. |
| Fixed Table Bypass | The fixed_count header scalar is entirely decorative. It is not part of the active runtime read path (files with nonzero values are accepted but never mapped). |
| Direct Asset Extraction | When reading a physical asset out of the .bif, the engine isolates the entry_index using (resource_id & 0x3fff) * 0x10. It then calls a direct C fseek(SEEK_SET) strictly matching the raw data_offset extracted from the 16-byte variable table entry. The index is bounds-checked against variable_count before use, so an out-of-range resource_id is rejected rather than read past the table. What is not applied is any alignment or structural normalization to the data_offset itself, which goes to fseek exactly as stored. |
Caution
Because the engine passes the internal
data_offsetinteger directly into a raw Cfseek(SEEK_SET), any custom BIF files must meticulously guarantee byte-perfect offset tables. If the offset is even slightly misaligned, the engine will read garbage data into the stream, inevitably crashing the game.