biff8/README.md

3 KiB

biff8

A read-only Zig reader for legacy binary Excel workbooks: .xls files from Excel 97 through 2003 (BIFF8), which some sites still export as their only "spreadsheet" download.

It unwraps the Compound File Binary ("OLE2") container, decodes the BIFF8 record stream, and gives you each worksheet's cell values. That is all it does.

Usage

const biff8 = @import("biff8");

var wb = try biff8.Workbook.parse(allocator, bytes);
defer wb.deinit();

const sheet = wb.sheet("Positions") orelse return error.NoSuchSheet;
for (sheet.rows, 0..) |row, r| {
    for (row, 0..) |cell, c| switch (cell) {
        .text => |t| std.debug.print("{d},{d}: {s}\n", .{ r, c, t }),
        .number => |n| std.debug.print("{d},{d}: {d}\n", .{ r, c, n }),
        .boolean, .error_code, .empty => {},
    };
}

sheet.cell(row, col) does bounds-safe random access and returns .empty outside the populated area. Sheet has public fields, so a consumer can build one as a literal in its own tests instead of shipping binary fixtures.

biff8.isCompoundFile(bytes) is a cheap signature check for content sniffing. Every .xls passes it, but so does every other legacy Office file, so it is not proof of a workbook.

What is decoded

Record Becomes
LABELSST, LABEL, RSTRING .text (UTF-8)
NUMBER, RK, MULRK .number
BOOLERR .boolean or .error_code
FORMULA (+ STRING) the cached result, as any of the above

Shared strings split across CONTINUE records are handled, including the case where the split falls mid-string and the remainder switches between 1-byte and 2-byte characters. Unpaired UTF-16 surrogates decode as U+FFFD.

What is not

  • Formatting. Numbers are returned as stored, so a date-formatted cell is an Excel serial day number; telling dates apart needs the number format, which is not decoded.
  • Formulas. Only their cached results.
  • Anything but BIFF8. Excel 95 and earlier fail with error.UnsupportedBiffVersion. .xlsx is a different format (zipped XML) and fails with error.NotCompoundFile.
  • Encrypted workbooks. error.Encrypted.
  • Writing.

Errors

Workbook.parse returns biff8.ParseError:

Error Meaning
NotCompoundFile Not an OLE2 file at all
NoWorkbookStream An OLE2 file, but not a workbook (a .doc, an .msg, ...)
UnsupportedBiffVersion Excel 95 or older
Encrypted Password-protected
Truncated The file ends early, usually an interrupted download
CorruptFile Internally inconsistent structure
OutOfMemory

Every sector chain walk is bounded and every declared size is checked before allocating, so hostile input produces an error rather than a hang or a huge allocation.

Specs

  • [MS-CFB] Compound File Binary File Format
  • [MS-XLS] Excel Binary File Format (.xls) Structure

Development

zig build test
zig build coverage   # kcov, Linux x86_64/aarch64

Test fixtures are built in code by src/test_writer.zig rather than checked in as binary files.