@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/rtf.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.inspectRtf = inspectRtf;
4
4
  const cfb_1 = require("./cfb");
5
+ const contained_1 = require("./contained");
5
6
  const context_1 = require("./context");
6
7
  const errors_1 = require("./errors");
7
8
  const fields_1 = require("./fields");
@@ -43,6 +44,8 @@ const MAX_WORD = 250;
43
44
  const CHUNK = 262144;
44
45
  /** Collected text other than an object's data is short: a class name, a template path, a link. */
45
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' };
46
49
  /** An object's data as Word decodes its hex digits, with the quirks rtfobj documents. */
47
50
  class HexData {
48
51
  limit;
@@ -100,7 +103,9 @@ const isDigit = (c) => c >= 0x30 && c <= 0x39;
100
103
  */
101
104
  async function inspectRtf(ctx, source, size) {
102
105
  ctx.format = { format: 'rtf', mimeType: 'application/rtf' };
103
- const reader = new RtfReader(ctx, size);
106
+ const cuts = [];
107
+ ctx.run.output = { kind: 'rtf', size, cuts };
108
+ const reader = new RtfReader(ctx, size, cuts);
104
109
  for (let pos = 0; pos < size; pos += CHUNK) {
105
110
  (0, errors_1.checkTime)(ctx.settings.deadline);
106
111
  const chunk = await source.read(pos, Math.min(CHUNK, size - pos));
@@ -111,6 +116,7 @@ async function inspectRtf(ctx, source, size) {
111
116
  class RtfReader {
112
117
  ctx;
113
118
  size;
119
+ cuts;
114
120
  frames;
115
121
  fields;
116
122
  depth = 0;
@@ -130,11 +136,30 @@ class RtfReader {
130
136
  decoded = 0;
131
137
  /** Objects whose groups closed, read in order once the chunk is done. */
132
138
  pending = [];
133
- constructor(ctx, size) {
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) {
134
152
  this.ctx = ctx;
135
153
  this.size = size;
136
- this.frames = [{ kind: 'document', depth: 0, uc: 1 }];
137
- this.fields = new fields_1.FieldStack(ctx, 'document');
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
+ });
138
163
  }
139
164
  get top() {
140
165
  return this.frames[this.frames.length - 1];
@@ -152,6 +177,11 @@ class RtfReader {
152
177
  }
153
178
  else if (top.kind === 'objskip')
154
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
+ }
155
185
  this.note(b, i, i + n);
156
186
  this.binLeft -= n;
157
187
  i += n;
@@ -160,12 +190,12 @@ class RtfReader {
160
190
  const c = b[i];
161
191
  if (c === 0x7b) {
162
192
  this.note(b, i, i + 1);
163
- this.open();
193
+ this.open(this.base + i);
164
194
  i++;
165
195
  }
166
196
  else if (c === 0x7d) {
167
197
  this.note(b, i, i + 1);
168
- this.close(this.base + i);
198
+ this.close(this.base + i + 1);
169
199
  i++;
170
200
  }
171
201
  else if (c === 0x5c) {
@@ -190,7 +220,9 @@ class RtfReader {
190
220
  }
191
221
  async objects() {
192
222
  for (const o of this.pending.splice(0))
193
- await objectRules(this.ctx, o);
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);
194
226
  }
195
227
  async end() {
196
228
  // A file can end inside groups. Word reads what it has, and so does this.
@@ -198,8 +230,8 @@ class RtfReader {
198
230
  this.close(this.size);
199
231
  this.fields.finish();
200
232
  await this.objects();
201
- if (this.trailing > 0)
202
- this.ctx.report(types_1.OfficeCategory.Container, types_1.OfficeDetail.TrailingBytes, undefined, { bytes: this.trailing });
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 });
203
235
  }
204
236
  /** Counts bytes past the document's outermost group that are not white space or NUL. */
205
237
  note(b, from, to) {
@@ -209,23 +241,26 @@ class RtfReader {
209
241
  if (b[k] > 0x20)
210
242
  this.trailing++;
211
243
  }
212
- open() {
244
+ open(offset) {
245
+ this.groups.push(offset);
213
246
  this.depth++;
214
247
  this.skipChars = 0;
215
248
  const limit = this.ctx.settings.rtfDepth;
216
249
  if (this.depth > limit)
217
250
  throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.RtfDepth, `Groups nest deeper than ${limit}`, { limit });
218
251
  }
219
- close(offset) {
252
+ /** Closes the group open now. `end` is the offset after its brace, or the end of the file. */
253
+ close(end) {
220
254
  this.skipChars = 0;
221
255
  this.starred = false;
222
256
  while (this.frames.length > 1 && this.top.depth >= this.depth)
223
- this.finishFrame(this.frames.pop());
257
+ this.finishFrame(this.frames.pop(), end);
224
258
  if (this.depth === 0)
225
259
  return;
226
260
  this.depth--;
261
+ this.groups.pop();
227
262
  if (this.depth === 0 && this.closedAt < 0)
228
- this.closedAt = offset;
263
+ this.closedAt = end;
229
264
  }
230
265
  /**
231
266
  * Opens a destination for the rest of the group. One opened in the same group as the last ends that one, as in
@@ -234,11 +269,11 @@ class RtfReader {
234
269
  push(body) {
235
270
  const top = this.top;
236
271
  if (this.frames.length > 1 && top.depth === this.depth && top.kind !== 'field' && top.kind !== 'object')
237
- this.finishFrame(this.frames.pop());
272
+ this.finishFrame(this.frames.pop(), -1);
238
273
  const limit = this.ctx.settings.rtfDepth;
239
274
  if (this.frames.length > limit)
240
275
  throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.RtfDepth, `More than ${limit} destinations are open at once`, { limit });
241
- this.frames.push({ ...body, depth: this.depth, uc: this.top.uc });
276
+ this.frames.push({ ...body, depth: this.depth, uc: this.top.uc, at: this.groups[this.depth - 1] ?? 0 });
242
277
  }
243
278
  /** Reads a control word or symbol at `i`. Returns the bytes it takes, or 0 when the buffer ends inside it. */
244
279
  control(b, i, last) {
@@ -277,6 +312,7 @@ class RtfReader {
277
312
  const param = digits === '' ? undefined : Number(digits);
278
313
  // A space ends the control word and belongs to it.
279
314
  const used = k - i + (k < b.length && b[k] === 0x20 ? 1 : 0);
315
+ this.wordEnd = this.base + i + used;
280
316
  this.word(name, param, this.base + i);
281
317
  return used;
282
318
  }
@@ -287,6 +323,10 @@ class RtfReader {
287
323
  if (name === 'bin') {
288
324
  // Word reads a negative or missing length as none.
289
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;
290
330
  return;
291
331
  }
292
332
  const top = this.top;
@@ -304,23 +344,44 @@ class RtfReader {
304
344
  return;
305
345
  }
306
346
  const object = this.object();
307
- if (INERT.has(name))
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))
308
357
  this.push({ kind: 'skip' });
309
358
  else if (name === 'field') {
310
- this.fields.begin();
311
- this.push({ kind: 'field' });
359
+ const field = this.fieldCount++;
360
+ this.fields.begin(field);
361
+ this.push({ kind: 'field', field });
312
362
  }
313
363
  else if (name === 'fldinst')
314
- this.push({ kind: 'fldinst' });
364
+ this.push({ kind: 'fldinst', field: this.field() });
315
365
  else if (name === 'fldrslt') {
316
366
  this.fields.separate();
317
367
  this.push({ kind: 'fldrslt' });
318
368
  }
319
- else if (name === 'object')
369
+ else if (name === 'object') {
320
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
+ }
321
375
  else if (name === 'objupdate') {
322
- if (object)
376
+ if (object) {
323
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 });
324
385
  }
325
386
  else if (OBJECT_KINDS.has(name))
326
387
  object?.kinds.add(name);
@@ -350,6 +411,15 @@ class RtfReader {
350
411
  else if (starred)
351
412
  this.push({ kind: 'skip' });
352
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
+ }
353
423
  /** The object of the innermost object group the reader is in. */
354
424
  object() {
355
425
  for (let k = this.frames.length - 1; k > 0; k--) {
@@ -373,6 +443,10 @@ class RtfReader {
373
443
  top.data.hex.quote();
374
444
  return;
375
445
  }
446
+ if (top.kind === 'pict') {
447
+ top.hex.quote();
448
+ return;
449
+ }
376
450
  // A file can end before the two characters of a \'hh.
377
451
  const hi = hexValue(b[at] ?? -1);
378
452
  const lo = hexValue(b[at + 1] ?? -1);
@@ -384,6 +458,17 @@ class RtfReader {
384
458
  if (top.kind === 'skip')
385
459
  return;
386
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
+ }
387
472
  if (top.kind === 'objdata' || top.kind === 'objskip') {
388
473
  const both = top.kind === 'objdata';
389
474
  const hex = both ? top.hex : top.data.hex;
@@ -421,12 +506,41 @@ class RtfReader {
421
506
  const top = this.top;
422
507
  if (top.kind === 'fldinst')
423
508
  this.fields.text(s);
509
+ else if (top.kind === 'fldrslt')
510
+ this.fields.result(s);
424
511
  else if ((top.kind === 'objclass' || top.kind === 'template' || top.kind === 'hlsrc') && top.text.length < MAX_TEXT)
425
512
  top.text += s;
426
513
  }
427
- finishFrame(f) {
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
+ };
428
521
  if (f.kind === 'field')
429
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
+ }
430
544
  else if (f.kind === 'objclass') {
431
545
  if (f.object)
432
546
  f.object.className = f.text.trim();
@@ -434,14 +548,15 @@ class RtfReader {
434
548
  else if (f.kind === 'template') {
435
549
  const path = f.text.trim();
436
550
  if (path)
437
- this.ctx.external(types_1.OfficeDetail.Template, path, 'template', { source: 'RTF template' }, 'template');
551
+ cut(this.ctx.external(types_1.OfficeDetail.Template, path, 'template', { source: 'RTF template' }, 'template'));
438
552
  }
439
553
  else if (f.kind === 'hlsrc') {
440
554
  const target = f.text.trim();
441
555
  if (target && !(0, links_1.isFragment)(target))
442
- this.ctx.link(target, 'hyperlink', { source: 'RTF shape hyperlink' });
556
+ cut(this.ctx.link(target, 'hyperlink', { source: 'RTF shape hyperlink' }));
443
557
  }
444
558
  else if (f.kind === 'objdata') {
559
+ f.object.objdata = [f.at, end];
445
560
  f.object.data = f.hex.result();
446
561
  const alt = f.alt.result();
447
562
  if (!Buffer.from(alt).equals(Buffer.from(f.object.data)))
@@ -454,28 +569,77 @@ class RtfReader {
454
569
  if (f.standalone)
455
570
  this.pending.push(f.object);
456
571
  }
457
- else if (f.kind === 'object')
572
+ else if (f.kind === 'object') {
573
+ if (f.object.group)
574
+ f.object.group[1] = end;
458
575
  this.pending.push(f.object);
576
+ }
459
577
  }
460
578
  }
461
- /** One embedded or linked object: what its OLE 1.0 header names, and what its data holds. */
462
- async function objectRules(ctx, o) {
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) {
463
585
  const location = `object at offset ${o.offset}`;
464
586
  const kinds = [...o.kinds];
465
587
  const base = { object: location, ...(kinds.length ? { kind: kinds.join(' ') } : {}) };
466
- if (o.update)
467
- ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.ObjectUpdate, location, base);
468
- if (o.kinds.has('objocx')) {
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'))
469
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)
470
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') });
471
634
  }
472
- await objectData(ctx, o, o.data, location, base);
473
- // The data read without the groups \* marks, where that reads differently, is the object a reader that skips them sees.
474
- if (o.alt)
475
- await objectData(ctx, o, o.alt, location, { ...base, reading: 'without the groups \\* marks' });
476
635
  }
477
- /** An object's data: its OLE 1.0 header, and the Packager data or storage it holds. */
478
- async function objectData(ctx, o, bytes, location, base) {
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) {
479
643
  const ole1 = bytes?.length ? (0, oleobj_1.parseOle1)(bytes) : undefined;
480
644
  const className = (ole1?.className || o.className).replace(/\0+$/, '');
481
645
  const data = { ...base, ...(className ? { className: (0, context_1.clip)(className) } : {}) };
@@ -494,11 +658,20 @@ async function objectData(ctx, o, bytes, location, base) {
494
658
  if (!ole1)
495
659
  data.reason = 'its data is not an OLE 1.0 object';
496
660
  const native = ole1?.data;
497
- if (native && className.toLowerCase() === 'package') {
498
- const pkg = (0, oleobj_1.parsePackage)(native);
499
- if (pkg)
500
- Object.assign(data, (0, objects_1.packageData)(pkg));
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
+ }
501
671
  }
672
+ const act = (0, contained_1.presence)(decision);
673
+ if (pkg)
674
+ Object.assign(data, (0, objects_1.packageData)(pkg));
502
675
  else if (native && native.length >= 8 && cfb_1.CFB_SIGNATURE.every((v, i) => native[i] === v)) {
503
676
  let cfb;
504
677
  try {
@@ -533,7 +706,7 @@ async function objectData(ctx, o, bytes, location, base) {
533
706
  if (className.toLowerCase() === 'ole2link')
534
707
  ctx.report(types_1.OfficeCategory.External, types_1.OfficeDetail.Object, location, data);
535
708
  else
536
- ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, location, data);
709
+ ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, location, data, act);
537
710
  if (equation)
538
711
  ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Equation, location, data);
539
712
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Decompresses an Outlook message's PidTagRtfCompressed (MS-OXRTFCP 2.1.3.1). Returns undefined for input the
3
+ * specification calls corrupt: cut short, of an unknown type, with a CRC that does not match, or longer than `limit`.
4
+ */
5
+ export declare function decompressRtf(b: Uint8Array, limit: number): Uint8Array | undefined;
6
+ /** RTF stored as a PidTagRtfCompressed value of type UNCOMPRESSED, whose CRC is 0 (MS-OXRTFCP 2.1.3.1.1). */
7
+ export declare function storeRtf(rtf: Uint8Array): Uint8Array;
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.decompressRtf = decompressRtf;
4
+ exports.storeRtf = storeRtf;
5
+ const crc32_1 = require("./crc32");
6
+ /** The dictionary MS-OXRTFCP 2.1.3.1.2 starts from: 207 bytes of RTF that most bodies begin with. */
7
+ const PRESET = Buffer.from('{\\rtf1\\ansi\\mac\\deff0\\deftab720{\\fonttbl;}{\\f0\\fnil \\froman \\fswiss \\fmodern \\fscript \\fdecor MS Sans SerifSymbolArialTimes New RomanCourier{\\colortbl\\red0\\green0\\blue0\r\n\\par \\pard\\plain\\f0\\fs20\\b\\i\\u\\tab\\tx', 'latin1');
8
+ /** COMPTYPE: LZFu for compressed, MELA for stored. */
9
+ const COMPRESSED = 0x75465a4c;
10
+ const UNCOMPRESSED = 0x414c454d;
11
+ /**
12
+ * The CRC of MS-OXRTFCP 2.1.3.2: CRC-32's table, started at 0 and never inverted. zlib's CRC-32 inverts the value it
13
+ * starts from and the value it ends with, so starting it from all ones and inverting its result gives this one.
14
+ */
15
+ const rtfCrc = (b) => ~(0, crc32_1.crc32)(b, 0xffffffff) >>> 0;
16
+ /**
17
+ * Decompresses an Outlook message's PidTagRtfCompressed (MS-OXRTFCP 2.1.3.1). Returns undefined for input the
18
+ * specification calls corrupt: cut short, of an unknown type, with a CRC that does not match, or longer than `limit`.
19
+ */
20
+ function decompressRtf(b, limit) {
21
+ if (b.length < 16)
22
+ return undefined;
23
+ const view = Buffer.from(b.buffer, b.byteOffset, b.byteLength);
24
+ const compSize = view.readUInt32LE(0);
25
+ const rawSize = view.readUInt32LE(4);
26
+ const type = view.readUInt32LE(8);
27
+ const crc = view.readUInt32LE(12);
28
+ if (compSize < 12 || compSize + 4 > b.length || rawSize > limit)
29
+ return undefined;
30
+ const contents = view.subarray(16, 4 + compSize);
31
+ if (type === UNCOMPRESSED)
32
+ return contents.subarray(0, rawSize);
33
+ if (type !== COMPRESSED || rtfCrc(contents) !== crc)
34
+ return undefined;
35
+ const dict = Buffer.alloc(4096);
36
+ PRESET.copy(dict);
37
+ let write = PRESET.length;
38
+ const out = Buffer.alloc(rawSize);
39
+ let n = 0;
40
+ let at = 0;
41
+ while (at < contents.length) {
42
+ const control = contents[at++];
43
+ for (let bit = 0; bit < 8; bit++) {
44
+ if ((control & (1 << bit)) === 0) {
45
+ if (at >= contents.length)
46
+ return undefined;
47
+ const c = contents[at++];
48
+ if (n < rawSize)
49
+ out[n++] = c;
50
+ dict[write] = c;
51
+ write = (write + 1) & 4095;
52
+ continue;
53
+ }
54
+ if (at + 2 > contents.length)
55
+ return undefined;
56
+ const ref = (contents[at] << 8) | contents[at + 1];
57
+ at += 2;
58
+ let read = ref >> 4;
59
+ if (read === write)
60
+ return out.subarray(0, n);
61
+ for (let k = (ref & 0x0f) + 2; k > 0; k--) {
62
+ const c = dict[read];
63
+ read = (read + 1) & 4095;
64
+ if (n < rawSize)
65
+ out[n++] = c;
66
+ dict[write] = c;
67
+ write = (write + 1) & 4095;
68
+ }
69
+ }
70
+ }
71
+ return undefined;
72
+ }
73
+ /** RTF stored as a PidTagRtfCompressed value of type UNCOMPRESSED, whose CRC is 0 (MS-OXRTFCP 2.1.3.1.1). */
74
+ function storeRtf(rtf) {
75
+ const head = Buffer.alloc(16);
76
+ head.writeUInt32LE(rtf.length + 12, 0);
77
+ head.writeUInt32LE(rtf.length, 4);
78
+ head.writeUInt32LE(UNCOMPRESSED, 8);
79
+ return Buffer.concat([head, rtf]);
80
+ }
package/dist/score.js CHANGED
@@ -19,7 +19,7 @@ exports.COMBINATIONS = [
19
19
  {
20
20
  name: 'code with data hidden in the archive',
21
21
  weight: 10,
22
- test: has => runsCode(has) && (has(types_1.OfficeCategory.Container, types_1.OfficeDetail.LeadingBytes) || has(types_1.OfficeCategory.Container, types_1.OfficeDetail.HiddenData) || has(types_1.OfficeCategory.Container, types_1.OfficeDetail.TrailingBytes)),
22
+ test: has => runsCode(has) && (has(types_1.OfficeCategory.Container, types_1.OfficeDetail.LeadingBytes) || has(types_1.OfficeCategory.Container, types_1.OfficeDetail.HiddenData) || has(types_1.OfficeCategory.Container, types_1.OfficeDetail.HiddenEntry) || has(types_1.OfficeCategory.Container, types_1.OfficeDetail.TrailingBytes)),
23
23
  },
24
24
  ];
25
25
  /** The caller's weights first, then the built-in table. A kind from another package scores at the weight it carries. */
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Identifies a file type from its first bytes, with the MIME types pdf-defuse gives, so plugins of the family see one
3
+ * name for each type. Returns a MIME type or undefined.
4
+ */
5
+ export declare function sniffType(head: Uint8Array): string | undefined;
package/dist/sniff.js ADDED
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sniffType = sniffType;
4
+ /**
5
+ * Identifies a file type from its first bytes, with the MIME types pdf-defuse gives, so plugins of the family see one
6
+ * name for each type. Returns a MIME type or undefined.
7
+ */
8
+ function sniffType(head) {
9
+ const b = Buffer.from(head.buffer, head.byteOffset, head.byteLength);
10
+ // PDF readers accept the header anywhere in the first 1024 bytes.
11
+ const s = b.subarray(0, 1024).toString('latin1');
12
+ if (s.includes('%PDF-'))
13
+ return 'application/pdf';
14
+ if (b[0] === 0x50 && b[1] === 0x4b && (b[2] === 0x03 || b[2] === 0x05 || b[2] === 0x07))
15
+ return 'application/zip';
16
+ if (b[0] === 0x89 && s.startsWith('\x89PNG'))
17
+ return 'image/png';
18
+ if (b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff)
19
+ return 'image/jpeg';
20
+ if (s.startsWith('GIF87a') || s.startsWith('GIF89a'))
21
+ return 'image/gif';
22
+ if (b[0] === 0xd0 && b[1] === 0xcf && b[2] === 0x11 && b[3] === 0xe0)
23
+ return 'application/x-ole-storage';
24
+ if (s.startsWith('{\\rt'))
25
+ return 'application/rtf';
26
+ if (s.startsWith('MZ'))
27
+ return 'application/x-msdownload';
28
+ if (s.startsWith('\x7fELF'))
29
+ return 'application/x-executable';
30
+ const text = s.replace(/^\xef\xbb\xbf/, '').trimStart();
31
+ if (/^<svg[\s>/]/i.test(text) || (/^<\?xml/i.test(text) && /<svg[\s>/]/i.test(s)))
32
+ return 'image/svg+xml';
33
+ if (/^<\?xml/i.test(text))
34
+ return 'application/xml';
35
+ if (/^<!doctype html|^<html[\s>]/i.test(text))
36
+ return 'text/html';
37
+ return undefined;
38
+ }