biff8/README.md

93 lines
3 KiB
Markdown

# 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
```zig
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
```sh
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.