@patrickdk77/office-defuse 0.1.1 → 0.2.1

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 (82) hide show
  1. package/README.md +211 -43
  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/detect.d.ts +13 -0
  11. package/dist/detect.js +83 -0
  12. package/dist/engine.d.ts +66 -1
  13. package/dist/engine.js +290 -35
  14. package/dist/errors.d.ts +17 -0
  15. package/dist/errors.js +27 -1
  16. package/dist/fields.d.ts +20 -13
  17. package/dist/fields.js +65 -23
  18. package/dist/findings.d.ts +2 -1
  19. package/dist/findings.js +17 -1
  20. package/dist/flat.d.ts +33 -0
  21. package/dist/flat.js +555 -0
  22. package/dist/formats.d.ts +9 -0
  23. package/dist/formats.js +7 -1
  24. package/dist/formulas.d.ts +27 -0
  25. package/dist/formulas.js +157 -1
  26. package/dist/index.d.ts +4 -2
  27. package/dist/index.js +11 -1
  28. package/dist/index.mjs +1 -1
  29. package/dist/io.d.ts +62 -1
  30. package/dist/io.js +245 -1
  31. package/dist/links.d.ts +27 -0
  32. package/dist/links.js +106 -1
  33. package/dist/msg.d.ts +3 -2
  34. package/dist/msg.js +172 -18
  35. package/dist/objects.d.ts +8 -5
  36. package/dist/objects.js +90 -20
  37. package/dist/odf.d.ts +87 -1
  38. package/dist/odf.js +204 -76
  39. package/dist/offcrypto.d.ts +24 -1
  40. package/dist/offcrypto.js +308 -69
  41. package/dist/ole.d.ts +19 -2
  42. package/dist/ole.js +98 -58
  43. package/dist/oleobj.d.ts +11 -0
  44. package/dist/oleobj.js +20 -3
  45. package/dist/ooxml.d.ts +108 -2
  46. package/dist/ooxml.js +392 -109
  47. package/dist/package.d.ts +14 -0
  48. package/dist/package.js +23 -7
  49. package/dist/plan.d.ts +104 -0
  50. package/dist/plan.js +73 -0
  51. package/dist/plugins.d.ts +8 -0
  52. package/dist/plugins.js +35 -0
  53. package/dist/ppt.js +45 -5
  54. package/dist/rewrite-cfb.d.ts +14 -0
  55. package/dist/rewrite-cfb.js +52 -0
  56. package/dist/rewrite-rtf.d.ts +9 -0
  57. package/dist/rewrite-rtf.js +45 -0
  58. package/dist/rewrite-xml.d.ts +36 -0
  59. package/dist/rewrite-xml.js +341 -0
  60. package/dist/rewrite-zip.d.ts +8 -0
  61. package/dist/rewrite-zip.js +498 -0
  62. package/dist/rtf.js +216 -42
  63. package/dist/rtfcomp.d.ts +7 -0
  64. package/dist/rtfcomp.js +80 -0
  65. package/dist/score.js +1 -1
  66. package/dist/sniff.d.ts +5 -0
  67. package/dist/sniff.js +38 -0
  68. package/dist/types.d.ts +66 -6
  69. package/dist/types.js +14 -0
  70. package/dist/vsd.d.ts +13 -0
  71. package/dist/vsd.js +689 -0
  72. package/dist/word.d.ts +4 -4
  73. package/dist/word.js +70 -23
  74. package/dist/xls.d.ts +10 -4
  75. package/dist/xls.js +58 -10
  76. package/dist/xml.d.ts +30 -0
  77. package/dist/xml.js +45 -23
  78. package/dist/zip-writer.d.ts +4 -0
  79. package/dist/zip-writer.js +20 -7
  80. package/dist/zip.d.ts +11 -2
  81. package/dist/zip.js +67 -6
  82. package/package.json +4 -1
package/dist/rtf.js CHANGED
@@ -2,9 +2,11 @@
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");
9
+ const formats_1 = require("./formats");
8
10
  const io_1 = require("./io");
9
11
  const links_1 = require("./links");
10
12
  const objects_1 = require("./objects");
@@ -43,6 +45,8 @@ const MAX_WORD = 250;
43
45
  const CHUNK = 262144;
44
46
  /** Collected text other than an object's data is short: a class name, a template path, a link. */
45
47
  const MAX_TEXT = 4096;
48
+ /** The pictures RTF holds as whole files of their own, by the control word that names their type. */
49
+ const PICTURE_TYPES = { pngblip: 'image/png', jpegblip: 'image/jpeg', emfblip: 'image/emf' };
46
50
  /** An object's data as Word decodes its hex digits, with the quirks rtfobj documents. */
47
51
  class HexData {
48
52
  limit;
@@ -99,8 +103,10 @@ const isDigit = (c) => c >= 0x30 && c <= 0x39;
99
103
  * text after the document's last brace is still read, as rtfobj reads it, and reported as trailing data.
100
104
  */
101
105
  async function inspectRtf(ctx, source, size) {
102
- ctx.format = { format: 'rtf', mimeType: 'application/rtf' };
103
- const reader = new RtfReader(ctx, size);
106
+ ctx.format = formats_1.RTF_FORMAT;
107
+ const cuts = [];
108
+ ctx.run.output = { kind: 'rtf', size, cuts };
109
+ const reader = new RtfReader(ctx, size, cuts);
104
110
  for (let pos = 0; pos < size; pos += CHUNK) {
105
111
  (0, errors_1.checkTime)(ctx.settings.deadline);
106
112
  const chunk = await source.read(pos, Math.min(CHUNK, size - pos));
@@ -111,6 +117,7 @@ async function inspectRtf(ctx, source, size) {
111
117
  class RtfReader {
112
118
  ctx;
113
119
  size;
120
+ cuts;
114
121
  frames;
115
122
  fields;
116
123
  depth = 0;
@@ -130,11 +137,30 @@ class RtfReader {
130
137
  decoded = 0;
131
138
  /** Objects whose groups closed, read in order once the chunk is done. */
132
139
  pending = [];
133
- constructor(ctx, size) {
140
+ /** The offsets of the braces of the groups open now. */
141
+ groups = [];
142
+ /** Where the control word being read ends. */
143
+ wordEnd = 0;
144
+ fieldCount = 0;
145
+ /** The fldinst groups of each field, by the field's number, which go when a finding strips the field. */
146
+ codes = new Map();
147
+ /** Pictures go to the plugins only when there are any, or expected verdicts to check, so only then are they decoded. */
148
+ pictures;
149
+ pendingPictures = [];
150
+ constructor(ctx, size,
151
+ /** Where the rewrite cuts, which the rules fill in as their findings strip. */
152
+ cuts) {
134
153
  this.ctx = ctx;
135
154
  this.size = size;
136
- this.frames = [{ kind: 'document', depth: 0, uc: 1 }];
137
- this.fields = new fields_1.FieldStack(ctx, 'document');
155
+ this.cuts = cuts;
156
+ this.frames = [{ kind: 'document', depth: 0, uc: 1, at: 0 }];
157
+ this.pictures = (ctx.run.options.filePlugins?.length ?? 0) > 0 || (ctx.run.verify?.size ?? 0) > 0;
158
+ // A field a strip finding names loses its instructions and keeps its result.
159
+ this.fields = new fields_1.FieldStack(ctx, 'document', marks => {
160
+ for (const [start, end] of this.codes.get(marks.begin) ?? [])
161
+ if (end >= 0)
162
+ cuts.push({ start, end });
163
+ });
138
164
  }
139
165
  get top() {
140
166
  return this.frames[this.frames.length - 1];
@@ -152,6 +178,11 @@ class RtfReader {
152
178
  }
153
179
  else if (top.kind === 'objskip')
154
180
  top.data.hex.binary(b.subarray(i, i + n));
181
+ else if (top.kind === 'pict') {
182
+ if (top.picture.dataAt < 0)
183
+ top.picture.dataAt = this.base + i;
184
+ top.hex.binary(b.subarray(i, i + n));
185
+ }
155
186
  this.note(b, i, i + n);
156
187
  this.binLeft -= n;
157
188
  i += n;
@@ -160,12 +191,12 @@ class RtfReader {
160
191
  const c = b[i];
161
192
  if (c === 0x7b) {
162
193
  this.note(b, i, i + 1);
163
- this.open();
194
+ this.open(this.base + i);
164
195
  i++;
165
196
  }
166
197
  else if (c === 0x7d) {
167
198
  this.note(b, i, i + 1);
168
- this.close(this.base + i);
199
+ this.close(this.base + i + 1);
169
200
  i++;
170
201
  }
171
202
  else if (c === 0x5c) {
@@ -190,7 +221,9 @@ class RtfReader {
190
221
  }
191
222
  async objects() {
192
223
  for (const o of this.pending.splice(0))
193
- await objectRules(this.ctx, o);
224
+ await objectRules(this.ctx, o, this.cuts);
225
+ for (const p of this.pendingPictures.splice(0))
226
+ await pictureRules(this.ctx, p, this.cuts);
194
227
  }
195
228
  async end() {
196
229
  // A file can end inside groups. Word reads what it has, and so does this.
@@ -198,8 +231,8 @@ class RtfReader {
198
231
  this.close(this.size);
199
232
  this.fields.finish();
200
233
  await this.objects();
201
- if (this.trailing > 0)
202
- this.ctx.report(types_1.OfficeCategory.Container, types_1.OfficeDetail.TrailingBytes, undefined, { bytes: this.trailing });
234
+ if (this.trailing > 0 && this.ctx.report(types_1.OfficeCategory.Container, types_1.OfficeDetail.TrailingBytes, undefined, { bytes: this.trailing }).action === 'strip')
235
+ this.cuts.push({ start: this.closedAt, end: this.size });
203
236
  }
204
237
  /** Counts bytes past the document's outermost group that are not white space or NUL. */
205
238
  note(b, from, to) {
@@ -209,23 +242,26 @@ class RtfReader {
209
242
  if (b[k] > 0x20)
210
243
  this.trailing++;
211
244
  }
212
- open() {
245
+ open(offset) {
246
+ this.groups.push(offset);
213
247
  this.depth++;
214
248
  this.skipChars = 0;
215
249
  const limit = this.ctx.settings.rtfDepth;
216
250
  if (this.depth > limit)
217
251
  throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.RtfDepth, `Groups nest deeper than ${limit}`, { limit });
218
252
  }
219
- close(offset) {
253
+ /** Closes the group open now. `end` is the offset after its brace, or the end of the file. */
254
+ close(end) {
220
255
  this.skipChars = 0;
221
256
  this.starred = false;
222
257
  while (this.frames.length > 1 && this.top.depth >= this.depth)
223
- this.finishFrame(this.frames.pop());
258
+ this.finishFrame(this.frames.pop(), end);
224
259
  if (this.depth === 0)
225
260
  return;
226
261
  this.depth--;
262
+ this.groups.pop();
227
263
  if (this.depth === 0 && this.closedAt < 0)
228
- this.closedAt = offset;
264
+ this.closedAt = end;
229
265
  }
230
266
  /**
231
267
  * 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 +270,11 @@ class RtfReader {
234
270
  push(body) {
235
271
  const top = this.top;
236
272
  if (this.frames.length > 1 && top.depth === this.depth && top.kind !== 'field' && top.kind !== 'object')
237
- this.finishFrame(this.frames.pop());
273
+ this.finishFrame(this.frames.pop(), -1);
238
274
  const limit = this.ctx.settings.rtfDepth;
239
275
  if (this.frames.length > limit)
240
276
  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 });
277
+ this.frames.push({ ...body, depth: this.depth, uc: this.top.uc, at: this.groups[this.depth - 1] ?? 0 });
242
278
  }
243
279
  /** Reads a control word or symbol at `i`. Returns the bytes it takes, or 0 when the buffer ends inside it. */
244
280
  control(b, i, last) {
@@ -277,6 +313,7 @@ class RtfReader {
277
313
  const param = digits === '' ? undefined : Number(digits);
278
314
  // A space ends the control word and belongs to it.
279
315
  const used = k - i + (k < b.length && b[k] === 0x20 ? 1 : 0);
316
+ this.wordEnd = this.base + i + used;
280
317
  this.word(name, param, this.base + i);
281
318
  return used;
282
319
  }
@@ -287,6 +324,10 @@ class RtfReader {
287
324
  if (name === 'bin') {
288
325
  // Word reads a negative or missing length as none.
289
326
  this.binLeft = param !== undefined && param > 0 ? Math.min(param, Number.MAX_SAFE_INTEGER) : 0;
327
+ // A picture's data that starts with \bin is replaced from the control word on, so no length is left behind.
328
+ const pict = this.top;
329
+ if (pict.kind === 'pict' && pict.picture.dataAt < 0)
330
+ pict.picture.dataAt = offset;
290
331
  return;
291
332
  }
292
333
  const top = this.top;
@@ -304,23 +345,44 @@ class RtfReader {
304
345
  return;
305
346
  }
306
347
  const object = this.object();
307
- if (INERT.has(name))
348
+ if (name === 'pict' && this.pictures) {
349
+ const limit = this.ctx.settings.decompressedBytes;
350
+ this.push({ kind: 'pict', picture: { offset, dataAt: -1, group: [0, -1] }, hex: new HexData(offset, limit) });
351
+ const opened = this.top;
352
+ if (opened.kind === 'pict')
353
+ opened.picture.group[0] = opened.at;
354
+ }
355
+ else if (top.kind === 'pict' && Object.hasOwn(PICTURE_TYPES, name))
356
+ top.picture.type = PICTURE_TYPES[name];
357
+ else if (INERT.has(name))
308
358
  this.push({ kind: 'skip' });
309
359
  else if (name === 'field') {
310
- this.fields.begin();
311
- this.push({ kind: 'field' });
360
+ const field = this.fieldCount++;
361
+ this.fields.begin(field);
362
+ this.push({ kind: 'field', field });
312
363
  }
313
364
  else if (name === 'fldinst')
314
- this.push({ kind: 'fldinst' });
365
+ this.push({ kind: 'fldinst', field: this.field() });
315
366
  else if (name === 'fldrslt') {
316
367
  this.fields.separate();
317
368
  this.push({ kind: 'fldrslt' });
318
369
  }
319
- else if (name === 'object')
370
+ else if (name === 'object') {
320
371
  this.push({ kind: 'object', object: { offset, kinds: new Set(), update: false, className: '' } });
372
+ const opened = this.top;
373
+ if (opened.kind === 'object')
374
+ opened.object.group = [opened.at, -1];
375
+ }
321
376
  else if (name === 'objupdate') {
322
- if (object)
377
+ if (object) {
323
378
  object.update = true;
379
+ object.updateAt ??= [offset, this.wordEnd];
380
+ }
381
+ }
382
+ else if (name === 'result' && object && !object.result) {
383
+ // What Word shows in place of the object, which stays when the object goes.
384
+ object.result = [this.wordEnd, -1];
385
+ this.push({ kind: 'result', object });
324
386
  }
325
387
  else if (OBJECT_KINDS.has(name))
326
388
  object?.kinds.add(name);
@@ -350,6 +412,15 @@ class RtfReader {
350
412
  else if (starred)
351
413
  this.push({ kind: 'skip' });
352
414
  }
415
+ /** The number of the innermost field the reader is in, or -1. */
416
+ field() {
417
+ for (let k = this.frames.length - 1; k > 0; k--) {
418
+ const f = this.frames[k];
419
+ if (f.kind === 'field')
420
+ return f.field;
421
+ }
422
+ return -1;
423
+ }
353
424
  /** The object of the innermost object group the reader is in. */
354
425
  object() {
355
426
  for (let k = this.frames.length - 1; k > 0; k--) {
@@ -373,6 +444,10 @@ class RtfReader {
373
444
  top.data.hex.quote();
374
445
  return;
375
446
  }
447
+ if (top.kind === 'pict') {
448
+ top.hex.quote();
449
+ return;
450
+ }
376
451
  // A file can end before the two characters of a \'hh.
377
452
  const hi = hexValue(b[at] ?? -1);
378
453
  const lo = hexValue(b[at + 1] ?? -1);
@@ -384,6 +459,17 @@ class RtfReader {
384
459
  if (top.kind === 'skip')
385
460
  return;
386
461
  this.starred = false;
462
+ if (top.kind === 'pict') {
463
+ for (let k = from; k < to; k++) {
464
+ const v = hexValue(b[k]);
465
+ if (v < 0)
466
+ continue;
467
+ if (top.picture.dataAt < 0)
468
+ top.picture.dataAt = this.base + k;
469
+ top.hex.digit(v);
470
+ }
471
+ return;
472
+ }
387
473
  if (top.kind === 'objdata' || top.kind === 'objskip') {
388
474
  const both = top.kind === 'objdata';
389
475
  const hex = both ? top.hex : top.data.hex;
@@ -421,12 +507,41 @@ class RtfReader {
421
507
  const top = this.top;
422
508
  if (top.kind === 'fldinst')
423
509
  this.fields.text(s);
510
+ else if (top.kind === 'fldrslt')
511
+ this.fields.result(s);
424
512
  else if ((top.kind === 'objclass' || top.kind === 'template' || top.kind === 'hlsrc') && top.text.length < MAX_TEXT)
425
513
  top.text += s;
426
514
  }
427
- finishFrame(f) {
515
+ /** Ends a destination. `end` is where its group ends, or -1 when another destination in its group ended it. */
516
+ finishFrame(f, end) {
517
+ // A cut takes a whole group, which only a group that closed has.
518
+ const cut = (r) => {
519
+ if (r.action === 'strip' && end >= 0)
520
+ this.cuts.push({ start: f.at, end });
521
+ };
428
522
  if (f.kind === 'field')
429
523
  this.fields.end();
524
+ else if (f.kind === 'fldinst') {
525
+ const list = this.codes.get(f.field) ?? [];
526
+ list.push([f.at, end]);
527
+ this.codes.set(f.field, list);
528
+ }
529
+ else if (f.kind === 'result') {
530
+ if (f.object.result)
531
+ f.object.result[1] = end;
532
+ }
533
+ else if (f.kind === 'pict') {
534
+ const data = f.hex.result();
535
+ this.decoded += data.length;
536
+ const total = this.ctx.settings.totalDecompressedBytes;
537
+ if (this.decoded > total)
538
+ throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.TotalDecompressedSize, `The objects and pictures decode past ${total} bytes`, { limit: total });
539
+ f.picture.group[1] = end;
540
+ if (f.picture.type && data.length) {
541
+ f.picture.data = data;
542
+ this.pendingPictures.push(f.picture);
543
+ }
544
+ }
430
545
  else if (f.kind === 'objclass') {
431
546
  if (f.object)
432
547
  f.object.className = f.text.trim();
@@ -434,14 +549,15 @@ class RtfReader {
434
549
  else if (f.kind === 'template') {
435
550
  const path = f.text.trim();
436
551
  if (path)
437
- this.ctx.external(types_1.OfficeDetail.Template, path, 'template', { source: 'RTF template' }, 'template');
552
+ cut(this.ctx.external(types_1.OfficeDetail.Template, path, 'template', { source: 'RTF template' }, 'template'));
438
553
  }
439
554
  else if (f.kind === 'hlsrc') {
440
555
  const target = f.text.trim();
441
556
  if (target && !(0, links_1.isFragment)(target))
442
- this.ctx.link(target, 'hyperlink', { source: 'RTF shape hyperlink' });
557
+ cut(this.ctx.link(target, 'hyperlink', { source: 'RTF shape hyperlink' }));
443
558
  }
444
559
  else if (f.kind === 'objdata') {
560
+ f.object.objdata = [f.at, end];
445
561
  f.object.data = f.hex.result();
446
562
  const alt = f.alt.result();
447
563
  if (!Buffer.from(alt).equals(Buffer.from(f.object.data)))
@@ -454,28 +570,77 @@ class RtfReader {
454
570
  if (f.standalone)
455
571
  this.pending.push(f.object);
456
572
  }
457
- else if (f.kind === 'object')
573
+ else if (f.kind === 'object') {
574
+ if (f.object.group)
575
+ f.object.group[1] = end;
458
576
  this.pending.push(f.object);
577
+ }
459
578
  }
460
579
  }
461
- /** One embedded or linked object: what its OLE 1.0 header names, and what its data holds. */
462
- async function objectRules(ctx, o) {
580
+ /**
581
+ * One embedded or linked object: what its OLE 1.0 header names, and what its data holds. When anything read inside it
582
+ * makes a strip finding, the object's group goes and its result group, the picture Word shows, takes its place.
583
+ * \objupdate, which ACTION/OBJECT_UPDATE names, goes on its own.
584
+ */
585
+ async function objectRules(ctx, o, cuts) {
463
586
  const location = `object at offset ${o.offset}`;
464
587
  const kinds = [...o.kinds];
465
588
  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')) {
589
+ if (o.update && ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.ObjectUpdate, location, base).action === 'strip' && o.updateAt)
590
+ cuts.push({ start: o.updateAt[0], end: o.updateAt[1] });
591
+ const before = ctx.strips;
592
+ if (o.kinds.has('objocx'))
469
593
  ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, location, { ...base, ...(o.className ? { className: (0, context_1.clip)(o.className) } : {}) });
594
+ else {
595
+ await objectData(ctx, o, o.data, location, base, cuts);
596
+ // The data read without the groups \* marks, where that reads differently, is the object a reader that skips them sees.
597
+ if (o.alt)
598
+ await objectData(ctx, o, o.alt, location, { ...base, reading: 'without the groups \\* marks' });
599
+ }
600
+ if (ctx.strips === before)
601
+ return;
602
+ const [start, end] = o.group ?? o.objdata ?? [-1, -1];
603
+ if (end < 0)
604
+ return;
605
+ const result = o.group && o.result && o.result[1] >= 0 ? o.result : undefined;
606
+ cuts.push(result ? { start, end, text: Buffer.from('{', 'latin1'), copy: result } : { start, end });
607
+ }
608
+ /** Bytes as the hex digits of an objdata group, in lines of 128 digits as Word writes them. */
609
+ function objdataGroup(bytes) {
610
+ const hex = Buffer.from(bytes).toString('hex');
611
+ const lines = [];
612
+ for (let i = 0; i < hex.length; i += 128)
613
+ lines.push(hex.slice(i, i + 128));
614
+ return Buffer.from(`{\\*\\objdata\r\n${lines.join('\r\n')}}`, 'latin1');
615
+ }
616
+ /**
617
+ * A PNG, JPEG or EMF picture goes to the plugins, as the file it is. A plugin that removes it removes its \pict group,
618
+ * and bytes a plugin wrote take the place of its data. A picture no plugin takes stays, since no rule names it.
619
+ */
620
+ async function pictureRules(ctx, p, cuts) {
621
+ const data = p.data ?? new Uint8Array(0);
622
+ 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) });
623
+ const [start, end] = p.group;
624
+ if (!decision || end < 0)
470
625
  return;
626
+ if (!decision.keep)
627
+ cuts.push({ start, end });
628
+ else if (decision.replacement && p.dataAt >= 0) {
629
+ const hex = Buffer.from(await (0, io_1.readAll)(decision.replacement)).toString('hex');
630
+ const lines = [];
631
+ for (let i = 0; i < hex.length; i += 128)
632
+ lines.push(hex.slice(i, i + 128));
633
+ // The data runs to the brace that closes the group, which stays.
634
+ cuts.push({ start: p.dataAt, end: end - 1, text: Buffer.from(lines.join('\r\n'), 'latin1') });
471
635
  }
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
636
  }
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) {
637
+ /**
638
+ * An object's data: its OLE 1.0 header, and the Packager data or storage it holds. With `cuts`, for the first reading
639
+ * of the data, the file it holds goes to the plugins first: the file a Packager object holds, or the native data of
640
+ * any other embedded object. A plugin's verdict decides whether the object stays, and the bytes a plugin wrote take
641
+ * the place of the file in the object's data.
642
+ */
643
+ async function objectData(ctx, o, bytes, location, base, cuts) {
479
644
  const ole1 = bytes?.length ? (0, oleobj_1.parseOle1)(bytes) : undefined;
480
645
  const className = (ole1?.className || o.className).replace(/\0+$/, '');
481
646
  const data = { ...base, ...(className ? { className: (0, context_1.clip)(className) } : {}) };
@@ -494,11 +659,20 @@ async function objectData(ctx, o, bytes, location, base) {
494
659
  if (!ole1)
495
660
  data.reason = 'its data is not an OLE 1.0 object';
496
661
  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));
662
+ const pkg = native && className.toLowerCase() === 'package' ? (0, oleobj_1.parsePackage)(native) : undefined;
663
+ let decision;
664
+ if (cuts && native && ole1?.dataAt !== undefined && className.toLowerCase() !== 'ole2link') {
665
+ const held = pkg?.embedded ? native.subarray(pkg.dataAt, pkg.dataAt + pkg.size) : native;
666
+ 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) });
667
+ if (decision?.replacement && o.objdata && o.objdata[1] >= 0) {
668
+ const replaced = await (0, io_1.readAll)(decision.replacement);
669
+ const newNative = pkg?.embedded ? (0, oleobj_1.replaceSized)(native, pkg.dataAt, pkg.size, replaced) : replaced;
670
+ cuts.push({ start: o.objdata[0], end: o.objdata[1], text: objdataGroup((0, oleobj_1.replaceSized)(bytes, ole1.dataAt, native.length, newNative)) });
671
+ }
501
672
  }
673
+ const act = (0, contained_1.presence)(decision);
674
+ if (pkg)
675
+ Object.assign(data, (0, objects_1.packageData)(pkg));
502
676
  else if (native && native.length >= 8 && cfb_1.CFB_SIGNATURE.every((v, i) => native[i] === v)) {
503
677
  let cfb;
504
678
  try {
@@ -533,7 +707,7 @@ async function objectData(ctx, o, bytes, location, base) {
533
707
  if (className.toLowerCase() === 'ole2link')
534
708
  ctx.report(types_1.OfficeCategory.External, types_1.OfficeDetail.Object, location, data);
535
709
  else
536
- ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, location, data);
710
+ ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, location, data, act);
537
711
  if (equation)
538
712
  ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Equation, location, data);
539
713
  }
@@ -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
+ }