Bodu.IO.Biff
Bodu.IO.Biff is a low-level codec for the Excel Binary Interchange File Format - the BIFF5 (Excel 5.0/95) and BIFF8 (Excel 97-2003) record streams stored inside legacy .xls workbooks. Part of the Binary Formats & I/O topic, it is the substrate beneath Bodu.Formats.Excel.Binary in the same relation Bodu.IO.Pst has to Bodu.Formats.Outlook.Pst: it understands how BIFF is encoded, not what an Excel workbook means. There is no compound-file dependency, no workbook or cell model, and no formula evaluation.
An .xls file is an OLE2 compound file whose Workbook (BIFF8) or Book (BIFF5) stream is a flat sequence of records - a two-byte identifier, a two-byte length, and a payload. BiffReader walks that sequence forward over a span, one physical record at a time, and BiffWriter emits it. Both are ref structs in the mould of the Utf8BencodeReader / Utf8TomlReader family: allocation-free traversal, typed accessors that decode only the current record, and raw access to every record the codec does not name.
| Concept | Type | Role |
|---|---|---|
| Reader | BiffReader | Frames records, establishes the version and code page, exposes RecordId / RecordType / ValueSpan, decodes the named records through Get… accessors. |
| Shared strings | BiffSstReader | Walks the BIFF8 shared string table one string at a time across its CONTINUE records. |
| Writer | BiffWriter | Emits records under an explicit version - raw from an identifier and payload, or typed - splitting oversized structures into CONTINUE records. |
| Text | BiffString | A span-backed view over a BIFF8 Unicode string or a BIFF5 code-page byte string; nothing is materialized until GetString(). |
Key concepts
| Concept | Plain-language meaning |
|---|---|
| Record | The unit of the stream: id (2) · length (2) · payload. BiffRecordType names the common identifiers; any other identifier is still framed and readable. |
| Version | BiffVersion - established from the first BOF record (0x0500 BIFF5, 0x0600 BIFF8) or supplied through BiffReaderOptions. It selects string representation, the DIMENSIONS layout, and the maximum record length. |
| Substream | A BOF … EOF bracket: the workbook globals first, then one per sheet. BiffSubstreamType names the kinds. |
| CONTINUE | A record carrying the overflow of the record before it. The reader reports it as a physical record; the shared-string reader and TryReadContinuation consume it where a logical value spans records. |
| Code page | BIFF5 text is bytes in the code page the CODEPAGE record declares; the reader tracks it as state, like the version, so BiffString.GetString() needs no argument. |
| RK number | A compact 30-bit numeric encoding used by RK and MULRK cells; BiffRk encodes and decodes it. |
For the full glossary, see Core concepts; for every record the codec names - identifier, BIFF5 and BIFF8 layouts, decoded type, accessor, and writer - see the Record reference.
Scope and limitations
- A codec, not a spreadsheet library. The reader keeps only the state needed to interpret the forward stream - the version, the code page, the current record. Sheets, cells by address, formats, and formula results as values belong to
Bodu.Formats.Excel.Binary. - BIFF5 and BIFF8. A stream opening with a BIFF2, BIFF3, or BIFF4 beginning-of-file record is rejected with BiffUnsupportedVersionException - recognized, not mis-parsed.
- Untrusted input. Every declared length is bounds-checked; a truncated header, a payload that overruns the buffer, or a known record shorter than its layout is a BiffFormatException. An unknown record identifier is never an error.
- Reader over a span. Supply the whole stream, or read incrementally by carrying BiffReaderState between buffers; a record is at most 8,228 bytes, so a modest chunk always holds one.
- Writer over
IBufferWriter<byte>. The writer serializes records; it does not order them, and the sheet offsets aBOUNDSHEETcarries are computed by the caller from BytesCommitted.
Worked example - walk a workbook stream
The stream bytes come from wherever the workbook is stored - here, the Workbook stream of a compound file read with Bodu.IO.Compound, which this package does not itself reference.
using Bodu.IO.Biff;
var reader = new BiffReader(workbookStreamBytes);
string[] sharedStrings = [];
while (reader.Read())
{
switch (reader.RecordType)
{
case BiffRecordType.Bof:
Console.WriteLine($"BOF {reader.Version} {reader.GetBof().SubstreamType}");
break;
case BiffRecordType.Sst:
var strings = new BiffSstReader(ref reader);
var list = new List<string>();
while (strings.Read(ref reader))
list.Add(strings.GetString());
sharedStrings = [.. list];
break;
case BiffRecordType.Number:
BiffNumberRecord number = reader.GetNumber();
Console.WriteLine($"R{number.Row}C{number.Column} = {number.Value}");
break;
case BiffRecordType.LabelSst:
BiffLabelSstRecord label = reader.GetLabelSst();
Console.WriteLine($"R{label.Row}C{label.Column} = {sharedStrings[label.SstIndex]}");
break;
default:
// Every other record stays inspectable: reader.RecordId, reader.RecordLength, reader.ValueSpan.
break;
}
}
Common scenarios
| Scenario | Reach for |
|---|---|
| Walk every record of a stream | new BiffReader(bytes) → while (reader.Read()) |
| Identify a record the codec does not name | reader.RecordId / reader.ValueSpan |
| Learn the version without reading further | reader.Version after the first Read() (a BOF record) |
| Read a sheet substream knowing the version already | new BiffReader(bytes, new BiffReaderOptions { Version = …, CodePage = … }) |
| Read incrementally from a stream | Capture reader.CurrentState and reader.BytesConsumed; construct the next reader with isFinalBlock: false |
| Materialize the shared string table | new BiffSstReader(ref reader) → strings.Read(ref reader) / strings.GetString() |
| Decode a cell record | reader.GetNumber(), GetRk(), GetMulRk(), GetLabel(), GetLabelSst(), GetBoolErr(), GetFormula() |
| Read text without allocating | record.Text.RawCharacters, record.Text.CopyTo(Span<char>) |
| Encode a number compactly | BiffRk.TryEncode(value, out uint rk) → writer.WriteRk(…); fall back to WriteNumber |
| Copy a stream record for record | writer.WriteRecord(reader.RecordId, reader.ValueSpan) |
| Write a shared string table | writer.WriteSst(strings) - CONTINUE splitting is handled |
| Distinguish "corrupt" from "too old" | catch (BiffFormatException) versus catch (BiffUnsupportedVersionException) |
Headline types - Bodu.IO.Biff
| Type | Purpose |
|---|---|
| BiffReader | The forward-only reader: Read, RecordId / RecordType / RecordLength / ValueSpan, Version / CodePage, TryReadContinuation, the typed Get… accessors, and CurrentState / BytesConsumed for resumption. |
| BiffSstReader | The shared-string-table walker: Header, Read(ref reader), Index, Current (contiguous view) or GetString() / CopyTo for fragmented strings. |
| BiffWriter | The writer: WriteRecord, WriteContinuedRecord, WriteBof / WriteEof, the cell and globals writers, WriteSst, BytesCommitted. |
| BiffString | The text view: Length, IsUnicode / IsHighByte, RawCharacters, rich-run and extended-data trailers, GetString() / CopyTo. |
| BiffRk | The RK number codec: TryEncode for the compact form a cell may take, Decode to reverse it. |
| Decoded records - BiffBofRecord, BiffBoundSheetRecord, BiffDimensionsRecord, BiffRowRecord, BiffNumberRecord, BiffRkRecord, BiffMulRkRecord (+ BiffRkCell), BiffBlankRecord, BiffMulBlankRecord, BiffBoolErrRecord, BiffLabelRecord, BiffLabelSstRecord, BiffRStringRecord, BiffFormulaRecord (+ BiffCachedResultKind), BiffStringRecord, BiffXfRecord, BiffFormatRecord, BiffFontRecord, BiffCodePageRecord, BiffDateModeRecord, BiffFilePassRecord (+ BiffEncryptionType), BiffSstHeader | One decoded type per record the reader's Get… accessors return and the writer's Write… methods take; ref structs where they carry text or spans, record structs otherwise. Layouts per version are tabulated in the Record reference. |
| BiffRecordType / BiffVersion / BiffSubstreamType | The catalogue of named records, the two versions, and the substream kinds. |
| BiffSheetState / BiffSheetType | The visibility and kind a BOUNDSHEET record declares. |
| BiffReaderOptions / BiffReaderState / BiffWriterOptions | Seed a known version and code page; carry the established state between readers; select the version and BIFF5 code page a writer emits. |
| BiffRecordHeader / BiffLimits | The four-byte header (TryParse / WriteTo) and the format's structural limits. |
| BiffFormatException / BiffUnsupportedVersionException | Invalid data (with the record offset) versus a version the codec does not process (with the raw marker). |
The Excel reader built on this package
Bodu.Formats.Excel.Binary layers the spreadsheet vocabulary on top: ExcelBinaryWorkbook opens an .xls through Bodu.IO.Compound, parses the globals with this codec, and surfaces each sheet's cells as ExcelCell values with number-format and date detection.
Where to go next
- Core concepts - full vocabulary: records and framing, versions, substreams, strings and code pages, continuation, RK numbers.
- Record reference - every named record with its identifier, BIFF5 and BIFF8 layouts, decoded type, accessor, and writer.
- Getting started - install + minimal samples for reading, resuming, decoding strings, and writing.
- Runnable sample - the
BiffBasicsconsole project. - API reference - Bodu.IO.Biff · Bodu.Formats.Excel.
- Binary Formats & I/O topic overview - where the codec sits between the container and the format reader.