@shbernal/ts-xlsx 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +230 -0
- package/dist/core/address.d.ts +49 -0
- package/dist/core/address.js +125 -0
- package/dist/core/autofilter.d.ts +55 -0
- package/dist/core/autofilter.js +48 -0
- package/dist/core/cell.d.ts +132 -0
- package/dist/core/cell.js +249 -0
- package/dist/core/color-resolution.d.ts +58 -0
- package/dist/core/color-resolution.js +170 -0
- package/dist/core/column.d.ts +87 -0
- package/dist/core/column.js +167 -0
- package/dist/core/comment-thread.d.ts +116 -0
- package/dist/core/comment-thread.js +62 -0
- package/dist/core/conditional-formatting-overlay.d.ts +15 -0
- package/dist/core/conditional-formatting-overlay.js +24 -0
- package/dist/core/conditional-formatting.d.ts +70 -0
- package/dist/core/conditional-formatting.js +35 -0
- package/dist/core/containers.d.ts +2 -0
- package/dist/core/containers.js +25 -0
- package/dist/core/data-validation-overlay.d.ts +28 -0
- package/dist/core/data-validation-overlay.js +56 -0
- package/dist/core/data-validation.d.ts +38 -0
- package/dist/core/data-validation.js +13 -0
- package/dist/core/date.d.ts +25 -0
- package/dist/core/date.js +58 -0
- package/dist/core/formula.d.ts +49 -0
- package/dist/core/formula.js +319 -0
- package/dist/core/grid-edits.d.ts +22 -0
- package/dist/core/grid-edits.js +206 -0
- package/dist/core/image.d.ts +76 -0
- package/dist/core/image.js +65 -0
- package/dist/core/internal.d.ts +11 -0
- package/dist/core/internal.js +29 -0
- package/dist/core/merge.d.ts +13 -0
- package/dist/core/merge.js +26 -0
- package/dist/core/modern-functions.d.ts +1 -0
- package/dist/core/modern-functions.js +165 -0
- package/dist/core/page-setup.d.ts +94 -0
- package/dist/core/page-setup.js +6 -0
- package/dist/core/pivot-table.d.ts +113 -0
- package/dist/core/pivot-table.js +249 -0
- package/dist/core/preserved.d.ts +58 -0
- package/dist/core/preserved.js +5 -0
- package/dist/core/protection.d.ts +67 -0
- package/dist/core/protection.js +68 -0
- package/dist/core/range.d.ts +112 -0
- package/dist/core/range.js +283 -0
- package/dist/core/row.d.ts +73 -0
- package/dist/core/row.js +135 -0
- package/dist/core/style.d.ts +246 -0
- package/dist/core/style.js +125 -0
- package/dist/core/table-style.d.ts +67 -0
- package/dist/core/table-style.js +90 -0
- package/dist/core/table.d.ts +194 -0
- package/dist/core/table.js +291 -0
- package/dist/core/theme.d.ts +90 -0
- package/dist/core/theme.js +272 -0
- package/dist/core/value.d.ts +115 -0
- package/dist/core/value.js +144 -0
- package/dist/core/workbook-protection.d.ts +27 -0
- package/dist/core/workbook-protection.js +25 -0
- package/dist/core/workbook.d.ts +528 -0
- package/dist/core/workbook.js +732 -0
- package/dist/core/worksheet-model.d.ts +34 -0
- package/dist/core/worksheet-model.js +100 -0
- package/dist/core/worksheet.d.ts +647 -0
- package/dist/core/worksheet.js +1025 -0
- package/dist/customui/errors.d.ts +15 -0
- package/dist/customui/errors.js +15 -0
- package/dist/customui/index.d.ts +2 -0
- package/dist/customui/index.js +7 -0
- package/dist/customui/ribbon.d.ts +86 -0
- package/dist/customui/ribbon.js +180 -0
- package/dist/entries/core.d.ts +23 -0
- package/dist/entries/core.js +20 -0
- package/dist/entries/csv.d.ts +2 -0
- package/dist/entries/csv.js +6 -0
- package/dist/entries/customui.d.ts +1 -0
- package/dist/entries/customui.js +3 -0
- package/dist/entries/errors.d.ts +7 -0
- package/dist/entries/errors.js +20 -0
- package/dist/entries/vba.d.ts +2 -0
- package/dist/entries/vba.js +7 -0
- package/dist/entries/xlsb.d.ts +1 -0
- package/dist/entries/xlsb.js +7 -0
- package/dist/entries/xlsx.d.ts +5 -0
- package/dist/entries/xlsx.js +18 -0
- package/dist/errors.d.ts +65 -0
- package/dist/errors.js +66 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +19 -0
- package/dist/io/csv/read.d.ts +15 -0
- package/dist/io/csv/read.js +113 -0
- package/dist/io/csv/write.d.ts +28 -0
- package/dist/io/csv/write.js +124 -0
- package/dist/io/opc/errors.d.ts +46 -0
- package/dist/io/opc/errors.js +49 -0
- package/dist/io/opc/inflate.d.ts +15 -0
- package/dist/io/opc/inflate.js +91 -0
- package/dist/io/opc/namespaces.d.ts +18 -0
- package/dist/io/opc/namespaces.js +18 -0
- package/dist/io/opc/part-paths.d.ts +4 -0
- package/dist/io/opc/part-paths.js +35 -0
- package/dist/io/opc/read-opc.d.ts +24 -0
- package/dist/io/opc/read-opc.js +153 -0
- package/dist/io/opc/read-options.d.ts +10 -0
- package/dist/io/opc/read-options.js +7 -0
- package/dist/io/opc/rels.d.ts +15 -0
- package/dist/io/opc/rels.js +28 -0
- package/dist/io/opc/sniff-format.d.ts +27 -0
- package/dist/io/opc/sniff-format.js +92 -0
- package/dist/io/style/xf-style.d.ts +47 -0
- package/dist/io/style/xf-style.js +95 -0
- package/dist/io/xlsb/errors.d.ts +15 -0
- package/dist/io/xlsb/errors.js +15 -0
- package/dist/io/xlsb/formula.d.ts +48 -0
- package/dist/io/xlsb/formula.js +398 -0
- package/dist/io/xlsb/primitives.d.ts +86 -0
- package/dist/io/xlsb/primitives.js +230 -0
- package/dist/io/xlsb/ptg-functions.d.ts +16 -0
- package/dist/io/xlsb/ptg-functions.js +659 -0
- package/dist/io/xlsb/read-shared-strings.d.ts +2 -0
- package/dist/io/xlsb/read-shared-strings.js +22 -0
- package/dist/io/xlsb/read-styles.d.ts +4 -0
- package/dist/io/xlsb/read-styles.js +386 -0
- package/dist/io/xlsb/read-worksheet.d.ts +9 -0
- package/dist/io/xlsb/read-worksheet.js +301 -0
- package/dist/io/xlsb/read.d.ts +20 -0
- package/dist/io/xlsb/read.js +199 -0
- package/dist/io/xlsb/record-stream.d.ts +14 -0
- package/dist/io/xlsb/record-stream.js +56 -0
- package/dist/io/xlsb/record-types.d.ts +55 -0
- package/dist/io/xlsb/record-types.js +73 -0
- package/dist/io/xlsx/cell-accumulator.d.ts +25 -0
- package/dist/io/xlsx/cell-accumulator.js +172 -0
- package/dist/io/xlsx/cell-value.d.ts +32 -0
- package/dist/io/xlsx/cell-value.js +85 -0
- package/dist/io/xlsx/comments.d.ts +74 -0
- package/dist/io/xlsx/comments.js +243 -0
- package/dist/io/xlsx/conditional-formatting.d.ts +32 -0
- package/dist/io/xlsx/conditional-formatting.js +464 -0
- package/dist/io/xlsx/data-validation.d.ts +20 -0
- package/dist/io/xlsx/data-validation.js +258 -0
- package/dist/io/xlsx/edit-vba.d.ts +23 -0
- package/dist/io/xlsx/edit-vba.js +139 -0
- package/dist/io/xlsx/errors.d.ts +17 -0
- package/dist/io/xlsx/errors.js +17 -0
- package/dist/io/xlsx/hyperlinks.d.ts +42 -0
- package/dist/io/xlsx/hyperlinks.js +139 -0
- package/dist/io/xlsx/images.d.ts +34 -0
- package/dist/io/xlsx/images.js +218 -0
- package/dist/io/xlsx/namespaces.d.ts +55 -0
- package/dist/io/xlsx/namespaces.js +55 -0
- package/dist/io/xlsx/package-plan.d.ts +103 -0
- package/dist/io/xlsx/package-plan.js +168 -0
- package/dist/io/xlsx/pivot-read.d.ts +5 -0
- package/dist/io/xlsx/pivot-read.js +134 -0
- package/dist/io/xlsx/pivot.d.ts +12 -0
- package/dist/io/xlsx/pivot.js +141 -0
- package/dist/io/xlsx/read-rows.d.ts +85 -0
- package/dist/io/xlsx/read-rows.js +286 -0
- package/dist/io/xlsx/read-styles.d.ts +8 -0
- package/dist/io/xlsx/read-styles.js +551 -0
- package/dist/io/xlsx/read-worksheet.d.ts +4 -0
- package/dist/io/xlsx/read-worksheet.js +528 -0
- package/dist/io/xlsx/read.d.ts +32 -0
- package/dist/io/xlsx/read.js +680 -0
- package/dist/io/xlsx/relationships.d.ts +33 -0
- package/dist/io/xlsx/relationships.js +47 -0
- package/dist/io/xlsx/rich-runs.d.ts +13 -0
- package/dist/io/xlsx/rich-runs.js +59 -0
- package/dist/io/xlsx/rich-text.d.ts +7 -0
- package/dist/io/xlsx/rich-text.js +23 -0
- package/dist/io/xlsx/shared-formulas.d.ts +6 -0
- package/dist/io/xlsx/shared-formulas.js +52 -0
- package/dist/io/xlsx/shared-strings-read.d.ts +2 -0
- package/dist/io/xlsx/shared-strings-read.js +70 -0
- package/dist/io/xlsx/shared-strings.d.ts +17 -0
- package/dist/io/xlsx/shared-strings.js +50 -0
- package/dist/io/xlsx/sheet-properties.d.ts +13 -0
- package/dist/io/xlsx/sheet-properties.js +211 -0
- package/dist/io/xlsx/styles.d.ts +138 -0
- package/dist/io/xlsx/styles.js +821 -0
- package/dist/io/xlsx/tables.d.ts +9 -0
- package/dist/io/xlsx/tables.js +208 -0
- package/dist/io/xlsx/threaded-comments.d.ts +102 -0
- package/dist/io/xlsx/threaded-comments.js +308 -0
- package/dist/io/xlsx/workbook-xml.d.ts +12 -0
- package/dist/io/xlsx/workbook-xml.js +353 -0
- package/dist/io/xlsx/worksheet-xml.d.ts +86 -0
- package/dist/io/xlsx/worksheet-xml.js +643 -0
- package/dist/io/xlsx/write-stream.d.ts +171 -0
- package/dist/io/xlsx/write-stream.js +399 -0
- package/dist/io/xlsx/write.d.ts +81 -0
- package/dist/io/xlsx/write.js +396 -0
- package/dist/io/xlsx/x14-ext.d.ts +9 -0
- package/dist/io/xlsx/x14-ext.js +12 -0
- package/dist/vba/cfb-writer.d.ts +19 -0
- package/dist/vba/cfb-writer.js +277 -0
- package/dist/vba/cfb.d.ts +16 -0
- package/dist/vba/cfb.js +324 -0
- package/dist/vba/codepage.d.ts +18 -0
- package/dist/vba/codepage.js +87 -0
- package/dist/vba/errors.d.ts +24 -0
- package/dist/vba/errors.js +24 -0
- package/dist/vba/index.d.ts +5 -0
- package/dist/vba/index.js +15 -0
- package/dist/vba/ms-ovba.d.ts +12 -0
- package/dist/vba/ms-ovba.js +176 -0
- package/dist/vba/project-editor.d.ts +62 -0
- package/dist/vba/project-editor.js +377 -0
- package/dist/vba/project.d.ts +45 -0
- package/dist/vba/project.js +160 -0
- package/dist/vba/vba-encoding.d.ts +14 -0
- package/dist/vba/vba-encoding.js +39 -0
- package/dist/xml/errors.d.ts +15 -0
- package/dist/xml/errors.js +15 -0
- package/dist/xml/xml-read.d.ts +107 -0
- package/dist/xml/xml-read.js +283 -0
- package/dist/xml/xml.d.ts +41 -0
- package/dist/xml/xml.js +86 -0
- package/package.json +117 -0
|
@@ -0,0 +1,277 @@
|
|
|
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
|
+
import { VbaAuthorError } from "./errors.js";
|
|
14
|
+
const SECTOR = 512;
|
|
15
|
+
const MINI_SECTOR = 64;
|
|
16
|
+
const MINI_CUTOFF = 4096;
|
|
17
|
+
const DIR_ENTRY_SIZE = 128;
|
|
18
|
+
const ENTRIES_PER_DIR_SECTOR = SECTOR / DIR_ENTRY_SIZE; // 4
|
|
19
|
+
const FAT_ENTRIES_PER_SECTOR = SECTOR / 4; // 128
|
|
20
|
+
const DIFAT_HEADER_SLOTS = 109; // FAT-sector pointers that fit in the header before DIFAT sectors
|
|
21
|
+
const MAX_NAME_CHARS = 31; // 32 UTF-16 code units incl. the NUL terminator
|
|
22
|
+
// Sector chain markers ([MS-CFB] 2.2).
|
|
23
|
+
const FREESECT = 0xffffffff;
|
|
24
|
+
const ENDOFCHAIN = 0xfffffffe;
|
|
25
|
+
const FATSECT = 0xfffffffd;
|
|
26
|
+
const DIFSECT = 0xfffffffc;
|
|
27
|
+
const NOSTREAM = 0xffffffff;
|
|
28
|
+
// Object types ([MS-CFB] 2.6.1).
|
|
29
|
+
const TYPE_STORAGE = 1;
|
|
30
|
+
const TYPE_STREAM = 2;
|
|
31
|
+
const TYPE_ROOT = 5;
|
|
32
|
+
const COLOR_BLACK = 1;
|
|
33
|
+
function isStream(node) {
|
|
34
|
+
return 'data' in node;
|
|
35
|
+
}
|
|
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
|
+
export function writeCompoundFile(root) {
|
|
45
|
+
const entries = [];
|
|
46
|
+
const addEntry = (name, type, startSector) => {
|
|
47
|
+
const entry = {
|
|
48
|
+
index: entries.length,
|
|
49
|
+
name,
|
|
50
|
+
type,
|
|
51
|
+
startSector,
|
|
52
|
+
size: 0,
|
|
53
|
+
left: NOSTREAM,
|
|
54
|
+
right: NOSTREAM,
|
|
55
|
+
child: NOSTREAM,
|
|
56
|
+
};
|
|
57
|
+
entries.push(entry);
|
|
58
|
+
return entry;
|
|
59
|
+
};
|
|
60
|
+
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
|
+
const miniBytes = [];
|
|
64
|
+
const miniFat = [];
|
|
65
|
+
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
|
+
const addNode = (node, siblings) => {
|
|
69
|
+
if (isStream(node)) {
|
|
70
|
+
const entry = addEntry(node.name, TYPE_STREAM, ENDOFCHAIN);
|
|
71
|
+
entry.size = node.data.length;
|
|
72
|
+
siblings.push(entry);
|
|
73
|
+
if (node.data.length === 0) {
|
|
74
|
+
// an empty stream owns no sectors
|
|
75
|
+
}
|
|
76
|
+
else if (node.data.length >= MINI_CUTOFF) {
|
|
77
|
+
bigStreams.push({ entry, data: node.data, sectors: Math.ceil(node.data.length / SECTOR) });
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
const startMini = miniBytes.length / MINI_SECTOR;
|
|
81
|
+
const numMini = Math.ceil(node.data.length / MINI_SECTOR);
|
|
82
|
+
for (let k = 0; k < numMini; k++)
|
|
83
|
+
miniFat.push(k < numMini - 1 ? startMini + k + 1 : ENDOFCHAIN);
|
|
84
|
+
miniBytes.push(...node.data, ...new Array(numMini * MINI_SECTOR - node.data.length).fill(0));
|
|
85
|
+
entry.startSector = startMini;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const entry = addEntry(node.name, TYPE_STORAGE, 0);
|
|
90
|
+
siblings.push(entry);
|
|
91
|
+
const kids = [];
|
|
92
|
+
for (const c of node.children)
|
|
93
|
+
addNode(c, kids);
|
|
94
|
+
// Each storage (Root included) links its children as a balanced search tree the host navigates.
|
|
95
|
+
linkChildren(entry, kids);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
validateSiblingNames('Root Entry', root);
|
|
99
|
+
const rootKids = [];
|
|
100
|
+
for (const n of root)
|
|
101
|
+
addNode(n, rootKids);
|
|
102
|
+
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
|
+
const dirSectors = Math.ceil(entries.length / ENTRIES_PER_DIR_SECTOR);
|
|
107
|
+
const miniFatSectors = miniFat.length > 0 ? Math.ceil((miniFat.length * 4) / FAT_ENTRIES_PER_SECTOR) : 0;
|
|
108
|
+
const miniStreamSectors = Math.ceil(miniBytes.length / SECTOR);
|
|
109
|
+
const baseSectors = dirSectors +
|
|
110
|
+
miniFatSectors +
|
|
111
|
+
miniStreamSectors +
|
|
112
|
+
bigStreams.reduce((total, big) => total + big.sectors, 0);
|
|
113
|
+
let fatSectors = 0;
|
|
114
|
+
let difatSectors = 0;
|
|
115
|
+
for (;;) {
|
|
116
|
+
const total = baseSectors + fatSectors + difatSectors;
|
|
117
|
+
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
|
+
const needDifat = needFat > DIFAT_HEADER_SLOTS
|
|
120
|
+
? Math.ceil((needFat - DIFAT_HEADER_SLOTS) / (FAT_ENTRIES_PER_SECTOR - 1))
|
|
121
|
+
: 0;
|
|
122
|
+
if (needFat === fatSectors && needDifat === difatSectors)
|
|
123
|
+
break;
|
|
124
|
+
fatSectors = needFat;
|
|
125
|
+
difatSectors = needDifat;
|
|
126
|
+
}
|
|
127
|
+
if (fatSectors > DIFAT_HEADER_SLOTS) {
|
|
128
|
+
throw new VbaAuthorError(`project needs ${fatSectors} FAT sectors, exceeding the ${DIFAT_HEADER_SLOTS}-sector single-header bound`);
|
|
129
|
+
}
|
|
130
|
+
let cursor = 0;
|
|
131
|
+
const dirStart = cursor;
|
|
132
|
+
cursor += dirSectors;
|
|
133
|
+
const miniFatStart = miniFatSectors > 0 ? cursor : ENDOFCHAIN;
|
|
134
|
+
cursor += miniFatSectors;
|
|
135
|
+
const miniStreamStart = miniStreamSectors > 0 ? cursor : ENDOFCHAIN;
|
|
136
|
+
cursor += miniStreamSectors;
|
|
137
|
+
for (const big of bigStreams) {
|
|
138
|
+
big.entry.startSector = cursor;
|
|
139
|
+
cursor += big.sectors;
|
|
140
|
+
}
|
|
141
|
+
const fatStart = cursor;
|
|
142
|
+
cursor += fatSectors;
|
|
143
|
+
const difatStart = difatSectors > 0 ? cursor : ENDOFCHAIN;
|
|
144
|
+
cursor += difatSectors;
|
|
145
|
+
const totalSectors = cursor;
|
|
146
|
+
rootEntry.startSector = miniStreamStart;
|
|
147
|
+
rootEntry.size = miniBytes.length;
|
|
148
|
+
// ── FAT ─────────────────────────────────────────────────────────────────────────────────────────
|
|
149
|
+
const fat = new Array(fatSectors * FAT_ENTRIES_PER_SECTOR).fill(FREESECT);
|
|
150
|
+
const chainRegion = (start, count) => {
|
|
151
|
+
for (let k = 0; k < count; k++)
|
|
152
|
+
fat[start + k] = k < count - 1 ? start + k + 1 : ENDOFCHAIN;
|
|
153
|
+
};
|
|
154
|
+
chainRegion(dirStart, dirSectors);
|
|
155
|
+
if (miniFatSectors > 0)
|
|
156
|
+
chainRegion(miniFatStart, miniFatSectors);
|
|
157
|
+
if (miniStreamSectors > 0)
|
|
158
|
+
chainRegion(miniStreamStart, miniStreamSectors);
|
|
159
|
+
for (const big of bigStreams)
|
|
160
|
+
chainRegion(big.entry.startSector, big.sectors);
|
|
161
|
+
for (let k = 0; k < fatSectors; k++)
|
|
162
|
+
fat[fatStart + k] = FATSECT;
|
|
163
|
+
for (let k = 0; k < difatSectors; k++)
|
|
164
|
+
fat[difatStart + k] = DIFSECT;
|
|
165
|
+
// ── Serialize ──────────────────────────────────────────────────────────────────────────────────
|
|
166
|
+
const buf = new Uint8Array((totalSectors + 1) * SECTOR); // +1 for the header sector
|
|
167
|
+
const dv = new DataView(buf.buffer);
|
|
168
|
+
const at = (sector) => (sector + 1) * SECTOR;
|
|
169
|
+
writeHeader(dv, {
|
|
170
|
+
fatSectors,
|
|
171
|
+
dirStart,
|
|
172
|
+
miniFatStart,
|
|
173
|
+
miniFatSectors,
|
|
174
|
+
difatStart,
|
|
175
|
+
difatSectors,
|
|
176
|
+
fatStart,
|
|
177
|
+
});
|
|
178
|
+
for (const e of entries)
|
|
179
|
+
writeDirEntry(dv, at(dirStart) + e.index * DIR_ENTRY_SIZE, e);
|
|
180
|
+
for (let i = 0; i < miniFatSectors * FAT_ENTRIES_PER_SECTOR; i++) {
|
|
181
|
+
dv.setUint32(at(miniFatStart) + i * 4, miniFat[i] ?? FREESECT, true);
|
|
182
|
+
}
|
|
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
|
+
if (miniBytes.length > 0)
|
|
186
|
+
buf.set(Uint8Array.from(miniBytes), at(miniStreamStart));
|
|
187
|
+
for (const big of bigStreams)
|
|
188
|
+
buf.set(big.data, at(big.entry.startSector));
|
|
189
|
+
for (const [i, sector] of fat.entries())
|
|
190
|
+
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
|
+
for (let d = 0; d < difatSectors; d++) {
|
|
194
|
+
const base = at(difatStart + d);
|
|
195
|
+
for (let i = 0; i < FAT_ENTRIES_PER_SECTOR - 1; i++) {
|
|
196
|
+
const fatIdx = DIFAT_HEADER_SLOTS + d * (FAT_ENTRIES_PER_SECTOR - 1) + i;
|
|
197
|
+
dv.setUint32(base + i * 4, fatIdx < fatSectors ? fatStart + fatIdx : FREESECT, true);
|
|
198
|
+
}
|
|
199
|
+
dv.setUint32(base + (FAT_ENTRIES_PER_SECTOR - 1) * 4, d < difatSectors - 1 ? difatStart + d + 1 : ENDOFCHAIN, true);
|
|
200
|
+
}
|
|
201
|
+
return buf;
|
|
202
|
+
}
|
|
203
|
+
function validateSiblingNames(storageName, siblings) {
|
|
204
|
+
const seen = new Set();
|
|
205
|
+
for (const node of siblings) {
|
|
206
|
+
if (node.name.length === 0)
|
|
207
|
+
throw new VbaAuthorError('entry name must not be empty');
|
|
208
|
+
if (node.name.length > MAX_NAME_CHARS) {
|
|
209
|
+
throw new VbaAuthorError(`entry name '${node.name}' exceeds the ${MAX_NAME_CHARS}-character CFB limit`);
|
|
210
|
+
}
|
|
211
|
+
if (seen.has(node.name)) {
|
|
212
|
+
throw new VbaAuthorError(`duplicate entry name '${node.name}' under storage '${storageName}'`);
|
|
213
|
+
}
|
|
214
|
+
seen.add(node.name);
|
|
215
|
+
if (!isStream(node))
|
|
216
|
+
validateSiblingNames(node.name, node.children);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
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
|
+
function linkChildren(storage, kids) {
|
|
227
|
+
const build = (nodes) => {
|
|
228
|
+
const mid = nodes.length >> 1;
|
|
229
|
+
const node = nodes[mid];
|
|
230
|
+
if (node === undefined)
|
|
231
|
+
return NOSTREAM;
|
|
232
|
+
node.left = build(nodes.slice(0, mid));
|
|
233
|
+
node.right = build(nodes.slice(mid + 1));
|
|
234
|
+
return node.index;
|
|
235
|
+
};
|
|
236
|
+
storage.child = build([...kids].sort((a, b) => compareNames(a.name, b.name)));
|
|
237
|
+
}
|
|
238
|
+
function compareNames(a, b) {
|
|
239
|
+
if (a.length !== b.length)
|
|
240
|
+
return a.length - b.length;
|
|
241
|
+
const ua = a.toUpperCase();
|
|
242
|
+
const ub = b.toUpperCase();
|
|
243
|
+
return ua < ub ? -1 : ua > ub ? 1 : 0;
|
|
244
|
+
}
|
|
245
|
+
function writeHeader(dv, p) {
|
|
246
|
+
dv.setUint32(0, 0xe011cfd0, true); // OLE2 signature (lo/hi)
|
|
247
|
+
dv.setUint32(4, 0xe11ab1a1, true);
|
|
248
|
+
dv.setUint16(24, 0x003e, true); // minor version
|
|
249
|
+
dv.setUint16(26, 0x0003, true); // major version → v3 (512-byte sectors)
|
|
250
|
+
dv.setUint16(28, 0xfffe, true); // little-endian byte order mark
|
|
251
|
+
dv.setUint16(30, 9, true); // sector shift → 512
|
|
252
|
+
dv.setUint16(32, 6, true); // mini-sector shift → 64
|
|
253
|
+
dv.setUint32(44, p.fatSectors, true);
|
|
254
|
+
dv.setUint32(48, p.dirStart, true);
|
|
255
|
+
dv.setUint32(56, MINI_CUTOFF, true);
|
|
256
|
+
dv.setUint32(60, p.miniFatStart, true);
|
|
257
|
+
dv.setUint32(64, p.miniFatSectors, true);
|
|
258
|
+
dv.setUint32(68, p.difatSectors > 0 ? p.difatStart : ENDOFCHAIN, true);
|
|
259
|
+
dv.setUint32(72, p.difatSectors, true);
|
|
260
|
+
// Header DIFAT: the first 109 FAT-sector pointers. Contiguous from fatStart under the enforced bound.
|
|
261
|
+
for (let i = 0; i < DIFAT_HEADER_SLOTS; i++) {
|
|
262
|
+
dv.setUint32(76 + i * 4, i < p.fatSectors ? p.fatStart + i : FREESECT, true);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function writeDirEntry(dv, off, e) {
|
|
266
|
+
for (let i = 0; i < e.name.length; i++)
|
|
267
|
+
dv.setUint16(off + i * 2, e.name.charCodeAt(i), true);
|
|
268
|
+
dv.setUint16(off + 64, (e.name.length + 1) * 2, true); // name byte length incl. NUL terminator
|
|
269
|
+
dv.setUint8(off + 66, e.type);
|
|
270
|
+
dv.setUint8(off + 67, COLOR_BLACK);
|
|
271
|
+
dv.setUint32(off + 68, e.left, true);
|
|
272
|
+
dv.setUint32(off + 72, e.right, true);
|
|
273
|
+
dv.setUint32(off + 76, e.child, true);
|
|
274
|
+
dv.setUint32(off + 116, e.startSector, true);
|
|
275
|
+
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
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CfbNode } from './cfb-writer.ts';
|
|
2
|
+
export declare class CompoundFile {
|
|
3
|
+
#private;
|
|
4
|
+
constructor(buf: Uint8Array);
|
|
5
|
+
/** List every stream/storage name in the directory (order as stored). */
|
|
6
|
+
names(): string[];
|
|
7
|
+
/** Read a stream's raw bytes by exact entry name, or `undefined` if absent. */
|
|
8
|
+
readStream(name: string): Uint8Array | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Reconstruct the container's top-level children as the writer's node shape, recursing into every
|
|
11
|
+
* storage — so a caller can swap one stream and re-emit the whole hierarchy with {@link writeCompoundFile}.
|
|
12
|
+
* Walks the red-black sibling tree each storage navigates (not the linear directory scan), so any part
|
|
13
|
+
* a host reaches is carried through. Cycle- and bounds-guarded like every other chain walk here.
|
|
14
|
+
*/
|
|
15
|
+
tree(): CfbNode[];
|
|
16
|
+
}
|
package/dist/vba/cfb.js
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
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
|
+
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
|
+
const MAX_REGULAR_SECTOR = 0xfffffffa;
|
|
17
|
+
const NOSTREAM = 0xffffffff;
|
|
18
|
+
const TYPE_EMPTY = 0;
|
|
19
|
+
const TYPE_STORAGE = 1;
|
|
20
|
+
const TYPE_STREAM = 2;
|
|
21
|
+
const TYPE_ROOT = 5;
|
|
22
|
+
const CFB_SIGNATURE_LO = 0xe011cfd0;
|
|
23
|
+
const CFB_SIGNATURE_HI = 0xe11ab1a1;
|
|
24
|
+
const DIR_ENTRY_SIZE = 128;
|
|
25
|
+
export class CompoundFile {
|
|
26
|
+
#buf;
|
|
27
|
+
#view;
|
|
28
|
+
#sectorSize;
|
|
29
|
+
#miniSectorSize;
|
|
30
|
+
#miniCutoff;
|
|
31
|
+
#maxSector;
|
|
32
|
+
#fat;
|
|
33
|
+
#miniFat;
|
|
34
|
+
#dir;
|
|
35
|
+
#miniStream;
|
|
36
|
+
constructor(buf) {
|
|
37
|
+
this.#buf = buf;
|
|
38
|
+
this.#view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
39
|
+
if (buf.length < 512)
|
|
40
|
+
throw new VbaParseError('compound file shorter than its 512-byte header');
|
|
41
|
+
if (this.#u32(0) !== CFB_SIGNATURE_LO || this.#u32(4) !== CFB_SIGNATURE_HI) {
|
|
42
|
+
throw new VbaParseError('not a compound file (bad OLE2 signature)');
|
|
43
|
+
}
|
|
44
|
+
const sectorShift = this.#u16(30);
|
|
45
|
+
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
|
+
if (sectorShift !== 9 && sectorShift !== 12) {
|
|
48
|
+
throw new VbaParseError(`unsupported sector shift ${sectorShift}`);
|
|
49
|
+
}
|
|
50
|
+
if (miniSectorShift !== 6)
|
|
51
|
+
throw new VbaParseError(`unsupported mini-sector shift ${miniSectorShift}`);
|
|
52
|
+
this.#sectorSize = 1 << sectorShift;
|
|
53
|
+
this.#miniSectorSize = 1 << miniSectorShift;
|
|
54
|
+
this.#maxSector = Math.floor(buf.length / this.#sectorSize);
|
|
55
|
+
const numFatSectors = this.#u32(44);
|
|
56
|
+
const firstDirSector = this.#u32(48);
|
|
57
|
+
this.#miniCutoff = this.#u32(56);
|
|
58
|
+
const firstMiniFatSector = this.#u32(60);
|
|
59
|
+
const firstDifatSector = this.#u32(68);
|
|
60
|
+
const numDifatSectors = this.#u32(72);
|
|
61
|
+
const fatSectorIds = this.#readDifat(numFatSectors, firstDifatSector, numDifatSectors);
|
|
62
|
+
this.#fat = this.#readFat(fatSectorIds);
|
|
63
|
+
this.#miniFat = this.#readChainValues(firstMiniFatSector);
|
|
64
|
+
this.#dir = this.#readDirectory(firstDirSector);
|
|
65
|
+
const root = this.#dir.find((e) => e.type === TYPE_ROOT);
|
|
66
|
+
if (!root)
|
|
67
|
+
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
|
+
this.#miniStream = this.#readViaFat(root.startSector, root.size);
|
|
70
|
+
}
|
|
71
|
+
/** List every stream/storage name in the directory (order as stored). */
|
|
72
|
+
names() {
|
|
73
|
+
return this.#dir
|
|
74
|
+
.filter((e) => e.type === TYPE_STREAM || e.type === TYPE_STORAGE)
|
|
75
|
+
.map((e) => e.name);
|
|
76
|
+
}
|
|
77
|
+
/** Read a stream's raw bytes by exact entry name, or `undefined` if absent. */
|
|
78
|
+
readStream(name) {
|
|
79
|
+
const entry = this.#dir.find((e) => e.type === TYPE_STREAM && e.name === name);
|
|
80
|
+
if (!entry)
|
|
81
|
+
return undefined;
|
|
82
|
+
return this.#readEntryData(entry);
|
|
83
|
+
}
|
|
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
|
+
tree() {
|
|
91
|
+
const rootIdx = this.#dir.findIndex((e) => e.type === TYPE_ROOT);
|
|
92
|
+
if (rootIdx < 0)
|
|
93
|
+
throw new VbaParseError('compound file has no root storage entry');
|
|
94
|
+
const root = this.#dir[rootIdx];
|
|
95
|
+
return this.#buildSiblings(root.child, new Set([rootIdx]));
|
|
96
|
+
}
|
|
97
|
+
#buildSiblings(firstChild, seen) {
|
|
98
|
+
const nodes = [];
|
|
99
|
+
const walk = (idx) => {
|
|
100
|
+
if (idx >= MAX_REGULAR_SECTOR)
|
|
101
|
+
return; // NOSTREAM / terminal marker → no such sibling
|
|
102
|
+
if (idx >= this.#dir.length)
|
|
103
|
+
throw new VbaParseError('directory sibling index out of range');
|
|
104
|
+
if (seen.has(idx))
|
|
105
|
+
throw new VbaParseError('cycle in directory sibling tree');
|
|
106
|
+
seen.add(idx);
|
|
107
|
+
const e = this.#dir[idx];
|
|
108
|
+
if (e.type === TYPE_EMPTY)
|
|
109
|
+
throw new VbaParseError('directory tree links an empty entry');
|
|
110
|
+
walk(e.left);
|
|
111
|
+
if (e.type === TYPE_STORAGE) {
|
|
112
|
+
nodes.push({ name: e.name, children: this.#buildSiblings(e.child, seen) });
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
nodes.push({ name: e.name, data: this.#readEntryData(e) });
|
|
116
|
+
}
|
|
117
|
+
walk(e.right);
|
|
118
|
+
};
|
|
119
|
+
walk(firstChild);
|
|
120
|
+
return nodes;
|
|
121
|
+
}
|
|
122
|
+
#readEntryData(entry) {
|
|
123
|
+
if (entry.size >= this.#miniCutoff)
|
|
124
|
+
return this.#readViaFat(entry.startSector, entry.size);
|
|
125
|
+
return this.#readViaMiniFat(entry.startSector, entry.size);
|
|
126
|
+
}
|
|
127
|
+
#readDifat(numFatSectors, firstDifat, numDifat) {
|
|
128
|
+
const ids = [];
|
|
129
|
+
// The first 109 FAT-sector pointers live in the header; the rest chain through DIFAT sectors.
|
|
130
|
+
for (let i = 0; i < 109 && ids.length < numFatSectors; i++) {
|
|
131
|
+
const v = this.#u32(76 + i * 4);
|
|
132
|
+
if (v >= MAX_REGULAR_SECTOR)
|
|
133
|
+
break;
|
|
134
|
+
ids.push(v);
|
|
135
|
+
}
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
let sector = firstDifat;
|
|
138
|
+
const perSector = this.#sectorSize / 4 - 1;
|
|
139
|
+
for (let s = 0; s < numDifat && sector < MAX_REGULAR_SECTOR; s++) {
|
|
140
|
+
if (seen.has(sector))
|
|
141
|
+
throw new VbaParseError('cycle in DIFAT sector chain');
|
|
142
|
+
seen.add(sector);
|
|
143
|
+
const base = this.#dataSectorOffset(sector);
|
|
144
|
+
for (let i = 0; i < perSector; i++) {
|
|
145
|
+
const v = this.#u32(base + i * 4);
|
|
146
|
+
if (v < MAX_REGULAR_SECTOR)
|
|
147
|
+
ids.push(v);
|
|
148
|
+
}
|
|
149
|
+
sector = this.#u32(base + perSector * 4);
|
|
150
|
+
}
|
|
151
|
+
return ids;
|
|
152
|
+
}
|
|
153
|
+
#readFat(fatSectorIds) {
|
|
154
|
+
const fat = [];
|
|
155
|
+
const perSector = this.#sectorSize / 4;
|
|
156
|
+
for (const sid of fatSectorIds) {
|
|
157
|
+
const base = this.#dataSectorOffset(sid);
|
|
158
|
+
for (let i = 0; i < perSector; i++)
|
|
159
|
+
fat.push(this.#u32(base + i * 4));
|
|
160
|
+
}
|
|
161
|
+
return fat;
|
|
162
|
+
}
|
|
163
|
+
#readChainValues(firstSector) {
|
|
164
|
+
// Every uint32 in a sector chain (used for the mini-FAT), cycle-guarded.
|
|
165
|
+
const values = [];
|
|
166
|
+
const perSector = this.#sectorSize / 4;
|
|
167
|
+
const seen = new Set();
|
|
168
|
+
let sector = firstSector;
|
|
169
|
+
while (sector < MAX_REGULAR_SECTOR) {
|
|
170
|
+
if (seen.has(sector))
|
|
171
|
+
throw new VbaParseError('cycle in mini-FAT sector chain');
|
|
172
|
+
seen.add(sector);
|
|
173
|
+
const base = this.#dataSectorOffset(sector);
|
|
174
|
+
for (let i = 0; i < perSector; i++)
|
|
175
|
+
values.push(this.#u32(base + i * 4));
|
|
176
|
+
sector = this.#nextInFat(sector);
|
|
177
|
+
}
|
|
178
|
+
return values;
|
|
179
|
+
}
|
|
180
|
+
#readDirectory(firstDirSector) {
|
|
181
|
+
const raw = this.#readChainFull(firstDirSector);
|
|
182
|
+
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
|
+
for (let off = 0; off + DIR_ENTRY_SIZE <= raw.length; off += DIR_ENTRY_SIZE) {
|
|
186
|
+
const type = raw[off + 66];
|
|
187
|
+
if (type !== TYPE_EMPTY &&
|
|
188
|
+
type !== TYPE_STORAGE &&
|
|
189
|
+
type !== TYPE_STREAM &&
|
|
190
|
+
type !== TYPE_ROOT) {
|
|
191
|
+
throw new VbaParseError(`directory entry has invalid object type ${type}`);
|
|
192
|
+
}
|
|
193
|
+
const left = readU32(raw, off + 68);
|
|
194
|
+
const right = readU32(raw, off + 72);
|
|
195
|
+
const child = readU32(raw, off + 76);
|
|
196
|
+
if (type === TYPE_EMPTY) {
|
|
197
|
+
entries.push({
|
|
198
|
+
name: '',
|
|
199
|
+
type,
|
|
200
|
+
startSector: 0,
|
|
201
|
+
size: 0,
|
|
202
|
+
left: NOSTREAM,
|
|
203
|
+
right: NOSTREAM,
|
|
204
|
+
child: NOSTREAM,
|
|
205
|
+
});
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const nameLen = readU16(raw, off + 64);
|
|
209
|
+
if (nameLen > 64 || nameLen % 2 !== 0) {
|
|
210
|
+
throw new VbaParseError(`directory entry has invalid name length ${nameLen}`);
|
|
211
|
+
}
|
|
212
|
+
const name = decodeUtf16le(raw.subarray(off, off + Math.max(0, nameLen - 2)));
|
|
213
|
+
const startSector = readU32(raw, off + 116);
|
|
214
|
+
const size = readU32(raw, off + 120); // low 32 bits — ample for a VBA project
|
|
215
|
+
entries.push({ name, type, startSector, size, left, right, child });
|
|
216
|
+
}
|
|
217
|
+
return entries;
|
|
218
|
+
}
|
|
219
|
+
#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
|
+
const chunks = [];
|
|
223
|
+
const seen = new Set();
|
|
224
|
+
let sector = startSector;
|
|
225
|
+
while (sector < MAX_REGULAR_SECTOR) {
|
|
226
|
+
if (seen.has(sector))
|
|
227
|
+
throw new VbaParseError('cycle in FAT sector chain');
|
|
228
|
+
seen.add(sector);
|
|
229
|
+
const base = this.#dataSectorOffset(sector);
|
|
230
|
+
chunks.push(this.#buf.subarray(base, base + this.#sectorSize));
|
|
231
|
+
sector = this.#nextInFat(sector);
|
|
232
|
+
}
|
|
233
|
+
return concat(chunks);
|
|
234
|
+
}
|
|
235
|
+
#readViaFat(startSector, size) {
|
|
236
|
+
const chunks = [];
|
|
237
|
+
const seen = new Set();
|
|
238
|
+
let sector = startSector;
|
|
239
|
+
let remaining = size;
|
|
240
|
+
while (sector < MAX_REGULAR_SECTOR && remaining > 0) {
|
|
241
|
+
if (seen.has(sector))
|
|
242
|
+
throw new VbaParseError('cycle in stream FAT chain');
|
|
243
|
+
seen.add(sector);
|
|
244
|
+
const base = this.#dataSectorOffset(sector);
|
|
245
|
+
const take = Math.min(this.#sectorSize, remaining);
|
|
246
|
+
chunks.push(this.#buf.subarray(base, base + take));
|
|
247
|
+
remaining -= take;
|
|
248
|
+
sector = this.#nextInFat(sector);
|
|
249
|
+
}
|
|
250
|
+
return concat(chunks);
|
|
251
|
+
}
|
|
252
|
+
#readViaMiniFat(startSector, size) {
|
|
253
|
+
const chunks = [];
|
|
254
|
+
const seen = new Set();
|
|
255
|
+
let sector = startSector;
|
|
256
|
+
let remaining = size;
|
|
257
|
+
while (sector < MAX_REGULAR_SECTOR && remaining > 0) {
|
|
258
|
+
if (seen.has(sector))
|
|
259
|
+
throw new VbaParseError('cycle in stream mini-FAT chain');
|
|
260
|
+
seen.add(sector);
|
|
261
|
+
const base = sector * this.#miniSectorSize;
|
|
262
|
+
const take = Math.min(this.#miniSectorSize, remaining);
|
|
263
|
+
if (base + take > this.#miniStream.length) {
|
|
264
|
+
throw new VbaParseError('mini-stream sector runs past end of the mini stream');
|
|
265
|
+
}
|
|
266
|
+
chunks.push(this.#miniStream.subarray(base, base + take));
|
|
267
|
+
remaining -= take;
|
|
268
|
+
if (sector >= this.#miniFat.length)
|
|
269
|
+
throw new VbaParseError('mini-FAT index out of range');
|
|
270
|
+
sector = this.#miniFat[sector];
|
|
271
|
+
}
|
|
272
|
+
return concat(chunks);
|
|
273
|
+
}
|
|
274
|
+
#nextInFat(sector) {
|
|
275
|
+
if (sector >= this.#fat.length)
|
|
276
|
+
throw new VbaParseError('FAT index out of range');
|
|
277
|
+
return this.#fat[sector];
|
|
278
|
+
}
|
|
279
|
+
#dataSectorOffset(sector) {
|
|
280
|
+
if (sector >= this.#maxSector) {
|
|
281
|
+
throw new VbaParseError(`sector ${sector} is out of range (file has ${this.#maxSector})`);
|
|
282
|
+
}
|
|
283
|
+
const base = (sector + 1) * this.#sectorSize;
|
|
284
|
+
if (base + this.#sectorSize > this.#buf.length) {
|
|
285
|
+
throw new VbaParseError(`sector ${sector} runs past end of file`);
|
|
286
|
+
}
|
|
287
|
+
return base;
|
|
288
|
+
}
|
|
289
|
+
#u16(at) {
|
|
290
|
+
return this.#view.getUint16(at, true);
|
|
291
|
+
}
|
|
292
|
+
#u32(at) {
|
|
293
|
+
return this.#view.getUint32(at, true);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function readU16(buf, at) {
|
|
297
|
+
return buf[at] | (buf[at + 1] << 8);
|
|
298
|
+
}
|
|
299
|
+
function readU32(buf, at) {
|
|
300
|
+
return ((buf[at] |
|
|
301
|
+
(buf[at + 1] << 8) |
|
|
302
|
+
(buf[at + 2] << 16) |
|
|
303
|
+
(buf[at + 3] << 24)) >>>
|
|
304
|
+
0);
|
|
305
|
+
}
|
|
306
|
+
function decodeUtf16le(bytes) {
|
|
307
|
+
let s = '';
|
|
308
|
+
for (let i = 0; i + 1 < bytes.length; i += 2) {
|
|
309
|
+
s += String.fromCharCode(bytes[i] | (bytes[i + 1] << 8));
|
|
310
|
+
}
|
|
311
|
+
return s;
|
|
312
|
+
}
|
|
313
|
+
function concat(chunks) {
|
|
314
|
+
let total = 0;
|
|
315
|
+
for (const c of chunks)
|
|
316
|
+
total += c.length;
|
|
317
|
+
const out = new Uint8Array(total);
|
|
318
|
+
let off = 0;
|
|
319
|
+
for (const c of chunks) {
|
|
320
|
+
out.set(c, off);
|
|
321
|
+
off += c.length;
|
|
322
|
+
}
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type Decoder = InstanceType<typeof TextDecoder>;
|
|
2
|
+
/** Encodes text back to the project code page's MBCS bytes; throws on a character it cannot represent. */
|
|
3
|
+
export type Encoder = (text: string) => Uint8Array;
|
|
4
|
+
/**
|
|
5
|
+
* A `TextDecoder` for the given VBA project code page. Non-fatal (malformed bytes become U+FFFD rather
|
|
6
|
+
* than throwing) because recovered source is for reading, not re-encoding — a stray byte must not sink
|
|
7
|
+
* the whole extraction. An unknown code page falls back to windows-1252.
|
|
8
|
+
*/
|
|
9
|
+
export declare function decoderForCodePage(codePage: number): Decoder;
|
|
10
|
+
/**
|
|
11
|
+
* The write-side inverse of {@link decoderForCodePage}: an encoder for a project's code page, built by
|
|
12
|
+
* decoding every byte 0x00..0xFF through the matching decoder and inverting the map. This is exact for
|
|
13
|
+
* the single-byte code pages a VBA project realistically declares. Under a multi-byte code page (Shift
|
|
14
|
+
* JIS, GBK, …) the high bytes are lead bytes, not standalone characters, so only ASCII inverts and any
|
|
15
|
+
* non-ASCII character is rejected — fail-closed rather than emit a corrupt byte. A character the chosen
|
|
16
|
+
* code page cannot represent throws {@link VbaAuthorError}.
|
|
17
|
+
*/
|
|
18
|
+
export declare function encoderForCodePage(codePage: number): Encoder;
|