xls-codec 0.0.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +128 -0
  3. package/dist/biff/cursor.cjs +73 -0
  4. package/dist/biff/cursor.d.cts +28 -0
  5. package/dist/biff/cursor.d.ts +28 -0
  6. package/dist/biff/cursor.js +72 -0
  7. package/dist/biff/errors.cjs +18 -0
  8. package/dist/biff/errors.d.cts +5 -0
  9. package/dist/biff/errors.d.ts +5 -0
  10. package/dist/biff/errors.js +17 -0
  11. package/dist/biff/record-types.cjs +108 -0
  12. package/dist/biff/record-types.d.cts +73 -0
  13. package/dist/biff/record-types.d.ts +73 -0
  14. package/dist/biff/record-types.js +73 -0
  15. package/dist/biff/records.cjs +40 -0
  16. package/dist/biff/records.d.cts +2 -0
  17. package/dist/biff/records.d.ts +2 -0
  18. package/dist/biff/records.js +38 -0
  19. package/dist/biff/rk.cjs +29 -0
  20. package/dist/biff/rk.d.cts +5 -0
  21. package/dist/biff/rk.d.ts +5 -0
  22. package/dist/biff/rk.js +28 -0
  23. package/dist/biff/strings.cjs +66 -0
  24. package/dist/biff/strings.d.cts +14 -0
  25. package/dist/biff/strings.d.ts +14 -0
  26. package/dist/biff/strings.js +63 -0
  27. package/dist/biff/substreams.cjs +71 -0
  28. package/dist/biff/substreams.d.cts +21 -0
  29. package/dist/biff/substreams.d.ts +21 -0
  30. package/dist/biff/substreams.js +69 -0
  31. package/dist/container.cjs +46 -0
  32. package/dist/container.d.cts +15 -0
  33. package/dist/container.d.ts +15 -0
  34. package/dist/container.js +44 -0
  35. package/dist/content.cjs +230 -0
  36. package/dist/content.d.cts +20 -0
  37. package/dist/content.d.ts +20 -0
  38. package/dist/content.js +228 -0
  39. package/dist/index.cjs +74 -0
  40. package/dist/index.d.cts +15 -0
  41. package/dist/index.d.ts +15 -0
  42. package/dist/index.js +15 -0
  43. package/dist/number-format.cjs +298 -0
  44. package/dist/number-format.d.cts +32 -0
  45. package/dist/number-format.d.ts +32 -0
  46. package/dist/number-format.js +296 -0
  47. package/dist/records-DVIqXFKk.d.cts +20 -0
  48. package/dist/records-DVIqXFKk.d.ts +20 -0
  49. package/dist/serial.cjs +62 -0
  50. package/dist/serial.d.cts +7 -0
  51. package/dist/serial.d.ts +7 -0
  52. package/dist/serial.js +59 -0
  53. package/dist/units.cjs +30 -0
  54. package/dist/units.d.cts +11 -0
  55. package/dist/units.d.ts +11 -0
  56. package/dist/units.js +28 -0
  57. package/dist/workbook/globals.cjs +106 -0
  58. package/dist/workbook/globals.d.cts +45 -0
  59. package/dist/workbook/globals.d.ts +45 -0
  60. package/dist/workbook/globals.js +104 -0
  61. package/dist/workbook/sheet.cjs +372 -0
  62. package/dist/workbook/sheet.d.cts +58 -0
  63. package/dist/workbook/sheet.d.ts +58 -0
  64. package/dist/workbook/sheet.js +371 -0
  65. package/package.json +85 -2
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joseph Mearman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # xls-codec
2
+
3
+ [![GitHub](https://img.shields.io/badge/GitHub-181717?logo=github&logoColor=white)](https://github.com/ExaDev/documents.js/tree/main/packages/xls-codec) [![npm](https://img.shields.io/badge/npm-CB3837?logo=npm&logoColor=white)](https://www.npmjs.com/package/xls-codec) [![npm version](https://img.shields.io/npm/v/xls-codec)](https://www.npmjs.com/package/xls-codec) [![CI](https://img.shields.io/github/actions/workflow/status/ExaDev/documents.js/ci.yml?branch=main)](https://github.com/ExaDev/documents.js/actions)
4
+
5
+ > Hand-written reader for the legacy Excel Binary File Format (`.xls`, BIFF8) as specified by [MS-XLS], mapping a workbook's record stream onto the same `document-schema.js` spreadsheet model `ooxml.js`'s xlsx support and `odf.js`'s ods support target. Worker-isomorphic: the same code runs under Node and inside a Cloudflare Workers isolate.
6
+
7
+ A `.xls` file is not one format but two nested ones. The outer container is an [MS-CFB] compound file — the same "filesystem in a file" that carries `.doc` and `.ppt` — holding a stream named `Workbook`. Inside that stream is BIFF8: a flat sequence of records, each a two-byte type, a two-byte size, and that many bytes of data, organised into substreams delimited by `BOF`/`EOF`. This package leaves the outer layer to [`archive-codec`](../archive-codec/README.md)'s bounded CFB reader and implements the inner one, from the record framing up to a `ContentDocument`.
8
+
9
+ ## Status
10
+
11
+ Under active development, **read-only**. Built and shipped:
12
+
13
+ - **Record framing** (`src/biff/records.ts`) — the three-component record structure of [MS-XLS] 2.1.4, with the 8224-byte data ceiling enforced and every malformed stream thrown on rather than silently truncated.
14
+ - **Continuation-aware cursor and strings** (`src/biff/cursor.ts`, `src/biff/strings.ts`) — `Continue` records ([MS-XLS] 2.4.58) joined per the rules of the record being continued, including the case a naive reader gets wrong: an `XLUnicodeRichExtendedString` ([MS-XLS] 2.5.293) resuming after a boundary re-states its own `fHighByte` flag, which may differ from the flag the string started with. All three string shapes (`XLUnicodeString`, `ShortXLUnicodeString`, `XLUnicodeRichExtendedString`) are read.
15
+ - **Workbook globals** (`src/workbook/globals.ts`) — `BoundSheet8` (sheet names, tab order, hidden state, type, and substream offsets), `SST` with its `Continue` chain, `Format` (custom number-format codes), `XF`'s fixed prefix (font index, format identifier, style flag), and `Date1904`.
16
+ - **Worksheet substreams** (`src/workbook/sheet.ts`) — `Dimensions`, `Row` (height and hidden state), `ColInfo` (width and hidden state, expanded across the column range it covers), `MergeCells`, and the whole cell-value family: `Blank`, `MulBlank`, `RK`, `MulRk`, `Number`, `BoolErr`, `LabelSst`, `Label`, and `Formula` with its `String` result record.
17
+ - **Number-format classification and date serials** (`src/number-format.ts`, `src/serial.ts`) — what turns a bare number into the schema's own `percentage`/`currency`/`date`/`time`/`dateTime` value kinds, honouring the workbook's own epoch flag and refusing the 1900 system's phantom leap day.
18
+ - **Schema mapping** (`src/content.ts`) — `readXlsContent` produces a `ContentDocument` of `kind: 'spreadsheet'`, and `readXls` the tree-form `DocumentTree`, in the same shape `readXlsxContent`/`readXlsx` produce: sparse 0-based cells, `displayText` on every cell, merged ranges as `colSpan`/`rowSpan` on the anchor cell, and the producer's raw format code kept alongside the classified value.
19
+
20
+ ### Not built yet
21
+
22
+ **The write path**, which is the substantial one. A conformant workbook has to emit a `BOF` history block, a complete `XF`/`Font`/`Format` table with the fifteen mandatory style records preceding any cell format, an `Index`/`DBCell` row-block lookup structure whose file offsets must agree with where the records actually land, and a compound-file _writer_ (`archive-codec` reads [MS-CFB] but does not write it). Shipping a plausible-looking writer that produced files Excel rejects would be worse than shipping none. Tracked on [#815](https://github.com/ExaDev/documents.js/issues/815).
23
+
24
+ Read-side gaps, each deliberate rather than overlooked:
25
+
26
+ - **Formula expressions.** A `Formula` record's cached _result_ is read, so a formula cell shows the right value, but `ContentSheetCell.formula` stays absent. BIFF8 stores the expression as a compiled `Ptg` token stream rather than as text, and recovering it means implementing the whole `Ptg` vocabulary plus shared-formula (`ShrFmla`) and external-reference (`SupBook`/`ExternSheet`) resolution.
27
+ - **Cell decoration** — fill, borders, and alignment from `XF`'s trailing `CellXF` payload, whose colours are palette indices needing the `Palette` record and the default colour table to resolve. `Font` records are not read either: `ContentSheetCell` has no cell-level font field, and `ooxml.js`'s xlsx reader likewise maps only the number format and decoration from a cell format.
28
+ - **Print settings** are emitted as Excel's documented "Normal" preset rather than read from the file. The real values need `Setup` (including its paper-size code table), the four margin records, `PrintGrid`, and `PrintRowCol`.
29
+ - **Workbook metadata** — `metadata` is empty. Title, author, and dates live in the `\x05SummaryInformation` property-set stream ([MS-OSHARED]), a different format from BIFF8 sitting beside it in the same container.
30
+ - **Not read at all:** charts, drawings and images, cell comments (`Note`/`Txo`), data validation, conditional formatting, and defined names (`Lbl`).
31
+ - **Encrypted workbooks** are refused rather than mis-read: a `FilePass` record means every record after it is ciphertext.
32
+
33
+ This package is also not yet wired into `documents.js`'s conversion registry — it stands alone.
34
+
35
+ ## Getting started
36
+
37
+ Requires Node.js `>=20` and pnpm `11.6.0`.
38
+
39
+ ```sh
40
+ pnpm install
41
+ pnpm build # tsdown -> dist/ (ESM + CJS + .d.ts, one file set per src module)
42
+ pnpm typecheck # tsc -p tsconfig.json && tsc -p tsconfig.node.json (dual tsconfig)
43
+ pnpm lint # eslint . --fix --cache --max-warnings 0
44
+ pnpm test # vitest run --project unit
45
+ pnpm test:watch # vitest --project unit
46
+ pnpm test:workers # vitest run --config vitest.workers.config.ts, inside a real Cloudflare Workers (workerd) isolate
47
+ pnpm test:smoke # builds dist/, then loads the built ESM and CJS barrels and every advertised deep import
48
+ ```
49
+
50
+ ## Usage
51
+
52
+ ```ts
53
+ import { isXlsFile, readXls, readXlsContent } from "xls-codec";
54
+
55
+ const bytes = new Uint8Array(await file.arrayBuffer());
56
+
57
+ if (isXlsFile(bytes)) {
58
+ const content = readXlsContent(bytes); // ContentDocument, kind: 'spreadsheet'
59
+ const tree = readXls(bytes); // DocumentTree, the same read decomposed
60
+ }
61
+ ```
62
+
63
+ `readXlsContent` mirrors `ooxml.js`'s `readXlsxContent` deliberately, down to returning a `ContentDocument` rather than a bare `{ metadata, sheets }` object, so a caller can hold either behind one type. The difference is the input: an `.xls` has no `Package` equivalent to decode first, so these take the file's raw bytes and select the `Workbook` stream themselves.
64
+
65
+ The record layer is exported in its own right, for a caller inspecting a workbook rather than converting it:
66
+
67
+ ```ts
68
+ import { readRecords, readWorkbookStream } from "xls-codec";
69
+
70
+ const stream = readWorkbookStream(bytes); // the raw BIFF8 record stream out of the compound file
71
+ for (const rec of readRecords(stream)) {
72
+ console.log(rec.type.toString(16), rec.data.length);
73
+ }
74
+ ```
75
+
76
+ ### Microsoft Works spreadsheets (`.xlr`)
77
+
78
+ Works 9's `.xlr` is BIFF8 in the same compound-file container, carrying the identical `Workbook` stream alongside a Works-specific `WksSSWorkBook` stream ([SheetJS format notes](https://docs.sheetjs.com/docs/miscellany/formats/)). Because this package selects the `Workbook` stream by name and ignores every other stream in the container, an `.xlr` reads through exactly the same path with no special-casing; `isXlsFile` accepts one, and there is a test pinning that.
79
+
80
+ ## Architecture
81
+
82
+ Layered bottom-up, each layer testable against hand-built byte sequences taken from the spec's own field-layout tables:
83
+
84
+ - **`src/biff/record-types.ts`** — the record type numbers, each cited to [MS-XLS] 2.3.1's own enumeration rather than copied from another implementation.
85
+ - **`src/biff/records.ts`** — the record framing, and nothing above it. Deliberately does not merge `Continue` records: whether a continuation's bytes simply append or re-state a flag byte first is decided by the record being continued, so the blocks are reported as written.
86
+ - **`src/biff/cursor.ts`** — a field cursor over one record's blocks that reads across a continuation boundary transparently while keeping the boundary observable, which is exactly what the string reader needs.
87
+ - **`src/biff/strings.ts`**, **`src/biff/rk.ts`**, **`src/biff/errors.ts`** — the shared value encodings: the three string shapes, the `RkNumber` packed-numeric encoding, and the `BErr` error-value vocabulary.
88
+ - **`src/workbook/globals.ts`**, **`src/workbook/sheet.ts`** — the two substream readers, each walking the record sequence its ABNF in [MS-XLS] 2.1.7.20.3 / 2.1.7.20.5 defines.
89
+ - **`src/number-format.ts`**, **`src/serial.ts`** — number-format classification and date-serial conversion, the two pieces of xlsx semantics BIFF8 shares because ECMA-376 inherited them from BIFF.
90
+ - **`src/content.ts`** — the mapping onto `document-schema.js`.
91
+
92
+ ### Deliberately not depended on
93
+
94
+ No third-party spreadsheet or compound-file library — not `xlsx`/SheetJS, `exceljs`, or `cfb` — enforced by an ESLint `no-restricted-imports` rule in this package's own config. The format is implemented against its published Open Specification, and the compound-file layer comes from `archive-codec`, a sibling in this workspace.
95
+
96
+ ## Conventions
97
+
98
+ - Worker-isomorphic (see the [family-wide convention](../../README.md#conventions)): runtime `src/` must not import `node:*`, a bare Node builtin, or use the `Buffer` global — enforced by a `no-restricted-imports`/`no-restricted-globals` ESLint rule and exercised in CI by running a test suite inside an actual `workerd` isolate (`pnpm test:workers`). Test files under `src/**/*.test.ts` and `src/test-support/` are exempt and may use Node APIs for fixtures.
99
+ - Only `src/index.ts` may be named `index.*` — a custom ESLint rule (`local/no-non-barrel-index`) rejects any other module using an `index` basename, since that would be a hidden entry point the `exports` map in `package.json` doesn't advertise.
100
+ - Every record layout is cited to its own [MS-XLS] section, by URL, at the point it is read. A field offset with no citation is a field offset nobody can check.
101
+
102
+ ## Install
103
+
104
+ ```sh
105
+ pnpm add xls-codec
106
+ # or
107
+ npm install xls-codec
108
+ ```
109
+
110
+ ## Release and publishing
111
+
112
+ Release, CI, and commit-message conventions are all workspace-wide, not package-local — see the [monorepo root README](../../README.md#releases) for the mechanism (topological per-package `semantic-release` via `@exadev/semantic-release-workspace`, OIDC trusted npm publishing, automatic sibling dependency-range rewriting) and its [post-release republishing and attestation](../../README.md#releases) note on the restored GitHub Packages mirrors, npm aliases, and SBOM/provenance signing.
113
+
114
+ ## Contributing
115
+
116
+ Conventional Commits, enforced workspace-wide by commitlint through a root `commit-msg` hook. Work inside `packages/xls-codec/`; see [CONTRIBUTING.md](../../CONTRIBUTING.md) for the shared git hooks and history conventions.
117
+
118
+ ## References
119
+
120
+ - [MS-XLS]: [Excel Binary File Format (.xls) Structure](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-xls/cd03cb5f-ca02-4934-a391-bb674cb8aa06) — the specification this package implements.
121
+ - [MS-CFB]: [Compound File Binary File Format](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/53989ce4-7b05-4f8d-829b-d08d6148375b) — the container, read through `archive-codec`.
122
+ - [archive-codec](../archive-codec/README.md) — the bounded CFB reader this package selects the `Workbook` stream through.
123
+ - [document-schema.js](../document-schema.js/README.md) — the `ContentDocument`/`ContentSheet`/`ContentSheetCell` vocabulary this package maps onto, and the `assembleTree` transform behind `readXls`.
124
+ - [ooxml.js](../ooxml.js/README.md) — the sibling reading `.xlsx`, BIFF8's successor, onto the same schema.
125
+
126
+ ## License
127
+
128
+ MIT
@@ -0,0 +1,73 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_biff_records = require("./records.cjs");
3
+ //#region src/biff/cursor.ts
4
+ var BlockCursor = class {
5
+ blocks;
6
+ blockIndex = 0;
7
+ offset = 0;
8
+ constructor(blocks) {
9
+ this.blocks = blocks;
10
+ this.settle();
11
+ }
12
+ /** Advances past any exhausted or empty blocks, so the cursor always rests either on a readable byte or past the end of the last block. A Continue carrying no data is legal and must not read as the end of the record. */
13
+ settle() {
14
+ while (this.blockIndex < this.blocks.length && this.offset >= (this.blocks[this.blockIndex]?.length ?? 0)) {
15
+ this.blockIndex += 1;
16
+ this.offset = 0;
17
+ }
18
+ }
19
+ nextByte(context) {
20
+ this.settle();
21
+ const block = this.blocks[this.blockIndex];
22
+ if (block === void 0) throw new require_biff_records.BiffFormatError(`${context} runs past the end of the record data`);
23
+ const byte = block[this.offset];
24
+ if (byte === void 0) throw new require_biff_records.BiffFormatError(`${context} runs past the end of the record data`);
25
+ this.offset += 1;
26
+ return byte;
27
+ }
28
+ /** Whether any unread byte remains, in this block or a later one. */
29
+ hasMore() {
30
+ this.settle();
31
+ return this.blockIndex < this.blocks.length;
32
+ }
33
+ /** How many unread bytes remain in the block the cursor currently rests in -- the distance to the next continuation boundary, which a string's character run must not cross without consuming a new flag byte. */
34
+ remainingInBlock() {
35
+ this.settle();
36
+ const block = this.blocks[this.blockIndex];
37
+ return block === void 0 ? 0 : block.length - this.offset;
38
+ }
39
+ /** The index of the block the cursor currently rests in, so a caller can detect that a read crossed into a new one. */
40
+ blockPosition() {
41
+ this.settle();
42
+ return this.blockIndex;
43
+ }
44
+ u8() {
45
+ return this.nextByte("u8");
46
+ }
47
+ u16() {
48
+ return this.nextByte("u16") | this.nextByte("u16") << 8;
49
+ }
50
+ u32() {
51
+ return this.u16() + this.u16() * 65536;
52
+ }
53
+ i32() {
54
+ return this.u32() | 0;
55
+ }
56
+ /** An Xnum ([MS-XLS] 2.5.342): a little-endian IEEE 754 double. */
57
+ f64() {
58
+ const raw = this.take(8);
59
+ return new DataView(raw.buffer, raw.byteOffset, raw.byteLength).getFloat64(0, true);
60
+ }
61
+ /** The next `count` bytes, copied out. Spans block boundaries. */
62
+ take(count) {
63
+ const out = new Uint8Array(count);
64
+ for (let index = 0; index < count; index += 1) out[index] = this.nextByte(`${count}-byte run`);
65
+ return out;
66
+ }
67
+ /** Advances `count` bytes without materialising them -- for a field this package reads past rather than reads. */
68
+ skip(count) {
69
+ for (let index = 0; index < count; index += 1) this.nextByte(`${count}-byte skip`);
70
+ }
71
+ };
72
+ //#endregion
73
+ exports.BlockCursor = BlockCursor;
@@ -0,0 +1,28 @@
1
+ //#region src/biff/cursor.d.ts
2
+ declare class BlockCursor {
3
+ private readonly blocks;
4
+ private blockIndex;
5
+ private offset;
6
+ constructor(blocks: readonly Uint8Array<ArrayBuffer>[]);
7
+ /** Advances past any exhausted or empty blocks, so the cursor always rests either on a readable byte or past the end of the last block. A Continue carrying no data is legal and must not read as the end of the record. */
8
+ private settle;
9
+ private nextByte;
10
+ /** Whether any unread byte remains, in this block or a later one. */
11
+ hasMore(): boolean;
12
+ /** How many unread bytes remain in the block the cursor currently rests in -- the distance to the next continuation boundary, which a string's character run must not cross without consuming a new flag byte. */
13
+ remainingInBlock(): number;
14
+ /** The index of the block the cursor currently rests in, so a caller can detect that a read crossed into a new one. */
15
+ blockPosition(): number;
16
+ u8(): number;
17
+ u16(): number;
18
+ u32(): number;
19
+ i32(): number;
20
+ /** An Xnum ([MS-XLS] 2.5.342): a little-endian IEEE 754 double. */
21
+ f64(): number;
22
+ /** The next `count` bytes, copied out. Spans block boundaries. */
23
+ take(count: number): Uint8Array<ArrayBuffer>;
24
+ /** Advances `count` bytes without materialising them -- for a field this package reads past rather than reads. */
25
+ skip(count: number): void;
26
+ }
27
+ //#endregion
28
+ export { BlockCursor };
@@ -0,0 +1,28 @@
1
+ //#region src/biff/cursor.d.ts
2
+ declare class BlockCursor {
3
+ private readonly blocks;
4
+ private blockIndex;
5
+ private offset;
6
+ constructor(blocks: readonly Uint8Array<ArrayBuffer>[]);
7
+ /** Advances past any exhausted or empty blocks, so the cursor always rests either on a readable byte or past the end of the last block. A Continue carrying no data is legal and must not read as the end of the record. */
8
+ private settle;
9
+ private nextByte;
10
+ /** Whether any unread byte remains, in this block or a later one. */
11
+ hasMore(): boolean;
12
+ /** How many unread bytes remain in the block the cursor currently rests in -- the distance to the next continuation boundary, which a string's character run must not cross without consuming a new flag byte. */
13
+ remainingInBlock(): number;
14
+ /** The index of the block the cursor currently rests in, so a caller can detect that a read crossed into a new one. */
15
+ blockPosition(): number;
16
+ u8(): number;
17
+ u16(): number;
18
+ u32(): number;
19
+ i32(): number;
20
+ /** An Xnum ([MS-XLS] 2.5.342): a little-endian IEEE 754 double. */
21
+ f64(): number;
22
+ /** The next `count` bytes, copied out. Spans block boundaries. */
23
+ take(count: number): Uint8Array<ArrayBuffer>;
24
+ /** Advances `count` bytes without materialising them -- for a field this package reads past rather than reads. */
25
+ skip(count: number): void;
26
+ }
27
+ //#endregion
28
+ export { BlockCursor };
@@ -0,0 +1,72 @@
1
+ import { BiffFormatError } from "./records.js";
2
+ //#region src/biff/cursor.ts
3
+ var BlockCursor = class {
4
+ blocks;
5
+ blockIndex = 0;
6
+ offset = 0;
7
+ constructor(blocks) {
8
+ this.blocks = blocks;
9
+ this.settle();
10
+ }
11
+ /** Advances past any exhausted or empty blocks, so the cursor always rests either on a readable byte or past the end of the last block. A Continue carrying no data is legal and must not read as the end of the record. */
12
+ settle() {
13
+ while (this.blockIndex < this.blocks.length && this.offset >= (this.blocks[this.blockIndex]?.length ?? 0)) {
14
+ this.blockIndex += 1;
15
+ this.offset = 0;
16
+ }
17
+ }
18
+ nextByte(context) {
19
+ this.settle();
20
+ const block = this.blocks[this.blockIndex];
21
+ if (block === void 0) throw new BiffFormatError(`${context} runs past the end of the record data`);
22
+ const byte = block[this.offset];
23
+ if (byte === void 0) throw new BiffFormatError(`${context} runs past the end of the record data`);
24
+ this.offset += 1;
25
+ return byte;
26
+ }
27
+ /** Whether any unread byte remains, in this block or a later one. */
28
+ hasMore() {
29
+ this.settle();
30
+ return this.blockIndex < this.blocks.length;
31
+ }
32
+ /** How many unread bytes remain in the block the cursor currently rests in -- the distance to the next continuation boundary, which a string's character run must not cross without consuming a new flag byte. */
33
+ remainingInBlock() {
34
+ this.settle();
35
+ const block = this.blocks[this.blockIndex];
36
+ return block === void 0 ? 0 : block.length - this.offset;
37
+ }
38
+ /** The index of the block the cursor currently rests in, so a caller can detect that a read crossed into a new one. */
39
+ blockPosition() {
40
+ this.settle();
41
+ return this.blockIndex;
42
+ }
43
+ u8() {
44
+ return this.nextByte("u8");
45
+ }
46
+ u16() {
47
+ return this.nextByte("u16") | this.nextByte("u16") << 8;
48
+ }
49
+ u32() {
50
+ return this.u16() + this.u16() * 65536;
51
+ }
52
+ i32() {
53
+ return this.u32() | 0;
54
+ }
55
+ /** An Xnum ([MS-XLS] 2.5.342): a little-endian IEEE 754 double. */
56
+ f64() {
57
+ const raw = this.take(8);
58
+ return new DataView(raw.buffer, raw.byteOffset, raw.byteLength).getFloat64(0, true);
59
+ }
60
+ /** The next `count` bytes, copied out. Spans block boundaries. */
61
+ take(count) {
62
+ const out = new Uint8Array(count);
63
+ for (let index = 0; index < count; index += 1) out[index] = this.nextByte(`${count}-byte run`);
64
+ return out;
65
+ }
66
+ /** Advances `count` bytes without materialising them -- for a field this package reads past rather than reads. */
67
+ skip(count) {
68
+ for (let index = 0; index < count; index += 1) this.nextByte(`${count}-byte skip`);
69
+ }
70
+ };
71
+ //#endregion
72
+ export { BlockCursor };
@@ -0,0 +1,18 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/biff/errors.ts
3
+ const ERROR_TEXT = /* @__PURE__ */ new Map([
4
+ [0, "#NULL!"],
5
+ [7, "#DIV/0!"],
6
+ [15, "#VALUE!"],
7
+ [23, "#REF!"],
8
+ [29, "#NAME?"],
9
+ [36, "#NUM!"],
10
+ [42, "#N/A"],
11
+ [43, "#GETTING_DATA"]
12
+ ]);
13
+ /** The displayed spelling of a BIFF8 error code, or undefined for a code [MS-XLS] does not define -- which the caller degrades rather than guessing a spelling for. */
14
+ function errorTextOf(code) {
15
+ return ERROR_TEXT.get(code);
16
+ }
17
+ //#endregion
18
+ exports.errorTextOf = errorTextOf;
@@ -0,0 +1,5 @@
1
+ //#region src/biff/errors.d.ts
2
+ /** The displayed spelling of a BIFF8 error code, or undefined for a code [MS-XLS] does not define -- which the caller degrades rather than guessing a spelling for. */
3
+ declare function errorTextOf(code: number): string | undefined;
4
+ //#endregion
5
+ export { errorTextOf };
@@ -0,0 +1,5 @@
1
+ //#region src/biff/errors.d.ts
2
+ /** The displayed spelling of a BIFF8 error code, or undefined for a code [MS-XLS] does not define -- which the caller degrades rather than guessing a spelling for. */
3
+ declare function errorTextOf(code: number): string | undefined;
4
+ //#endregion
5
+ export { errorTextOf };
@@ -0,0 +1,17 @@
1
+ //#region src/biff/errors.ts
2
+ const ERROR_TEXT = /* @__PURE__ */ new Map([
3
+ [0, "#NULL!"],
4
+ [7, "#DIV/0!"],
5
+ [15, "#VALUE!"],
6
+ [23, "#REF!"],
7
+ [29, "#NAME?"],
8
+ [36, "#NUM!"],
9
+ [42, "#N/A"],
10
+ [43, "#GETTING_DATA"]
11
+ ]);
12
+ /** The displayed spelling of a BIFF8 error code, or undefined for a code [MS-XLS] does not define -- which the caller degrades rather than guessing a spelling for. */
13
+ function errorTextOf(code) {
14
+ return ERROR_TEXT.get(code);
15
+ }
16
+ //#endregion
17
+ export { errorTextOf };
@@ -0,0 +1,108 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/biff/record-types.ts
3
+ /** Marks the beginning of a substream and names its document type ([MS-XLS] 2.4.21). */
4
+ const RECORD_BOF = 2057;
5
+ /** Marks the end of a substream ([MS-XLS] 2.4.103). */
6
+ const RECORD_EOF = 10;
7
+ /** Carries the overflow of a preceding record whose data exceeds what one record can hold ([MS-XLS] 2.4.58). */
8
+ const RECORD_CONTINUE = 60;
9
+ /** One per sheet: its name, hidden state, type, and the stream offset of its own BOF ([MS-XLS] 2.4.28). */
10
+ const RECORD_BOUNDSHEET8 = 133;
11
+ /** The shared string table every LabelSst cell indexes into ([MS-XLS] 2.4.265). */
12
+ const RECORD_SST = 252;
13
+ /** A custom number-format code and the identifier cells reference it by ([MS-XLS] 2.4.126). */
14
+ const RECORD_FORMAT = 1054;
15
+ /** A cell or cell-style extended format record ([MS-XLS] 2.4.353). */
16
+ const RECORD_XF = 224;
17
+ /** A font definition, referenced by index from an XF ([MS-XLS] 2.4.122). */
18
+ const RECORD_FONT = 49;
19
+ /** Which of the two date epochs this workbook's serials count from ([MS-XLS] 2.4.77). */
20
+ const RECORD_DATE1904 = 34;
21
+ /** Present only in an encrypted workbook, where every record after it is ciphertext ([MS-XLS] 2.4.117). */
22
+ const RECORD_FILEPASS = 47;
23
+ /** The sheet's used range ([MS-XLS] 2.4.90). */
24
+ const RECORD_DIMENSIONS = 512;
25
+ /** A single row's height, hidden state, and formatting ([MS-XLS] 2.4.221). */
26
+ const RECORD_ROW = 520;
27
+ /** Column width, hidden state, and default format for a range of columns ([MS-XLS] 2.4.53). */
28
+ const RECORD_COLINFO = 125;
29
+ /** The sheet's default column width, in whole character widths ([MS-XLS] 2.4.89). */
30
+ const RECORD_DEFCOLWIDTH = 85;
31
+ /** The sheet's default row height ([MS-XLS] 2.4.87). */
32
+ const RECORD_DEFAULTROWHEIGHT = 549;
33
+ /** Merged cell ranges ([MS-XLS] 2.4.168). */
34
+ const RECORD_MERGECELLS = 229;
35
+ /** An empty cell carrying only formatting ([MS-XLS] 2.4.20). */
36
+ const RECORD_BLANK = 513;
37
+ /** A run of empty cells in one row ([MS-XLS] 2.4.174). */
38
+ const RECORD_MULBLANK = 190;
39
+ /** A cell holding an RK-encoded number ([MS-XLS] 2.4.220). */
40
+ const RECORD_RK = 638;
41
+ /** A run of RK-encoded numeric cells in one row ([MS-XLS] 2.4.175). */
42
+ const RECORD_MULRK = 189;
43
+ /** A cell holding an IEEE 754 double ([MS-XLS] 2.4.180). */
44
+ const RECORD_NUMBER = 515;
45
+ /** A cell holding a boolean or an error value ([MS-XLS] 2.4.24). */
46
+ const RECORD_BOOLERR = 517;
47
+ /** A cell holding a string by index into the shared string table ([MS-XLS] 2.4.149). */
48
+ const RECORD_LABELSST = 253;
49
+ /** A cell holding an inline string ([MS-XLS] 2.4.148). See the reader's own note on why BIFF8 still meets this in the wild. */
50
+ const RECORD_LABEL = 516;
51
+ /** A cell holding a formula, its cached result, and its parsed expression ([MS-XLS] 2.4.127). */
52
+ const RECORD_FORMULA = 6;
53
+ /** The string result of the Formula record preceding it ([MS-XLS] 2.4.268). */
54
+ const RECORD_STRING = 519;
55
+ /** The expression of an array formula, following the Formula record of its top-left cell ([MS-XLS] 2.4.4). */
56
+ const RECORD_ARRAY = 545;
57
+ /** A data-table definition following a Formula record ([MS-XLS] 2.4.334). */
58
+ const RECORD_TABLE = 566;
59
+ /** The shared expression a run of Formula records refers to ([MS-XLS] 2.4.260). */
60
+ const RECORD_SHRFMLA = 1212;
61
+ /** The workbook globals substream. */
62
+ const BOF_TYPE_WORKBOOK = 5;
63
+ /** A worksheet or dialog sheet substream. */
64
+ const BOF_TYPE_WORKSHEET = 16;
65
+ /** A chart sheet substream. */
66
+ const BOF_TYPE_CHART = 32;
67
+ /** A macro sheet substream. */
68
+ const BOF_TYPE_MACRO = 64;
69
+ /** The BIFF version every BOF in a BIFF8 workbook stream declares ([MS-XLS] 2.4.21: "The value MUST be 0x0600"). */
70
+ const BIFF8_VERSION = 1536;
71
+ /** [MS-XLS] 2.1.4: "The record size ... MUST be less than or equal to 8224." The ceiling a writer splits a record at, and the bound a reader can sanity-check a declared size against. */
72
+ const MAX_RECORD_DATA_SIZE = 8224;
73
+ //#endregion
74
+ exports.BIFF8_VERSION = BIFF8_VERSION;
75
+ exports.BOF_TYPE_CHART = BOF_TYPE_CHART;
76
+ exports.BOF_TYPE_MACRO = BOF_TYPE_MACRO;
77
+ exports.BOF_TYPE_WORKBOOK = BOF_TYPE_WORKBOOK;
78
+ exports.BOF_TYPE_WORKSHEET = BOF_TYPE_WORKSHEET;
79
+ exports.MAX_RECORD_DATA_SIZE = MAX_RECORD_DATA_SIZE;
80
+ exports.RECORD_ARRAY = RECORD_ARRAY;
81
+ exports.RECORD_BLANK = RECORD_BLANK;
82
+ exports.RECORD_BOF = RECORD_BOF;
83
+ exports.RECORD_BOOLERR = RECORD_BOOLERR;
84
+ exports.RECORD_BOUNDSHEET8 = RECORD_BOUNDSHEET8;
85
+ exports.RECORD_COLINFO = RECORD_COLINFO;
86
+ exports.RECORD_CONTINUE = RECORD_CONTINUE;
87
+ exports.RECORD_DATE1904 = RECORD_DATE1904;
88
+ exports.RECORD_DEFAULTROWHEIGHT = RECORD_DEFAULTROWHEIGHT;
89
+ exports.RECORD_DEFCOLWIDTH = RECORD_DEFCOLWIDTH;
90
+ exports.RECORD_DIMENSIONS = RECORD_DIMENSIONS;
91
+ exports.RECORD_EOF = RECORD_EOF;
92
+ exports.RECORD_FILEPASS = RECORD_FILEPASS;
93
+ exports.RECORD_FONT = RECORD_FONT;
94
+ exports.RECORD_FORMAT = RECORD_FORMAT;
95
+ exports.RECORD_FORMULA = RECORD_FORMULA;
96
+ exports.RECORD_LABEL = RECORD_LABEL;
97
+ exports.RECORD_LABELSST = RECORD_LABELSST;
98
+ exports.RECORD_MERGECELLS = RECORD_MERGECELLS;
99
+ exports.RECORD_MULBLANK = RECORD_MULBLANK;
100
+ exports.RECORD_MULRK = RECORD_MULRK;
101
+ exports.RECORD_NUMBER = RECORD_NUMBER;
102
+ exports.RECORD_RK = RECORD_RK;
103
+ exports.RECORD_ROW = RECORD_ROW;
104
+ exports.RECORD_SHRFMLA = RECORD_SHRFMLA;
105
+ exports.RECORD_SST = RECORD_SST;
106
+ exports.RECORD_STRING = RECORD_STRING;
107
+ exports.RECORD_TABLE = RECORD_TABLE;
108
+ exports.RECORD_XF = RECORD_XF;
@@ -0,0 +1,73 @@
1
+ //#region src/biff/record-types.d.ts
2
+ /** Marks the beginning of a substream and names its document type ([MS-XLS] 2.4.21). */
3
+ declare const RECORD_BOF = 2057;
4
+ /** Marks the end of a substream ([MS-XLS] 2.4.103). */
5
+ declare const RECORD_EOF = 10;
6
+ /** Carries the overflow of a preceding record whose data exceeds what one record can hold ([MS-XLS] 2.4.58). */
7
+ declare const RECORD_CONTINUE = 60;
8
+ /** One per sheet: its name, hidden state, type, and the stream offset of its own BOF ([MS-XLS] 2.4.28). */
9
+ declare const RECORD_BOUNDSHEET8 = 133;
10
+ /** The shared string table every LabelSst cell indexes into ([MS-XLS] 2.4.265). */
11
+ declare const RECORD_SST = 252;
12
+ /** A custom number-format code and the identifier cells reference it by ([MS-XLS] 2.4.126). */
13
+ declare const RECORD_FORMAT = 1054;
14
+ /** A cell or cell-style extended format record ([MS-XLS] 2.4.353). */
15
+ declare const RECORD_XF = 224;
16
+ /** A font definition, referenced by index from an XF ([MS-XLS] 2.4.122). */
17
+ declare const RECORD_FONT = 49;
18
+ /** Which of the two date epochs this workbook's serials count from ([MS-XLS] 2.4.77). */
19
+ declare const RECORD_DATE1904 = 34;
20
+ /** Present only in an encrypted workbook, where every record after it is ciphertext ([MS-XLS] 2.4.117). */
21
+ declare const RECORD_FILEPASS = 47;
22
+ /** The sheet's used range ([MS-XLS] 2.4.90). */
23
+ declare const RECORD_DIMENSIONS = 512;
24
+ /** A single row's height, hidden state, and formatting ([MS-XLS] 2.4.221). */
25
+ declare const RECORD_ROW = 520;
26
+ /** Column width, hidden state, and default format for a range of columns ([MS-XLS] 2.4.53). */
27
+ declare const RECORD_COLINFO = 125;
28
+ /** The sheet's default column width, in whole character widths ([MS-XLS] 2.4.89). */
29
+ declare const RECORD_DEFCOLWIDTH = 85;
30
+ /** The sheet's default row height ([MS-XLS] 2.4.87). */
31
+ declare const RECORD_DEFAULTROWHEIGHT = 549;
32
+ /** Merged cell ranges ([MS-XLS] 2.4.168). */
33
+ declare const RECORD_MERGECELLS = 229;
34
+ /** An empty cell carrying only formatting ([MS-XLS] 2.4.20). */
35
+ declare const RECORD_BLANK = 513;
36
+ /** A run of empty cells in one row ([MS-XLS] 2.4.174). */
37
+ declare const RECORD_MULBLANK = 190;
38
+ /** A cell holding an RK-encoded number ([MS-XLS] 2.4.220). */
39
+ declare const RECORD_RK = 638;
40
+ /** A run of RK-encoded numeric cells in one row ([MS-XLS] 2.4.175). */
41
+ declare const RECORD_MULRK = 189;
42
+ /** A cell holding an IEEE 754 double ([MS-XLS] 2.4.180). */
43
+ declare const RECORD_NUMBER = 515;
44
+ /** A cell holding a boolean or an error value ([MS-XLS] 2.4.24). */
45
+ declare const RECORD_BOOLERR = 517;
46
+ /** A cell holding a string by index into the shared string table ([MS-XLS] 2.4.149). */
47
+ declare const RECORD_LABELSST = 253;
48
+ /** A cell holding an inline string ([MS-XLS] 2.4.148). See the reader's own note on why BIFF8 still meets this in the wild. */
49
+ declare const RECORD_LABEL = 516;
50
+ /** A cell holding a formula, its cached result, and its parsed expression ([MS-XLS] 2.4.127). */
51
+ declare const RECORD_FORMULA = 6;
52
+ /** The string result of the Formula record preceding it ([MS-XLS] 2.4.268). */
53
+ declare const RECORD_STRING = 519;
54
+ /** The expression of an array formula, following the Formula record of its top-left cell ([MS-XLS] 2.4.4). */
55
+ declare const RECORD_ARRAY = 545;
56
+ /** A data-table definition following a Formula record ([MS-XLS] 2.4.334). */
57
+ declare const RECORD_TABLE = 566;
58
+ /** The shared expression a run of Formula records refers to ([MS-XLS] 2.4.260). */
59
+ declare const RECORD_SHRFMLA = 1212;
60
+ /** The workbook globals substream. */
61
+ declare const BOF_TYPE_WORKBOOK = 5;
62
+ /** A worksheet or dialog sheet substream. */
63
+ declare const BOF_TYPE_WORKSHEET = 16;
64
+ /** A chart sheet substream. */
65
+ declare const BOF_TYPE_CHART = 32;
66
+ /** A macro sheet substream. */
67
+ declare const BOF_TYPE_MACRO = 64;
68
+ /** The BIFF version every BOF in a BIFF8 workbook stream declares ([MS-XLS] 2.4.21: "The value MUST be 0x0600"). */
69
+ declare const BIFF8_VERSION = 1536;
70
+ /** [MS-XLS] 2.1.4: "The record size ... MUST be less than or equal to 8224." The ceiling a writer splits a record at, and the bound a reader can sanity-check a declared size against. */
71
+ declare const MAX_RECORD_DATA_SIZE = 8224;
72
+ //#endregion
73
+ export { BIFF8_VERSION, BOF_TYPE_CHART, BOF_TYPE_MACRO, BOF_TYPE_WORKBOOK, BOF_TYPE_WORKSHEET, MAX_RECORD_DATA_SIZE, RECORD_ARRAY, RECORD_BLANK, RECORD_BOF, RECORD_BOOLERR, RECORD_BOUNDSHEET8, RECORD_COLINFO, RECORD_CONTINUE, RECORD_DATE1904, RECORD_DEFAULTROWHEIGHT, RECORD_DEFCOLWIDTH, RECORD_DIMENSIONS, RECORD_EOF, RECORD_FILEPASS, RECORD_FONT, RECORD_FORMAT, RECORD_FORMULA, RECORD_LABEL, RECORD_LABELSST, RECORD_MERGECELLS, RECORD_MULBLANK, RECORD_MULRK, RECORD_NUMBER, RECORD_RK, RECORD_ROW, RECORD_SHRFMLA, RECORD_SST, RECORD_STRING, RECORD_TABLE, RECORD_XF };