@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/ooxml.js CHANGED
@@ -1,26 +1,37 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.relsSource = exports.R_NS = exports.SHEET_NS = exports.WORD_NS = exports.isXmlType = exports.TYPE = exports.REL = exports.XPS_NS = exports.XPS_MAIN = exports.RELS_NS = exports.CONTENT_TYPES_NS = void 0;
4
+ exports.resolveTarget = resolveTarget;
5
+ exports.resolvePartRef = resolvePartRef;
6
+ exports.xpsResources = xpsResources;
7
+ exports.readRelationships = readRelationships;
3
8
  exports.inspectOoxml = inspectOoxml;
4
9
  const biff_1 = require("./biff");
10
+ const contained_1 = require("./contained");
5
11
  const context_1 = require("./context");
6
12
  const errors_1 = require("./errors");
7
13
  const fields_1 = require("./fields");
8
14
  const formats_1 = require("./formats");
9
15
  const formulas_1 = require("./formulas");
10
16
  const links_1 = require("./links");
17
+ const plan_1 = require("./plan");
11
18
  const types_1 = require("./types");
12
19
  const xlsb_1 = require("./xlsb");
13
20
  const xml_1 = require("./xml");
14
21
  const zip_1 = require("./zip");
15
- const CONTENT_TYPES_NS = 'http://schemas.openxmlformats.org/package/2006/content-types';
16
- const RELS_NS = 'http://schemas.openxmlformats.org/package/2006/relationships';
22
+ exports.CONTENT_TYPES_NS = 'http://schemas.openxmlformats.org/package/2006/content-types';
23
+ exports.RELS_NS = 'http://schemas.openxmlformats.org/package/2006/relationships';
17
24
  const T = 'http://schemas.openxmlformats.org/officedocument/2006/relationships/';
18
25
  const S = 'http://purl.oclc.org/ooxml/officedocument/relationships/';
19
26
  const MS = 'http://schemas.microsoft.com/office/2006/relationships/';
20
27
  const both = (name) => [T + name, S + name];
28
+ /** The relationship types that name an XPS package's FixedDocumentSequence, lowercased. */
29
+ exports.XPS_MAIN = { xps: 'http://schemas.microsoft.com/xps/2005/06/fixedrepresentation', oxps: 'http://schemas.openxps.org/oxps/v1.0/fixedrepresentation' };
30
+ /** The XPS and OpenXPS markup namespaces, lowercased. */
31
+ exports.XPS_NS = new Set(['http://schemas.microsoft.com/xps/2005/06', 'http://schemas.openxps.org/oxps/v1.0']);
21
32
  /** Relationship types, lowercased, as OPC compares them without regard to case. */
22
- const REL = {
23
- main: new Set([...both('officedocument'), 'http://schemas.microsoft.com/visio/2010/relationships/document']),
33
+ exports.REL = {
34
+ main: new Set([...both('officedocument'), 'http://schemas.microsoft.com/visio/2010/relationships/document', exports.XPS_MAIN.xps, exports.XPS_MAIN.oxps]),
24
35
  hyperlink: new Set(both('hyperlink')),
25
36
  template: new Set(both('attachedtemplate')),
26
37
  image: new Set([...both('image'), 'http://schemas.microsoft.com/office/2007/relationships/hdphoto']),
@@ -46,7 +57,7 @@ const REACHED_AS = new Map([
46
57
  [`${MS}activexcontrolbinary`, AS.controlBinary],
47
58
  ]);
48
59
  /** Content types, lowercased. */
49
- const TYPE = {
60
+ exports.TYPE = {
50
61
  vba: new Set(['application/vnd.ms-office.vbaproject']),
51
62
  macrosheet: new Set(['application/vnd.ms-excel.macrosheet+xml', 'application/vnd.ms-excel.intlmacrosheet+xml', 'application/vnd.ms-excel.macrosheet', 'application/vnd.ms-excel.intlmacrosheet']),
52
63
  ole: new Set(['application/vnd.openxmlformats-officedocument.oleobject']),
@@ -55,30 +66,50 @@ const TYPE = {
55
66
  vml: 'application/vnd.openxmlformats-officedocument.vmldrawing',
56
67
  xlsbExternalLink: 'application/vnd.ms-excel.externallink',
57
68
  };
69
+ /** Whether the rules read a part of this content type as XML. */
70
+ const isXmlType = (ct) => ct !== undefined && (ct.endsWith('+xml') || ct === 'application/xml' || ct === 'text/xml' || ct === exports.TYPE.vml);
71
+ exports.isXmlType = isXmlType;
72
+ /**
73
+ * Binary parts that belong to the package's own format rather than being files of their own: the records of a binary
74
+ * workbook, printer settings, obfuscated fonts, VBA project signatures and the origin of digital signatures.
75
+ */
76
+ const formatBinary = (ct) => ct.startsWith('application/vnd.ms-excel.') ||
77
+ ct.endsWith('.printersettings') ||
78
+ ct === 'application/vnd.openxmlformats-officedocument.obfuscatedfont' ||
79
+ ct === 'application/vnd.ms-package.obfuscated-opentype' ||
80
+ ct.startsWith('application/vnd.ms-office.vbaprojectsignature') ||
81
+ ct === 'application/vnd.openxmlformats-package.digital-signature-origin';
58
82
  /** Relationship types that leave the package, by the finding they make. Any other is EXTERNAL/OTHER. */
59
83
  const EXTERNAL_KINDS = [
60
- [REL.template, types_1.OfficeDetail.Template],
84
+ [exports.REL.template, types_1.OfficeDetail.Template],
61
85
  [new Set([...both('oleobject'), ...both('package')]), types_1.OfficeDetail.Object],
62
- [REL.image, types_1.OfficeDetail.Image],
63
- [REL.frame, types_1.OfficeDetail.Frame],
64
- [REL.subDocument, types_1.OfficeDetail.Include],
65
- [REL.workbook, types_1.OfficeDetail.Workbook],
86
+ [exports.REL.image, types_1.OfficeDetail.Image],
87
+ [exports.REL.frame, types_1.OfficeDetail.Frame],
88
+ [exports.REL.subDocument, types_1.OfficeDetail.Include],
89
+ [exports.REL.workbook, types_1.OfficeDetail.Workbook],
66
90
  ];
67
- const WORD_NS = new Set(['http://schemas.openxmlformats.org/wordprocessingml/2006/main', 'http://purl.oclc.org/ooxml/wordprocessingml/main']);
68
- const SHEET_NS = new Set(['http://schemas.openxmlformats.org/spreadsheetml/2006/main', 'http://purl.oclc.org/ooxml/spreadsheetml/main']);
91
+ exports.WORD_NS = new Set(['http://schemas.openxmlformats.org/wordprocessingml/2006/main', 'http://purl.oclc.org/ooxml/wordprocessingml/main']);
92
+ exports.SHEET_NS = new Set(['http://schemas.openxmlformats.org/spreadsheetml/2006/main', 'http://purl.oclc.org/ooxml/spreadsheetml/main']);
69
93
  const XDR_NS = new Set(['http://schemas.openxmlformats.org/drawingml/2006/spreadsheetdrawing', 'http://purl.oclc.org/ooxml/drawingml/spreadsheetdrawing']);
70
- const R_NS = new Set(['http://schemas.openxmlformats.org/officedocument/2006/relationships', 'http://purl.oclc.org/ooxml/officedocument/relationships']);
94
+ exports.R_NS = new Set(['http://schemas.openxmlformats.org/officedocument/2006/relationships', 'http://purl.oclc.org/ooxml/officedocument/relationships']);
71
95
  const CUSTOM_UI_NS = new Set(['http://schemas.microsoft.com/office/2006/01/customui', 'http://schemas.microsoft.com/office/2009/07/customui']);
72
96
  const VML_EXCEL_NS = 'urn:schemas-microsoft-com:office:excel';
73
97
  const VISIO_NS = 'http://schemas.microsoft.com/office/visio/2012/main';
98
+ const EXTENDED_NS = new Set(['http://schemas.openxmlformats.org/officedocument/2006/extended-properties', 'http://purl.oclc.org/ooxml/officedocument/extendedproperties']);
99
+ /**
100
+ * The attributes of a SpreadsheetML data connection that name where it loads from (ECMA-376 18.13): an ODC file or an
101
+ * offline cube on the connection, a connection string and command, a web query's URL, or a text file.
102
+ */
103
+ const CONNECTION_SOURCES = { connection: ['odcFile', 'sourceFile'], dbPr: ['connection', 'command'], webPr: ['url'], textPr: ['sourceFile'] };
74
104
  const VISIO_CODE = /\b(RUNADDON|RUNADDONWARGS|CALLTHIS|RUNMACRO|OPENFILE|DOOLEVERB)\s*\(/i;
75
105
  /** A Word attribute, qualified as Word writes it or unqualified. */
76
- const wordAttr = (el, local) => el.attributes.find(a => a.local === local && (a.ns === '' || WORD_NS.has(a.ns.toLowerCase())))?.value;
106
+ const wordAttr = (el, local) => el.attributes.find(a => a.local === local && (a.ns === '' || exports.WORD_NS.has(a.ns.toLowerCase())))?.value;
77
107
  /** The key of the part a relationships part belongs to, '' for the package, or undefined for any other part. */
78
108
  const relsSource = (relsName) => {
79
109
  const m = /^(.*?)_rels\/([^/]*)\.rels$/i.exec(relsName);
80
110
  return m ? (0, zip_1.nameKey)(m[1] + m[2]) : undefined;
81
111
  };
112
+ exports.relsSource = relsSource;
82
113
  /**
83
114
  * Resolves an internal target against the folder of the part it belongs to, as RFC 3986 resolves a relative
84
115
  * reference: a ".." at the root of the package stays at the root. LibreOffice writes "../customXml/item1.xml" in the
@@ -96,6 +127,52 @@ function resolveTarget(relsName, target) {
96
127
  }
97
128
  return (0, zip_1.nameKey)(out.join('/'));
98
129
  }
130
+ /** A reference in an XPS part to another part: an absolute part name, or a path relative to the part's folder. */
131
+ function resolvePartRef(part, ref) {
132
+ const folder = part.includes('/') ? part.slice(0, part.lastIndexOf('/') + 1) : '';
133
+ const path = (ref.startsWith('/') ? ref.slice(1) : folder + ref).split('#')[0];
134
+ const out = [];
135
+ for (const seg of path.split('/')) {
136
+ if (seg === '..')
137
+ out.pop();
138
+ else if (seg !== '.' && seg !== '')
139
+ out.push(seg);
140
+ }
141
+ return (0, zip_1.nameKey)(out.join('/'));
142
+ }
143
+ /** The URIs an XPS ImageSource names: the value itself, or a ColorConvertedBitmap's image and color profile. */
144
+ function imageSourceUris(value) {
145
+ const t = value.trim();
146
+ if (!t.startsWith('{'))
147
+ return [t];
148
+ const words = t
149
+ .replace(/^\{|\}$/g, '')
150
+ .trim()
151
+ .split(/\s+/);
152
+ return words[0] === 'ColorConvertedBitmap' ? words.slice(1) : [];
153
+ }
154
+ /**
155
+ * The resources an XPS element names in its attributes, as the attribute and the URI: a document, page or resource
156
+ * dictionary's Source, a font's FontUri, an image brush's image and profile, and a ContextColor's profile.
157
+ */
158
+ function xpsResources(el) {
159
+ const out = [];
160
+ for (const a of el.attributes) {
161
+ if (a.ns !== '')
162
+ continue;
163
+ if (a.local === 'Source' || a.local === 'FontUri')
164
+ out.push([a.local, a.value.trim()]);
165
+ else if (a.local === 'ImageSource')
166
+ for (const uri of imageSourceUris(a.value))
167
+ out.push([a.local, uri]);
168
+ else {
169
+ const m = /^\s*ContextColor\s+(\S+)/.exec(a.value);
170
+ if (m)
171
+ out.push([a.local, m[1]]);
172
+ }
173
+ }
174
+ return out.filter(([, uri]) => uri !== '');
175
+ }
99
176
  const duplicate = (ctx, location, kind, name) => ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.DuplicateEntry, location, { kind, name: (0, context_1.clip)(name) });
100
177
  /**
101
178
  * The defaults and overrides of [Content_Types].xml. Only those that type an entry of the archive are kept, so a
@@ -108,7 +185,7 @@ async function readContentTypes(ctx, entry) {
108
185
  const overrides = new Map();
109
186
  await ctx.parse(entry, {
110
187
  start(el) {
111
- if (el.ns.toLowerCase() !== CONTENT_TYPES_NS)
188
+ if (el.ns.toLowerCase() !== exports.CONTENT_TYPES_NS)
112
189
  return;
113
190
  const type = (0, xml_1.attr)(el, '', 'ContentType')?.trim().toLowerCase();
114
191
  if (type === undefined)
@@ -141,7 +218,7 @@ async function readRelationships(ctx, entry, each) {
141
218
  const ids = new Set();
142
219
  await ctx.parse(entry, {
143
220
  start(el) {
144
- if (el.ns.toLowerCase() !== RELS_NS || el.local !== 'Relationship')
221
+ if (el.ns.toLowerCase() !== exports.RELS_NS || el.local !== 'Relationship')
145
222
  return;
146
223
  const id = (0, xml_1.attr)(el, '', 'Id') ?? '';
147
224
  if (ids.has(id))
@@ -155,35 +232,48 @@ async function readRelationships(ctx, entry, each) {
155
232
  },
156
233
  });
157
234
  }
158
- /** Word field codes in WordprocessingML: fldSimple, and fldChar marks around instrText. */
235
+ /**
236
+ * Word field codes in WordprocessingML: fldSimple, and fldChar marks around instrText. A field a strip finding names
237
+ * goes into the plan by the element numbers of its marks.
238
+ */
159
239
  class Fields {
160
240
  stack;
161
241
  capture = false;
242
+ /** Inside a w:t, whose text is part of a field's result when a field's code has ended. */
243
+ shown = false;
162
244
  constructor(ctx, part) {
163
- this.stack = new fields_1.FieldStack(ctx, part);
245
+ this.stack = new fields_1.FieldStack(ctx, part, marks => ctx.plan.part(part).fields.push(marks));
164
246
  }
165
247
  start(el) {
166
248
  if (el.local === 'fldSimple')
167
- this.stack.simple(wordAttr(el, 'instr') ?? '');
249
+ this.stack.simpleBegin(wordAttr(el, 'instr') ?? '', el.index);
168
250
  else if (el.local === 'fldChar') {
169
251
  const kind = wordAttr(el, 'fldCharType');
170
252
  if (kind === 'begin')
171
- this.stack.begin();
253
+ this.stack.begin(el.index);
172
254
  else if (kind === 'separate')
173
- this.stack.separate();
255
+ this.stack.separate(el.index);
174
256
  else if (kind === 'end')
175
- this.stack.end();
257
+ this.stack.end(el.index);
176
258
  }
177
259
  else if (el.local === 'instrText' || el.local === 'delInstrText')
178
260
  this.capture = true;
261
+ else if (el.local === 't')
262
+ this.shown = true;
179
263
  }
180
264
  end(el) {
181
265
  if (el.local === 'instrText' || el.local === 'delInstrText')
182
266
  this.capture = false;
267
+ else if (el.local === 't')
268
+ this.shown = false;
269
+ else if (el.local === 'fldSimple')
270
+ this.stack.simpleEnd();
183
271
  }
184
272
  text(t) {
185
273
  if (this.capture)
186
274
  this.stack.text(t);
275
+ else if (this.shown)
276
+ this.stack.result(t);
187
277
  }
188
278
  finish() {
189
279
  this.stack.finish();
@@ -211,6 +301,19 @@ class PartRules {
211
301
  */
212
302
  formula;
213
303
  macroText;
304
+ /** The part is an Excel 4.0 macro sheet, whose formulas are its macros and which its own finding covers. */
305
+ macroSheet = false;
306
+ /** Word's hyperlink base, in the extended properties, which relative hyperlinks resolve against. */
307
+ hyperlinkBase;
308
+ /** The parts this XPS part names in its markup, by key, which count as reached from it. */
309
+ refs = new Set();
310
+ /**
311
+ * The labels of the hyperlinks the part names by relationship Id: their text, tooltips and display text. The
312
+ * relationships part comes after the part, so the targets are compared with these as it is read.
313
+ */
314
+ labels = new Map();
315
+ /** A hyperlink whose text is being read, with the depth of the element whose end ends it. */
316
+ label;
214
317
  /** The last namespace seen and its lowercase form. Neighboring elements share one, and this spares a copy each. */
215
318
  ns = '';
216
319
  lower = '';
@@ -219,6 +322,11 @@ class PartRules {
219
322
  this.part = part;
220
323
  this.fields = new Fields(ctx, part);
221
324
  }
325
+ /** Records that the rewrite removes the element, when the finding strips. */
326
+ remove(r, el) {
327
+ if (r.action === 'strip')
328
+ this.ctx.plan.element(this.part, el.index, { remove: true });
329
+ }
222
330
  lowerNs(el) {
223
331
  if (el.ns !== this.ns) {
224
332
  this.ns = el.ns;
@@ -226,45 +334,88 @@ class PartRules {
226
334
  }
227
335
  return this.lower;
228
336
  }
337
+ /** Adds labels for the hyperlink a relationship Id names. */
338
+ addLabels(id, labels) {
339
+ const list = this.labels.get(id) ?? [];
340
+ for (const l of labels)
341
+ if (l?.trim())
342
+ list.push(l);
343
+ if (list.length)
344
+ this.labels.set(id, list);
345
+ }
346
+ /** The labels a hyperlink element gives: the text of `textDepth`'s element, its tooltip, and an Excel display name. */
347
+ hyperlink(el, textDepth, also) {
348
+ const id = el.attributes.find(a => a.local === 'id' && exports.R_NS.has(a.ns.toLowerCase()))?.value;
349
+ if (id === undefined)
350
+ return;
351
+ if (textDepth === undefined || this.label)
352
+ this.addLabels(id, also);
353
+ else
354
+ this.label = { id, depth: textDepth, text: '', also: also.filter((l) => l !== undefined) };
355
+ }
229
356
  start(el) {
230
357
  const ns = this.lowerNs(el);
231
358
  if (CUSTOM_UI_NS.has(ns)) {
232
359
  if (el.depth === 1)
233
360
  this.ctx.report(types_1.OfficeCategory.Ui, types_1.OfficeDetail.Ribbon, this.part);
234
361
  for (const a of el.attributes) {
235
- if (a.ns === '' && a.value.trim() && /^(on[A-Z]|get[A-Z]|loadImage$)/.test(a.local))
236
- this.ctx.report(types_1.OfficeCategory.Ui, types_1.OfficeDetail.Callback, this.part, { callback: `${a.local}=${(0, context_1.clip)(a.value)}` });
362
+ if (a.ns !== '' || !a.value.trim() || !/^(on[A-Z]|get[A-Z]|loadImage$)/.test(a.local))
363
+ continue;
364
+ // The callback goes and the control stays, doing nothing.
365
+ if (this.ctx.report(types_1.OfficeCategory.Ui, types_1.OfficeDetail.Callback, this.part, { callback: `${a.local}=${(0, context_1.clip)(a.value)}` }).action === 'strip') {
366
+ this.ctx.plan.element(this.part, el.index, { strip: new Set([(0, plan_1.attributeKey)('', a.local)]) });
367
+ }
237
368
  }
238
369
  return;
239
370
  }
240
- if (WORD_NS.has(ns))
371
+ if (el.depth === 1 && el.local === 'macrosheet')
372
+ this.macroSheet = true;
373
+ if (exports.WORD_NS.has(ns)) {
241
374
  this.fields.start(el);
242
- else if (SHEET_NS.has(ns)) {
375
+ if (el.local === 'hyperlink')
376
+ this.hyperlink(el, el.depth, [wordAttr(el, 'tooltip')]);
377
+ }
378
+ else if (exports.SHEET_NS.has(ns)) {
379
+ if (el.local === 'hyperlink')
380
+ this.hyperlink(el, undefined, [(0, xml_1.attr)(el, '', 'display'), (0, xml_1.attr)(el, '', 'tooltip')]);
243
381
  if (el.local === 'f' || el.local === 'definedName')
244
- this.formula = { shown: new Shown(), quotes: new formulas_1.Quotes(), ddeBar: false };
382
+ this.formula = { shown: new Shown(), quotes: new formulas_1.Quotes(), ddeBar: false, index: el.index, calls: new formulas_1.FunctionCalls() };
245
383
  if (el.local === 'definedName') {
246
384
  // Excel runs the macro a name like _xlnm.Auto_Open refers to, as it does in a binary workbook.
247
385
  const name = (0, xml_1.attr)(el, '', 'name') ?? '';
248
386
  const auto = (0, biff_1.autoName)(name);
249
387
  if (auto)
250
- this.ctx.report(types_1.OfficeCategory.Macro, auto.onOpen ? types_1.OfficeDetail.EventOnOpen : types_1.OfficeDetail.Event, this.part, { event: auto.event, name: (0, context_1.clip)(name) });
388
+ this.remove(this.ctx.report(types_1.OfficeCategory.Macro, auto.onOpen ? types_1.OfficeDetail.EventOnOpen : types_1.OfficeDetail.Event, this.part, { event: auto.event, name: (0, context_1.clip)(name) }), el);
251
389
  }
252
390
  else if (el.local === 'ddeLink')
253
- this.ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Link, this.part, { application: (0, context_1.clip)((0, xml_1.attr)(el, '', 'ddeService') ?? ''), topic: (0, context_1.clip)((0, xml_1.attr)(el, '', 'ddeTopic') ?? '') });
391
+ this.remove(this.ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Link, this.part, { application: (0, context_1.clip)((0, xml_1.attr)(el, '', 'ddeService') ?? ''), topic: (0, context_1.clip)((0, xml_1.attr)(el, '', 'ddeTopic') ?? '') }), el);
392
+ else if (Object.hasOwn(CONNECTION_SOURCES, el.local))
393
+ this.connection(el);
254
394
  }
255
395
  else if (XDR_NS.has(ns)) {
256
396
  const macro = (0, xml_1.attr)(el, '', 'macro');
257
- if (macro?.trim())
258
- this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, this.part, { macro: (0, context_1.clip)(macro) });
397
+ // The shape stays, and clicking it runs nothing.
398
+ if (macro?.trim() && this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, this.part, { macro: (0, context_1.clip)(macro) }).action === 'strip') {
399
+ this.ctx.plan.element(this.part, el.index, { strip: new Set([(0, plan_1.attributeKey)('', 'macro')]) });
400
+ }
259
401
  }
260
402
  else if (ns === VML_EXCEL_NS && el.local === 'FmlaMacro')
261
- this.macroText = new Shown();
403
+ this.macroText = { shown: new Shown(), index: el.index };
262
404
  else if (ns === VISIO_NS && el.local === 'Cell')
263
405
  this.visioCell(el);
264
- if (el.local === 'hlinkClick' || el.local === 'hlinkHover' || el.local === 'hlinkMouseOver')
406
+ else if (exports.XPS_NS.has(ns))
407
+ this.xps(el);
408
+ else if (EXTENDED_NS.has(ns) && el.local === 'HyperlinkBase')
409
+ this.hyperlinkBase = { shown: new Shown(), index: el.index };
410
+ if (el.local === 'hlinkClick' || el.local === 'hlinkHover' || el.local === 'hlinkMouseOver') {
265
411
  this.drawingAction(el);
412
+ // In a text run the link sits in the run's properties, and the run's text is what the reader sees.
413
+ this.hyperlink(el, el.depth >= 2 ? el.depth - 2 : undefined, [(0, xml_1.attr)(el, '', 'tooltip')]);
414
+ }
266
415
  }
267
416
  text(t) {
417
+ if (this.label && this.label.text.length < 4096)
418
+ this.label.text += t;
268
419
  this.fields.text(t);
269
420
  const f = this.formula;
270
421
  if (f) {
@@ -273,58 +424,120 @@ class PartRules {
273
424
  for (const c of t)
274
425
  if (f.quotes.outside(c) && c === '|')
275
426
  f.ddeBar = true;
427
+ f.calls.write(t);
276
428
  if (f.entity === undefined && t.includes(xml_1.UNKNOWN))
277
429
  f.entity = (0, xml_1.unknownEntity)(t);
278
430
  }
279
- this.macroText?.add(t);
431
+ this.macroText?.shown.add(t);
432
+ this.hyperlinkBase?.shown.add(t);
280
433
  }
281
434
  end(el) {
282
435
  const ns = this.lowerNs(el);
283
- if (WORD_NS.has(ns))
436
+ if (this.label && el.depth === this.label.depth) {
437
+ this.addLabels(this.label.id, [this.label.text, ...this.label.also]);
438
+ this.label = undefined;
439
+ }
440
+ if (exports.WORD_NS.has(ns))
284
441
  this.fields.end(el);
285
- if (SHEET_NS.has(ns) && (el.local === 'f' || el.local === 'definedName') && this.formula) {
286
- const { ddeBar, entity, shown } = this.formula;
442
+ if (exports.SHEET_NS.has(ns) && (el.local === 'f' || el.local === 'definedName') && this.formula) {
443
+ // A cell keeps its cached value without the formula. A defined name goes whole.
444
+ const { ddeBar, entity, shown, calls } = this.formula;
287
445
  if (ddeBar)
288
- this.ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Formula, this.part, { formula: (0, context_1.clip)(shown.text) });
446
+ this.remove(this.ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Formula, this.part, { formula: (0, context_1.clip)(shown.text) }), this.formula);
289
447
  else if (entity !== undefined)
290
- this.ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Formula, this.part, { formula: (0, context_1.clip)(shown.text), entity: (0, context_1.clip)(entity) });
448
+ this.remove(this.ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Formula, this.part, { formula: (0, context_1.clip)(shown.text), entity: (0, context_1.clip)(entity) }), this.formula);
449
+ if (!this.macroSheet)
450
+ for (const [fn, kind] of calls.found)
451
+ this.remove(this.ctx.report(kind === 'fetch' ? types_1.OfficeCategory.External : types_1.OfficeCategory.Macro, types_1.OfficeDetail.Formula, this.part, { function: (0, context_1.clip)(fn), formula: (0, context_1.clip)(shown.text) }), this.formula);
291
452
  this.formula = undefined;
292
453
  }
293
454
  if (ns === VML_EXCEL_NS && el.local === 'FmlaMacro' && this.macroText) {
294
- if (this.macroText.content)
295
- this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, this.part, { macro: (0, context_1.clip)(this.macroText.text.trim()) });
455
+ if (this.macroText.shown.content)
456
+ this.remove(this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, this.part, { macro: (0, context_1.clip)(this.macroText.shown.text.trim()) }), this.macroText);
296
457
  this.macroText = undefined;
297
458
  }
459
+ if (this.hyperlinkBase && EXTENDED_NS.has(ns) && el.local === 'HyperlinkBase') {
460
+ const base = this.hyperlinkBase.shown.text.trim();
461
+ if (base && !(0, links_1.isFragment)(base))
462
+ this.remove(this.ctx.link(base, this.part, { source: 'hyperlink base' }), this.hyperlinkBase);
463
+ this.hyperlinkBase = undefined;
464
+ }
298
465
  if (el.depth === 1)
299
466
  this.fields.finish();
300
467
  }
468
+ /**
469
+ * A data connection's source. Each source a connection names is a finding, and the rewrite empties it, so the
470
+ * connection stays for the query tables and pivot caches that name it and loads nothing.
471
+ */
472
+ connection(el) {
473
+ const set = new Map();
474
+ for (const name of CONNECTION_SOURCES[el.local]) {
475
+ const a = el.attributes.find(x => x.ns === '' && x.local === name);
476
+ if (!a?.value.trim())
477
+ continue;
478
+ const data = { element: (0, context_1.clip)(el.qname), attribute: name, target: (0, context_1.clip)(a.value, 2000) };
479
+ if (this.ctx.report(types_1.OfficeCategory.External, types_1.OfficeDetail.DataConnection, this.part, data).action === 'strip')
480
+ set.set(a.qname, '');
481
+ }
482
+ if (set.size)
483
+ this.ctx.plan.element(this.part, el.index, { set });
484
+ }
301
485
  /** PowerPoint action settings, in a DrawingML hyperlink's action attribute. The relationship it names is reported on its own. */
302
486
  drawingAction(el) {
303
487
  const written = ((0, xml_1.attr)(el, '', 'action') ?? '').trim();
304
488
  const action = written.toLowerCase();
305
489
  if (!action)
306
490
  return;
307
- const id = el.attributes.find(a => a.local === 'id' && R_NS.has(a.ns.toLowerCase()))?.value;
491
+ const id = el.attributes.find(a => a.local === 'id' && exports.R_NS.has(a.ns.toLowerCase()))?.value;
308
492
  const data = { trigger: el.local, action: (0, context_1.clip)(written), ...(id === undefined ? {} : { relationship: (0, context_1.clip)(id) }) };
309
493
  if (action.startsWith('ppaction://program'))
310
- this.ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.RunProgram, this.part, data);
494
+ this.remove(this.ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.RunProgram, this.part, data), el);
311
495
  else if (action.startsWith('ppaction://macro'))
312
- this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, this.part, data);
496
+ this.remove(this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, this.part, data), el);
313
497
  else if (action.startsWith('ppaction://ole'))
314
- this.ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.OleVerb, this.part, data);
498
+ this.remove(this.ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.OleVerb, this.part, data), el);
315
499
  }
316
500
  visioCell(el) {
317
501
  const n = (0, xml_1.attr)(el, '', 'N') ?? '';
318
502
  const formula = (0, xml_1.attr)(el, '', 'F') ?? '';
319
503
  if (VISIO_CODE.test(formula)) {
320
504
  if (n.startsWith('Event'))
321
- this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Event, this.part, { event: (0, context_1.clip)(n), formula: (0, context_1.clip)(formula) });
505
+ this.remove(this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Event, this.part, { event: (0, context_1.clip)(n), formula: (0, context_1.clip)(formula) }), el);
322
506
  else
323
- this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, this.part, { cell: (0, context_1.clip)(n), formula: (0, context_1.clip)(formula) });
507
+ this.remove(this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, this.part, { cell: (0, context_1.clip)(n), formula: (0, context_1.clip)(formula) }), el);
324
508
  }
325
509
  const v = (0, xml_1.attr)(el, '', 'V');
326
510
  if (n === 'Address' && v?.trim() && !(0, links_1.isFragment)(v))
327
- this.ctx.link(v, this.part, { source: 'Visio hyperlink' });
511
+ this.remove(this.ctx.link(v, this.part, { source: 'Visio hyperlink' }), el);
512
+ }
513
+ /**
514
+ * XPS markup, which names the parts it uses in attributes rather than in relationships. A resource inside the
515
+ * package counts as reached from this part. One outside it loads from outside, and the element that names it goes.
516
+ * A link with a scheme or a network path gets the link checks and loses its target. A relative link moves inside
517
+ * the document.
518
+ */
519
+ xps(el) {
520
+ for (const a of el.attributes) {
521
+ if (a.ns !== '' || (a.local !== 'FixedPage.NavigateUri' && a.local !== 'OutlineTarget'))
522
+ continue;
523
+ if ((0, links_1.isFragment)(a.value) || !(0, links_1.isAbsoluteTarget)(a.value))
524
+ continue;
525
+ const data = { element: (0, context_1.clip)(el.qname) };
526
+ let r = this.ctx.link(a.value, this.part, data);
527
+ // The characters of a Glyphs element are what the reader sees.
528
+ if (r.action !== 'strip' && el.local === 'Glyphs')
529
+ r = this.ctx.labelMismatch(a.value, [(0, xml_1.attr)(el, '', 'UnicodeString')], this.part, data) ?? r;
530
+ if (r.action === 'strip')
531
+ this.ctx.plan.element(this.part, el.index, { strip: new Set([(0, plan_1.attributeKey)('', a.local)]) });
532
+ }
533
+ for (const [name, uri] of xpsResources(el)) {
534
+ if (!(0, links_1.isAbsoluteTarget)(uri)) {
535
+ this.refs.add(resolvePartRef(this.part, uri));
536
+ continue;
537
+ }
538
+ const detail = name === 'ImageSource' ? types_1.OfficeDetail.Image : name === 'Source' ? types_1.OfficeDetail.Include : types_1.OfficeDetail.Other;
539
+ this.remove(this.ctx.external(detail, uri, this.part, { element: (0, context_1.clip)(el.qname), attribute: name }, name), el);
540
+ }
328
541
  }
329
542
  }
330
543
  /**
@@ -336,6 +549,7 @@ async function inspectOoxml(ctx, contentTypes) {
336
549
  const { defaults, overrides } = await readContentTypes(ctx, contentTypes);
337
550
  const parts = new Map();
338
551
  const relsOf = new Map();
552
+ const plan = ctx.plan;
339
553
  for (const entry of ctx.zip.entries) {
340
554
  if (entry === contentTypes)
341
555
  continue;
@@ -344,7 +558,7 @@ async function inspectOoxml(ctx, contentTypes) {
344
558
  await ctx.drain(entry);
345
559
  continue;
346
560
  }
347
- const source = relsSource(entry.name);
561
+ const source = (0, exports.relsSource)(entry.name);
348
562
  if (source !== undefined) {
349
563
  relsOf.set(source, entry);
350
564
  continue;
@@ -355,30 +569,45 @@ async function inspectOoxml(ctx, contentTypes) {
355
569
  }
356
570
  const roots = new Set();
357
571
  const mains = [];
572
+ /** The labels each part gives its hyperlinks, by part key and relationship Id. */
573
+ const labelsOf = new Map();
358
574
  const relate = (rels, from, r) => {
359
575
  const location = `${rels.name}, ${(0, context_1.clip)(r.id)}`;
360
576
  const part = r.external ? undefined : parts.get(resolveTarget(rels.name, r.target));
361
577
  if (part) {
362
578
  part.reachedAs |= REACHED_AS.get(r.type) ?? 0;
363
- if (TYPE.control.has(from?.contentType ?? ''))
579
+ if (exports.TYPE.control.has(from?.contentType ?? ''))
364
580
  part.claimed = true;
365
581
  (from?.targets ?? roots).add(part);
366
582
  }
367
- // A hyperlink to a part of the package, or to a place in the document, goes nowhere else.
368
- if (REL.hyperlink.has(r.type)) {
369
- if (!part && !(0, links_1.isFragment)(r.target))
370
- ctx.link(r.target, location);
583
+ // A hyperlink to a part of the package, or to a place in the document, goes nowhere else. A relationship a strip
584
+ // finding names goes, and so does every element that refers to it.
585
+ let reported;
586
+ if (exports.REL.hyperlink.has(r.type)) {
587
+ if (!part && !(0, links_1.isFragment)(r.target)) {
588
+ reported = ctx.link(r.target, location);
589
+ const labels = from ? labelsOf.get((0, zip_1.nameKey)(from.entry.name))?.get(r.id) : undefined;
590
+ if (labels && reported.action !== 'strip')
591
+ reported = ctx.labelMismatch(r.target, labels, location) ?? reported;
592
+ }
371
593
  }
372
594
  else if (r.external) {
373
595
  const kind = EXTERNAL_KINDS.find(([types]) => types.has(r.type))?.[1] ?? types_1.OfficeDetail.Other;
374
- ctx.external(kind, r.target, location, { type: (0, context_1.clip)(r.type) }, (0, context_1.clip)(r.type.slice(r.type.lastIndexOf('/') + 1)));
596
+ reported = ctx.external(kind, r.target, location, { type: (0, context_1.clip)(r.type) }, (0, context_1.clip)(r.type.slice(r.type.lastIndexOf('/') + 1)));
375
597
  }
598
+ if (reported?.action === 'strip')
599
+ plan.dropRelationship(rels.name, r.id);
376
600
  };
377
601
  const packageRels = relsOf.get('');
602
+ let strict = false;
603
+ let mainType = '';
378
604
  if (packageRels) {
379
605
  await readRelationships(ctx, packageRels, r => {
380
- if (REL.main.has(r.type))
606
+ if (exports.REL.main.has(r.type)) {
381
607
  mains.push(r.external ? undefined : parts.get(resolveTarget(packageRels.name, r.target)));
608
+ strict = r.type.startsWith(S);
609
+ mainType = r.type;
610
+ }
382
611
  relate(packageRels, undefined, r);
383
612
  });
384
613
  }
@@ -387,25 +616,38 @@ async function inspectOoxml(ctx, contentTypes) {
387
616
  const main = mains[0];
388
617
  if (!main)
389
618
  throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The package has no main part', { reason: 'no main document part' });
390
- const format = formats_1.OOXML_FORMATS.get(main.contentType ?? '');
619
+ const xps = mainType === exports.XPS_MAIN.xps || mainType === exports.XPS_MAIN.oxps;
620
+ // XPS and OpenXPS share their content types, so the relationship type tells them apart.
621
+ const format = xps ? (main.contentType === formats_1.XPS_SEQUENCE ? formats_1.XPS_FORMATS[mainType === exports.XPS_MAIN.oxps ? 'oxps' : 'xps'] : undefined) : formats_1.OOXML_FORMATS.get(main.contentType ?? '');
391
622
  if (!format)
392
623
  throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The main part has an unknown content type', { contentType: (0, context_1.clip)(main.contentType ?? '') });
393
624
  ctx.format = format;
394
625
  for (const [key, p] of parts) {
395
626
  const ct = p.contentType;
396
627
  if (ct === undefined) {
397
- ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.UntypedPart, p.entry.name, { part: p.entry.name });
628
+ if (ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.UntypedPart, p.entry.name, { part: p.entry.name }).action === 'strip')
629
+ plan.drop.add(key);
398
630
  p.ole = await ctx.drain(p.entry);
399
631
  }
400
- else if (ct.endsWith('+xml') || ct === 'application/xml' || ct === 'text/xml' || ct === TYPE.vml) {
401
- await ctx.parse(p.entry, new PartRules(ctx, p.entry.name), ct === TYPE.vml);
632
+ else if ((0, exports.isXmlType)(ct)) {
633
+ const rules = new PartRules(ctx, p.entry.name);
634
+ await ctx.parse(p.entry, rules, ct === exports.TYPE.vml);
635
+ if (rules.labels.size)
636
+ labelsOf.set(key, rules.labels);
637
+ for (const k of rules.refs) {
638
+ const t = parts.get(k);
639
+ if (t && t !== p)
640
+ p.targets.add(t);
641
+ }
402
642
  }
403
- else if (ct === TYPE.xlsbExternalLink) {
643
+ else if (ct === exports.TYPE.xlsbExternalLink) {
404
644
  // The binary form of an Excel ddeLink.
405
645
  const links = new xlsb_1.XlsbLinks();
406
646
  p.ole = await ctx.drain(p.entry, c => links.write(c));
647
+ // The binary part is the link, so it goes whole.
407
648
  for (const l of links.dde)
408
- ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Link, p.entry.name, { application: (0, context_1.clip)(l.application), topic: (0, context_1.clip)(l.topic) });
649
+ if (ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Link, p.entry.name, { application: (0, context_1.clip)(l.application), topic: (0, context_1.clip)(l.topic) }).action === 'strip')
650
+ plan.drop.add(key);
409
651
  }
410
652
  else
411
653
  p.ole = await ctx.drain(p.entry);
@@ -418,32 +660,50 @@ async function inspectOoxml(ctx, contentTypes) {
418
660
  if (source !== '' && !parts.has(source))
419
661
  await readRelationships(ctx, rels, r => relate(rels, undefined, r));
420
662
  for (const p of parts.values())
421
- partRules(ctx, p);
663
+ await partRules(ctx, p);
422
664
  unreferenced(ctx, parts, relsOf, roots);
665
+ plan.graph = { contentTypes, parts, relsOf, roots, strict, xps };
423
666
  }
424
- /** Findings about a whole part, from its content type and the relationships that reach it. */
425
- function partRules(ctx, p) {
667
+ /**
668
+ * Findings about a whole part, from its content type and the relationships that reach it. The files inside the
669
+ * package, its OLE parts and embedded packages, go to the plugins first. A plugin's verdict decides whether such a
670
+ * part stays, and when no plugin takes it, the findings of this function do.
671
+ */
672
+ async function partRules(ctx, p) {
426
673
  const ct = p.contentType ?? '';
427
674
  const loc = p.entry.name;
675
+ const key = (0, zip_1.nameKey)(loc);
428
676
  const data = { part: loc, contentType: (0, context_1.clip)(ct) };
429
677
  const as = (bit) => (p.reachedAs & bit) !== 0;
430
- const vba = TYPE.vba.has(ct) || as(AS.vba);
431
- const ole = TYPE.ole.has(ct) || as(AS.ole);
432
- const control = TYPE.control.has(ct) || as(AS.control);
433
- const controlBinary = TYPE.controlBinary.has(ct) || as(AS.controlBinary);
678
+ const vba = exports.TYPE.vba.has(ct) || as(AS.vba);
679
+ const ole = exports.TYPE.ole.has(ct) || as(AS.ole);
680
+ const control = exports.TYPE.control.has(ct) || as(AS.control);
681
+ const controlBinary = exports.TYPE.controlBinary.has(ct) || as(AS.controlBinary);
434
682
  const pkg = as(AS.package);
683
+ // Besides OLE parts and packages, every part that is a whole file in a format of its own, such as an image, goes
684
+ // to the plugins. Such a part has no rule of its own, so one no plugin takes stays.
685
+ const wholeFile = !(0, exports.isXmlType)(p.contentType) && !formatBinary(ct);
686
+ const contained = vba || ole || pkg || controlBinary || p.ole || wholeFile;
687
+ const decision = contained ? await (0, contained_1.decideContained)(ctx, { name: loc, declaredType: ct || undefined, location: loc, chunks: () => ctx.zip.read(p.entry) }) : undefined;
688
+ const act = (0, contained_1.presence)(decision);
689
+ const found = [];
435
690
  if (vba)
436
- ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.VbaProject, loc, data);
437
- if (TYPE.macrosheet.has(ct) || as(AS.macrosheet))
438
- ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.XlmMacrosheet, loc, data);
691
+ found.push(ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.VbaProject, loc, data, act));
692
+ // A macro sheet becomes an empty worksheet, so the indexes of the sheets after it stay right.
693
+ if ((exports.TYPE.macrosheet.has(ct) || as(AS.macrosheet)) && ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.XlmMacrosheet, loc, data).action === 'strip')
694
+ ctx.plan.sheets.add(key);
439
695
  if (ole)
440
- ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, loc, data);
696
+ found.push(ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, loc, data, act));
441
697
  if (pkg)
442
- ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Package, loc, data);
698
+ found.push(ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Package, loc, data, act));
443
699
  if (control || (controlBinary && !p.claimed))
444
- ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, loc, data);
700
+ found.push(ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, loc, data, control ? undefined : act));
445
701
  if (p.ole && !vba && !ole && !control && !controlBinary && !pkg)
446
- ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleFile, loc, data);
702
+ found.push(ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleFile, loc, data, act));
703
+ if (decision ? !decision.keep : found.some(f => f.action === 'strip'))
704
+ ctx.plan.drop.add(key);
705
+ else if (decision?.replacement)
706
+ ctx.plan.replace.set(key, decision.replacement);
447
707
  }
448
708
  /** Parts that no chain of internal relationships reaches from the package. */
449
709
  function unreferenced(ctx, parts, relsOf, roots) {
@@ -457,12 +717,13 @@ function unreferenced(ctx, parts, relsOf, roots) {
457
717
  queue.push(t);
458
718
  }
459
719
  }
460
- for (const p of parts.values())
461
- if (!reached.has(p))
462
- ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.UnreferencedPart, p.entry.name, { part: p.entry.name });
720
+ for (const p of parts.values()) {
721
+ if (!reached.has(p) && ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.UnreferencedPart, p.entry.name, { part: p.entry.name }).action === 'strip')
722
+ ctx.plan.drop.add((0, zip_1.nameKey)(p.entry.name));
723
+ }
463
724
  for (const [source, rels] of relsOf) {
464
725
  const owner = parts.get(source);
465
- if (source !== '' && !(owner && reached.has(owner)))
466
- ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.UnreferencedPart, rels.name, { part: rels.name });
726
+ if (source !== '' && !(owner && reached.has(owner)) && ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.UnreferencedPart, rels.name, { part: rels.name }).action === 'strip')
727
+ ctx.plan.drop.add((0, zip_1.nameKey)(rels.name));
467
728
  }
468
729
  }