@patrickdk77/office-defuse 0.1.1 → 0.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.
Files changed (75) hide show
  1. package/README.md +197 -42
  2. package/dist/cfb-writer.d.ts +25 -0
  3. package/dist/cfb-writer.js +314 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +357 -0
  6. package/dist/contained.d.ts +46 -0
  7. package/dist/contained.js +152 -0
  8. package/dist/context.d.ts +107 -12
  9. package/dist/context.js +101 -24
  10. package/dist/engine.d.ts +12 -1
  11. package/dist/engine.js +276 -31
  12. package/dist/errors.d.ts +17 -0
  13. package/dist/errors.js +27 -1
  14. package/dist/fields.d.ts +20 -13
  15. package/dist/fields.js +65 -23
  16. package/dist/findings.d.ts +2 -1
  17. package/dist/findings.js +17 -1
  18. package/dist/flat.d.ts +33 -0
  19. package/dist/flat.js +486 -0
  20. package/dist/formats.d.ts +7 -0
  21. package/dist/formats.js +5 -1
  22. package/dist/formulas.d.ts +27 -0
  23. package/dist/formulas.js +157 -1
  24. package/dist/index.d.ts +3 -2
  25. package/dist/index.js +8 -1
  26. package/dist/index.mjs +1 -1
  27. package/dist/io.d.ts +62 -1
  28. package/dist/io.js +245 -1
  29. package/dist/links.d.ts +27 -0
  30. package/dist/links.js +106 -1
  31. package/dist/msg.d.ts +3 -2
  32. package/dist/msg.js +172 -18
  33. package/dist/objects.d.ts +8 -5
  34. package/dist/objects.js +90 -20
  35. package/dist/odf.d.ts +65 -1
  36. package/dist/odf.js +181 -64
  37. package/dist/offcrypto.d.ts +24 -1
  38. package/dist/offcrypto.js +308 -69
  39. package/dist/ole.js +17 -5
  40. package/dist/oleobj.d.ts +11 -0
  41. package/dist/oleobj.js +20 -3
  42. package/dist/ooxml.d.ts +81 -0
  43. package/dist/ooxml.js +342 -81
  44. package/dist/plan.d.ts +104 -0
  45. package/dist/plan.js +73 -0
  46. package/dist/plugins.d.ts +8 -0
  47. package/dist/plugins.js +60 -0
  48. package/dist/ppt.js +45 -5
  49. package/dist/rewrite-cfb.d.ts +14 -0
  50. package/dist/rewrite-cfb.js +52 -0
  51. package/dist/rewrite-rtf.d.ts +9 -0
  52. package/dist/rewrite-rtf.js +45 -0
  53. package/dist/rewrite-xml.d.ts +36 -0
  54. package/dist/rewrite-xml.js +341 -0
  55. package/dist/rewrite-zip.d.ts +8 -0
  56. package/dist/rewrite-zip.js +498 -0
  57. package/dist/rtf.js +214 -41
  58. package/dist/rtfcomp.d.ts +7 -0
  59. package/dist/rtfcomp.js +80 -0
  60. package/dist/score.js +1 -1
  61. package/dist/sniff.d.ts +5 -0
  62. package/dist/sniff.js +38 -0
  63. package/dist/types.d.ts +43 -6
  64. package/dist/types.js +14 -0
  65. package/dist/vsd.d.ts +13 -0
  66. package/dist/vsd.js +689 -0
  67. package/dist/word.js +51 -17
  68. package/dist/xls.js +38 -4
  69. package/dist/xml.d.ts +30 -0
  70. package/dist/xml.js +45 -23
  71. package/dist/zip-writer.d.ts +4 -0
  72. package/dist/zip-writer.js +20 -7
  73. package/dist/zip.d.ts +9 -2
  74. package/dist/zip.js +61 -4
  75. package/package.json +4 -1
package/dist/vsd.js ADDED
@@ -0,0 +1,689 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.visioRules = visioRules;
4
+ exports.inflate = inflate;
5
+ const context_1 = require("./context");
6
+ const errors_1 = require("./errors");
7
+ const oleobj_1 = require("./oleobj");
8
+ const types_1 = require("./types");
9
+ /*
10
+ * Visio 2000 (version 6) and Visio 2002 to 2010 (version 11) binary drawings. Microsoft has not published this format.
11
+ * Each fact below names the public source it comes from:
12
+ *
13
+ * - libvisio, src/lib/VSDParser.cpp: the version byte at 0x1A (VisioDocument.cpp), the trailer pointer at 0x24, the
14
+ * pointer and pointer list layout, the stream formats, the chunk header and trailer rules (getChunkHeader; v6 in
15
+ * VSD6Parser.cpp), the compression (VSDInternalStream.cpp), and the PolylineTo and NURBSTo cell blocks.
16
+ * - Apache POI, poi-scratchpad/src/main/resources/org/apache/poi/hdgf/chunks_parse_cmds.tbl: where the cell blocks
17
+ * start in each chunk type, for Visio 2000 (type 11 lines) and 2003 (type 21 lines), counted from the chunk header.
18
+ * - OLEToy, oletoy/vsdblock.py (re-lab): the cell block header, the formula tokens and their lengths, and the
19
+ * function numbers (fnames7ab). oletoy/vsdchunks.py gives the TextField and FrgnType block starts.
20
+ * - VBA-Docs, api/Visio.viscellindices.md: the cell numbers of the Events, Actions and User-defined Cells sections.
21
+ *
22
+ * Checked against the .vsd test files of libvisio and POI: every chunk of the types below ends its cell blocks exactly
23
+ * where the chunk ends, every formula block tokenizes to its exact length, and the cells that call these functions
24
+ * hold what Visio's own stencils put there, such as RUNADDONWARGS("AEC","/INITIALIZE=") in EventDrop.
25
+ */
26
+ const STREAM = 'VisioDocument';
27
+ const u16 = (b, at) => b[at] | (b[at + 1] << 8);
28
+ const u32 = (b, at) => (b[at] | (b[at + 1] << 8) | (b[at + 2] << 16) | (b[at + 3] << 24)) >>> 0;
29
+ /** The functions the .vsdx check looks for, by their number in a formula (OLEToy fnames7ab). */
30
+ const CALLS = new Map([
31
+ [0x4f, 'DOOLEVERB'],
32
+ [0x51, 'RUNADDON'],
33
+ [0x79, 'RUNADDONWARGS'],
34
+ [0x7b, 'OPENFILE'],
35
+ [0x9b, 'CALLTHIS'],
36
+ [0xc7, 'RUNMACRO'],
37
+ ]);
38
+ /** The arguments the fixed-argument form of each takes, for showing the formula. */
39
+ const ARITY = new Map([
40
+ [0x4f, 1],
41
+ [0x51, 1],
42
+ [0x79, 2],
43
+ [0x7b, 1],
44
+ ]);
45
+ /**
46
+ * Where the cell blocks start in each chunk type, counted from the end of the 19-byte chunk header. From POI's table,
47
+ * less 19, unless noted. POI lists no Visio 2003 start for some types, and the test files then show the Visio 2000
48
+ * start for them too (marked "files").
49
+ */
50
+ const V6 = new Map([
51
+ [0x84, 17],
52
+ [0x85, 35],
53
+ [0x86, 25],
54
+ [0x87, 71],
55
+ [0x88, 7],
56
+ [0x89, 3], // files
57
+ [0x8a, 20],
58
+ [0x8b, 20],
59
+ [0x8c, 29],
60
+ [0x8d, 38],
61
+ [0x8f, 56],
62
+ [0x90, 56],
63
+ [0x92, 131],
64
+ [0x93, 7],
65
+ [0x94, 35],
66
+ [0x95, 73],
67
+ [0x96, 29],
68
+ [0x97, 120],
69
+ [0x98, 62], // vsdchunks.py FrgnType
70
+ [0x99, 48],
71
+ [0x9b, 69],
72
+ [0x9c, 69],
73
+ [0x9d, 38],
74
+ [0x9e, 56],
75
+ [0x9f, 56],
76
+ [0xa0, 24],
77
+ [0xa1, 36], // vsdchunks.py TextField
78
+ [0xa2, 41],
79
+ [0xa3, 30],
80
+ [0xa4, 23],
81
+ [0xa5, 46],
82
+ [0xa6, 28],
83
+ [0xa7, 6],
84
+ [0xa8, 33],
85
+ [0xa9, 28],
86
+ [0xaa, 47],
87
+ [0xb4, 15],
88
+ [0xb5, 689],
89
+ [0xb6, 36],
90
+ [0xb7, 79],
91
+ [0xb9, 48],
92
+ [0xba, 60],
93
+ [0xbc, 43], // files
94
+ [0xbd, 45],
95
+ [0xbe, 25],
96
+ [0xbf, 38],
97
+ [0xc0, 144],
98
+ [0xc1, 29], // VSDParser.cpp readPolylineTo
99
+ [0xc3, 61],
100
+ [0xc4, 46],
101
+ [0xc5, 38],
102
+ [0xc8, 72],
103
+ ]);
104
+ const V11 = new Map([
105
+ [0x84, 17],
106
+ [0x85, 35],
107
+ [0x86, 61],
108
+ [0x87, 92],
109
+ [0x88, 7],
110
+ [0x89, 3],
111
+ [0x8a, 20],
112
+ [0x8b, 20],
113
+ [0x8c, 29], // files
114
+ [0x8f, 56], // files
115
+ [0x90, 56], // files
116
+ [0x92, 132], // files, one more than Visio 2000
117
+ [0x93, 7],
118
+ [0x94, 88],
119
+ [0x95, 123],
120
+ [0x96, 29], // files
121
+ [0x98, 62], // vsdchunks.py FrgnType, and files
122
+ [0x99, 48],
123
+ [0x9b, 69],
124
+ [0x9c, 69],
125
+ [0x9d, 38],
126
+ [0x9e, 56], // files
127
+ [0x9f, 60],
128
+ [0xa0, 24],
129
+ [0xa1, 54], // vsdchunks.py TextField, and files
130
+ [0xa2, 41],
131
+ [0xa3, 30],
132
+ [0xa4, 45],
133
+ [0xa5, 46], // files
134
+ [0xa6, 28], // files
135
+ [0xa7, 6],
136
+ [0xa8, 33],
137
+ [0xa9, 57],
138
+ [0xaa, 47],
139
+ [0xb4, 15],
140
+ [0xb6, 36], // files
141
+ [0xb7, 79],
142
+ [0xb9, 48],
143
+ [0xbc, 43], // files
144
+ [0xbd, 45],
145
+ [0xbe, 25],
146
+ [0xbf, 38],
147
+ [0xc0, 144],
148
+ [0xc1, 29], // VSDParser.cpp readPolylineTo
149
+ [0xc3, 61],
150
+ [0xc4, 46], // files
151
+ [0xc6, 61], // files
152
+ [0xc5, 38],
153
+ [0xc8, 72],
154
+ ]);
155
+ /** Names for the findings: the ShapeSheet section of each chunk type that VSDDocumentStructure.h and POI identify. */
156
+ const SECTIONS = new Map([
157
+ [0x84, 'Events'],
158
+ [0x85, 'Line'],
159
+ [0x86, 'Fill'],
160
+ [0x87, 'Text Block'],
161
+ [0x89, 'Geometry'],
162
+ [0x8a, 'MoveTo'],
163
+ [0x8b, 'LineTo'],
164
+ [0x8c, 'ArcTo'],
165
+ [0x8d, 'InfiniteLine'],
166
+ [0x8f, 'Ellipse'],
167
+ [0x90, 'EllipticalArcTo'],
168
+ [0x92, 'Page Properties'],
169
+ [0x93, 'Style Properties'],
170
+ [0x94, 'Character'],
171
+ [0x95, 'Paragraph'],
172
+ [0x98, 'Foreign Image Info'],
173
+ [0x99, 'Connection Points'],
174
+ [0x9b, 'Shape Transform'],
175
+ [0x9c, 'Text Transform'],
176
+ [0x9d, '1-D Endpoints'],
177
+ [0x9e, 'Scratch'],
178
+ [0x9f, 'Alignment'],
179
+ [0xa0, 'Protection'],
180
+ [0xa1, 'Text Fields'],
181
+ [0xa2, 'Controls'],
182
+ [0xa3, 'Help'],
183
+ [0xa4, 'Miscellaneous'],
184
+ [0xa5, 'SplineStart'],
185
+ [0xa6, 'SplineKnot'],
186
+ [0xa7, 'Layer Membership'],
187
+ [0xa8, 'Layers'],
188
+ [0xa9, 'Actions'],
189
+ [0xaa, 'Controls'],
190
+ [0xb4, 'User-defined Cells'],
191
+ [0xb6, 'Shape Data'],
192
+ [0xb7, 'Ruler & Grid'],
193
+ [0xb9, 'Connection Points'],
194
+ [0xba, 'Connection Points'],
195
+ [0xbc, 'Document Properties'],
196
+ [0xbd, 'Image Properties'],
197
+ [0xbe, 'Group Properties'],
198
+ [0xbf, 'Shape Layout'],
199
+ [0xc0, 'Page Layout'],
200
+ [0xc1, 'PolylineTo'],
201
+ [0xc3, 'NURBSTo'],
202
+ [0xc4, 'Hyperlinks'],
203
+ [0xc5, 'Reviewer'],
204
+ [0xc6, 'Annotation'],
205
+ [0xc7, 'Smart Tags'],
206
+ [0xc8, 'Print Properties'],
207
+ ]);
208
+ /** Cell names by section and cell number (VBA-Docs, VisCellIndices). */
209
+ const CELLS = new Map([
210
+ [
211
+ 0x84,
212
+ new Map([
213
+ [1, 'TheText'],
214
+ [2, 'EventDblClick'],
215
+ [3, 'EventXFMod'],
216
+ [4, 'EventDrop'],
217
+ [22, 'EventMultiDrop'],
218
+ ]),
219
+ ],
220
+ [
221
+ 0xa9,
222
+ new Map([
223
+ [0, 'Menu'],
224
+ [3, 'Action'],
225
+ [4, 'Checked'],
226
+ [5, 'Disabled'],
227
+ [6, 'ReadOnly'],
228
+ [7, 'Invisible'],
229
+ [8, 'BeginGroup'],
230
+ [14, 'TagName'],
231
+ [15, 'ButtonFace'],
232
+ [16, 'SortKey'],
233
+ ]),
234
+ ],
235
+ [
236
+ 0xb4,
237
+ new Map([
238
+ [0, 'Value'],
239
+ [1, 'Prompt'],
240
+ ]),
241
+ ],
242
+ ]);
243
+ /** Operators, by token (OLEToy sl_opnames), for showing a formula. */
244
+ const BINARY = new Map([
245
+ [0x03, '+'],
246
+ [0x04, '-'],
247
+ [0x05, '*'],
248
+ [0x06, '/'],
249
+ [0x07, '^'],
250
+ [0x08, '&'],
251
+ [0x09, '<'],
252
+ [0x0a, '<='],
253
+ [0x0b, '='],
254
+ [0x0c, '>='],
255
+ [0x0d, '>'],
256
+ [0x0e, '<>'],
257
+ ]);
258
+ const COLORS = 0x16;
259
+ /**
260
+ * Reads the ShapeSheet cells of a Visio 2000 to 2010 drawing for formulas that call RUNADDON, RUNADDONWARGS,
261
+ * CALLTHIS, RUNMACRO, OPENFILE or DOOLEVERB. Other versions of the stream go unread. A stream whose structure does
262
+ * not read stops the run as PACKAGE/UNREADABLE.
263
+ */
264
+ async function visioRules(ctx, cfb, stream) {
265
+ const doc = await cfb.bytes(stream);
266
+ const version = doc.length > 0x1a ? doc[0x1a] : 0;
267
+ if (version !== 6 && version !== 11)
268
+ return;
269
+ new VisioReader(ctx, doc, version).run();
270
+ }
271
+ class VisioReader {
272
+ ctx;
273
+ doc;
274
+ version;
275
+ blocks;
276
+ seen = new Set();
277
+ /** Bytes read or decompressed so far, against limits.totalDecompressedBytes. The same bytes read twice count twice. */
278
+ work = 0;
279
+ constructor(ctx, doc, version) {
280
+ this.ctx = ctx;
281
+ this.doc = doc;
282
+ this.version = version;
283
+ this.blocks = version === 11 ? V11 : V6;
284
+ }
285
+ fail(reason) {
286
+ throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unreadable, `The ${STREAM} stream does not read: ${reason}`, { stream: STREAM, reason });
287
+ }
288
+ run() {
289
+ if (this.doc.length < 0x24 + 18)
290
+ this.fail('its header is cut short');
291
+ // The trailer stream holds the pointers to every other stream, and is a pointer list itself.
292
+ const lists = [this.open(pointerAt(this.doc, 0x24))];
293
+ const streams = [];
294
+ // A work list, not recursion: a stream of pointers adds the streams it names.
295
+ while (lists.length > 0 || streams.length > 0) {
296
+ (0, errors_1.checkTime)(this.ctx.settings.deadline);
297
+ const list = lists.pop();
298
+ if (list) {
299
+ this.pointers(list.data, list.shift, streams);
300
+ continue;
301
+ }
302
+ const p = streams.pop();
303
+ if (!p)
304
+ break;
305
+ const kind = p.format >> 4;
306
+ const blob = kind === 0 || kind === 4 || kind === 5;
307
+ if (!blob && kind !== 8 && kind !== 0xc && kind !== 0xd)
308
+ continue;
309
+ // The same bytes read the same way once. A blob reads by its type, so the type is part of the key.
310
+ const key = `${p.offset}:${p.length}:${p.format}:${blob ? p.type : ''}`;
311
+ if (this.seen.has(key))
312
+ continue;
313
+ this.seen.add(key);
314
+ const s = this.open(p);
315
+ const seen = { calls: new Set(), chunks: [] };
316
+ if (blob) {
317
+ const start = Math.min(s.shift, s.data.length);
318
+ seen.chunks.push([start, s.data.length, p.type]);
319
+ this.section(p.type, s.data, start, s.data.length, seen);
320
+ if (kind === 5 && p.type !== COLORS)
321
+ lists.push(s);
322
+ }
323
+ else
324
+ this.chunks(s.data, seen);
325
+ this.sweep(s.data, seen);
326
+ }
327
+ }
328
+ /**
329
+ * A stream's bytes, decompressed when its format says so. A compressed blob or pointer list starts 4 bytes in, and a
330
+ * compressed chunk list at its first byte (VSDParser.cpp handleStream).
331
+ */
332
+ open(p) {
333
+ if (p.offset + p.length > this.doc.length)
334
+ this.fail(`a pointer names bytes ${p.offset} to ${p.offset + p.length}, past the end of the stream`);
335
+ const raw = this.doc.subarray(p.offset, p.offset + p.length);
336
+ if ((p.format & 2) === 0) {
337
+ this.count(raw.length);
338
+ return { data: raw, shift: 0 };
339
+ }
340
+ const data = inflate(raw, this.ctx.settings.decompressedBytes);
341
+ this.count(raw.length + data.length);
342
+ return { data, shift: 4 };
343
+ }
344
+ count(n) {
345
+ this.work += n;
346
+ const total = this.ctx.settings.totalDecompressedBytes;
347
+ if (this.work > total)
348
+ throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.TotalDecompressedSize, `The ${STREAM} stream decompresses past ${total} bytes`, { limit: total });
349
+ }
350
+ /** A pointer list: an offset to its header, which counts the 18-byte pointers that follow it. */
351
+ pointers(data, shift, out) {
352
+ if (shift + 4 > data.length)
353
+ this.fail('a pointer list is cut short');
354
+ const at = u32(data, shift) + shift - 4;
355
+ if (at < 0 || at + 12 > data.length)
356
+ this.fail('a pointer list header lies outside its stream');
357
+ const n = u32(data, at + 4);
358
+ if (n > (data.length - at - 12) / 18)
359
+ this.fail(`a pointer list counts ${n} pointers, more than its stream holds`);
360
+ for (let i = 0; i < n; i++) {
361
+ if (i % 65536 === 0)
362
+ (0, errors_1.checkTime)(this.ctx.settings.deadline);
363
+ const p = pointerAt(data, at + 12 + i * 18);
364
+ if (p.type !== 0)
365
+ out.push(p);
366
+ }
367
+ }
368
+ /** A list of chunks, each a 19-byte header, its data, and a trailer whose length its type and flags give. */
369
+ chunks(data, seen) {
370
+ let pos = 0;
371
+ while (pos < data.length) {
372
+ (0, errors_1.checkTime)(this.ctx.settings.deadline);
373
+ // Readers skip the zeros between chunks.
374
+ while (pos < data.length && data[pos] === 0)
375
+ pos++;
376
+ if (pos >= data.length)
377
+ return;
378
+ if (pos + 19 > data.length)
379
+ this.fail('a chunk header is cut short');
380
+ const type = u32(data, pos);
381
+ const list = u32(data, pos + 8);
382
+ const length = u32(data, pos + 12);
383
+ const level = u16(data, pos + 16);
384
+ const flags = data[pos + 18];
385
+ const start = pos + 19;
386
+ if (length > data.length - start)
387
+ this.fail(`a chunk of type 0x${hex(type)} runs past its stream`);
388
+ seen.chunks.push([start, start + length, type]);
389
+ this.section(type, data, start, start + length, seen);
390
+ pos = start + length + (this.version === 11 ? trailer11(type, list, level, flags) : trailer6(type, list));
391
+ }
392
+ }
393
+ /**
394
+ * One chunk's data, bytes `from` to `to` of its stream. In a chunk type with ShapeSheet cells, the cell blocks follow
395
+ * the fixed fields and run to the end of the data, or to four or more zeros.
396
+ */
397
+ section(type, data, from, to, seen) {
398
+ const start = this.blocks.get(type);
399
+ if (start === undefined)
400
+ return;
401
+ if (to - from < start)
402
+ this.fail(`a chunk of type 0x${hex(type)} is shorter than its fixed fields`);
403
+ let pos = from + start;
404
+ while (pos < to) {
405
+ if (to - pos >= 4 && u32(data, pos) === 0) {
406
+ if (data.subarray(pos, to).some(x => x !== 0))
407
+ this.fail(`a chunk of type 0x${hex(type)} holds data past the end of its cell blocks`);
408
+ return;
409
+ }
410
+ if (to - pos < 6)
411
+ this.fail(`a cell block in a chunk of type 0x${hex(type)} is cut short`);
412
+ const length = u32(data, pos);
413
+ if (length < 6 || length > to - pos)
414
+ this.fail(`a cell block in a chunk of type 0x${hex(type)} runs past its chunk`);
415
+ const kind = data[pos + 4];
416
+ // 2 is a formula. The others hold dependencies and flags.
417
+ if (kind === 2)
418
+ this.formula(type, data[pos + 5], data, pos + 6, pos + length, seen);
419
+ else if (kind < 1 || kind > 5)
420
+ this.fail(`a cell block in a chunk of type 0x${hex(type)} is of the unknown kind ${kind}`);
421
+ pos += length;
422
+ }
423
+ }
424
+ formula(type, cell, data, from, to, seen) {
425
+ const tokens = this.tokens(type, data.subarray(from, to));
426
+ const calls = tokens.filter(t => t.kind === 'call' && t.fn !== undefined && CALLS.has(t.fn));
427
+ if (calls.length === 0)
428
+ return;
429
+ for (const t of calls)
430
+ seen.calls.add(from + (t.at ?? 0));
431
+ const section = SECTIONS.get(type) ?? `chunk type 0x${hex(type)}`;
432
+ const name = CELLS.get(type)?.get(cell) ?? `cell ${cell}`;
433
+ const formula = (0, context_1.clip)(show(tokens) ?? callNames(calls));
434
+ if (type === 0x84 && name.startsWith('Event'))
435
+ this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Event, STREAM, { event: name, formula });
436
+ else
437
+ this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, STREAM, { section, cell: name, formula });
438
+ }
439
+ /** A formula's tokens, in the order Visio evaluates them (OLEToy vsdblock.py get_slice). */
440
+ tokens(type, b) {
441
+ const out = [];
442
+ const bad = (what) => this.fail(`a formula in a chunk of type 0x${hex(type)} ${what}`);
443
+ let i = 0;
444
+ const need = (n) => {
445
+ if (i + n > b.length)
446
+ bad('is cut short');
447
+ };
448
+ while (i < b.length) {
449
+ const c = b[i];
450
+ if (c === 0xfe) {
451
+ out.push({ kind: 'end' });
452
+ i += 1;
453
+ }
454
+ else if (c < 0x20 || c === 0xe4) {
455
+ // An operator. 0xE4 is a pair of parentheses.
456
+ out.push({ kind: 'operator', value: c });
457
+ i += 1;
458
+ }
459
+ else if (c < 0x60) {
460
+ // A number, in the unit the token names.
461
+ need(9);
462
+ out.push({ kind: 'number', value: Buffer.from(b.buffer, b.byteOffset + i + 1, 8).readDoubleLE(0) });
463
+ i += 9;
464
+ }
465
+ else if (c === 0x60) {
466
+ // A string: a count of characters, the characters, and a terminating NUL. Visio 2000 writes 8-bit characters.
467
+ need(2);
468
+ const n = b[i + 1];
469
+ const width = this.version === 11 ? 2 : 1;
470
+ need(2 + (n + 1) * width);
471
+ const s = b.subarray(i + 2, i + 2 + n * width);
472
+ out.push({ kind: 'string', value: width === 2 ? (0, oleobj_1.utf16)(s) : (0, oleobj_1.latin1)(s) });
473
+ i += 2 + (n + 1) * width;
474
+ }
475
+ else if (c === 0x69 && this.version === 11) {
476
+ // A string of more than 255 characters, counted in 4 bytes. Seen in a test file.
477
+ need(5);
478
+ const n = u32(b, i + 1);
479
+ if (n > (b.length - i - 7) / 2)
480
+ bad('holds a string longer than itself');
481
+ out.push({ kind: 'string', value: (0, oleobj_1.utf16)(b.subarray(i + 5, i + 5 + n * 2)) });
482
+ i += 5 + (n + 1) * 2;
483
+ }
484
+ else if (c === 0x61 || c === 0x62) {
485
+ // An integer of 1 or 2 bytes.
486
+ const n = c === 0x61 ? 1 : 2;
487
+ need(1 + n);
488
+ out.push({ kind: 'number', value: n === 1 ? b[i + 1] : u16(b, i + 1) });
489
+ i += 1 + n;
490
+ }
491
+ else if (REFERENCES.has(c)) {
492
+ // A reference to a cell, of a length by its kind.
493
+ const n = REFERENCES.get(c) ?? 0;
494
+ need(1 + n);
495
+ out.push({ kind: 'other' });
496
+ i += 1 + n;
497
+ }
498
+ else if (c === 0x7a || c === 0x80) {
499
+ // A function of fixed arguments: its number in 2 bytes, then 2 bytes of flags.
500
+ need(5);
501
+ out.push({ kind: 'call', fn: u16(b, i + 1), at: i });
502
+ i += 5;
503
+ }
504
+ else if (c === 0x7b || c === 0x81) {
505
+ // A function of variable arguments: the argument count in 4 bytes, its number in 2, and 2 bytes of flags.
506
+ need(9);
507
+ out.push({ kind: 'call', fn: u16(b, i + 5), args: u32(b, i + 1), at: i });
508
+ i += 9;
509
+ }
510
+ else if (c === 0x8a) {
511
+ // NURBS data (VSDParser.cpp readNURBSTo): 16 bytes, then 32 for each point. OLEToy counts 24 bytes more, which
512
+ // the next token reads instead, so no token goes unread.
513
+ need(17);
514
+ const points = u32(b, i + 13);
515
+ if (points > (b.length - i - 17) / 32)
516
+ bad('holds NURBS data longer than itself');
517
+ out.push({ kind: 'other' });
518
+ i += 17 + points * 32;
519
+ }
520
+ else if (c >= 0xa0 && c <= 0xa3) {
521
+ // AND, OR, IF and a branch, with the length of what they span in 4 bytes. What they span is read as tokens.
522
+ need(5);
523
+ out.push({ kind: 'other' });
524
+ i += 5;
525
+ }
526
+ else if ((c >= 0x90 && c <= 0x93) || c === 0xf0) {
527
+ // The marks that end an AND, OR or IF and their branches, and the filler that replaces a deleted reference.
528
+ out.push({ kind: 'other' });
529
+ i += 1;
530
+ }
531
+ else
532
+ bad(`holds the unknown token 0x${hex(c)}`);
533
+ }
534
+ return out;
535
+ }
536
+ /**
537
+ * Every byte of a stream searched for the token of a call to the functions above, with the flags Visio writes after
538
+ * one, outside the formulas read. That covers chunk types whose cells this does not know where to find, and bytes a
539
+ * chunk walk that went wrong would skip. In the test files, every such token is one a formula read found.
540
+ */
541
+ sweep(b, seen) {
542
+ const found = new Map();
543
+ let chunk = 0;
544
+ for (let i = 0; i + 5 <= b.length; i++) {
545
+ const c = b[i];
546
+ let fn;
547
+ if ((c === 0x7a || c === 0x80) && b[i + 3] === 0 && b[i + 4] === 0x40)
548
+ fn = u16(b, i + 1);
549
+ else if ((c === 0x7b || c === 0x81) && i + 9 <= b.length && b[i + 7] === 0 && b[i + 8] === 0x40)
550
+ fn = u16(b, i + 5);
551
+ if (fn === undefined || !CALLS.has(fn) || seen.calls.has(i))
552
+ continue;
553
+ // The chunks are in order, so the one that holds this byte is at or after the last one found.
554
+ while (chunk < seen.chunks.length && seen.chunks[chunk][1] <= i)
555
+ chunk++;
556
+ const span = seen.chunks[chunk];
557
+ const where = span && span[0] <= i ? (SECTIONS.get(span[2]) ?? `chunk type 0x${hex(span[2])}`) : 'between chunks';
558
+ const list = found.get(where) ?? [];
559
+ list.push({ kind: 'call', fn });
560
+ found.set(where, list);
561
+ }
562
+ for (const [section, calls] of found)
563
+ this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, STREAM, { section, cell: 'unknown', formula: (0, context_1.clip)(callNames(calls)) });
564
+ }
565
+ }
566
+ /** The lengths of the reference tokens after their first byte (OLEToy sl_names70 to sl_names76, Visio 2000 and later). */
567
+ const REFERENCES = new Map([
568
+ [0x70, 11],
569
+ [0x72, 7],
570
+ [0x74, 8],
571
+ [0x75, 4],
572
+ [0x76, 17],
573
+ ]);
574
+ const hex = (n) => n.toString(16).padStart(2, '0');
575
+ function pointerAt(b, at) {
576
+ return { type: u32(b, at), offset: u32(b, at + 8), length: u32(b, at + 12), format: u16(b, at + 16) };
577
+ }
578
+ /** Chunk trailers of Visio 2002 to 2010 (VSDParser.cpp getChunkHeader). */
579
+ function trailer11(type, list, level, flags) {
580
+ if (type === 0x1f || type === 0xc9 || type === 0x2d || type === 0xd1)
581
+ return 0;
582
+ let n = 0;
583
+ if (list !== 0 || [0x71, 0x70, 0x6b, 0x6a, 0x69, 0x66, 0x65, 0x2c].includes(type))
584
+ n += 8;
585
+ if (list !== 0 || (level === 2 && flags === 0x55) || (level === 2 && flags === 0x54 && type === 0xaa) || (level === 3 && flags !== 0x50 && flags !== 0x54))
586
+ n += 4;
587
+ if ([0x64, 0x65, 0x66, 0x69, 0x6a, 0x6b, 0x6f, 0x71, 0x92, 0xa9, 0xb4, 0xb6, 0xb9, 0xc7].includes(type) && n !== 12 && n !== 4)
588
+ n += 4;
589
+ return n;
590
+ }
591
+ /** Chunk trailers of Visio 2000 (VSD6Parser.cpp getChunkHeader). */
592
+ function trailer6(type, list) {
593
+ if (type === 0x1f || type === 0xc9)
594
+ return 0;
595
+ return list !== 0 || [0x76, 0x73, 0x72, 0x71, 0x70, 0x6f, 0x6e, 0x6d, 0x6c, 0x6b, 0x6a, 0x69, 0x68, 0x67, 0x66, 0x65, 0x64, 0x2c, 0x0d].includes(type) ? 8 : 0;
596
+ }
597
+ /**
598
+ * The LZ77 compression of Visio streams (VSDInternalStream.cpp): a flag byte for each eight items, a set bit for a
599
+ * literal byte and a clear one for a 2-byte back reference into a 4,096-byte window. Output past `limit` stops the run.
600
+ */
601
+ function inflate(src, limit) {
602
+ if (src.length < 2)
603
+ return new Uint8Array(0);
604
+ // An item of 2 bytes yields at most 18, so the output is at most 9 times the input.
605
+ const out = new Uint8Array(Math.min(limit + 18, src.length * 9));
606
+ const window = new Uint8Array(4096);
607
+ const over = () => {
608
+ throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.DecompressedSize, `A stream in ${STREAM} decompresses past ${limit} bytes`, { name: STREAM, limit });
609
+ };
610
+ let o = 0;
611
+ let pos = 0;
612
+ let i = 0;
613
+ while (i < src.length) {
614
+ const flag = src[i++];
615
+ if (i >= src.length)
616
+ break;
617
+ for (let bit = 0; bit < 8 && i < src.length; bit++) {
618
+ if (flag & (1 << bit)) {
619
+ if (o >= limit)
620
+ over();
621
+ window[pos & 4095] = src[i];
622
+ out[o++] = src[i++];
623
+ pos++;
624
+ }
625
+ else {
626
+ if (i + 2 > src.length)
627
+ return out.subarray(0, o);
628
+ const a = src[i++];
629
+ const b = src[i++];
630
+ const length = (b & 15) + 3;
631
+ let from = ((b & 0xf0) << 4) | a;
632
+ from = from > 4078 ? from - 4078 : from + 18;
633
+ if (o + length > limit)
634
+ over();
635
+ for (let j = 0; j < length; j++) {
636
+ const x = window[(from + j) & 4095];
637
+ window[(pos + j) & 4095] = x;
638
+ out[o++] = x;
639
+ }
640
+ pos += length;
641
+ }
642
+ }
643
+ }
644
+ return out.subarray(0, o);
645
+ }
646
+ const callNames = (calls) => calls.map(t => `${CALLS.get(t.fn ?? -1) ?? ''}(...)`).join(', ');
647
+ /**
648
+ * A formula written out, when it holds only constants, operators and the calls above. Visio stores formulas in
649
+ * reverse Polish order. Undefined when anything else is in it, such as a reference or another function.
650
+ */
651
+ function show(tokens) {
652
+ const stack = [];
653
+ for (const t of tokens) {
654
+ if (t.kind === 'end')
655
+ break;
656
+ if (t.kind === 'number')
657
+ stack.push(String(t.value));
658
+ else if (t.kind === 'string')
659
+ stack.push(`"${String(t.value).replace(/"/g, '""')}"`);
660
+ else if (t.kind === 'operator') {
661
+ const op = BINARY.get(Number(t.value));
662
+ if (op !== undefined) {
663
+ const right = stack.pop();
664
+ const left = stack.pop();
665
+ if (left === undefined || right === undefined)
666
+ return undefined;
667
+ stack.push(`${left}${op}${right}`);
668
+ }
669
+ else if (t.value === 0x13 || t.value === 0x14 || t.value === 0xe4) {
670
+ const x = stack.pop();
671
+ if (x === undefined)
672
+ return undefined;
673
+ stack.push(t.value === 0x13 ? `-${x}` : t.value === 0x14 ? `${x}%` : `(${x})`);
674
+ }
675
+ else
676
+ return undefined;
677
+ }
678
+ else if (t.kind === 'call') {
679
+ const name = CALLS.get(t.fn ?? -1);
680
+ const n = t.args ?? ARITY.get(t.fn ?? -1);
681
+ if (name === undefined || n === undefined || n > stack.length)
682
+ return undefined;
683
+ stack.push(`${name}(${stack.splice(stack.length - n, n).join(',')})`);
684
+ }
685
+ else
686
+ return undefined;
687
+ }
688
+ return stack.length === 1 ? stack[0] : undefined;
689
+ }