xls-codec 3.0.0 → 4.1.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 (38) hide show
  1. package/README.md +4 -4
  2. package/dist/biff/ptg.cjs +160 -7
  3. package/dist/biff/ptg.d.cts +2 -2
  4. package/dist/biff/ptg.d.ts +2 -2
  5. package/dist/biff/ptg.js +161 -9
  6. package/dist/biff/strings.cjs +6 -0
  7. package/dist/biff/strings.d.cts +3 -1
  8. package/dist/biff/strings.d.ts +3 -1
  9. package/dist/biff/strings.js +6 -1
  10. package/dist/biff/substreams.cjs +5 -0
  11. package/dist/biff/substreams.d.cts +2 -2
  12. package/dist/biff/substreams.d.ts +2 -2
  13. package/dist/biff/substreams.js +5 -1
  14. package/dist/index.cjs +2 -0
  15. package/dist/index.d.cts +3 -3
  16. package/dist/index.d.ts +3 -3
  17. package/dist/index.js +3 -3
  18. package/dist/{print-names-DUlpVE00.d.ts → print-names-C-PQ2vAy.d.ts} +1 -1
  19. package/dist/{print-names-D-njuzVw.d.cts → print-names-DyYloloV.d.cts} +1 -1
  20. package/dist/ptg-CCBbJLZJ.d.cts +49 -0
  21. package/dist/ptg-CCBbJLZJ.d.ts +49 -0
  22. package/dist/{substreams-D7dQiJbp.d.ts → substreams-CmyZMtuM.d.ts} +3 -1
  23. package/dist/{substreams-Ddtvn_Vr.d.cts → substreams-Cpy5Fi3d.d.cts} +3 -1
  24. package/dist/workbook/globals-writer.d.cts +1 -1
  25. package/dist/workbook/globals-writer.d.ts +1 -1
  26. package/dist/workbook/globals.cjs +137 -14
  27. package/dist/workbook/globals.d.cts +6 -6
  28. package/dist/workbook/globals.d.ts +6 -6
  29. package/dist/workbook/globals.js +138 -15
  30. package/dist/workbook/print-names.d.cts +1 -1
  31. package/dist/workbook/print-names.d.ts +1 -1
  32. package/dist/workbook/sheet.cjs +109 -6
  33. package/dist/workbook/sheet.d.cts +3 -3
  34. package/dist/workbook/sheet.d.ts +3 -3
  35. package/dist/workbook/sheet.js +110 -7
  36. package/package.json +10 -4
  37. package/dist/ptg-B2K8t3js.d.cts +0 -21
  38. package/dist/ptg-B2K8t3js.d.ts +0 -21
@@ -1,8 +1,9 @@
1
1
  import { RECORD_FORMAT } from "../biff/record-types.js";
2
2
  import { BiffFormatError } from "../biff/records.js";
3
+ import { recordByteLength } from "../biff/substreams.js";
3
4
  import { readLongRgbColor, unpackXfAlignment, unpackXfDecoration } from "../biff/xf-colors.js";
4
5
  import { BlockCursor } from "../biff/cursor.js";
5
- import { readRichExtendedString, readShortXLUnicodeString, readXLUnicodeString } from "../biff/strings.js";
6
+ import { readRichExtendedString, readShortXLUnicodeString, readXLUnicodeString, readXLUnicodeStringNoCch } from "../biff/strings.js";
6
7
  import { readPrintNames } from "./print-names.js";
7
8
  import { BUILTIN_NUMBER_FORMATS } from "excel-number-format";
8
9
  //#region src/workbook/globals.ts
@@ -18,7 +19,7 @@ function readWorkbookGlobals(records) {
18
19
  const sheets = [];
19
20
  const cellFormats = [];
20
21
  const customFormats = /* @__PURE__ */ new Map();
21
- const supBookSelfReferencing = [];
22
+ const supBooks = [];
22
23
  let sharedStrings = [];
23
24
  let date1904 = false;
24
25
  let externSheet;
@@ -42,7 +43,7 @@ function readWorkbookGlobals(records) {
42
43
  date1904 = readDate1904(record);
43
44
  break;
44
45
  case 430:
45
- supBookSelfReferencing.push(isSelfReferencingSupBook(record));
46
+ supBooks.push(readSupBookSafely(record));
46
47
  break;
47
48
  case 23:
48
49
  externSheet = record;
@@ -51,7 +52,7 @@ function readWorkbookGlobals(records) {
51
52
  }
52
53
  const numberFormats = new Map(BUILTIN_NUMBER_FORMATS);
53
54
  for (const [id, code] of customFormats) numberFormats.set(id, code);
54
- const sheetRanges = externSheet === void 0 ? [] : readSheetRanges(externSheet, supBookSelfReferencing);
55
+ const sheetRanges = externSheet === void 0 ? [] : readSheetRanges(externSheet, supBooks);
55
56
  return {
56
57
  sheets,
57
58
  sharedStrings,
@@ -63,16 +64,141 @@ function readWorkbookGlobals(records) {
63
64
  printNames: readPrintNames(records)
64
65
  };
65
66
  }
66
- /** [MS-XLS] 2.4.271's own cch table: a SupBook whose cch is exactly this value is a self-referencing supporting link -- this workbook itself -- rather than another workbook, a DDE/OLE data source, or an add-in. This is the only kind of supporting link a 3D reference resolves through here; every other kind genuinely needs bytes from outside this file. */
67
+ /** [MS-XLS] 2.4.271's own cch table: a SupBook whose cch is exactly this value is a self-referencing supporting link -- this workbook itself -- rather than another workbook, a DDE/OLE data source, or an add-in. */
67
68
  const SUPBOOK_SELF_REFERENCING_CCH = 1025;
68
- /** SupBook ([MS-XLS] 2.4.271): a two-byte ctab then a two-byte cch. Only cch is read here -- ctab is undefined for a self-referencing SupBook and unused for every other kind this reader does not resolve, and the virtPath/rgst payload that would follow for a non-self-referencing link is never reached. */
69
- function isSelfReferencingSupBook(record) {
69
+ /** The cch table's other named sentinel: an add-in-referencing supporting link, whose ExternName records name add-in functions this reader has no workbook or sheet to resolve a name from. */
70
+ const SUPBOOK_ADDIN_CCH = 14849;
71
+ /** The inclusive cch range meaning "virtPath is present, and its own grammar (rather than a further sentinel) says what kind of supporting link this is" ([MS-XLS] 2.4.271's own cch table). */
72
+ const SUPBOOK_VIRTPATH_MAX_CCH = 255;
73
+ /** virtPath's own single-character sentinels ([MS-XLS] 31ed3738's own virtPath value table): a lone NUL marks a same-sheet reference, a lone SPACE an unused supporting link. */
74
+ const SUPBOOK_SAME_SHEET_CHAR = "\0";
75
+ const SUPBOOK_UNUSED_CHAR = " ";
76
+ /**
77
+ * SupBook ([MS-XLS] 2.4.271): a two-byte ctab, a two-byte cch, then -- for every kind but self-referencing and add-in-referencing -- a virtPath (an XLUnicodeStringNoCch, cch characters long) and, for an external-workbook or unused link specifically, ctab sheet names (XLUnicodeString) in rgst.
78
+ */
79
+ function readSupBook(record) {
70
80
  const cursor = new BlockCursor(record.blocks);
71
- cursor.skip(2);
72
- return cursor.u16() === SUPBOOK_SELF_REFERENCING_CCH;
81
+ const ctab = cursor.u16();
82
+ const cch = cursor.u16();
83
+ if (cch === SUPBOOK_SELF_REFERENCING_CCH) return { kind: "self" };
84
+ if (cch === SUPBOOK_ADDIN_CCH) return {
85
+ kind: "unresolvable",
86
+ diagnostic: "add-in function reference"
87
+ };
88
+ if (cch < 1 || cch > SUPBOOK_VIRTPATH_MAX_CCH) return {
89
+ kind: "unresolvable",
90
+ diagnostic: `supporting link of unrecognised type (cch=0x${cch.toString(16).padStart(4, "0")})`
91
+ };
92
+ const virtPath = readXLUnicodeStringNoCch(cursor, cch);
93
+ if (virtPath === SUPBOOK_SAME_SHEET_CHAR) return {
94
+ kind: "unresolvable",
95
+ diagnostic: "same-sheet reference"
96
+ };
97
+ if (virtPath === SUPBOOK_UNUSED_CHAR) return {
98
+ kind: "unresolvable",
99
+ diagnostic: "unused supporting link"
100
+ };
101
+ if (ctab === 0) return {
102
+ kind: "unresolvable",
103
+ diagnostic: "DDE or OLE data source reference"
104
+ };
105
+ const fileName = fileNameFromVirtPath(virtPath);
106
+ const sheetNames = [];
107
+ for (let index = 0; index < ctab; index += 1) sheetNames.push(readXLUnicodeString(cursor));
108
+ return {
109
+ kind: "external-workbook",
110
+ fileName,
111
+ sheetNames
112
+ };
113
+ }
114
+ /**
115
+ * readSupBook, degrading a malformed record (an rgst shorter than its own declared ctab, most concretely) to an unresolvable diagnostic rather than letting a BiffFormatError propagate out of this one SupBook and abort the whole globals read -- and with it, the whole workbook. This is entirely new territory added alongside external-3D-reference resolution: before it, this reader never walked a SupBook's own virtPath/rgst at all, so a malformed one had nothing here to trip over. The per-record boundary keeps the damage to the XTI entries that resolve through this one SupBook, which already carry their own `unresolvable` label path for every other kind this reader cannot fully resolve.
116
+ */
117
+ function readSupBookSafely(record) {
118
+ try {
119
+ return readSupBook(record);
120
+ } catch (error) {
121
+ if (!(error instanceof BiffFormatError)) throw error;
122
+ return {
123
+ kind: "unresolvable",
124
+ diagnostic: "malformed supporting link"
125
+ };
126
+ }
127
+ }
128
+ /** [MS-XLS] 480c3d2a's own VirtualPath grammar directory separator (U+0003) -- never a printable character a real file or sheet name may contain, so splitting on it to find the trailing segment is unambiguous. */
129
+ const VIRTPATH_DIRECTORY_SEPARATOR = "";
130
+ /** [MS-XLS] 480c3d2a's own two-character virt-path markers this reader still resolves a trailing file name through: rel-volume, startup, alt-startup, and library -- each %x0001 followed by one of these bytes, then file-path itself. Distinct from simple-file-path, whose own leading %x0001 (when present at all) stands ALONE, with no second marker byte, directly followed by file-path -- so any OTHER second character belongs to that file-path, not to a marker this function should also consume. */
131
+ const VIRTPATH_REL_VOLUME_MARKER = 2;
132
+ const VIRTPATH_STARTUP_MARKER = 6;
133
+ const VIRTPATH_ALT_STARTUP_MARKER = 7;
134
+ const VIRTPATH_LIBRARY_MARKER = 8;
135
+ /**
136
+ * Isolates a plain trailing file name from a SupBook's own virtPath, when it uses one of the VirtualPath grammar's simpler forms: simple-file-path (no marker at all, or its own optional lone %x0001 with no second marker byte), or a genuine two-character marker saying the path is relative to the referencing workbook's own drive, the startup directory, the alternate startup directory, or the library directory (rel-volume/startup/alt-startup/library -- [MS-XLS] 480c3d2a's own virt-path alternatives). An absolute drive volume, a UNC share, or a transfer-protocol URL needs more of the grammar than a trailing path segment to reproduce faithfully, so those return undefined rather than a guess -- readSupBook's own caller then shows the sheet name(s) (still fully resolvable from rgst) against a placeholder workbook label instead of discarding them. file-path's own bracketed form (`"[" relative-path "]" sheet-name`, naming a sheet directly in the path rather than through SupBook's separate rgst array) is outside what this reader reconstructs too, and is declined the same way rather than folded into the file name and doubled up with the caller's own `[bookLabel]` bracketing.
137
+ */
138
+ function fileNameFromVirtPath(virtPath) {
139
+ let path = virtPath;
140
+ if (path.startsWith("")) switch (path.codePointAt(1)) {
141
+ case 1:
142
+ case 5: return;
143
+ case VIRTPATH_REL_VOLUME_MARKER:
144
+ case VIRTPATH_STARTUP_MARKER:
145
+ case VIRTPATH_ALT_STARTUP_MARKER:
146
+ case VIRTPATH_LIBRARY_MARKER:
147
+ path = path.slice(2);
148
+ break;
149
+ default: path = path.slice(1);
150
+ }
151
+ if (path.length === 0) return;
152
+ const last = path.split(VIRTPATH_DIRECTORY_SEPARATOR).at(-1);
153
+ if (last === void 0 || last.length === 0) return;
154
+ return last.includes("[") || last.includes("]") ? void 0 : last;
155
+ }
156
+ /** A bracketed diagnostic placeholder for a sheet label this reader could not fully resolve -- deliberately distinct from Excel's own bare `#REF!` error literal (which is valid, retypeable formula syntax on its own): this always carries a parenthesised reason, and resolveSheetLabel's own quoting (biff/ptg.ts) wraps the whole thing in single quotes regardless, since neither the reason text nor the surrounding punctuation matches a bare sheet-name pattern. */
157
+ function diagnosticLabel(reason) {
158
+ return `#REF!(${reason})`;
159
+ }
160
+ /**
161
+ * One XTI's own scope resolved against its SupBook -- a plain SheetRange when the SupBook is this same, self-referencing workbook and both sheet indices are real ([MS-XLS] 2.5.344's own `-1` "could not be found" and `-2` "workbook-level" sentinels aside), otherwise an ExternalSheetLabel carrying whatever this reader could recover (an external workbook's own file name and sheet name(s), when the SupBook resolves that far) or a diagnostic placeholder, for a supporting-link kind or a sheet index this reader does not resolve a name from at all.
162
+ *
163
+ * The SupBook's own kind is checked before the `-2` sentinel, not after: [MS-XLS] 2.5.344's itabFirst/itabLast table produces `-2` for a same-sheet, add-in, DDE, and OLE supporting link alike (none of them names a sheet at all), so treating every `-2` as a generic "workbook-level reference" before asking what kind of SupBook it belongs to would overwrite each of those already-specific `unresolvable` diagnostics with a less useful, wrong one. `-2` only means "workbook-level" for the two kinds that otherwise resolve a real sheet scope -- self and external-workbook -- so the sentinel is scoped to those.
164
+ */
165
+ function resolveXti(supBook, itabFirst, itabLast) {
166
+ if (supBook === void 0) return {
167
+ label: diagnosticLabel("supporting link index out of range"),
168
+ diagnostic: true
169
+ };
170
+ if (supBook.kind === "unresolvable") return {
171
+ label: diagnosticLabel(supBook.diagnostic),
172
+ diagnostic: true
173
+ };
174
+ if (itabFirst === -2 || itabLast === -2) return {
175
+ label: diagnosticLabel("workbook-level reference"),
176
+ diagnostic: true
177
+ };
178
+ if (supBook.kind === "self") return itabFirst >= 0 && itabLast >= 0 ? {
179
+ firstSheetIndex: itabFirst,
180
+ lastSheetIndex: itabLast
181
+ } : {
182
+ label: diagnosticLabel("sheet not found"),
183
+ diagnostic: true
184
+ };
185
+ const first = itabFirst >= 0 ? supBook.sheetNames[itabFirst] : void 0;
186
+ const last = itabLast >= 0 ? supBook.sheetNames[itabLast] : void 0;
187
+ if (first === void 0 || last === void 0) return {
188
+ label: `[${supBook.fileName ?? "EXTERNAL"}]${diagnosticLabel("sheet not found")}`,
189
+ diagnostic: true
190
+ };
191
+ if (supBook.fileName === void 0) return {
192
+ label: `[EXTERNAL]${first === last ? first : `${first}:${last}`}`,
193
+ diagnostic: true
194
+ };
195
+ return {
196
+ label: `[${supBook.fileName}]${first === last ? first : `${first}:${last}`}`,
197
+ diagnostic: false
198
+ };
73
199
  }
74
200
  /** ExternSheet ([MS-XLS] 2.4.106): a two-byte cXTI then that many XTI structures ([MS-XLS] 2.5.344) -- a two-byte iSupBook and two signed 16-bit sheet-scope bounds each. iSupBook indexes the SupBook collection positionally, in the order those records appeared. */
75
- function readSheetRanges(record, supBookSelfReferencing) {
201
+ function readSheetRanges(record, supBooks) {
76
202
  const cursor = new BlockCursor(record.blocks);
77
203
  const count = cursor.u16();
78
204
  const ranges = [];
@@ -80,10 +206,7 @@ function readSheetRanges(record, supBookSelfReferencing) {
80
206
  const iSupBook = cursor.u16();
81
207
  const itabFirst = cursor.i16();
82
208
  const itabLast = cursor.i16();
83
- ranges.push((supBookSelfReferencing[iSupBook] ?? false) && itabFirst >= 0 && itabLast >= 0 ? {
84
- firstSheetIndex: itabFirst,
85
- lastSheetIndex: itabLast
86
- } : void 0);
209
+ ranges.push(resolveXti(supBooks[iSupBook], itabFirst, itabLast));
87
210
  }
88
211
  return ranges;
89
212
  }
@@ -106,7 +229,7 @@ function readSharedStrings(record) {
106
229
  cursor.i32();
107
230
  const uniqueCount = cursor.i32();
108
231
  if (uniqueCount < 0) throw new BiffFormatError(`SST declares a negative unique-string count (${uniqueCount})`);
109
- const totalBytes = record.blocks.reduce((sum, block) => sum + block.length, 0);
232
+ const totalBytes = recordByteLength(record);
110
233
  if (uniqueCount * MIN_SST_ENTRY_BYTES > totalBytes) throw new BiffFormatError(`SST declares ${uniqueCount} unique strings, more than its ${totalBytes} bytes could carry`);
111
234
  const strings = [];
112
235
  for (let index = 0; index < uniqueCount; index += 1) strings.push(readRichExtendedString(cursor));
@@ -1,2 +1,2 @@
1
- import { a as writePrintNameRecords, i as readPrintNames, n as SheetPrintNames, r as printNameEntriesFor, t as PrintNamePlanEntry } from "../print-names-D-njuzVw.cjs";
1
+ import { a as writePrintNameRecords, i as readPrintNames, n as SheetPrintNames, r as printNameEntriesFor, t as PrintNamePlanEntry } from "../print-names-DyYloloV.cjs";
2
2
  export { PrintNamePlanEntry, SheetPrintNames, printNameEntriesFor, readPrintNames, writePrintNameRecords };
@@ -1,2 +1,2 @@
1
- import { a as writePrintNameRecords, i as readPrintNames, n as SheetPrintNames, r as printNameEntriesFor, t as PrintNamePlanEntry } from "../print-names-DUlpVE00.js";
1
+ import { a as writePrintNameRecords, i as readPrintNames, n as SheetPrintNames, r as printNameEntriesFor, t as PrintNamePlanEntry } from "../print-names-C-PQ2vAy.js";
2
2
  export { PrintNamePlanEntry, SheetPrintNames, printNameEntriesFor, readPrintNames, writePrintNameRecords };
@@ -3,6 +3,7 @@ require("../biff/record-types.cjs");
3
3
  const require_biff_records = require("../biff/records.cjs");
4
4
  const require_units = require("../units.cjs");
5
5
  const require_biff_print_setup = require("../biff/print-setup.cjs");
6
+ const require_biff_substreams = require("../biff/substreams.cjs");
6
7
  const require_biff_cursor = require("../biff/cursor.cjs");
7
8
  const require_biff_strings = require("../biff/strings.cjs");
8
9
  const require_biff_errors = require("../biff/errors.cjs");
@@ -31,8 +32,80 @@ const EMPTY_FORMULA_SHEET_CONTEXT = {
31
32
  sheets: [],
32
33
  sheetRanges: []
33
34
  };
35
+ /** The key collectFormulaGroups and its lookup agree on: a shared/array formula group's own base cell, the same (row, column) a PtgExp token elsewhere in the sheet points back to. */
36
+ function groupKey(row, column) {
37
+ return `${row},${column}`;
38
+ }
39
+ /**
40
+ * Walks every record once, looking for a Formula record immediately followed by a ShrFmla or Array record ([MS-XLS] 2.1.7.20.6's own FORMULA production, and 984826cc/c6ee7512's own "this record is preceded by a single Formula record"), and returns the shared/array expression each one carries, keyed by that Formula record's own cell -- the same (row, column) a PtgExp token names when it points back to this group (see readPtgExpBase, and readFormula below which performs the actual lookup).
41
+ *
42
+ * Built as a single upfront pass over the whole sheet rather than interleaved into readSheetRecords' own per-record loop: every Formula record that uses a shared/array formula (including the group's own base cell, which points at itself) needs this map already complete when it is reached, and although [MS-XLS] guarantees the base pair precedes every other use, resolving the whole map first removes that ordering as a correctness dependency rather than merely relying on it.
43
+ */
44
+ function collectFormulaGroups(records) {
45
+ const groups = /* @__PURE__ */ new Map();
46
+ for (let index = 0; index < records.length; index += 1) {
47
+ const record = records[index];
48
+ const next = records[index + 1];
49
+ if (record === void 0 || next === void 0) continue;
50
+ if (record.type !== 6) continue;
51
+ if (next.type === 1212) collectFormulaGroup(groups, record, next, readShrFmlaGroup);
52
+ else if (next.type === 545) collectFormulaGroup(groups, record, next, readArrayGroup);
53
+ }
54
+ return groups;
55
+ }
56
+ /**
57
+ * Reads one shared/array formula group and keys it by its base Formula record's own cell, degrading a malformed ShrFmla/Array record to "no group recovered for this base cell" rather than letting a BiffFormatError propagate out of collectFormulaGroups and abort the whole sheet read (and every other cell in it, formula or not). readCellHeader and readGroup (readShrFmlaGroup or readArrayGroup) between them make several cursor reads capable of raising that error, not only the final `cursor.take(cce)` that copies out rgce itself: an undersized record already runs out of bytes during readCellHeader's own Cell fields, or during readShrFmlaGroup/readArrayGroup's leading `cursor.skip` past their fixed header, or during the `cursor.u16()` that reads cce -- every one of those, like the `take`, is a plain read past this record's own declared bytes, and every one is caught here the same way. This is the same per-record boundary readSupBookSafely already draws for a malformed SupBook (workbook/globals.ts): before this reader ever walked a ShrFmla/Array record's own length fields, a malformed one had nothing here to trip over, so this is entirely new territory the read-every-record-once contract now needs to hold against. A cell whose Formula record points at this base through a PtgExp then resolves to no formula text at all -- exactly the same outcome "leaves formula absent for a PtgExp whose base cell has no matching ShrFmla/Array group" already documents for a dangling reference, since from resolveFormulaText's own vantage point the two cases are indistinguishable.
58
+ */
59
+ function collectFormulaGroup(groups, record, next, readGroup) {
60
+ try {
61
+ const header = readCellHeader(new require_biff_cursor.BlockCursor(record.blocks));
62
+ groups.set(groupKey(header.row, header.column), readGroup(next));
63
+ } catch (error) {
64
+ if (!(error instanceof require_biff_records.BiffFormatError)) throw error;
65
+ }
66
+ }
67
+ /** ShrFmla ([MS-XLS] 984826cc): a RefU range (6 bytes, not needed here -- the group is looked up by its base cell's own coordinates, not by re-deriving them from this range), a reserved byte, a cUse byte, then a SharedParsedFormula (458bbec0): a two-byte cce and that many bytes of rgce. Its own rgce is forbidden from containing PtgArray ([MS-XLS] 458bbec0's own "MUST NOT contain... PtgArray"), so no rgcb is read here. */
68
+ const SHRFMLA_HEADER_BYTES = 8;
69
+ function readShrFmlaGroup(record) {
70
+ const cursor = new require_biff_cursor.BlockCursor(record.blocks);
71
+ cursor.skip(SHRFMLA_HEADER_BYTES);
72
+ const cce = cursor.u16();
73
+ return {
74
+ kind: "shared",
75
+ rgce: cursor.take(cce)
76
+ };
77
+ }
78
+ /** Array ([MS-XLS] c6ee7512): a Ref range (6 bytes), a flags word (fAlwaysCalc plus reserved bits), four unused bytes, then an ArrayParsedFormula (242bcf20): a two-byte cce, that many bytes of rgce, and -- unlike ShrFmla's own SharedParsedFormula -- a real rgcb trailer, since an array formula's rgce CAN contain a PtgArray for an array-constant literal used within it (e.g. `{=A1:A3+{1;2;3}}`). rgcb's own length is never stated directly: it is whatever bytes remain in the record once the header and rgce are accounted for. */
79
+ const ARRAY_HEADER_BYTES = 12;
80
+ function readArrayGroup(record) {
81
+ const cursor = new require_biff_cursor.BlockCursor(record.blocks);
82
+ cursor.skip(ARRAY_HEADER_BYTES);
83
+ const cce = cursor.u16();
84
+ const rgce = cursor.take(cce);
85
+ const rgcbLength = require_biff_substreams.recordByteLength(record) - (14 + cce);
86
+ if (rgcbLength <= 0) return {
87
+ kind: "array",
88
+ rgce,
89
+ rgcb: void 0
90
+ };
91
+ try {
92
+ return {
93
+ kind: "array",
94
+ rgce,
95
+ rgcb: cursor.take(rgcbLength)
96
+ };
97
+ } catch (error) {
98
+ if (!(error instanceof require_biff_records.BiffFormatError)) throw error;
99
+ return {
100
+ kind: "array",
101
+ rgce,
102
+ rgcb: void 0
103
+ };
104
+ }
105
+ }
34
106
  /** Reads one worksheet substream's records. */
35
107
  function readSheetRecords(records, sharedStrings, formulaSheets = EMPTY_FORMULA_SHEET_CONTEXT) {
108
+ const formulaGroups = collectFormulaGroups(records);
36
109
  const cells = [];
37
110
  const rows = [];
38
111
  const columns = [];
@@ -88,7 +161,7 @@ function readSheetRecords(records, sharedStrings, formulaSheets = EMPTY_FORMULA_
88
161
  cells.push(readLabel(record));
89
162
  break;
90
163
  case 6:
91
- cells.push(readFormula(record, stringResultAfter(records, index), formulaSheets));
164
+ cells.push(readFormula(record, stringResultAfter(records, index), formulaSheets, formulaGroups));
92
165
  break;
93
166
  case 161:
94
167
  setup = readSetup(record);
@@ -345,7 +418,7 @@ function readMulRk(record) {
345
418
  }
346
419
  /** Both Mul records put colLast after their variable-length array, so the entry count follows from the record's own length: total = rw + colFirst + N*entry + colLast. */
347
420
  function mulEntryCount(record, entryBytes) {
348
- const total = record.blocks.reduce((sum, block) => sum + block.length, 0);
421
+ const total = require_biff_substreams.recordByteLength(record);
349
422
  const payload = total - MUL_FIXED_BYTES;
350
423
  if (payload < 0 || payload % entryBytes !== 0) throw new require_biff_records.BiffFormatError(`multiple-cell record of ${total} bytes does not hold a whole number of ${entryBytes}-byte entries`);
351
424
  return payload / entryBytes;
@@ -417,12 +490,14 @@ function readLabel(record) {
417
490
  /** The Formula record's own flags field ([MS-XLS] 2.4.127) and calculation cache, between the cached value and the compiled expression -- neither read for its own content; see the two-byte and four-byte skips in readFormula. */
418
491
  const FORMULA_FLAGS_BYTES = 2;
419
492
  const FORMULA_CALC_CACHE_BYTES = 4;
493
+ /** The Cell (6 bytes) and FormulaValue (8 bytes) fields readFormula has already consumed by the time it reaches cce, plus the flags and calculation-cache fields above and the cce field itself (2 bytes) -- what's left of the record past `FORMULA_HEADER_BYTES + cce` is the CellParsedFormula's own rgcb trailer. */
494
+ const FORMULA_HEADER_BYTES = 22;
420
495
  /**
421
- * Formula ([MS-XLS] 2.4.127): a Cell, an eight-byte FormulaValue, flags, a calculation cache, then a CellParsedFormula -- a two-byte cce followed by exactly that many bytes of compiled Ptg tokens ([MS-XLS] 2.5.198.3).
496
+ * Formula ([MS-XLS] 2.4.127): a Cell, an eight-byte FormulaValue, flags, a calculation cache, then a CellParsedFormula -- a two-byte cce, that many bytes of compiled Ptg tokens ([MS-XLS] 2.5.198.3), and (whenever rgce contains a PtgArray -- an inline array-constant literal like `=SUM({1,2,3})`, unrelated to whether the cell itself is CSE-array-entered) an RgbExtra trailer of whatever bytes remain in the record.
422
497
  *
423
- * Both the cached value and the expression are read: the value from the FormulaValue exactly as before, and the expression by handing the token bytes to biff/ptg.ts's parseFormulaText, which resolves the whole Ptg vocabulary this reader supports and returns undefined for the constructs it does not (a shared formula, an array formula, a defined name, a natural-language reference, a genuinely external 3D reference -- see that module's own boundary). `formula` is attached only when it resolves; a cell it does not resolve for keeps exactly the behaviour this reader always had, its cached value present and `formula` absent.
498
+ * Both the cached value and the expression are read: the value from the FormulaValue exactly as before, and the expression by resolveFormulaText below, which joins a lone PtgExp against the shared/array formula group collectFormulaGroups found for it and otherwise hands the token bytes straight to biff/ptg.ts's parseFormulaText. `formula` is attached only when it resolves; a cell it does not resolve for keeps exactly the behaviour this reader always had, its cached value present and `formula` absent.
424
499
  */
425
- function readFormula(record, next, formulaSheets) {
500
+ function readFormula(record, next, formulaSheets, formulaGroups) {
426
501
  const cursor = new require_biff_cursor.BlockCursor(record.blocks);
427
502
  const header = readCellHeader(cursor);
428
503
  const bytes = cursor.take(8);
@@ -434,7 +509,18 @@ function readFormula(record, next, formulaSheets) {
434
509
  cursor.skip(FORMULA_FLAGS_BYTES);
435
510
  cursor.skip(FORMULA_CALC_CACHE_BYTES);
436
511
  const cce = cursor.u16();
437
- const formula = require_biff_ptg.parseFormulaText(cursor.take(cce), formulaSheets);
512
+ let rgce;
513
+ let rgcb;
514
+ try {
515
+ rgce = cursor.take(cce);
516
+ const rgcbLength = require_biff_substreams.recordByteLength(record) - (FORMULA_HEADER_BYTES + cce);
517
+ rgcb = rgcbLength > 0 ? cursor.take(rgcbLength) : void 0;
518
+ } catch (error) {
519
+ if (!(error instanceof require_biff_records.BiffFormatError)) throw error;
520
+ rgce = void 0;
521
+ rgcb = void 0;
522
+ }
523
+ const formula = rgce === void 0 ? void 0 : resolveFormulaText(rgce, rgcb, header, formulaSheets, formulaGroups);
438
524
  return formula === void 0 ? {
439
525
  ...header,
440
526
  value,
@@ -446,6 +532,23 @@ function readFormula(record, next, formulaSheets) {
446
532
  formula
447
533
  };
448
534
  }
535
+ /**
536
+ * A Formula record's rgce resolves one of three ways: a lone PtgExp pointing back to a shared-formula base cell, whose real expression (a ShrFmla's SharedParsedFormula) is expanded relative to THIS cell's own position; a lone PtgExp pointing back to an array-formula base cell, whose real expression (an Array's ArrayParsedFormula) is identical for every cell in the range and is returned as-is, with no CSE bracing (see ArrayFormulaGroup's own comment for why); or an ordinary rgce, handed to parseFormulaText as-is (with this record's own rgcb, for an inline array-constant literal). A PtgExp with no matching group -- a dangling or malformed reference this reader cannot join -- resolves to undefined exactly like any other unsupported construct.
537
+ *
538
+ * All three branches share the same hazard and are wrapped in one catch below: a token's own embedded length (a PtgStr's character count is the reachable case, [MS-XLS] 2.5.240's cch, read straight from the file with nothing to cross-check it against) can claim more bytes than the buffer parseFormulaText's cursor was actually handed, running that cursor past its end -- for the group-joining branches because `group.rgce` was extracted from a DIFFERENT, well-formed record whose own cce correctly bounds it, and for the ordinary-rgce branch because THIS record's own cce can just as easily be too short for the token it claims to end after (or a token inside an otherwise-correctly-bounded rgce can lie the same way `group.rgce`'s can). Neither case is a bug in this reader's own token walking; both are file-controlled malformed input, and left uncaught either would propagate out of readFormula and abort every other cell on the sheet along with the one cell whose formula this is -- exactly the failure collectFormulaGroup already prevents for a group whose OWN record is malformed, extended here to malformed token content reached through any of the three paths above.
539
+ */
540
+ function resolveFormulaText(rgce, rgcb, header, formulaSheets, formulaGroups) {
541
+ const base = require_biff_ptg.readPtgExpBase(rgce);
542
+ try {
543
+ if (base === void 0) return require_biff_ptg.parseFormulaText(rgce, formulaSheets, { rgcb });
544
+ const group = formulaGroups.get(groupKey(base.row, base.column));
545
+ if (group === void 0) return;
546
+ return group.kind === "shared" ? require_biff_ptg.parseFormulaText(group.rgce, formulaSheets, { relativeTo: header }) : require_biff_ptg.parseFormulaText(group.rgce, formulaSheets, { rgcb: group.rgcb });
547
+ } catch (error) {
548
+ if (!(error instanceof require_biff_records.BiffFormatError)) throw error;
549
+ return;
550
+ }
551
+ }
449
552
  /** The non-numeric readings of a FormulaValue ([MS-XLS] 2.5.133), selected by its first byte. */
450
553
  function taggedFormulaValue(view, next) {
451
554
  switch (view.getUint8(0)) {
@@ -1,6 +1,6 @@
1
1
  import { n as SetupFields } from "../print-setup-B_ihDvm5.cjs";
2
- import { t as FormulaSheetContext } from "../ptg-B2K8t3js.cjs";
3
- import { t as RecordGroup } from "../substreams-Ddtvn_Vr.cjs";
2
+ import { r as FormulaSheetContext } from "../ptg-CCBbJLZJ.cjs";
3
+ import { t as RecordGroup } from "../substreams-Cpy5Fi3d.cjs";
4
4
  //#region src/workbook/sheet.d.ts
5
5
  /** A cell's value as its own record carries it, before number-format classification decides whether a number is really a date, a percentage, or an amount of money. */
6
6
  type RawCellValue = {
@@ -27,7 +27,7 @@ interface RawCell {
27
27
  readonly value: RawCellValue;
28
28
  /** True when the value is a Formula record's CACHED result rather than a literal. */
29
29
  readonly fromFormula: boolean;
30
- /** The formula's own text, recovered from its compiled Ptg token stream ([MS-XLS] 2.5.198), when every token in it is one this reader resolves -- absent for a shared-formula member, an array formula, a defined-name or natural-language reference, or a 3D reference into a genuinely external workbook (see biff/ptg.ts). */
30
+ /** The formula's own text, recovered from its compiled Ptg token stream ([MS-XLS] 2.5.198), when every token in it is one this reader resolves -- absent for a defined-name or natural-language reference, or a data table (see biff/ptg.ts). */
31
31
  readonly formula?: string;
32
32
  }
33
33
  interface RawRow {
@@ -1,6 +1,6 @@
1
1
  import { n as SetupFields } from "../print-setup-B_ihDvm5.js";
2
- import { t as FormulaSheetContext } from "../ptg-B2K8t3js.js";
3
- import { t as RecordGroup } from "../substreams-D7dQiJbp.js";
2
+ import { r as FormulaSheetContext } from "../ptg-CCBbJLZJ.js";
3
+ import { t as RecordGroup } from "../substreams-CmyZMtuM.js";
4
4
  //#region src/workbook/sheet.d.ts
5
5
  /** A cell's value as its own record carries it, before number-format classification decides whether a number is really a date, a percentage, or an amount of money. */
6
6
  type RawCellValue = {
@@ -27,7 +27,7 @@ interface RawCell {
27
27
  readonly value: RawCellValue;
28
28
  /** True when the value is a Formula record's CACHED result rather than a literal. */
29
29
  readonly fromFormula: boolean;
30
- /** The formula's own text, recovered from its compiled Ptg token stream ([MS-XLS] 2.5.198), when every token in it is one this reader resolves -- absent for a shared-formula member, an array formula, a defined-name or natural-language reference, or a 3D reference into a genuinely external workbook (see biff/ptg.ts). */
30
+ /** The formula's own text, recovered from its compiled Ptg token stream ([MS-XLS] 2.5.198), when every token in it is one this reader resolves -- absent for a defined-name or natural-language reference, or a data table (see biff/ptg.ts). */
31
31
  readonly formula?: string;
32
32
  }
33
33
  interface RawRow {
@@ -2,10 +2,11 @@ import "../biff/record-types.js";
2
2
  import { BiffFormatError } from "../biff/records.js";
3
3
  import { columnWidthToPoints, inchesToPoints, twipsToPoints } from "../units.js";
4
4
  import { unpackSetupFlags } from "../biff/print-setup.js";
5
+ import { recordByteLength } from "../biff/substreams.js";
5
6
  import { BlockCursor } from "../biff/cursor.js";
6
7
  import { readXLUnicodeString } from "../biff/strings.js";
7
8
  import { errorTextOf } from "../biff/errors.js";
8
- import { parseFormulaText } from "../biff/ptg.js";
9
+ import { parseFormulaText, readPtgExpBase } from "../biff/ptg.js";
9
10
  import { decodeRkNumber } from "../biff/rk.js";
10
11
  //#region src/workbook/sheet.ts
11
12
  /** Row record flag bits, in the 32-bit field following unused1 ([MS-XLS] 2.4.221). */
@@ -30,8 +31,80 @@ const EMPTY_FORMULA_SHEET_CONTEXT = {
30
31
  sheets: [],
31
32
  sheetRanges: []
32
33
  };
34
+ /** The key collectFormulaGroups and its lookup agree on: a shared/array formula group's own base cell, the same (row, column) a PtgExp token elsewhere in the sheet points back to. */
35
+ function groupKey(row, column) {
36
+ return `${row},${column}`;
37
+ }
38
+ /**
39
+ * Walks every record once, looking for a Formula record immediately followed by a ShrFmla or Array record ([MS-XLS] 2.1.7.20.6's own FORMULA production, and 984826cc/c6ee7512's own "this record is preceded by a single Formula record"), and returns the shared/array expression each one carries, keyed by that Formula record's own cell -- the same (row, column) a PtgExp token names when it points back to this group (see readPtgExpBase, and readFormula below which performs the actual lookup).
40
+ *
41
+ * Built as a single upfront pass over the whole sheet rather than interleaved into readSheetRecords' own per-record loop: every Formula record that uses a shared/array formula (including the group's own base cell, which points at itself) needs this map already complete when it is reached, and although [MS-XLS] guarantees the base pair precedes every other use, resolving the whole map first removes that ordering as a correctness dependency rather than merely relying on it.
42
+ */
43
+ function collectFormulaGroups(records) {
44
+ const groups = /* @__PURE__ */ new Map();
45
+ for (let index = 0; index < records.length; index += 1) {
46
+ const record = records[index];
47
+ const next = records[index + 1];
48
+ if (record === void 0 || next === void 0) continue;
49
+ if (record.type !== 6) continue;
50
+ if (next.type === 1212) collectFormulaGroup(groups, record, next, readShrFmlaGroup);
51
+ else if (next.type === 545) collectFormulaGroup(groups, record, next, readArrayGroup);
52
+ }
53
+ return groups;
54
+ }
55
+ /**
56
+ * Reads one shared/array formula group and keys it by its base Formula record's own cell, degrading a malformed ShrFmla/Array record to "no group recovered for this base cell" rather than letting a BiffFormatError propagate out of collectFormulaGroups and abort the whole sheet read (and every other cell in it, formula or not). readCellHeader and readGroup (readShrFmlaGroup or readArrayGroup) between them make several cursor reads capable of raising that error, not only the final `cursor.take(cce)` that copies out rgce itself: an undersized record already runs out of bytes during readCellHeader's own Cell fields, or during readShrFmlaGroup/readArrayGroup's leading `cursor.skip` past their fixed header, or during the `cursor.u16()` that reads cce -- every one of those, like the `take`, is a plain read past this record's own declared bytes, and every one is caught here the same way. This is the same per-record boundary readSupBookSafely already draws for a malformed SupBook (workbook/globals.ts): before this reader ever walked a ShrFmla/Array record's own length fields, a malformed one had nothing here to trip over, so this is entirely new territory the read-every-record-once contract now needs to hold against. A cell whose Formula record points at this base through a PtgExp then resolves to no formula text at all -- exactly the same outcome "leaves formula absent for a PtgExp whose base cell has no matching ShrFmla/Array group" already documents for a dangling reference, since from resolveFormulaText's own vantage point the two cases are indistinguishable.
57
+ */
58
+ function collectFormulaGroup(groups, record, next, readGroup) {
59
+ try {
60
+ const header = readCellHeader(new BlockCursor(record.blocks));
61
+ groups.set(groupKey(header.row, header.column), readGroup(next));
62
+ } catch (error) {
63
+ if (!(error instanceof BiffFormatError)) throw error;
64
+ }
65
+ }
66
+ /** ShrFmla ([MS-XLS] 984826cc): a RefU range (6 bytes, not needed here -- the group is looked up by its base cell's own coordinates, not by re-deriving them from this range), a reserved byte, a cUse byte, then a SharedParsedFormula (458bbec0): a two-byte cce and that many bytes of rgce. Its own rgce is forbidden from containing PtgArray ([MS-XLS] 458bbec0's own "MUST NOT contain... PtgArray"), so no rgcb is read here. */
67
+ const SHRFMLA_HEADER_BYTES = 8;
68
+ function readShrFmlaGroup(record) {
69
+ const cursor = new BlockCursor(record.blocks);
70
+ cursor.skip(SHRFMLA_HEADER_BYTES);
71
+ const cce = cursor.u16();
72
+ return {
73
+ kind: "shared",
74
+ rgce: cursor.take(cce)
75
+ };
76
+ }
77
+ /** Array ([MS-XLS] c6ee7512): a Ref range (6 bytes), a flags word (fAlwaysCalc plus reserved bits), four unused bytes, then an ArrayParsedFormula (242bcf20): a two-byte cce, that many bytes of rgce, and -- unlike ShrFmla's own SharedParsedFormula -- a real rgcb trailer, since an array formula's rgce CAN contain a PtgArray for an array-constant literal used within it (e.g. `{=A1:A3+{1;2;3}}`). rgcb's own length is never stated directly: it is whatever bytes remain in the record once the header and rgce are accounted for. */
78
+ const ARRAY_HEADER_BYTES = 12;
79
+ function readArrayGroup(record) {
80
+ const cursor = new BlockCursor(record.blocks);
81
+ cursor.skip(ARRAY_HEADER_BYTES);
82
+ const cce = cursor.u16();
83
+ const rgce = cursor.take(cce);
84
+ const rgcbLength = recordByteLength(record) - (14 + cce);
85
+ if (rgcbLength <= 0) return {
86
+ kind: "array",
87
+ rgce,
88
+ rgcb: void 0
89
+ };
90
+ try {
91
+ return {
92
+ kind: "array",
93
+ rgce,
94
+ rgcb: cursor.take(rgcbLength)
95
+ };
96
+ } catch (error) {
97
+ if (!(error instanceof BiffFormatError)) throw error;
98
+ return {
99
+ kind: "array",
100
+ rgce,
101
+ rgcb: void 0
102
+ };
103
+ }
104
+ }
33
105
  /** Reads one worksheet substream's records. */
34
106
  function readSheetRecords(records, sharedStrings, formulaSheets = EMPTY_FORMULA_SHEET_CONTEXT) {
107
+ const formulaGroups = collectFormulaGroups(records);
35
108
  const cells = [];
36
109
  const rows = [];
37
110
  const columns = [];
@@ -87,7 +160,7 @@ function readSheetRecords(records, sharedStrings, formulaSheets = EMPTY_FORMULA_
87
160
  cells.push(readLabel(record));
88
161
  break;
89
162
  case 6:
90
- cells.push(readFormula(record, stringResultAfter(records, index), formulaSheets));
163
+ cells.push(readFormula(record, stringResultAfter(records, index), formulaSheets, formulaGroups));
91
164
  break;
92
165
  case 161:
93
166
  setup = readSetup(record);
@@ -344,7 +417,7 @@ function readMulRk(record) {
344
417
  }
345
418
  /** Both Mul records put colLast after their variable-length array, so the entry count follows from the record's own length: total = rw + colFirst + N*entry + colLast. */
346
419
  function mulEntryCount(record, entryBytes) {
347
- const total = record.blocks.reduce((sum, block) => sum + block.length, 0);
420
+ const total = recordByteLength(record);
348
421
  const payload = total - MUL_FIXED_BYTES;
349
422
  if (payload < 0 || payload % entryBytes !== 0) throw new BiffFormatError(`multiple-cell record of ${total} bytes does not hold a whole number of ${entryBytes}-byte entries`);
350
423
  return payload / entryBytes;
@@ -416,12 +489,14 @@ function readLabel(record) {
416
489
  /** The Formula record's own flags field ([MS-XLS] 2.4.127) and calculation cache, between the cached value and the compiled expression -- neither read for its own content; see the two-byte and four-byte skips in readFormula. */
417
490
  const FORMULA_FLAGS_BYTES = 2;
418
491
  const FORMULA_CALC_CACHE_BYTES = 4;
492
+ /** The Cell (6 bytes) and FormulaValue (8 bytes) fields readFormula has already consumed by the time it reaches cce, plus the flags and calculation-cache fields above and the cce field itself (2 bytes) -- what's left of the record past `FORMULA_HEADER_BYTES + cce` is the CellParsedFormula's own rgcb trailer. */
493
+ const FORMULA_HEADER_BYTES = 22;
419
494
  /**
420
- * Formula ([MS-XLS] 2.4.127): a Cell, an eight-byte FormulaValue, flags, a calculation cache, then a CellParsedFormula -- a two-byte cce followed by exactly that many bytes of compiled Ptg tokens ([MS-XLS] 2.5.198.3).
495
+ * Formula ([MS-XLS] 2.4.127): a Cell, an eight-byte FormulaValue, flags, a calculation cache, then a CellParsedFormula -- a two-byte cce, that many bytes of compiled Ptg tokens ([MS-XLS] 2.5.198.3), and (whenever rgce contains a PtgArray -- an inline array-constant literal like `=SUM({1,2,3})`, unrelated to whether the cell itself is CSE-array-entered) an RgbExtra trailer of whatever bytes remain in the record.
421
496
  *
422
- * Both the cached value and the expression are read: the value from the FormulaValue exactly as before, and the expression by handing the token bytes to biff/ptg.ts's parseFormulaText, which resolves the whole Ptg vocabulary this reader supports and returns undefined for the constructs it does not (a shared formula, an array formula, a defined name, a natural-language reference, a genuinely external 3D reference -- see that module's own boundary). `formula` is attached only when it resolves; a cell it does not resolve for keeps exactly the behaviour this reader always had, its cached value present and `formula` absent.
497
+ * Both the cached value and the expression are read: the value from the FormulaValue exactly as before, and the expression by resolveFormulaText below, which joins a lone PtgExp against the shared/array formula group collectFormulaGroups found for it and otherwise hands the token bytes straight to biff/ptg.ts's parseFormulaText. `formula` is attached only when it resolves; a cell it does not resolve for keeps exactly the behaviour this reader always had, its cached value present and `formula` absent.
423
498
  */
424
- function readFormula(record, next, formulaSheets) {
499
+ function readFormula(record, next, formulaSheets, formulaGroups) {
425
500
  const cursor = new BlockCursor(record.blocks);
426
501
  const header = readCellHeader(cursor);
427
502
  const bytes = cursor.take(8);
@@ -433,7 +508,18 @@ function readFormula(record, next, formulaSheets) {
433
508
  cursor.skip(FORMULA_FLAGS_BYTES);
434
509
  cursor.skip(FORMULA_CALC_CACHE_BYTES);
435
510
  const cce = cursor.u16();
436
- const formula = parseFormulaText(cursor.take(cce), formulaSheets);
511
+ let rgce;
512
+ let rgcb;
513
+ try {
514
+ rgce = cursor.take(cce);
515
+ const rgcbLength = recordByteLength(record) - (FORMULA_HEADER_BYTES + cce);
516
+ rgcb = rgcbLength > 0 ? cursor.take(rgcbLength) : void 0;
517
+ } catch (error) {
518
+ if (!(error instanceof BiffFormatError)) throw error;
519
+ rgce = void 0;
520
+ rgcb = void 0;
521
+ }
522
+ const formula = rgce === void 0 ? void 0 : resolveFormulaText(rgce, rgcb, header, formulaSheets, formulaGroups);
437
523
  return formula === void 0 ? {
438
524
  ...header,
439
525
  value,
@@ -445,6 +531,23 @@ function readFormula(record, next, formulaSheets) {
445
531
  formula
446
532
  };
447
533
  }
534
+ /**
535
+ * A Formula record's rgce resolves one of three ways: a lone PtgExp pointing back to a shared-formula base cell, whose real expression (a ShrFmla's SharedParsedFormula) is expanded relative to THIS cell's own position; a lone PtgExp pointing back to an array-formula base cell, whose real expression (an Array's ArrayParsedFormula) is identical for every cell in the range and is returned as-is, with no CSE bracing (see ArrayFormulaGroup's own comment for why); or an ordinary rgce, handed to parseFormulaText as-is (with this record's own rgcb, for an inline array-constant literal). A PtgExp with no matching group -- a dangling or malformed reference this reader cannot join -- resolves to undefined exactly like any other unsupported construct.
536
+ *
537
+ * All three branches share the same hazard and are wrapped in one catch below: a token's own embedded length (a PtgStr's character count is the reachable case, [MS-XLS] 2.5.240's cch, read straight from the file with nothing to cross-check it against) can claim more bytes than the buffer parseFormulaText's cursor was actually handed, running that cursor past its end -- for the group-joining branches because `group.rgce` was extracted from a DIFFERENT, well-formed record whose own cce correctly bounds it, and for the ordinary-rgce branch because THIS record's own cce can just as easily be too short for the token it claims to end after (or a token inside an otherwise-correctly-bounded rgce can lie the same way `group.rgce`'s can). Neither case is a bug in this reader's own token walking; both are file-controlled malformed input, and left uncaught either would propagate out of readFormula and abort every other cell on the sheet along with the one cell whose formula this is -- exactly the failure collectFormulaGroup already prevents for a group whose OWN record is malformed, extended here to malformed token content reached through any of the three paths above.
538
+ */
539
+ function resolveFormulaText(rgce, rgcb, header, formulaSheets, formulaGroups) {
540
+ const base = readPtgExpBase(rgce);
541
+ try {
542
+ if (base === void 0) return parseFormulaText(rgce, formulaSheets, { rgcb });
543
+ const group = formulaGroups.get(groupKey(base.row, base.column));
544
+ if (group === void 0) return;
545
+ return group.kind === "shared" ? parseFormulaText(group.rgce, formulaSheets, { relativeTo: header }) : parseFormulaText(group.rgce, formulaSheets, { rgcb: group.rgcb });
546
+ } catch (error) {
547
+ if (!(error instanceof BiffFormatError)) throw error;
548
+ return;
549
+ }
550
+ }
448
551
  /** The non-numeric readings of a FormulaValue ([MS-XLS] 2.5.133), selected by its first byte. */
449
552
  function taggedFormulaValue(view, next) {
450
553
  switch (view.getUint8(0)) {