@shbernal/ts-xlsx 1.0.3 → 1.2.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 +21 -0
- package/dist/core/address.js +0 -33
- package/dist/core/autofilter.js +0 -9
- package/dist/core/cell.d.ts +10 -0
- package/dist/core/cell.js +4 -123
- package/dist/core/color-resolution.js +0 -60
- package/dist/core/column.d.ts +9 -1
- package/dist/core/column.js +0 -52
- package/dist/core/comment-thread.js +0 -45
- package/dist/core/conditional-formatting-overlay.js +0 -11
- package/dist/core/conditional-formatting.js +0 -10
- package/dist/core/containers.js +0 -15
- package/dist/core/data-validation-overlay.js +0 -25
- package/dist/core/data-validation.js +0 -7
- package/dist/core/date.js +0 -39
- package/dist/core/formula.js +0 -117
- package/dist/core/grid-edits.js +0 -38
- package/dist/core/image.js +0 -25
- package/dist/core/internal.js +0 -27
- package/dist/core/limits.d.ts +30 -0
- package/dist/core/limits.js +2 -0
- package/dist/core/merge.d.ts +18 -0
- package/dist/core/merge.js +22 -7
- package/dist/core/modern-functions.js +0 -17
- package/dist/core/page-setup.js +0 -5
- package/dist/core/pivot-table.d.ts +1 -1
- package/dist/core/pivot-table.js +0 -44
- package/dist/core/preserved.js +0 -4
- package/dist/core/protection.js +0 -26
- package/dist/core/range.js +0 -112
- package/dist/core/row-input.d.ts +24 -0
- package/dist/core/row-input.js +29 -0
- package/dist/core/row.d.ts +9 -1
- package/dist/core/row.js +0 -58
- package/dist/core/style.js +0 -26
- package/dist/core/table-style.js +0 -28
- package/dist/core/table.js +0 -97
- package/dist/core/text-metrics.d.ts +20 -0
- package/dist/core/text-metrics.js +10 -0
- package/dist/core/theme.js +0 -85
- package/dist/core/value.d.ts +40 -0
- package/dist/core/value.js +30 -48
- package/dist/core/workbook-protection.js +0 -13
- package/dist/core/workbook.d.ts +13 -0
- package/dist/core/workbook.js +11 -368
- package/dist/core/worksheet-model.js +2 -23
- package/dist/core/worksheet.d.ts +22 -0
- package/dist/core/worksheet.js +14 -485
- package/dist/customui/errors.js +0 -10
- package/dist/customui/index.js +0 -5
- package/dist/customui/ribbon.js +0 -43
- package/dist/entries/core.d.ts +3 -1
- package/dist/entries/core.js +3 -7
- package/dist/entries/csv.js +0 -4
- package/dist/entries/customui.js +0 -2
- package/dist/entries/errors.js +0 -13
- package/dist/entries/vba.js +0 -5
- package/dist/entries/xlsb.js +0 -6
- package/dist/entries/xlsx.js +0 -13
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +5 -56
- package/dist/index.js +0 -12
- package/dist/io/csv/read.js +0 -18
- package/dist/io/csv/write.js +5 -35
- package/dist/io/opc/errors.js +0 -30
- package/dist/io/opc/inflate.js +0 -35
- package/dist/io/opc/namespaces.js +0 -15
- package/dist/io/opc/part-paths.js +0 -11
- package/dist/io/opc/read-opc.js +0 -31
- package/dist/io/opc/read-options.js +0 -6
- package/dist/io/opc/rels.js +0 -13
- package/dist/io/opc/sniff-format.js +0 -50
- package/dist/io/style/xf-style.js +0 -31
- package/dist/io/xlsb/errors.js +0 -10
- package/dist/io/xlsb/formula.js +1 -99
- package/dist/io/xlsb/primitives.js +1 -83
- package/dist/io/xlsb/ptg-functions.js +0 -34
- package/dist/io/xlsb/read-shared-strings.js +0 -7
- package/dist/io/xlsb/read-styles.js +3 -66
- package/dist/io/xlsb/read-worksheet.js +4 -87
- package/dist/io/xlsb/read.js +2 -60
- package/dist/io/xlsb/record-stream.js +0 -28
- package/dist/io/xlsb/record-types.js +0 -19
- package/dist/io/xlsx/cell-accumulator.js +0 -32
- package/dist/io/xlsx/cell-value.js +0 -29
- package/dist/io/xlsx/color-xml.d.ts +6 -0
- package/dist/io/xlsx/color-xml.js +42 -0
- package/dist/io/xlsx/comments.js +0 -84
- package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
- package/dist/io/xlsx/conditional-formatting.js +1 -97
- package/dist/io/xlsx/data-validation.js +0 -56
- package/dist/io/xlsx/edit-vba.js +0 -54
- package/dist/io/xlsx/errors.js +0 -12
- package/dist/io/xlsx/hyperlinks.js +0 -40
- package/dist/io/xlsx/images.js +0 -32
- package/dist/io/xlsx/namespaces.js +0 -42
- package/dist/io/xlsx/package-plan.js +0 -47
- package/dist/io/xlsx/pivot.js +0 -22
- package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
- package/dist/io/xlsx/read-rows.js +1 -83
- package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
- package/dist/io/xlsx/read-styles.d.ts +25 -1
- package/dist/io/xlsx/read-styles.js +61 -108
- package/dist/io/xlsx/read-worksheet.js +1 -88
- package/dist/io/xlsx/read.js +3 -211
- package/dist/io/xlsx/relationships.js +0 -11
- package/dist/io/xlsx/rich-runs.js +0 -16
- package/dist/io/xlsx/rich-text.js +0 -12
- package/dist/io/xlsx/shared-formulas.js +0 -7
- package/dist/io/xlsx/shared-strings.js +0 -21
- package/dist/io/xlsx/sheet-properties.js +1 -64
- package/dist/io/xlsx/styles.d.ts +1 -29
- package/dist/io/xlsx/styles.js +2 -366
- package/dist/io/xlsx/tables.js +5 -52
- package/dist/io/xlsx/threaded-comments.js +0 -103
- package/dist/io/xlsx/workbook-xml.js +0 -94
- package/dist/io/xlsx/worksheet-xml.js +0 -174
- package/dist/io/xlsx/write-stream.js +0 -137
- package/dist/io/xlsx/write.js +1 -134
- package/dist/io/xlsx/x14-ext.js +0 -8
- package/dist/vba/cfb-writer.js +12 -59
- package/dist/vba/cfb.js +2 -32
- package/dist/vba/codepage.js +1 -23
- package/dist/vba/errors.js +0 -15
- package/dist/vba/index.js +0 -10
- package/dist/vba/ms-ovba.js +3 -46
- package/dist/vba/project-editor.js +4 -89
- package/dist/vba/project.js +2 -34
- package/dist/vba/vba-encoding.js +1 -17
- package/dist/xml/errors.js +0 -10
- package/dist/xml/xml-read.js +3 -82
- package/dist/xml/xml.js +0 -39
- package/package.json +9 -10
- package/skills/ts-xlsx-upstream/SKILL.md +178 -0
- /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
- /package/dist/io/xlsx/{shared-strings-read.d.ts → read-shared-strings.d.ts} +0 -0
package/dist/vba/cfb-writer.js
CHANGED
|
@@ -1,31 +1,17 @@
|
|
|
1
|
-
// Writer for the OLE2 / Compound File Binary format ([MS-CFB]) — the encode counterpart to cfb.ts.
|
|
2
|
-
//
|
|
3
|
-
// Produces a v3 (512-byte sector) container from a hierarchy of storages and streams, the substrate a
|
|
4
|
-
// synthesized vbaProject.bin is built on (its modules live inside a `VBA` storage, with `PROJECT` and
|
|
5
|
-
// `PROJECTwm` at the root). Streams below the 4096-byte mini cutoff are packed into the mini stream and
|
|
6
|
-
// chained through the mini-FAT; larger streams take whole regular sectors. Each storage's children are
|
|
7
|
-
// emitted as a name-ordered balanced binary tree ([MS-CFB] 2.6.4), so a host that *navigates* the tree
|
|
8
|
-
// (Excel) reaches every entry — not only a linear scanner like this library's own reader.
|
|
9
|
-
//
|
|
10
|
-
// Unlike the reader, this is not a hostile-input path: we are the producer. It still validates its
|
|
11
|
-
// contract (name length, sibling-name uniqueness, size bound) and fails closed with VbaAuthorError,
|
|
12
|
-
// because a silently malformed container would surface far downstream as an unopenable workbook.
|
|
13
1
|
import { VbaAuthorError } from './errors.js';
|
|
14
2
|
const SECTOR = 512;
|
|
15
3
|
const MINI_SECTOR = 64;
|
|
16
4
|
const MINI_CUTOFF = 4096;
|
|
17
5
|
const DIR_ENTRY_SIZE = 128;
|
|
18
|
-
const ENTRIES_PER_DIR_SECTOR = SECTOR / DIR_ENTRY_SIZE;
|
|
19
|
-
const FAT_ENTRIES_PER_SECTOR = SECTOR / 4;
|
|
20
|
-
const DIFAT_HEADER_SLOTS = 109;
|
|
21
|
-
const MAX_NAME_CHARS = 31;
|
|
22
|
-
// Sector chain markers ([MS-CFB] 2.2).
|
|
6
|
+
const ENTRIES_PER_DIR_SECTOR = SECTOR / DIR_ENTRY_SIZE;
|
|
7
|
+
const FAT_ENTRIES_PER_SECTOR = SECTOR / 4;
|
|
8
|
+
const DIFAT_HEADER_SLOTS = 109;
|
|
9
|
+
const MAX_NAME_CHARS = 31;
|
|
23
10
|
const FREESECT = 0xffffffff;
|
|
24
11
|
const ENDOFCHAIN = 0xfffffffe;
|
|
25
12
|
const FATSECT = 0xfffffffd;
|
|
26
13
|
const DIFSECT = 0xfffffffc;
|
|
27
14
|
const NOSTREAM = 0xffffffff;
|
|
28
|
-
// Object types ([MS-CFB] 2.6.1).
|
|
29
15
|
const TYPE_STORAGE = 1;
|
|
30
16
|
const TYPE_STREAM = 2;
|
|
31
17
|
const TYPE_ROOT = 5;
|
|
@@ -33,14 +19,6 @@ const COLOR_BLACK = 1;
|
|
|
33
19
|
function isStream(node) {
|
|
34
20
|
return 'data' in node;
|
|
35
21
|
}
|
|
36
|
-
/**
|
|
37
|
-
* Encode a hierarchy of storages and streams into a v3 compound file. The Root Entry is synthesized
|
|
38
|
-
* automatically; `root` is its top-level children. Every stream becomes a directory entry reachable both
|
|
39
|
-
* by linear scan and by tree navigation.
|
|
40
|
-
*
|
|
41
|
-
* @throws {VbaAuthorError} if any name is empty or exceeds 31 characters, sibling names collide, or the
|
|
42
|
-
* project is so large it would need more than 109 FAT sectors (~7 MB — far beyond any real project).
|
|
43
|
-
*/
|
|
44
22
|
export function writeCompoundFile(root) {
|
|
45
23
|
const entries = [];
|
|
46
24
|
const addEntry = (name, type, startSector) => {
|
|
@@ -58,20 +36,15 @@ export function writeCompoundFile(root) {
|
|
|
58
36
|
return entry;
|
|
59
37
|
};
|
|
60
38
|
const rootEntry = addEntry('Root Entry', TYPE_ROOT, ENDOFCHAIN);
|
|
61
|
-
// Sub-cutoff stream bytes accumulate into the mini stream (chained in the mini-FAT); larger streams
|
|
62
|
-
// are laid out later directly in the regular FAT. Depth-first walk fixes a deterministic layout.
|
|
63
39
|
const miniBytes = [];
|
|
64
40
|
const miniFat = [];
|
|
65
41
|
const bigStreams = [];
|
|
66
|
-
// `siblings` is the parent's child list itself rather than its index, so a child is appended to an
|
|
67
|
-
// array we hold — no lookup that could come back empty.
|
|
68
42
|
const addNode = (node, siblings) => {
|
|
69
43
|
if (isStream(node)) {
|
|
70
44
|
const entry = addEntry(node.name, TYPE_STREAM, ENDOFCHAIN);
|
|
71
45
|
entry.size = node.data.length;
|
|
72
46
|
siblings.push(entry);
|
|
73
47
|
if (node.data.length === 0) {
|
|
74
|
-
// an empty stream owns no sectors
|
|
75
48
|
}
|
|
76
49
|
else if (node.data.length >= MINI_CUTOFF) {
|
|
77
50
|
bigStreams.push({ entry, data: node.data, sectors: Math.ceil(node.data.length / SECTOR) });
|
|
@@ -91,7 +64,6 @@ export function writeCompoundFile(root) {
|
|
|
91
64
|
const kids = [];
|
|
92
65
|
for (const c of node.children)
|
|
93
66
|
addNode(c, kids);
|
|
94
|
-
// Each storage (Root included) links its children as a balanced search tree the host navigates.
|
|
95
67
|
linkChildren(entry, kids);
|
|
96
68
|
}
|
|
97
69
|
};
|
|
@@ -100,9 +72,6 @@ export function writeCompoundFile(root) {
|
|
|
100
72
|
for (const n of root)
|
|
101
73
|
addNode(n, rootKids);
|
|
102
74
|
linkChildren(rootEntry, rootKids);
|
|
103
|
-
// ── Sector layout ─────────────────────────────────────────────────────────────────────────────────
|
|
104
|
-
// Physical order: directory, mini-FAT, mini stream, each big stream, FAT, DIFAT. Region starts are
|
|
105
|
-
// assigned first so chains can reference them; the FAT is filled last, once every sector is placed.
|
|
106
75
|
const dirSectors = Math.ceil(entries.length / ENTRIES_PER_DIR_SECTOR);
|
|
107
76
|
const miniFatSectors = miniFat.length > 0 ? Math.ceil((miniFat.length * 4) / FAT_ENTRIES_PER_SECTOR) : 0;
|
|
108
77
|
const miniStreamSectors = Math.ceil(miniBytes.length / SECTOR);
|
|
@@ -115,7 +84,6 @@ export function writeCompoundFile(root) {
|
|
|
115
84
|
for (;;) {
|
|
116
85
|
const total = baseSectors + fatSectors + difatSectors;
|
|
117
86
|
const needFat = Math.ceil(total / FAT_ENTRIES_PER_SECTOR);
|
|
118
|
-
// Each DIFAT sector holds 127 FAT pointers + a next-DIFAT pointer; the first 109 live in the header.
|
|
119
87
|
const needDifat = needFat > DIFAT_HEADER_SLOTS
|
|
120
88
|
? Math.ceil((needFat - DIFAT_HEADER_SLOTS) / (FAT_ENTRIES_PER_SECTOR - 1))
|
|
121
89
|
: 0;
|
|
@@ -145,7 +113,6 @@ export function writeCompoundFile(root) {
|
|
|
145
113
|
const totalSectors = cursor;
|
|
146
114
|
rootEntry.startSector = miniStreamStart;
|
|
147
115
|
rootEntry.size = miniBytes.length;
|
|
148
|
-
// ── FAT ─────────────────────────────────────────────────────────────────────────────────────────
|
|
149
116
|
const fat = new Array(fatSectors * FAT_ENTRIES_PER_SECTOR).fill(FREESECT);
|
|
150
117
|
const chainRegion = (start, count) => {
|
|
151
118
|
for (let k = 0; k < count; k++)
|
|
@@ -162,8 +129,7 @@ export function writeCompoundFile(root) {
|
|
|
162
129
|
fat[fatStart + k] = FATSECT;
|
|
163
130
|
for (let k = 0; k < difatSectors; k++)
|
|
164
131
|
fat[difatStart + k] = DIFSECT;
|
|
165
|
-
|
|
166
|
-
const buf = new Uint8Array((totalSectors + 1) * SECTOR); // +1 for the header sector
|
|
132
|
+
const buf = new Uint8Array((totalSectors + 1) * SECTOR);
|
|
167
133
|
const dv = new DataView(buf.buffer);
|
|
168
134
|
const at = (sector) => (sector + 1) * SECTOR;
|
|
169
135
|
writeHeader(dv, {
|
|
@@ -180,16 +146,12 @@ export function writeCompoundFile(root) {
|
|
|
180
146
|
for (let i = 0; i < miniFatSectors * FAT_ENTRIES_PER_SECTOR; i++) {
|
|
181
147
|
dv.setUint32(at(miniFatStart) + i * 4, miniFat[i] ?? FREESECT, true);
|
|
182
148
|
}
|
|
183
|
-
// Guarded rather than looped-and-skipped: with no mini stream, miniStreamStart is ENDOFCHAIN and
|
|
184
|
-
// `at()` of it is far outside the buffer, which a zero-length `set` would still reject.
|
|
185
149
|
if (miniBytes.length > 0)
|
|
186
150
|
buf.set(Uint8Array.from(miniBytes), at(miniStreamStart));
|
|
187
151
|
for (const big of bigStreams)
|
|
188
152
|
buf.set(big.data, at(big.entry.startSector));
|
|
189
153
|
for (const [i, sector] of fat.entries())
|
|
190
154
|
dv.setUint32(at(fatStart) + i * 4, sector, true);
|
|
191
|
-
// DIFAT sectors carry FAT-sector pointers 110.. (unreachable under the enforced bound, but the layout
|
|
192
|
-
// is honoured: each DIFAT sector's tail points to the next, the last to ENDOFCHAIN).
|
|
193
155
|
for (let d = 0; d < difatSectors; d++) {
|
|
194
156
|
const base = at(difatStart + d);
|
|
195
157
|
for (let i = 0; i < FAT_ENTRIES_PER_SECTOR - 1; i++) {
|
|
@@ -216,13 +178,6 @@ function validateSiblingNames(storageName, siblings) {
|
|
|
216
178
|
validateSiblingNames(node.name, node.children);
|
|
217
179
|
}
|
|
218
180
|
}
|
|
219
|
-
// Order a storage's children by the [MS-CFB] 2.6.4 comparison (shorter names sort first; ties broken by
|
|
220
|
-
// uppercased UTF-16 code units) and link them as a balanced binary tree. A host locates a child by
|
|
221
|
-
// walking this tree from the storage's `child` pointer, so the ordering and links must form a valid
|
|
222
|
-
// search tree.
|
|
223
|
-
//
|
|
224
|
-
// The recursion halves a slice rather than a lo/hi index pair, which makes "the slice is empty" and
|
|
225
|
-
// "there is no midpoint entry" the same observable fact: the `undefined` check *is* the base case.
|
|
226
181
|
function linkChildren(storage, kids) {
|
|
227
182
|
const build = (nodes) => {
|
|
228
183
|
const mid = nodes.length >> 1;
|
|
@@ -243,13 +198,13 @@ function compareNames(a, b) {
|
|
|
243
198
|
return ua < ub ? -1 : ua > ub ? 1 : 0;
|
|
244
199
|
}
|
|
245
200
|
function writeHeader(dv, p) {
|
|
246
|
-
dv.setUint32(0, 0xe011cfd0, true);
|
|
201
|
+
dv.setUint32(0, 0xe011cfd0, true);
|
|
247
202
|
dv.setUint32(4, 0xe11ab1a1, true);
|
|
248
|
-
dv.setUint16(24, 0x003e, true);
|
|
249
|
-
dv.setUint16(26, 0x0003, true);
|
|
250
|
-
dv.setUint16(28, 0xfffe, true);
|
|
251
|
-
dv.setUint16(30, 9, true);
|
|
252
|
-
dv.setUint16(32, 6, true);
|
|
203
|
+
dv.setUint16(24, 0x003e, true);
|
|
204
|
+
dv.setUint16(26, 0x0003, true);
|
|
205
|
+
dv.setUint16(28, 0xfffe, true);
|
|
206
|
+
dv.setUint16(30, 9, true);
|
|
207
|
+
dv.setUint16(32, 6, true);
|
|
253
208
|
dv.setUint32(44, p.fatSectors, true);
|
|
254
209
|
dv.setUint32(48, p.dirStart, true);
|
|
255
210
|
dv.setUint32(56, MINI_CUTOFF, true);
|
|
@@ -257,7 +212,6 @@ function writeHeader(dv, p) {
|
|
|
257
212
|
dv.setUint32(64, p.miniFatSectors, true);
|
|
258
213
|
dv.setUint32(68, p.difatSectors > 0 ? p.difatStart : ENDOFCHAIN, true);
|
|
259
214
|
dv.setUint32(72, p.difatSectors, true);
|
|
260
|
-
// Header DIFAT: the first 109 FAT-sector pointers. Contiguous from fatStart under the enforced bound.
|
|
261
215
|
for (let i = 0; i < DIFAT_HEADER_SLOTS; i++) {
|
|
262
216
|
dv.setUint32(76 + i * 4, i < p.fatSectors ? p.fatStart + i : FREESECT, true);
|
|
263
217
|
}
|
|
@@ -265,7 +219,7 @@ function writeHeader(dv, p) {
|
|
|
265
219
|
function writeDirEntry(dv, off, e) {
|
|
266
220
|
for (let i = 0; i < e.name.length; i++)
|
|
267
221
|
dv.setUint16(off + i * 2, e.name.charCodeAt(i), true);
|
|
268
|
-
dv.setUint16(off + 64, (e.name.length + 1) * 2, true);
|
|
222
|
+
dv.setUint16(off + 64, (e.name.length + 1) * 2, true);
|
|
269
223
|
dv.setUint8(off + 66, e.type);
|
|
270
224
|
dv.setUint8(off + 67, COLOR_BLACK);
|
|
271
225
|
dv.setUint32(off + 68, e.left, true);
|
|
@@ -273,5 +227,4 @@ function writeDirEntry(dv, off, e) {
|
|
|
273
227
|
dv.setUint32(off + 76, e.child, true);
|
|
274
228
|
dv.setUint32(off + 116, e.startSector, true);
|
|
275
229
|
dv.setUint32(off + 120, e.size, true);
|
|
276
|
-
// Size high (124), CLSID (80..95), state/time fields stay zero — valid for a v3 entry.
|
|
277
230
|
}
|
package/dist/vba/cfb.js
CHANGED
|
@@ -1,18 +1,4 @@
|
|
|
1
|
-
// Reader for the OLE2 / Compound File Binary format ([MS-CFB]).
|
|
2
|
-
//
|
|
3
|
-
// `vbaProject.bin` is a CFB container (the same "structured storage" behind legacy .doc/.xls). We read
|
|
4
|
-
// streams by name and, for the edit-in-place path, reconstruct the whole storage/stream hierarchy so it
|
|
5
|
-
// can be re-emitted through the writer with one stream swapped — so this is a deliberate subset of
|
|
6
|
-
// [MS-CFB]: header → FAT → directory, plus the mini-FAT for sub-cutoff streams, and the red-black
|
|
7
|
-
// sibling tree each storage navigates.
|
|
8
|
-
//
|
|
9
|
-
// It parses an untrusted blob, so every sector index, chain, and stream size is bounds-checked against
|
|
10
|
-
// the file and every chain walk is cycle-guarded. A malformed container fails closed with a
|
|
11
|
-
// VbaParseError instead of reading out of bounds, looping forever, or over-allocating.
|
|
12
1
|
import { VbaParseError } from './errors.js';
|
|
13
|
-
// Sector values 0xFFFFFFFA..0xFFFFFFFF are reserved markers (DIFSECT/FATSECT/ENDOFCHAIN/FREESECT), not
|
|
14
|
-
// data-sector indices; any value at or above this is chain-terminal. Directory-tree links reuse the same
|
|
15
|
-
// convention: NOSTREAM (0xFFFFFFFF) and any value at or above the ceiling mean "no such sibling/child".
|
|
16
2
|
const MAX_REGULAR_SECTOR = 0xfffffffa;
|
|
17
3
|
const NOSTREAM = 0xffffffff;
|
|
18
4
|
const TYPE_EMPTY = 0;
|
|
@@ -43,7 +29,6 @@ export class CompoundFile {
|
|
|
43
29
|
}
|
|
44
30
|
const sectorShift = this.#u16(30);
|
|
45
31
|
const miniSectorShift = this.#u16(32);
|
|
46
|
-
// [MS-CFB] fixes these: 512-byte sectors (shift 9) for v3, 4096 (shift 12) for v4; mini shift 6.
|
|
47
32
|
if (sectorShift !== 9 && sectorShift !== 12) {
|
|
48
33
|
throw new VbaParseError(`unsupported sector shift ${sectorShift}`);
|
|
49
34
|
}
|
|
@@ -65,28 +50,19 @@ export class CompoundFile {
|
|
|
65
50
|
const root = this.#dir.find((e) => e.type === TYPE_ROOT);
|
|
66
51
|
if (!root)
|
|
67
52
|
throw new VbaParseError('compound file has no root storage entry');
|
|
68
|
-
// The mini-stream lives in the regular FAT, addressed from the root entry's start sector.
|
|
69
53
|
this.#miniStream = this.#readViaFat(root.startSector, root.size);
|
|
70
54
|
}
|
|
71
|
-
/** List every stream/storage name in the directory (order as stored). */
|
|
72
55
|
names() {
|
|
73
56
|
return this.#dir
|
|
74
57
|
.filter((e) => e.type === TYPE_STREAM || e.type === TYPE_STORAGE)
|
|
75
58
|
.map((e) => e.name);
|
|
76
59
|
}
|
|
77
|
-
/** Read a stream's raw bytes by exact entry name, or `undefined` if absent. */
|
|
78
60
|
readStream(name) {
|
|
79
61
|
const entry = this.#dir.find((e) => e.type === TYPE_STREAM && e.name === name);
|
|
80
62
|
if (!entry)
|
|
81
63
|
return undefined;
|
|
82
64
|
return this.#readEntryData(entry);
|
|
83
65
|
}
|
|
84
|
-
/**
|
|
85
|
-
* Reconstruct the container's top-level children as the writer's node shape, recursing into every
|
|
86
|
-
* storage — so a caller can swap one stream and re-emit the whole hierarchy with {@link writeCompoundFile}.
|
|
87
|
-
* Walks the red-black sibling tree each storage navigates (not the linear directory scan), so any part
|
|
88
|
-
* a host reaches is carried through. Cycle- and bounds-guarded like every other chain walk here.
|
|
89
|
-
*/
|
|
90
66
|
tree() {
|
|
91
67
|
const rootIdx = this.#dir.findIndex((e) => e.type === TYPE_ROOT);
|
|
92
68
|
if (rootIdx < 0)
|
|
@@ -98,7 +74,7 @@ export class CompoundFile {
|
|
|
98
74
|
const nodes = [];
|
|
99
75
|
const walk = (idx) => {
|
|
100
76
|
if (idx >= MAX_REGULAR_SECTOR)
|
|
101
|
-
return;
|
|
77
|
+
return;
|
|
102
78
|
if (idx >= this.#dir.length)
|
|
103
79
|
throw new VbaParseError('directory sibling index out of range');
|
|
104
80
|
if (seen.has(idx))
|
|
@@ -126,7 +102,6 @@ export class CompoundFile {
|
|
|
126
102
|
}
|
|
127
103
|
#readDifat(numFatSectors, firstDifat, numDifat) {
|
|
128
104
|
const ids = [];
|
|
129
|
-
// The first 109 FAT-sector pointers live in the header; the rest chain through DIFAT sectors.
|
|
130
105
|
for (let i = 0; i < 109 && ids.length < numFatSectors; i++) {
|
|
131
106
|
const v = this.#u32(76 + i * 4);
|
|
132
107
|
if (v >= MAX_REGULAR_SECTOR)
|
|
@@ -161,7 +136,6 @@ export class CompoundFile {
|
|
|
161
136
|
return fat;
|
|
162
137
|
}
|
|
163
138
|
#readChainValues(firstSector) {
|
|
164
|
-
// Every uint32 in a sector chain (used for the mini-FAT), cycle-guarded.
|
|
165
139
|
const values = [];
|
|
166
140
|
const perSector = this.#sectorSize / 4;
|
|
167
141
|
const seen = new Set();
|
|
@@ -180,8 +154,6 @@ export class CompoundFile {
|
|
|
180
154
|
#readDirectory(firstDirSector) {
|
|
181
155
|
const raw = this.#readChainFull(firstDirSector);
|
|
182
156
|
const entries = [];
|
|
183
|
-
// Empty slots are kept as placeholders (not skipped) so array indices stay equal to the on-disk
|
|
184
|
-
// directory-entry ids the sibling-tree links reference — the tree walk in #buildSiblings needs them.
|
|
185
157
|
for (let off = 0; off + DIR_ENTRY_SIZE <= raw.length; off += DIR_ENTRY_SIZE) {
|
|
186
158
|
const type = raw[off + 66];
|
|
187
159
|
if (type !== TYPE_EMPTY &&
|
|
@@ -211,14 +183,12 @@ export class CompoundFile {
|
|
|
211
183
|
}
|
|
212
184
|
const name = decodeUtf16le(raw.subarray(off, off + Math.max(0, nameLen - 2)));
|
|
213
185
|
const startSector = readU32(raw, off + 116);
|
|
214
|
-
const size = readU32(raw, off + 120);
|
|
186
|
+
const size = readU32(raw, off + 120);
|
|
215
187
|
entries.push({ name, type, startSector, size, left, right, child });
|
|
216
188
|
}
|
|
217
189
|
return entries;
|
|
218
190
|
}
|
|
219
191
|
#readChainFull(startSector) {
|
|
220
|
-
// Follow a FAT chain to its end, collecting whole sectors (used for the directory, whose byte
|
|
221
|
-
// length is not declared). Cycle-guarded and bounded by the sector count.
|
|
222
192
|
const chunks = [];
|
|
223
193
|
const seen = new Set();
|
|
224
194
|
let sector = startSector;
|
package/dist/vba/codepage.js
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
// The VBA project stores module names and source as MBCS bytes in the project code page
|
|
2
|
-
// (`PROJECTCODEPAGE`, [MS-OVBA] 2.3.4.2). To recover text we decode those bytes with the matching
|
|
3
|
-
// encoding rather than assuming latin1 — a CJK or Cyrillic project would otherwise mojibake.
|
|
4
1
|
import { VbaAuthorError, VbaParseError } from './errors.js';
|
|
5
|
-
// Windows code-page numbers → the WHATWG encoding label `TextDecoder` understands. Node's built-in
|
|
6
|
-
// ICU covers all of these. Only the code pages a VBA project realistically declares are listed; an
|
|
7
|
-
// unlisted one falls back to windows-1252 (the overwhelmingly common Western-European default) so a
|
|
8
|
-
// rare project still decodes readably instead of throwing.
|
|
9
2
|
const CODEPAGE_LABEL = new Map([
|
|
10
3
|
[1250, 'windows-1250'],
|
|
11
4
|
[1251, 'windows-1251'],
|
|
@@ -28,19 +21,12 @@ const CODEPAGE_LABEL = new Map([
|
|
|
28
21
|
[28592, 'iso-8859-2'],
|
|
29
22
|
[65001, 'utf-8'],
|
|
30
23
|
]);
|
|
31
|
-
/**
|
|
32
|
-
* A `TextDecoder` for the given VBA project code page. Non-fatal (malformed bytes become U+FFFD rather
|
|
33
|
-
* than throwing) because recovered source is for reading, not re-encoding — a stray byte must not sink
|
|
34
|
-
* the whole extraction. An unknown code page falls back to windows-1252.
|
|
35
|
-
*/
|
|
36
24
|
export function decoderForCodePage(codePage) {
|
|
37
25
|
const label = CODEPAGE_LABEL.get(codePage) ?? 'windows-1252';
|
|
38
26
|
try {
|
|
39
27
|
return new TextDecoder(label, { fatal: false });
|
|
40
28
|
}
|
|
41
29
|
catch {
|
|
42
|
-
// A runtime whose ICU lacks the label (a minimal build) still gets usable Western-European text
|
|
43
|
-
// rather than a hard failure on an otherwise-valid project.
|
|
44
30
|
try {
|
|
45
31
|
return new TextDecoder('windows-1252', { fatal: false });
|
|
46
32
|
}
|
|
@@ -51,14 +37,6 @@ export function decoderForCodePage(codePage) {
|
|
|
51
37
|
}
|
|
52
38
|
}
|
|
53
39
|
}
|
|
54
|
-
/**
|
|
55
|
-
* The write-side inverse of {@link decoderForCodePage}: an encoder for a project's code page, built by
|
|
56
|
-
* decoding every byte 0x00..0xFF through the matching decoder and inverting the map. This is exact for
|
|
57
|
-
* the single-byte code pages a VBA project realistically declares. Under a multi-byte code page (Shift
|
|
58
|
-
* JIS, GBK, …) the high bytes are lead bytes, not standalone characters, so only ASCII inverts and any
|
|
59
|
-
* non-ASCII character is rejected — fail-closed rather than emit a corrupt byte. A character the chosen
|
|
60
|
-
* code page cannot represent throws {@link VbaAuthorError}.
|
|
61
|
-
*/
|
|
62
40
|
export function encoderForCodePage(codePage) {
|
|
63
41
|
const decoder = decoderForCodePage(codePage);
|
|
64
42
|
const charToByte = new Map();
|
|
@@ -67,7 +45,7 @@ export function encoderForCodePage(codePage) {
|
|
|
67
45
|
one[0] = b;
|
|
68
46
|
const decoded = decoder.decode(one);
|
|
69
47
|
if (decoded.length !== 1)
|
|
70
|
-
continue;
|
|
48
|
+
continue;
|
|
71
49
|
const code = decoded.charCodeAt(0);
|
|
72
50
|
if (code !== 0xfffd && !charToByte.has(code))
|
|
73
51
|
charToByte.set(code, b);
|
package/dist/vba/errors.js
CHANGED
|
@@ -1,23 +1,8 @@
|
|
|
1
1
|
import { XlsxError } from '../errors.js';
|
|
2
|
-
/**
|
|
3
|
-
* Thrown when a VBA project (`vbaProject.bin`) is present but cannot be parsed — a malformed
|
|
4
|
-
* compound file, a corrupt compressed stream, or a `dir` record that does not conform to [MS-OVBA].
|
|
5
|
-
* A workbook with no macros never produces this: {@link Workbook.vbaProject} is `undefined` instead.
|
|
6
|
-
*
|
|
7
|
-
* The parser treats the blob as hostile input (a spreadsheet library parses untrusted files), so every
|
|
8
|
-
* malformed structure fails closed with this error rather than crashing, hanging, or over-allocating.
|
|
9
|
-
*/
|
|
10
2
|
export class VbaParseError extends XlsxError {
|
|
11
3
|
name = 'VbaParseError';
|
|
12
4
|
code = 'malformed-input';
|
|
13
5
|
}
|
|
14
|
-
/**
|
|
15
|
-
* Thrown when authoring a VBA project (synthesizing a `vbaProject.bin` from module source) is asked to
|
|
16
|
-
* produce something that cannot be encoded to a well-formed container — a stream name longer than the
|
|
17
|
-
* [MS-CFB] 31-character limit, a duplicate stream name, or a project so large it would exceed the
|
|
18
|
-
* writer's single-header DIFAT bound. This is a caller-side contract violation, distinct from
|
|
19
|
-
* {@link VbaParseError} (which reports a malformed blob *read* from an untrusted file).
|
|
20
|
-
*/
|
|
21
6
|
export class VbaAuthorError extends XlsxError {
|
|
22
7
|
name = 'VbaAuthorError';
|
|
23
8
|
code = 'authoring';
|
package/dist/vba/index.js
CHANGED
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
// Native VBA read, plus pure-TS structural edits: decode a macro-enabled workbook's `vbaProject.bin`
|
|
2
|
-
// into readable module source, and remove a module or add a library reference from an existing project
|
|
3
|
-
// by splicing the original bytes (every module's compiled p-code preserved untouched).
|
|
4
|
-
//
|
|
5
|
-
// Authoring or editing module SOURCE is NOT here. Excel runs a module's compiled p-code, not its source,
|
|
6
|
-
// and only a real Excel can produce source-matched p-code — so that lives in the offline
|
|
7
|
-
// `tools/vba-compiler` (VBIDE), whose output is attached via `Workbook.vbaProjectBytes`. The read path
|
|
8
|
-
// is a projection over bytes the model preserves opaquely on round-trip. For the design invariants and
|
|
9
|
-
// the wider VBA feature map see the ADRs: read view (docs/decisions/0016), authoring (0017/0019), and
|
|
10
|
-
// structural edits (0018/0019), plus docs/knowledge/specs/xlsm-macro-preservation.md.
|
|
11
1
|
export { writeCompoundFile, } from './cfb-writer.js';
|
|
12
2
|
export { VbaAuthorError, VbaParseError } from './errors.js';
|
|
13
3
|
export { compressContainer, decompressContainer } from './ms-ovba.js';
|
package/dist/vba/ms-ovba.js
CHANGED
|
@@ -1,29 +1,8 @@
|
|
|
1
|
-
// MS-OVBA §2.4.1 — compression and decompression of a "CompressedContainer".
|
|
2
|
-
//
|
|
3
|
-
// VBA module source and the project `dir` stream are stored in Office's own run-length compression,
|
|
4
|
-
// NOT deflate. A container is a 0x01 signature byte followed by one or more chunks; each chunk
|
|
5
|
-
// decompresses to at most 4096 bytes and is either a raw 4096-byte copy or a stream of literal/copy
|
|
6
|
-
// tokens. Reference: [MS-OVBA] 2.4.1.3.6 (decompressing a CompressedContainer), 2.4.1.3.19.3 (the
|
|
7
|
-
// CopyToken bit-packing), and 2.4.1.3.7 (compressing a chunk).
|
|
8
|
-
//
|
|
9
|
-
// The decompressor is a hostile-input parser: the container comes from an untrusted file, so every
|
|
10
|
-
// length and back-reference is bounds-checked and the total output is capped. A malformed container
|
|
11
|
-
// fails closed with a VbaParseError rather than over-allocating, looping, or reading out of bounds. The
|
|
12
|
-
// compressor is the authoring inverse — it is fed our own bytes, and its output re-expands to the input
|
|
13
|
-
// byte-for-byte (the round-trip is the correctness contract).
|
|
14
1
|
import { VbaParseError } from './errors.js';
|
|
15
|
-
// A decompressed chunk covers at most 4096 bytes; both directions honour this window ([MS-OVBA]
|
|
16
|
-
// 2.4.1.3.6). The chunk header's bits 12-14 carry a fixed 0b011 signature, bit 15 the compressed flag.
|
|
17
2
|
const MAX_CHUNK_DECOMPRESSED = 4096;
|
|
18
3
|
const CHUNK_SIGNATURE = 0b011 << 12;
|
|
19
4
|
const CHUNK_COMPRESSED_FLAG = 0x8000;
|
|
20
|
-
// A single VBA project is well under a megabyte; 64 MiB is far above any legitimate container yet
|
|
21
|
-
// bounds a decompression bomb (a small container that expands without limit) to a survivable size.
|
|
22
5
|
const DEFAULT_MAX_OUTPUT = 64 * 1024 * 1024;
|
|
23
|
-
/**
|
|
24
|
-
* Decompress an MS-OVBA CompressedContainer beginning at `start` in `buf`.
|
|
25
|
-
* @param maxOutput hard ceiling on decompressed bytes; exceeding it throws (bomb guard).
|
|
26
|
-
*/
|
|
27
6
|
export function decompressContainer(buf, start = 0, maxOutput = DEFAULT_MAX_OUTPUT) {
|
|
28
7
|
if (start >= buf.length) {
|
|
29
8
|
throw new VbaParseError(`compressed container starts past end of stream (${start} >= ${buf.length})`);
|
|
@@ -36,7 +15,6 @@ export function decompressContainer(buf, start = 0, maxOutput = DEFAULT_MAX_OUTP
|
|
|
36
15
|
while (pos + 2 <= buf.length) {
|
|
37
16
|
const header = readU16(buf, pos);
|
|
38
17
|
pos += 2;
|
|
39
|
-
// Bits 0-11: (chunk data size - 1). Bit 15: compressed flag. Bits 12-14: the fixed 0b011 signature.
|
|
40
18
|
const chunkDataSize = (header & 0x0fff) + 1;
|
|
41
19
|
const compressed = (header & 0x8000) !== 0;
|
|
42
20
|
if (((header >> 12) & 0x7) !== 0b011) {
|
|
@@ -47,14 +25,13 @@ export function decompressContainer(buf, start = 0, maxOutput = DEFAULT_MAX_OUTP
|
|
|
47
25
|
throw new VbaParseError(`chunk data (${chunkDataSize} bytes) runs past end of stream`);
|
|
48
26
|
}
|
|
49
27
|
if (!compressed) {
|
|
50
|
-
// A raw chunk carries its bytes verbatim (Excel emits one only when compression would expand).
|
|
51
28
|
for (let i = pos; i < chunkEnd; i++)
|
|
52
29
|
out.push(buf[i]);
|
|
53
30
|
guardOutput(out.length, maxOutput);
|
|
54
31
|
pos = chunkEnd;
|
|
55
32
|
continue;
|
|
56
33
|
}
|
|
57
|
-
const chunkStart = out.length;
|
|
34
|
+
const chunkStart = out.length;
|
|
58
35
|
while (pos < chunkEnd) {
|
|
59
36
|
const flagByte = buf[pos++];
|
|
60
37
|
for (let bit = 0; bit < 8 && pos < chunkEnd; bit++) {
|
|
@@ -74,10 +51,8 @@ export function decompressContainer(buf, start = 0, maxOutput = DEFAULT_MAX_OUTP
|
|
|
74
51
|
const copyOffset = (token >> (16 - bitCount)) + 1;
|
|
75
52
|
const src = out.length - copyOffset;
|
|
76
53
|
if (src < chunkStart) {
|
|
77
|
-
// A back-reference may only reach data emitted since this chunk began ([MS-OVBA] 2.4.1.3.19).
|
|
78
54
|
throw new VbaParseError('copy token references before the start of its chunk');
|
|
79
55
|
}
|
|
80
|
-
// Byte-by-byte so overlapping runs (run-length expansion) grow correctly.
|
|
81
56
|
for (let i = 0; i < length; i++)
|
|
82
57
|
out.push(out[src + i]);
|
|
83
58
|
guardOutput(out.length, maxOutput);
|
|
@@ -87,19 +62,11 @@ export function decompressContainer(buf, start = 0, maxOutput = DEFAULT_MAX_OUTP
|
|
|
87
62
|
}
|
|
88
63
|
return Uint8Array.from(out);
|
|
89
64
|
}
|
|
90
|
-
/**
|
|
91
|
-
* Compress `data` into an MS-OVBA CompressedContainer — the inverse of {@link decompressContainer}.
|
|
92
|
-
* Every 4096-decompressed-byte window is emitted as a compressed chunk of literal and copy tokens, or
|
|
93
|
-
* stored verbatim when compression would not shrink it (so the encoded chunk never exceeds the 12-bit
|
|
94
|
-
* size field). The result re-expands to `data` byte-for-byte.
|
|
95
|
-
*/
|
|
96
65
|
export function compressContainer(data) {
|
|
97
|
-
const out = [0x01];
|
|
66
|
+
const out = [0x01];
|
|
98
67
|
for (let start = 0; start < data.length; start += MAX_CHUNK_DECOMPRESSED) {
|
|
99
68
|
const chunk = data.subarray(start, Math.min(start + MAX_CHUNK_DECOMPRESSED, data.length));
|
|
100
69
|
const tokens = compressChunk(chunk);
|
|
101
|
-
// Prefer the token stream only when it is strictly smaller; otherwise store the chunk raw. Both
|
|
102
|
-
// encode their exact length in the header, so the decompressor reconstructs the window either way.
|
|
103
70
|
const compressed = tokens.length < chunk.length;
|
|
104
71
|
const body = compressed ? tokens : chunk;
|
|
105
72
|
const header = (compressed ? CHUNK_COMPRESSED_FLAG : 0) | CHUNK_SIGNATURE | ((body.length - 1) & 0x0fff);
|
|
@@ -109,11 +76,6 @@ export function compressContainer(data) {
|
|
|
109
76
|
}
|
|
110
77
|
return Uint8Array.from(out);
|
|
111
78
|
}
|
|
112
|
-
// Encode one decompressed chunk (≤ 4096 bytes) as a sequence of MS-OVBA token groups: a flag byte whose
|
|
113
|
-
// bits mark the next up-to-8 tokens as literal (0) or copy (1). A copy token replaces a run of 3+ bytes
|
|
114
|
-
// that recurs earlier in the *same* chunk; matches may overlap the current position (run-length growth),
|
|
115
|
-
// which the decompressor reproduces byte-by-byte. The bit split between the offset and length fields
|
|
116
|
-
// widens as the chunk fills, exactly as the decoder computes it, so both agree on every token's shape.
|
|
117
79
|
function compressChunk(chunk) {
|
|
118
80
|
const tokens = [];
|
|
119
81
|
let pos = 0;
|
|
@@ -127,7 +89,6 @@ function compressChunk(chunk) {
|
|
|
127
89
|
const windowStart = Math.max(0, pos - (1 << bitCount));
|
|
128
90
|
let bestLength = 0;
|
|
129
91
|
let bestOffset = 0;
|
|
130
|
-
// Scan nearest-first so equal-length matches keep the smallest offset (a marginally cheaper token).
|
|
131
92
|
for (let cand = pos - 1; cand >= windowStart; cand--) {
|
|
132
93
|
let len = 0;
|
|
133
94
|
while (len < maxLength &&
|
|
@@ -139,7 +100,7 @@ function compressChunk(chunk) {
|
|
|
139
100
|
bestLength = len;
|
|
140
101
|
bestOffset = pos - cand;
|
|
141
102
|
if (bestLength === maxLength)
|
|
142
|
-
break;
|
|
103
|
+
break;
|
|
143
104
|
}
|
|
144
105
|
}
|
|
145
106
|
if (bestLength >= 3) {
|
|
@@ -162,10 +123,6 @@ function guardOutput(size, maxOutput) {
|
|
|
162
123
|
throw new VbaParseError(`decompressed output exceeds the ${maxOutput}-byte ceiling (possible bomb)`);
|
|
163
124
|
}
|
|
164
125
|
}
|
|
165
|
-
/**
|
|
166
|
-
* Bit split for a CopyToken given how many bytes have been emitted since the current chunk began
|
|
167
|
-
* ([MS-OVBA] 2.4.1.3.19.3): the offset field grows and the length field shrinks as the chunk fills.
|
|
168
|
-
*/
|
|
169
126
|
function copyTokenHelp(decompressedSoFar) {
|
|
170
127
|
const bitCount = Math.max(Math.ceil(Math.log2(Math.max(decompressedSoFar, 1))), 4);
|
|
171
128
|
const lengthMask = 0xffff >> bitCount;
|