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