93 lines
3 KiB
Markdown
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.
|