When you say “I’m going to reverse-engineer a binary file format,” people picture C, or Python with struct , or Kaitai. Nobody pictures Ruby. Ruby is for web apps and DSLs and being pleasant; it is not, in the popular imagination, for byte-banging floats out of a 2003 racing game.
That popular imagination is wrong. The reader for Codemasters’ BIGF archive format — the container that holds the AI data in TOCA Race Driver — is pure, dependency-free Ruby, and it reads four different games’ archives. I should be upfront about how it came to be: this was reverse engineering done with an AI the whole way — me steering, deciding what to trust and verifying every claim against the bytes; the model drafting code, recalling the corners of the standard library, and proposing hypotheses I then tested. What follows is the part of Ruby that made that collaboration genuinely pleasant: Ruby strings are byte buffers, and String#unpack is a tiny, fast binary parser hiding in plain sight.
Strings are bytes
The first thing to internalise is that a Ruby String is not “text.” It’s a sequence of bytes with an encoding label attached. Read a file in binary mode and you get the raw bytes, indexable and sliceable like any string:
data = File . binread ( "aib.big" ) # the whole file as an ASCII-8BIT String data [ 0 , 4 ] # => "BIGF" — the first four bytes data . bytesize # => 3448832
File.binread is the key: it reads the file as binary ( ASCII-8BIT / BINARY encoding), so no UTF-8 interpretation mangles your 0x80 + bytes. From there, data[offset, length] carves out byte ranges, and data.index(needle, from) finds a magic number or a marker anywhere in the file. That’s most of a parser already.
unpack : the binary decoder you already have
The workhorse is String#unpack (and its single-value sibling unpack1 ). You hand it a format string of directives and it decodes the bytes. The two directives that did 90% of the work here:
V — an unsigned 32-bit integer, little-endian . Every count, block index, offset and size in BIGF is a V .
— an unsigned 32-bit integer, . Every count, block index, offset and size in BIGF is a . e — a little-endian single-precision float (32-bit). The AI data is arrays of these: the racing-line coordinates, the control values, the padding.
... continue reading