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.
- package/README.md +4 -4
- package/dist/biff/ptg.cjs +160 -7
- package/dist/biff/ptg.d.cts +2 -2
- package/dist/biff/ptg.d.ts +2 -2
- package/dist/biff/ptg.js +161 -9
- package/dist/biff/strings.cjs +6 -0
- package/dist/biff/strings.d.cts +3 -1
- package/dist/biff/strings.d.ts +3 -1
- package/dist/biff/strings.js +6 -1
- package/dist/biff/substreams.cjs +5 -0
- package/dist/biff/substreams.d.cts +2 -2
- package/dist/biff/substreams.d.ts +2 -2
- package/dist/biff/substreams.js +5 -1
- package/dist/index.cjs +2 -0
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/{print-names-DUlpVE00.d.ts → print-names-C-PQ2vAy.d.ts} +1 -1
- package/dist/{print-names-D-njuzVw.d.cts → print-names-DyYloloV.d.cts} +1 -1
- package/dist/ptg-CCBbJLZJ.d.cts +49 -0
- package/dist/ptg-CCBbJLZJ.d.ts +49 -0
- package/dist/{substreams-D7dQiJbp.d.ts → substreams-CmyZMtuM.d.ts} +3 -1
- package/dist/{substreams-Ddtvn_Vr.d.cts → substreams-Cpy5Fi3d.d.cts} +3 -1
- package/dist/workbook/globals-writer.d.cts +1 -1
- package/dist/workbook/globals-writer.d.ts +1 -1
- package/dist/workbook/globals.cjs +137 -14
- package/dist/workbook/globals.d.cts +6 -6
- package/dist/workbook/globals.d.ts +6 -6
- package/dist/workbook/globals.js +138 -15
- package/dist/workbook/print-names.d.cts +1 -1
- package/dist/workbook/print-names.d.ts +1 -1
- package/dist/workbook/sheet.cjs +109 -6
- package/dist/workbook/sheet.d.cts +3 -3
- package/dist/workbook/sheet.d.ts +3 -3
- package/dist/workbook/sheet.js +110 -7
- package/package.json +10 -4
- package/dist/ptg-B2K8t3js.d.cts +0 -21
- package/dist/ptg-B2K8t3js.d.ts +0 -21
package/dist/workbook/globals.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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
|
-
/**
|
|
69
|
-
|
|
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.
|
|
72
|
-
|
|
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,
|
|
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((
|
|
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
|
|
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-
|
|
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-
|
|
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 };
|
package/dist/workbook/sheet.cjs
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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 {
|
|
3
|
-
import { t as RecordGroup } from "../substreams-
|
|
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
|
|
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 {
|
package/dist/workbook/sheet.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as SetupFields } from "../print-setup-B_ihDvm5.js";
|
|
2
|
-
import {
|
|
3
|
-
import { t as RecordGroup } from "../substreams-
|
|
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
|
|
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 {
|
package/dist/workbook/sheet.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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)) {
|