@patrickdk77/office-defuse 0.1.0 → 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 (88) hide show
  1. package/README.md +278 -36
  2. package/dist/biff.d.ts +62 -0
  3. package/dist/biff.js +95 -0
  4. package/dist/cfb-writer.d.ts +25 -0
  5. package/dist/cfb-writer.js +314 -0
  6. package/dist/cfb.d.ts +58 -0
  7. package/dist/cfb.js +456 -0
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.js +357 -0
  10. package/dist/contained.d.ts +46 -0
  11. package/dist/contained.js +152 -0
  12. package/dist/context.d.ts +140 -17
  13. package/dist/context.js +135 -37
  14. package/dist/crc32.d.ts +8 -2
  15. package/dist/crc32.js +44 -4
  16. package/dist/engine.d.ts +12 -1
  17. package/dist/engine.js +336 -42
  18. package/dist/errors.d.ts +17 -0
  19. package/dist/errors.js +27 -1
  20. package/dist/fields.d.ts +36 -0
  21. package/dist/fields.js +139 -0
  22. package/dist/findings.d.ts +2 -1
  23. package/dist/findings.js +44 -9
  24. package/dist/flat.d.ts +33 -0
  25. package/dist/flat.js +486 -0
  26. package/dist/formats.d.ts +7 -0
  27. package/dist/formats.js +5 -1
  28. package/dist/formulas.d.ts +27 -0
  29. package/dist/formulas.js +157 -1
  30. package/dist/index.d.ts +3 -2
  31. package/dist/index.js +8 -1
  32. package/dist/index.mjs +1 -1
  33. package/dist/io.d.ts +62 -1
  34. package/dist/io.js +245 -1
  35. package/dist/links.d.ts +27 -0
  36. package/dist/links.js +106 -1
  37. package/dist/msg.d.ts +8 -0
  38. package/dist/msg.js +251 -0
  39. package/dist/objects.d.ts +45 -0
  40. package/dist/objects.js +204 -0
  41. package/dist/odf.d.ts +66 -2
  42. package/dist/odf.js +181 -64
  43. package/dist/offcrypto.d.ts +54 -0
  44. package/dist/offcrypto.js +647 -0
  45. package/dist/ole.d.ts +12 -0
  46. package/dist/ole.js +139 -0
  47. package/dist/oleobj.d.ts +85 -0
  48. package/dist/oleobj.js +215 -0
  49. package/dist/ooxml.d.ts +83 -2
  50. package/dist/ooxml.js +358 -145
  51. package/dist/package.d.ts +3 -0
  52. package/dist/package.js +21 -0
  53. package/dist/plan.d.ts +104 -0
  54. package/dist/plan.js +73 -0
  55. package/dist/plugins.d.ts +8 -0
  56. package/dist/plugins.js +60 -0
  57. package/dist/ppt.d.ts +8 -0
  58. package/dist/ppt.js +281 -0
  59. package/dist/rewrite-cfb.d.ts +14 -0
  60. package/dist/rewrite-cfb.js +52 -0
  61. package/dist/rewrite-rtf.d.ts +9 -0
  62. package/dist/rewrite-rtf.js +45 -0
  63. package/dist/rewrite-xml.d.ts +36 -0
  64. package/dist/rewrite-xml.js +341 -0
  65. package/dist/rewrite-zip.d.ts +8 -0
  66. package/dist/rewrite-zip.js +498 -0
  67. package/dist/rtf.d.ts +8 -0
  68. package/dist/rtf.js +712 -0
  69. package/dist/rtfcomp.d.ts +7 -0
  70. package/dist/rtfcomp.js +80 -0
  71. package/dist/score.js +1 -1
  72. package/dist/sniff.d.ts +5 -0
  73. package/dist/sniff.js +38 -0
  74. package/dist/types.d.ts +66 -2
  75. package/dist/types.js +33 -0
  76. package/dist/vsd.d.ts +13 -0
  77. package/dist/vsd.js +689 -0
  78. package/dist/word.d.ts +9 -0
  79. package/dist/word.js +194 -0
  80. package/dist/xls.d.ts +27 -0
  81. package/dist/xls.js +208 -0
  82. package/dist/xml.d.ts +30 -0
  83. package/dist/xml.js +45 -23
  84. package/dist/zip-writer.d.ts +4 -0
  85. package/dist/zip-writer.js +20 -7
  86. package/dist/zip.d.ts +9 -2
  87. package/dist/zip.js +61 -4
  88. package/package.json +6 -3
package/dist/rtf.js ADDED
@@ -0,0 +1,712 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.inspectRtf = inspectRtf;
4
+ const cfb_1 = require("./cfb");
5
+ const contained_1 = require("./contained");
6
+ const context_1 = require("./context");
7
+ const errors_1 = require("./errors");
8
+ const fields_1 = require("./fields");
9
+ const io_1 = require("./io");
10
+ const links_1 = require("./links");
11
+ const objects_1 = require("./objects");
12
+ const oleobj_1 = require("./oleobj");
13
+ const types_1 = require("./types");
14
+ /**
15
+ * The destinations of the RTF 1.9.1 specification, which rtfobj lists too. Word sends the text of each to its own
16
+ * place, so none of it is object data or a field code. \rtf is not one, though it opens the document.
17
+ */
18
+ const DESTINATIONS = new Set(('aftncn aftnsep aftnsepc annotation atnauthor atndate atnid atnparent atnref atrfend atrfstart author background bkmkend bkmkstart ' +
19
+ 'blipuid buptim category colorschememapping colortbl comment company creatim datafield datastore defchp defpap do doccomm docvar ' +
20
+ 'dptxbxtext ebcend ebcstart factoidname falt fchars ffdeftext ffentrymcr ffexitmcr ffformat ffhelptext ffl ffname ffstattext field ' +
21
+ 'file filetbl fldinst fldrslt fldtype fontemb fonttbl footer footerf footerl footerr footnote formfield ftncn ftnsep ftnsepc g ' +
22
+ 'generator gridtbl header headerf headerl headerr hl hlfr hlinkbase hlloc hlsrc hsv info keywords latentstyles lchars levelnumbers ' +
23
+ 'leveltext lfolevel linkval list listlevel listname listoverride listoverridetable listpicture liststylename listtable listtext ' +
24
+ 'lsdlockedexcept macc maccPr mailmerge malnScr manager margPr mbar mbarPr mbaseJc mbegChr mborderBox mborderBoxPr mbox mboxPr mchr ' +
25
+ 'mcount mctrlPr md mdeg mdegHide mden mdiff mdPr me mendChr meqArr meqArrPr mf mfName mfPr mfunc mfuncPr mgroupChr mgroupChrPr mgrow ' +
26
+ 'mhideBot mhideLeft mhideRight mhideTop mlim mlimLoc mlimLow mlimLowPr mlimUpp mlimUppPr mm mmaddfieldname mmathPict mmaxDist mmc ' +
27
+ 'mmcJc mmconnectstr mmconnectstrdata mmcPr mmcs mmdatasource mmheadersource mmmailsubject mmodso mmodsofilter mmodsofldmpdata ' +
28
+ 'mmodsomappedname mmodsoname mmodsorecipdata mmodsosort mmodsosrc mmodsotable mmodsoudl mmodsoudldata mmodsouniquetag mmPr mmquery ' +
29
+ 'mmr mnary mnaryPr mnoBreak mnum mobjDist moMath moMathPara moMathParaPr mopEmu mphant mphantPr mplcHide mpos mr mrad mradPr mrPr ' +
30
+ 'msepChr mshow mshp msPre msPrePr msSub msSubPr msSubSup msSubSupPr msSup msSupPr mstrikeBLTR mstrikeH mstrikeTLBR mstrikeV msub ' +
31
+ 'msubHide msup msupHide mtransp mtype mvertJc mvfmf mvfml mvtof mvtol mzeroAsc mzeroDesc mzeroWid nesttableprops nonesttables ' +
32
+ 'objalias objclass objdata object objname objsect oldcprops oldpprops oldsprops oldtprops oleclsid operator panose password ' +
33
+ 'passwordhash pgp pgptbl picprop pict pn pnseclvl pntext pntxta pntxtb printim propname protend protstart protusertbl result revtbl ' +
34
+ 'revtim rxe shp shpgrp shpinst shppict shprslt shptxt sn sp staticval stylesheet subject sv svb tc template themedata title txe ud ' +
35
+ 'upr userprops wgrffmtfilter windowcaption writereservation writereservhash xe xform xmlattrname xmlattrvalue xmlclose xmlname ' +
36
+ 'xmlnstbl xmlopen margSz pnaiu pnaiud').split(' '));
37
+ /** Destinations whose content holds no field, object or link, so their groups are skipped whole: tables, styles, pictures, metadata. */
38
+ const INERT = new Set(('fonttbl colortbl stylesheet info pict themedata colorschememapping datastore latentstyles listtable listoverridetable revtbl ' +
39
+ 'generator xmlnstbl filetbl pgptbl protusertbl blipuid datafield fontemb panose falt password passwordhash wgrffmtfilter').split(' '));
40
+ /** Object kinds, from the control word that sets them. */
41
+ const OBJECT_KINDS = new Set(['objemb', 'objlink', 'objautlink', 'objsub', 'objpub', 'objicemb', 'objhtml', 'objocx']);
42
+ /** Word reads at most this many letters of a control word's name and digits of its parameter. */
43
+ const MAX_WORD = 250;
44
+ const CHUNK = 262144;
45
+ /** Collected text other than an object's data is short: a class name, a template path, a link. */
46
+ const MAX_TEXT = 4096;
47
+ /** The pictures RTF holds as whole files of their own, by the control word that names their type. */
48
+ const PICTURE_TYPES = { pngblip: 'image/png', jpegblip: 'image/jpeg', emfblip: 'image/emf' };
49
+ /** An object's data as Word decodes its hex digits, with the quirks rtfobj documents. */
50
+ class HexData {
51
+ limit;
52
+ bytes;
53
+ length = 0;
54
+ /** A hex digit read and not yet paired, or -1. */
55
+ nibble = -1;
56
+ offset;
57
+ constructor(offset, limit) {
58
+ this.limit = limit;
59
+ this.offset = offset;
60
+ this.bytes = new Uint8Array(1024);
61
+ }
62
+ digit(v) {
63
+ if (this.nibble < 0) {
64
+ this.nibble = v;
65
+ return;
66
+ }
67
+ this.push((this.nibble << 4) | v);
68
+ this.nibble = -1;
69
+ }
70
+ /** Word drops a hex digit left unpaired before a \'hh, and so does rtfobj. */
71
+ quote() {
72
+ this.nibble = -1;
73
+ }
74
+ /** \bin data in hex goes in as its bytes, two digits each, so an unpaired digit stays unpaired. */
75
+ binary(b) {
76
+ for (const x of b) {
77
+ this.digit(x >> 4);
78
+ this.digit(x & 15);
79
+ }
80
+ }
81
+ push(b) {
82
+ if (this.length >= this.limit)
83
+ throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.DecompressedSize, `The data of the object at offset ${this.offset} decodes past ${this.limit} bytes`, { limit: this.limit });
84
+ if (this.length === this.bytes.length) {
85
+ const grown = new Uint8Array(Math.min(this.limit, this.bytes.length * 2));
86
+ grown.set(this.bytes);
87
+ this.bytes = grown;
88
+ }
89
+ this.bytes[this.length++] = b;
90
+ }
91
+ /** The decoded bytes. A last digit left unpaired is dropped, as Word drops it. */
92
+ result() {
93
+ return this.bytes.subarray(0, this.length);
94
+ }
95
+ }
96
+ const hexValue = (c) => (c >= 0x30 && c <= 0x39 ? c - 0x30 : c >= 0x41 && c <= 0x46 ? c - 0x37 : c >= 0x61 && c <= 0x66 ? c - 0x57 : -1);
97
+ const isLetter = (c) => (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a);
98
+ const isDigit = (c) => c >= 0x30 && c <= 0x39;
99
+ /**
100
+ * Inspects an RTF file the way Word reads one. Objects with their data, fields, the attached template and shape
101
+ * hyperlinks are reported. Unknown destinations marked with \* are skipped, unknown control words are ignored, and
102
+ * text after the document's last brace is still read, as rtfobj reads it, and reported as trailing data.
103
+ */
104
+ async function inspectRtf(ctx, source, size) {
105
+ ctx.format = { format: 'rtf', mimeType: 'application/rtf' };
106
+ const cuts = [];
107
+ ctx.run.output = { kind: 'rtf', size, cuts };
108
+ const reader = new RtfReader(ctx, size, cuts);
109
+ for (let pos = 0; pos < size; pos += CHUNK) {
110
+ (0, errors_1.checkTime)(ctx.settings.deadline);
111
+ const chunk = await source.read(pos, Math.min(CHUNK, size - pos));
112
+ await reader.write(chunk, pos + chunk.length >= size);
113
+ }
114
+ await reader.end();
115
+ }
116
+ class RtfReader {
117
+ ctx;
118
+ size;
119
+ cuts;
120
+ frames;
121
+ fields;
122
+ depth = 0;
123
+ /** A partial token carried from one chunk to the next. */
124
+ carry = new Uint8Array(0);
125
+ /** The file offset of the first byte of the buffer being read. */
126
+ base = 0;
127
+ /** \bin bytes still to take, and where they go. */
128
+ binLeft = 0;
129
+ /** Fallback characters still to skip after a \u. */
130
+ skipChars = 0;
131
+ /** Set by \*, which makes an unknown destination that follows skipped. */
132
+ starred = false;
133
+ /** Where the document's outermost group closed, and how much that is not white space comes after it. */
134
+ closedAt = -1;
135
+ trailing = 0;
136
+ decoded = 0;
137
+ /** Objects whose groups closed, read in order once the chunk is done. */
138
+ pending = [];
139
+ /** The offsets of the braces of the groups open now. */
140
+ groups = [];
141
+ /** Where the control word being read ends. */
142
+ wordEnd = 0;
143
+ fieldCount = 0;
144
+ /** The fldinst groups of each field, by the field's number, which go when a finding strips the field. */
145
+ codes = new Map();
146
+ /** Pictures go to the plugins only when there are any, or expected verdicts to check, so only then are they decoded. */
147
+ pictures;
148
+ pendingPictures = [];
149
+ constructor(ctx, size,
150
+ /** Where the rewrite cuts, which the rules fill in as their findings strip. */
151
+ cuts) {
152
+ this.ctx = ctx;
153
+ this.size = size;
154
+ this.cuts = cuts;
155
+ this.frames = [{ kind: 'document', depth: 0, uc: 1, at: 0 }];
156
+ this.pictures = (ctx.run.options.filePlugins?.length ?? 0) > 0 || (ctx.run.verify?.size ?? 0) > 0;
157
+ // A field a strip finding names loses its instructions and keeps its result.
158
+ this.fields = new fields_1.FieldStack(ctx, 'document', marks => {
159
+ for (const [start, end] of this.codes.get(marks.begin) ?? [])
160
+ if (end >= 0)
161
+ cuts.push({ start, end });
162
+ });
163
+ }
164
+ get top() {
165
+ return this.frames[this.frames.length - 1];
166
+ }
167
+ async write(chunk, last) {
168
+ const b = this.carry.length ? Buffer.concat([this.carry, chunk]) : chunk;
169
+ let i = 0;
170
+ while (i < b.length) {
171
+ if (this.binLeft > 0) {
172
+ const n = Math.min(this.binLeft, b.length - i);
173
+ const top = this.top;
174
+ if (top.kind === 'objdata') {
175
+ top.hex.binary(b.subarray(i, i + n));
176
+ top.alt.binary(b.subarray(i, i + n));
177
+ }
178
+ else if (top.kind === 'objskip')
179
+ top.data.hex.binary(b.subarray(i, i + n));
180
+ else if (top.kind === 'pict') {
181
+ if (top.picture.dataAt < 0)
182
+ top.picture.dataAt = this.base + i;
183
+ top.hex.binary(b.subarray(i, i + n));
184
+ }
185
+ this.note(b, i, i + n);
186
+ this.binLeft -= n;
187
+ i += n;
188
+ continue;
189
+ }
190
+ const c = b[i];
191
+ if (c === 0x7b) {
192
+ this.note(b, i, i + 1);
193
+ this.open(this.base + i);
194
+ i++;
195
+ }
196
+ else if (c === 0x7d) {
197
+ this.note(b, i, i + 1);
198
+ this.close(this.base + i + 1);
199
+ i++;
200
+ }
201
+ else if (c === 0x5c) {
202
+ const used = this.control(b, i, last);
203
+ if (used === 0)
204
+ break;
205
+ this.note(b, i, i + used);
206
+ i += used;
207
+ }
208
+ else {
209
+ let j = i;
210
+ while (j < b.length && b[j] !== 0x7b && b[j] !== 0x7d && b[j] !== 0x5c)
211
+ j++;
212
+ this.note(b, i, j);
213
+ this.text(b, i, j);
214
+ i = j;
215
+ }
216
+ }
217
+ this.carry = i < b.length ? b.slice(i) : new Uint8Array(0);
218
+ this.base += i;
219
+ await this.objects();
220
+ }
221
+ async objects() {
222
+ for (const o of this.pending.splice(0))
223
+ await objectRules(this.ctx, o, this.cuts);
224
+ for (const p of this.pendingPictures.splice(0))
225
+ await pictureRules(this.ctx, p, this.cuts);
226
+ }
227
+ async end() {
228
+ // A file can end inside groups. Word reads what it has, and so does this.
229
+ while (this.depth > 0)
230
+ this.close(this.size);
231
+ this.fields.finish();
232
+ await this.objects();
233
+ if (this.trailing > 0 && this.ctx.report(types_1.OfficeCategory.Container, types_1.OfficeDetail.TrailingBytes, undefined, { bytes: this.trailing }).action === 'strip')
234
+ this.cuts.push({ start: this.closedAt, end: this.size });
235
+ }
236
+ /** Counts bytes past the document's outermost group that are not white space or NUL. */
237
+ note(b, from, to) {
238
+ if (this.closedAt < 0)
239
+ return;
240
+ for (let k = from; k < to; k++)
241
+ if (b[k] > 0x20)
242
+ this.trailing++;
243
+ }
244
+ open(offset) {
245
+ this.groups.push(offset);
246
+ this.depth++;
247
+ this.skipChars = 0;
248
+ const limit = this.ctx.settings.rtfDepth;
249
+ if (this.depth > limit)
250
+ throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.RtfDepth, `Groups nest deeper than ${limit}`, { limit });
251
+ }
252
+ /** Closes the group open now. `end` is the offset after its brace, or the end of the file. */
253
+ close(end) {
254
+ this.skipChars = 0;
255
+ this.starred = false;
256
+ while (this.frames.length > 1 && this.top.depth >= this.depth)
257
+ this.finishFrame(this.frames.pop(), end);
258
+ if (this.depth === 0)
259
+ return;
260
+ this.depth--;
261
+ this.groups.pop();
262
+ if (this.depth === 0 && this.closedAt < 0)
263
+ this.closedAt = end;
264
+ }
265
+ /**
266
+ * Opens a destination for the rest of the group. One opened in the same group as the last ends that one, as in
267
+ * rtfobj, unless the last is a field or an object, whose group the destination belongs to.
268
+ */
269
+ push(body) {
270
+ const top = this.top;
271
+ if (this.frames.length > 1 && top.depth === this.depth && top.kind !== 'field' && top.kind !== 'object')
272
+ this.finishFrame(this.frames.pop(), -1);
273
+ const limit = this.ctx.settings.rtfDepth;
274
+ if (this.frames.length > limit)
275
+ throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.RtfDepth, `More than ${limit} destinations are open at once`, { limit });
276
+ this.frames.push({ ...body, depth: this.depth, uc: this.top.uc, at: this.groups[this.depth - 1] ?? 0 });
277
+ }
278
+ /** Reads a control word or symbol at `i`. Returns the bytes it takes, or 0 when the buffer ends inside it. */
279
+ control(b, i, last) {
280
+ if (i + 1 >= b.length)
281
+ return last ? 1 : 0;
282
+ const c = b[i + 1];
283
+ if (!isLetter(c)) {
284
+ if (c === 0x27) {
285
+ if (i + 3 >= b.length && !last)
286
+ return 0;
287
+ this.quoted(b, i + 2);
288
+ return Math.min(4, b.length - i);
289
+ }
290
+ if (c === 0x2a)
291
+ this.starred = true;
292
+ else if (c === 0x5c || c === 0x7b || c === 0x7d)
293
+ this.char(String.fromCharCode(c));
294
+ else if (c === 0x0a || c === 0x0d)
295
+ this.char('\n');
296
+ else if (c === 0x7e)
297
+ this.char(' ');
298
+ return 2;
299
+ }
300
+ let j = i + 1;
301
+ while (j < b.length && isLetter(b[j]) && j - i - 1 < MAX_WORD)
302
+ j++;
303
+ let k = j;
304
+ if (k < b.length && b[k] === 0x2d && k + 1 < b.length && isDigit(b[k + 1]))
305
+ k++;
306
+ while (k < b.length && isDigit(b[k]) && k - j < MAX_WORD)
307
+ k++;
308
+ if (k >= b.length && !last)
309
+ return 0;
310
+ const name = (0, oleobj_1.latin1)(b.subarray(i + 1, j));
311
+ const digits = (0, oleobj_1.latin1)(b.subarray(j, k));
312
+ const param = digits === '' ? undefined : Number(digits);
313
+ // A space ends the control word and belongs to it.
314
+ const used = k - i + (k < b.length && b[k] === 0x20 ? 1 : 0);
315
+ this.wordEnd = this.base + i + used;
316
+ this.word(name, param, this.base + i);
317
+ return used;
318
+ }
319
+ word(name, param, offset) {
320
+ const starred = this.starred;
321
+ this.starred = false;
322
+ // \bin data is taken whole wherever it is, since its bytes may hold braces.
323
+ if (name === 'bin') {
324
+ // Word reads a negative or missing length as none.
325
+ this.binLeft = param !== undefined && param > 0 ? Math.min(param, Number.MAX_SAFE_INTEGER) : 0;
326
+ // A picture's data that starts with \bin is replaced from the control word on, so no length is left behind.
327
+ const pict = this.top;
328
+ if (pict.kind === 'pict' && pict.picture.dataAt < 0)
329
+ pict.picture.dataAt = offset;
330
+ return;
331
+ }
332
+ const top = this.top;
333
+ if (top.kind === 'skip')
334
+ return;
335
+ if (name === 'u' && param !== undefined) {
336
+ this.skipChars = 0;
337
+ this.char(String.fromCharCode(param < 0 ? param + 65536 : param & 0xffff));
338
+ this.skipChars = top.uc;
339
+ return;
340
+ }
341
+ this.skipChars = 0;
342
+ if (name === 'uc') {
343
+ top.uc = param ?? 1;
344
+ return;
345
+ }
346
+ const object = this.object();
347
+ if (name === 'pict' && this.pictures) {
348
+ const limit = this.ctx.settings.decompressedBytes;
349
+ this.push({ kind: 'pict', picture: { offset, dataAt: -1, group: [0, -1] }, hex: new HexData(offset, limit) });
350
+ const opened = this.top;
351
+ if (opened.kind === 'pict')
352
+ opened.picture.group[0] = opened.at;
353
+ }
354
+ else if (top.kind === 'pict' && Object.hasOwn(PICTURE_TYPES, name))
355
+ top.picture.type = PICTURE_TYPES[name];
356
+ else if (INERT.has(name))
357
+ this.push({ kind: 'skip' });
358
+ else if (name === 'field') {
359
+ const field = this.fieldCount++;
360
+ this.fields.begin(field);
361
+ this.push({ kind: 'field', field });
362
+ }
363
+ else if (name === 'fldinst')
364
+ this.push({ kind: 'fldinst', field: this.field() });
365
+ else if (name === 'fldrslt') {
366
+ this.fields.separate();
367
+ this.push({ kind: 'fldrslt' });
368
+ }
369
+ else if (name === 'object') {
370
+ this.push({ kind: 'object', object: { offset, kinds: new Set(), update: false, className: '' } });
371
+ const opened = this.top;
372
+ if (opened.kind === 'object')
373
+ opened.object.group = [opened.at, -1];
374
+ }
375
+ else if (name === 'objupdate') {
376
+ if (object) {
377
+ object.update = true;
378
+ object.updateAt ??= [offset, this.wordEnd];
379
+ }
380
+ }
381
+ else if (name === 'result' && object && !object.result) {
382
+ // What Word shows in place of the object, which stays when the object goes.
383
+ object.result = [this.wordEnd, -1];
384
+ this.push({ kind: 'result', object });
385
+ }
386
+ else if (OBJECT_KINDS.has(name))
387
+ object?.kinds.add(name);
388
+ else if (name === 'objdata') {
389
+ const limit = this.ctx.settings.decompressedBytes;
390
+ this.push({
391
+ kind: 'objdata',
392
+ object: object ?? { offset, kinds: new Set(), update: false, className: '' },
393
+ hex: new HexData(offset, limit),
394
+ alt: new HexData(offset, limit),
395
+ standalone: !object,
396
+ });
397
+ }
398
+ else if (name === 'objclass')
399
+ this.push({ kind: 'objclass', text: '', object });
400
+ else if (name === 'template' || (name === 'hlsrc' && this.frames.some(f => f.kind === 'hl')))
401
+ this.push({ kind: name, text: '' });
402
+ else if (name === 'hl')
403
+ this.push({ kind: 'hl' });
404
+ // Any other destination takes its own text. What it holds is still read for fields and objects.
405
+ else if (DESTINATIONS.has(name))
406
+ this.push({ kind: 'document' });
407
+ // Word skips a group that \* marks when it does not know its control word. Inside object data rtfobj keeps it, and
408
+ // a sample Word opens hides its hex digits in groups such as {\*\par574 0}. Both readings of the data are kept.
409
+ else if (starred && (top.kind === 'objdata' || top.kind === 'objskip'))
410
+ this.push({ kind: 'objskip', data: top.kind === 'objdata' ? top : top.data });
411
+ else if (starred)
412
+ this.push({ kind: 'skip' });
413
+ }
414
+ /** The number of the innermost field the reader is in, or -1. */
415
+ field() {
416
+ for (let k = this.frames.length - 1; k > 0; k--) {
417
+ const f = this.frames[k];
418
+ if (f.kind === 'field')
419
+ return f.field;
420
+ }
421
+ return -1;
422
+ }
423
+ /** The object of the innermost object group the reader is in. */
424
+ object() {
425
+ for (let k = this.frames.length - 1; k > 0; k--) {
426
+ const f = this.frames[k];
427
+ if (f.kind === 'object')
428
+ return f.object;
429
+ }
430
+ return undefined;
431
+ }
432
+ quoted(b, at) {
433
+ const top = this.top;
434
+ if (top.kind === 'skip')
435
+ return;
436
+ this.starred = false;
437
+ if (top.kind === 'objdata') {
438
+ top.hex.quote();
439
+ top.alt.quote();
440
+ return;
441
+ }
442
+ if (top.kind === 'objskip') {
443
+ top.data.hex.quote();
444
+ return;
445
+ }
446
+ if (top.kind === 'pict') {
447
+ top.hex.quote();
448
+ return;
449
+ }
450
+ // A file can end before the two characters of a \'hh.
451
+ const hi = hexValue(b[at] ?? -1);
452
+ const lo = hexValue(b[at + 1] ?? -1);
453
+ if (hi >= 0 && lo >= 0)
454
+ this.char(String.fromCharCode((hi << 4) | lo));
455
+ }
456
+ text(b, from, to) {
457
+ const top = this.top;
458
+ if (top.kind === 'skip')
459
+ return;
460
+ this.starred = false;
461
+ if (top.kind === 'pict') {
462
+ for (let k = from; k < to; k++) {
463
+ const v = hexValue(b[k]);
464
+ if (v < 0)
465
+ continue;
466
+ if (top.picture.dataAt < 0)
467
+ top.picture.dataAt = this.base + k;
468
+ top.hex.digit(v);
469
+ }
470
+ return;
471
+ }
472
+ if (top.kind === 'objdata' || top.kind === 'objskip') {
473
+ const both = top.kind === 'objdata';
474
+ const hex = both ? top.hex : top.data.hex;
475
+ for (let k = from; k < to; k++) {
476
+ const v = hexValue(b[k]);
477
+ if (v < 0)
478
+ continue;
479
+ hex.digit(v);
480
+ if (both)
481
+ top.alt.digit(v);
482
+ }
483
+ return;
484
+ }
485
+ // Line ends in RTF text are not text.
486
+ let s = '';
487
+ for (let k = from; k < to; k++) {
488
+ const c = b[k];
489
+ if (c === 0x0a || c === 0x0d)
490
+ continue;
491
+ if (this.skipChars > 0) {
492
+ this.skipChars--;
493
+ continue;
494
+ }
495
+ s += String.fromCharCode(c);
496
+ }
497
+ if (s)
498
+ this.char(s, true);
499
+ }
500
+ /** Text decoded from the file, for the destination being read. */
501
+ char(s, counted = false) {
502
+ if (!counted && this.skipChars > 0) {
503
+ this.skipChars--;
504
+ return;
505
+ }
506
+ const top = this.top;
507
+ if (top.kind === 'fldinst')
508
+ this.fields.text(s);
509
+ else if (top.kind === 'fldrslt')
510
+ this.fields.result(s);
511
+ else if ((top.kind === 'objclass' || top.kind === 'template' || top.kind === 'hlsrc') && top.text.length < MAX_TEXT)
512
+ top.text += s;
513
+ }
514
+ /** Ends a destination. `end` is where its group ends, or -1 when another destination in its group ended it. */
515
+ finishFrame(f, end) {
516
+ // A cut takes a whole group, which only a group that closed has.
517
+ const cut = (r) => {
518
+ if (r.action === 'strip' && end >= 0)
519
+ this.cuts.push({ start: f.at, end });
520
+ };
521
+ if (f.kind === 'field')
522
+ this.fields.end();
523
+ else if (f.kind === 'fldinst') {
524
+ const list = this.codes.get(f.field) ?? [];
525
+ list.push([f.at, end]);
526
+ this.codes.set(f.field, list);
527
+ }
528
+ else if (f.kind === 'result') {
529
+ if (f.object.result)
530
+ f.object.result[1] = end;
531
+ }
532
+ else if (f.kind === 'pict') {
533
+ const data = f.hex.result();
534
+ this.decoded += data.length;
535
+ const total = this.ctx.settings.totalDecompressedBytes;
536
+ if (this.decoded > total)
537
+ throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.TotalDecompressedSize, `The objects and pictures decode past ${total} bytes`, { limit: total });
538
+ f.picture.group[1] = end;
539
+ if (f.picture.type && data.length) {
540
+ f.picture.data = data;
541
+ this.pendingPictures.push(f.picture);
542
+ }
543
+ }
544
+ else if (f.kind === 'objclass') {
545
+ if (f.object)
546
+ f.object.className = f.text.trim();
547
+ }
548
+ else if (f.kind === 'template') {
549
+ const path = f.text.trim();
550
+ if (path)
551
+ cut(this.ctx.external(types_1.OfficeDetail.Template, path, 'template', { source: 'RTF template' }, 'template'));
552
+ }
553
+ else if (f.kind === 'hlsrc') {
554
+ const target = f.text.trim();
555
+ if (target && !(0, links_1.isFragment)(target))
556
+ cut(this.ctx.link(target, 'hyperlink', { source: 'RTF shape hyperlink' }));
557
+ }
558
+ else if (f.kind === 'objdata') {
559
+ f.object.objdata = [f.at, end];
560
+ f.object.data = f.hex.result();
561
+ const alt = f.alt.result();
562
+ if (!Buffer.from(alt).equals(Buffer.from(f.object.data)))
563
+ f.object.alt = alt;
564
+ this.decoded += f.object.data.length + (f.object.alt ? alt.length : 0);
565
+ const total = this.ctx.settings.totalDecompressedBytes;
566
+ if (this.decoded > total)
567
+ throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.TotalDecompressedSize, `The objects decode past ${total} bytes`, { limit: total });
568
+ // An objdata outside any object is an object of its own, as rtfobj reads it.
569
+ if (f.standalone)
570
+ this.pending.push(f.object);
571
+ }
572
+ else if (f.kind === 'object') {
573
+ if (f.object.group)
574
+ f.object.group[1] = end;
575
+ this.pending.push(f.object);
576
+ }
577
+ }
578
+ }
579
+ /**
580
+ * One embedded or linked object: what its OLE 1.0 header names, and what its data holds. When anything read inside it
581
+ * makes a strip finding, the object's group goes and its result group, the picture Word shows, takes its place.
582
+ * \objupdate, which ACTION/OBJECT_UPDATE names, goes on its own.
583
+ */
584
+ async function objectRules(ctx, o, cuts) {
585
+ const location = `object at offset ${o.offset}`;
586
+ const kinds = [...o.kinds];
587
+ const base = { object: location, ...(kinds.length ? { kind: kinds.join(' ') } : {}) };
588
+ if (o.update && ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.ObjectUpdate, location, base).action === 'strip' && o.updateAt)
589
+ cuts.push({ start: o.updateAt[0], end: o.updateAt[1] });
590
+ const before = ctx.strips;
591
+ if (o.kinds.has('objocx'))
592
+ ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, location, { ...base, ...(o.className ? { className: (0, context_1.clip)(o.className) } : {}) });
593
+ else {
594
+ await objectData(ctx, o, o.data, location, base, cuts);
595
+ // The data read without the groups \* marks, where that reads differently, is the object a reader that skips them sees.
596
+ if (o.alt)
597
+ await objectData(ctx, o, o.alt, location, { ...base, reading: 'without the groups \\* marks' });
598
+ }
599
+ if (ctx.strips === before)
600
+ return;
601
+ const [start, end] = o.group ?? o.objdata ?? [-1, -1];
602
+ if (end < 0)
603
+ return;
604
+ const result = o.group && o.result && o.result[1] >= 0 ? o.result : undefined;
605
+ cuts.push(result ? { start, end, text: Buffer.from('{', 'latin1'), copy: result } : { start, end });
606
+ }
607
+ /** Bytes as the hex digits of an objdata group, in lines of 128 digits as Word writes them. */
608
+ function objdataGroup(bytes) {
609
+ const hex = Buffer.from(bytes).toString('hex');
610
+ const lines = [];
611
+ for (let i = 0; i < hex.length; i += 128)
612
+ lines.push(hex.slice(i, i + 128));
613
+ return Buffer.from(`{\\*\\objdata\r\n${lines.join('\r\n')}}`, 'latin1');
614
+ }
615
+ /**
616
+ * A PNG, JPEG or EMF picture goes to the plugins, as the file it is. A plugin that removes it removes its \pict group,
617
+ * and bytes a plugin wrote take the place of its data. A picture no plugin takes stays, since no rule names it.
618
+ */
619
+ async function pictureRules(ctx, p, cuts) {
620
+ const data = p.data ?? new Uint8Array(0);
621
+ const decision = await (0, contained_1.decideContained)(ctx, { declaredType: p.type, location: `picture at offset ${p.offset}`, chunks: () => (0, io_1.chunksOf)((0, io_1.bufferSource)(data), data.length) });
622
+ const [start, end] = p.group;
623
+ if (!decision || end < 0)
624
+ return;
625
+ if (!decision.keep)
626
+ cuts.push({ start, end });
627
+ else if (decision.replacement && p.dataAt >= 0) {
628
+ const hex = Buffer.from(await (0, io_1.readAll)(decision.replacement)).toString('hex');
629
+ const lines = [];
630
+ for (let i = 0; i < hex.length; i += 128)
631
+ lines.push(hex.slice(i, i + 128));
632
+ // The data runs to the brace that closes the group, which stays.
633
+ cuts.push({ start: p.dataAt, end: end - 1, text: Buffer.from(lines.join('\r\n'), 'latin1') });
634
+ }
635
+ }
636
+ /**
637
+ * An object's data: its OLE 1.0 header, and the Packager data or storage it holds. With `cuts`, for the first reading
638
+ * of the data, the file it holds goes to the plugins first: the file a Packager object holds, or the native data of
639
+ * any other embedded object. A plugin's verdict decides whether the object stays, and the bytes a plugin wrote take
640
+ * the place of the file in the object's data.
641
+ */
642
+ async function objectData(ctx, o, bytes, location, base, cuts) {
643
+ const ole1 = bytes?.length ? (0, oleobj_1.parseOle1)(bytes) : undefined;
644
+ const className = (ole1?.className || o.className).replace(/\0+$/, '');
645
+ const data = { ...base, ...(className ? { className: (0, context_1.clip)(className) } : {}) };
646
+ const link = { object: location, ...(className ? { className: (0, context_1.clip)(className) } : {}) };
647
+ let equation = (0, objects_1.isEquationClass)(className);
648
+ if (ole1?.formatId === 1 || (!bytes?.length && (o.kinds.has('objlink') || o.kinds.has('objautlink')))) {
649
+ const target = ole1?.topicName ?? '';
650
+ if (target)
651
+ ctx.external(types_1.OfficeDetail.Object, target, location, link, 'linked object');
652
+ else
653
+ ctx.report(types_1.OfficeCategory.External, types_1.OfficeDetail.Object, location, data);
654
+ return;
655
+ }
656
+ if (!bytes?.length)
657
+ return;
658
+ if (!ole1)
659
+ data.reason = 'its data is not an OLE 1.0 object';
660
+ const native = ole1?.data;
661
+ const pkg = native && className.toLowerCase() === 'package' ? (0, oleobj_1.parsePackage)(native) : undefined;
662
+ let decision;
663
+ if (cuts && native && ole1?.dataAt !== undefined && className.toLowerCase() !== 'ole2link') {
664
+ const held = pkg?.embedded ? native.subarray(pkg.dataAt, pkg.dataAt + pkg.size) : native;
665
+ decision = await (0, contained_1.decideContained)(ctx, { name: (pkg?.embedded ? pkg.fileName : className) || undefined, location, chunks: () => (0, io_1.chunksOf)((0, io_1.bufferSource)(held), held.length) });
666
+ if (decision?.replacement && o.objdata && o.objdata[1] >= 0) {
667
+ const replaced = await (0, io_1.readAll)(decision.replacement);
668
+ const newNative = pkg?.embedded ? (0, oleobj_1.replaceSized)(native, pkg.dataAt, pkg.size, replaced) : replaced;
669
+ cuts.push({ start: o.objdata[0], end: o.objdata[1], text: objdataGroup((0, oleobj_1.replaceSized)(bytes, ole1.dataAt, native.length, newNative)) });
670
+ }
671
+ }
672
+ const act = (0, contained_1.presence)(decision);
673
+ if (pkg)
674
+ Object.assign(data, (0, objects_1.packageData)(pkg));
675
+ else if (native && native.length >= 8 && cfb_1.CFB_SIGNATURE.every((v, i) => native[i] === v)) {
676
+ let cfb;
677
+ try {
678
+ cfb = await cfb_1.CompoundFile.open((0, io_1.bufferSource)(native), ctx.settings, ctx.settings.deadline);
679
+ }
680
+ catch (e) {
681
+ if (!(e instanceof errors_1.StopError) || e.category !== types_1.OfficeCategory.Container)
682
+ throw e;
683
+ data.reason = `its storage does not read: ${e.message}`;
684
+ }
685
+ if (cfb) {
686
+ (0, objects_1.nestedNotes)(ctx, cfb, location);
687
+ const d = await (0, objects_1.describeObject)(cfb, cfb.root, location);
688
+ if (d.clsid)
689
+ data.clsid = d.clsid;
690
+ if (d.progId)
691
+ data.progId = (0, context_1.clip)(d.progId);
692
+ if (d.pkg)
693
+ Object.assign(data, (0, objects_1.packageData)(d.pkg));
694
+ equation ||= d.equation;
695
+ await (0, objects_1.compoundRules)(ctx, cfb, location, false);
696
+ if (d.linked) {
697
+ (0, objects_1.linkedObject)(ctx, d, location, link);
698
+ if (equation)
699
+ ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Equation, location, data);
700
+ return;
701
+ }
702
+ }
703
+ }
704
+ // OLE2Link is the class of a linked object, CVE-2017-0199's. Its storage names the target, and when the storage does
705
+ // not read, the class still says the object loads from outside.
706
+ if (className.toLowerCase() === 'ole2link')
707
+ ctx.report(types_1.OfficeCategory.External, types_1.OfficeDetail.Object, location, data);
708
+ else
709
+ ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, location, data, act);
710
+ if (equation)
711
+ ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Equation, location, data);
712
+ }