@patrickdk77/office-defuse 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +278 -36
  2. package/dist/biff.d.ts +62 -0
  3. package/dist/biff.js +95 -0
  4. package/dist/cfb-writer.d.ts +25 -0
  5. package/dist/cfb-writer.js +314 -0
  6. package/dist/cfb.d.ts +58 -0
  7. package/dist/cfb.js +456 -0
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.js +357 -0
  10. package/dist/contained.d.ts +46 -0
  11. package/dist/contained.js +152 -0
  12. package/dist/context.d.ts +140 -17
  13. package/dist/context.js +135 -37
  14. package/dist/crc32.d.ts +8 -2
  15. package/dist/crc32.js +44 -4
  16. package/dist/engine.d.ts +12 -1
  17. package/dist/engine.js +336 -42
  18. package/dist/errors.d.ts +17 -0
  19. package/dist/errors.js +27 -1
  20. package/dist/fields.d.ts +36 -0
  21. package/dist/fields.js +139 -0
  22. package/dist/findings.d.ts +2 -1
  23. package/dist/findings.js +44 -9
  24. package/dist/flat.d.ts +33 -0
  25. package/dist/flat.js +486 -0
  26. package/dist/formats.d.ts +7 -0
  27. package/dist/formats.js +5 -1
  28. package/dist/formulas.d.ts +27 -0
  29. package/dist/formulas.js +157 -1
  30. package/dist/index.d.ts +3 -2
  31. package/dist/index.js +8 -1
  32. package/dist/index.mjs +1 -1
  33. package/dist/io.d.ts +62 -1
  34. package/dist/io.js +245 -1
  35. package/dist/links.d.ts +27 -0
  36. package/dist/links.js +106 -1
  37. package/dist/msg.d.ts +8 -0
  38. package/dist/msg.js +251 -0
  39. package/dist/objects.d.ts +45 -0
  40. package/dist/objects.js +204 -0
  41. package/dist/odf.d.ts +66 -2
  42. package/dist/odf.js +181 -64
  43. package/dist/offcrypto.d.ts +54 -0
  44. package/dist/offcrypto.js +647 -0
  45. package/dist/ole.d.ts +12 -0
  46. package/dist/ole.js +139 -0
  47. package/dist/oleobj.d.ts +85 -0
  48. package/dist/oleobj.js +215 -0
  49. package/dist/ooxml.d.ts +83 -2
  50. package/dist/ooxml.js +358 -145
  51. package/dist/package.d.ts +3 -0
  52. package/dist/package.js +21 -0
  53. package/dist/plan.d.ts +104 -0
  54. package/dist/plan.js +73 -0
  55. package/dist/plugins.d.ts +8 -0
  56. package/dist/plugins.js +60 -0
  57. package/dist/ppt.d.ts +8 -0
  58. package/dist/ppt.js +281 -0
  59. package/dist/rewrite-cfb.d.ts +14 -0
  60. package/dist/rewrite-cfb.js +52 -0
  61. package/dist/rewrite-rtf.d.ts +9 -0
  62. package/dist/rewrite-rtf.js +45 -0
  63. package/dist/rewrite-xml.d.ts +36 -0
  64. package/dist/rewrite-xml.js +341 -0
  65. package/dist/rewrite-zip.d.ts +8 -0
  66. package/dist/rewrite-zip.js +498 -0
  67. package/dist/rtf.d.ts +8 -0
  68. package/dist/rtf.js +712 -0
  69. package/dist/rtfcomp.d.ts +7 -0
  70. package/dist/rtfcomp.js +80 -0
  71. package/dist/score.js +1 -1
  72. package/dist/sniff.d.ts +5 -0
  73. package/dist/sniff.js +38 -0
  74. package/dist/types.d.ts +66 -2
  75. package/dist/types.js +33 -0
  76. package/dist/vsd.d.ts +13 -0
  77. package/dist/vsd.js +689 -0
  78. package/dist/word.d.ts +9 -0
  79. package/dist/word.js +194 -0
  80. package/dist/xls.d.ts +27 -0
  81. package/dist/xls.js +208 -0
  82. package/dist/xml.d.ts +30 -0
  83. package/dist/xml.js +45 -23
  84. package/dist/zip-writer.d.ts +4 -0
  85. package/dist/zip-writer.js +20 -7
  86. package/dist/zip.d.ts +9 -2
  87. package/dist/zip.js +61 -4
  88. package/package.json +6 -3
package/dist/ooxml.js CHANGED
@@ -1,24 +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;
9
+ const biff_1 = require("./biff");
10
+ const contained_1 = require("./contained");
4
11
  const context_1 = require("./context");
5
12
  const errors_1 = require("./errors");
13
+ const fields_1 = require("./fields");
6
14
  const formats_1 = require("./formats");
7
15
  const formulas_1 = require("./formulas");
8
16
  const links_1 = require("./links");
17
+ const plan_1 = require("./plan");
9
18
  const types_1 = require("./types");
10
19
  const xlsb_1 = require("./xlsb");
11
20
  const xml_1 = require("./xml");
12
21
  const zip_1 = require("./zip");
13
- const CONTENT_TYPES_NS = 'http://schemas.openxmlformats.org/package/2006/content-types';
14
- 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';
15
24
  const T = 'http://schemas.openxmlformats.org/officedocument/2006/relationships/';
16
25
  const S = 'http://purl.oclc.org/ooxml/officedocument/relationships/';
17
26
  const MS = 'http://schemas.microsoft.com/office/2006/relationships/';
18
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']);
19
32
  /** Relationship types, lowercased, as OPC compares them without regard to case. */
20
- const REL = {
21
- 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]),
22
35
  hyperlink: new Set(both('hyperlink')),
23
36
  template: new Set(both('attachedtemplate')),
24
37
  image: new Set([...both('image'), 'http://schemas.microsoft.com/office/2007/relationships/hdphoto']),
@@ -44,7 +57,7 @@ const REACHED_AS = new Map([
44
57
  [`${MS}activexcontrolbinary`, AS.controlBinary],
45
58
  ]);
46
59
  /** Content types, lowercased. */
47
- const TYPE = {
60
+ exports.TYPE = {
48
61
  vba: new Set(['application/vnd.ms-office.vbaproject']),
49
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']),
50
63
  ole: new Set(['application/vnd.openxmlformats-officedocument.oleobject']),
@@ -53,32 +66,50 @@ const TYPE = {
53
66
  vml: 'application/vnd.openxmlformats-officedocument.vmldrawing',
54
67
  xlsbExternalLink: 'application/vnd.ms-excel.externallink',
55
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';
56
82
  /** Relationship types that leave the package, by the finding they make. Any other is EXTERNAL/OTHER. */
57
83
  const EXTERNAL_KINDS = [
58
- [REL.template, types_1.OfficeDetail.Template],
84
+ [exports.REL.template, types_1.OfficeDetail.Template],
59
85
  [new Set([...both('oleobject'), ...both('package')]), types_1.OfficeDetail.Object],
60
- [REL.image, types_1.OfficeDetail.Image],
61
- [REL.frame, types_1.OfficeDetail.Frame],
62
- [REL.subDocument, types_1.OfficeDetail.Include],
63
- [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],
64
90
  ];
65
- const WORD_NS = new Set(['http://schemas.openxmlformats.org/wordprocessingml/2006/main', 'http://purl.oclc.org/ooxml/wordprocessingml/main']);
66
- 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']);
67
93
  const XDR_NS = new Set(['http://schemas.openxmlformats.org/drawingml/2006/spreadsheetdrawing', 'http://purl.oclc.org/ooxml/drawingml/spreadsheetdrawing']);
68
- 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']);
69
95
  const CUSTOM_UI_NS = new Set(['http://schemas.microsoft.com/office/2006/01/customui', 'http://schemas.microsoft.com/office/2009/07/customui']);
70
96
  const VML_EXCEL_NS = 'urn:schemas-microsoft-com:office:excel';
71
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'] };
72
104
  const VISIO_CODE = /\b(RUNADDON|RUNADDONWARGS|CALLTHIS|RUNMACRO|OPENFILE|DOOLEVERB)\s*\(/i;
73
- /** How much of a field code is kept, counted from its first word, which names the field. */
74
- const MAX_FIELD_CODE = 4096;
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,93 +232,51 @@ async function readRelationships(ctx, entry, each) {
155
232
  },
156
233
  });
157
234
  }
158
- /** Word field codes, followed through nested fields, as one part's fields run. */
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
- ctx;
161
- part;
162
- stack = [];
240
+ stack;
163
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;
164
244
  constructor(ctx, part) {
165
- this.ctx = ctx;
166
- this.part = part;
167
- }
168
- /** A field starts. One that starts before its parent's code has a word in it makes that code at run time. */
169
- nest() {
170
- const top = this.stack[this.stack.length - 1];
171
- if (top?.inCode && top.code === '')
172
- top.computed = true;
245
+ this.stack = new fields_1.FieldStack(ctx, part, marks => ctx.plan.part(part).fields.push(marks));
173
246
  }
174
247
  start(el) {
175
- if (el.local === 'fldSimple') {
176
- this.nest();
177
- this.evaluate(wordAttr(el, 'instr') ?? '', false);
178
- }
248
+ if (el.local === 'fldSimple')
249
+ this.stack.simpleBegin(wordAttr(el, 'instr') ?? '', el.index);
179
250
  else if (el.local === 'fldChar') {
180
251
  const kind = wordAttr(el, 'fldCharType');
181
- if (kind === 'begin') {
182
- this.nest();
183
- this.stack.push({ code: '', inCode: true, computed: false });
184
- }
185
- else if (kind === 'separate') {
186
- const top = this.stack[this.stack.length - 1];
187
- if (top)
188
- top.inCode = false;
189
- }
190
- else if (kind === 'end') {
191
- const f = this.stack.pop();
192
- if (f)
193
- this.evaluate(f.code, f.computed);
194
- }
252
+ if (kind === 'begin')
253
+ this.stack.begin(el.index);
254
+ else if (kind === 'separate')
255
+ this.stack.separate(el.index);
256
+ else if (kind === 'end')
257
+ this.stack.end(el.index);
195
258
  }
196
259
  else if (el.local === 'instrText' || el.local === 'delInstrText')
197
260
  this.capture = true;
261
+ else if (el.local === 't')
262
+ this.shown = true;
198
263
  }
199
264
  end(el) {
200
265
  if (el.local === 'instrText' || el.local === 'delInstrText')
201
266
  this.capture = false;
267
+ else if (el.local === 't')
268
+ this.shown = false;
269
+ else if (el.local === 'fldSimple')
270
+ this.stack.simpleEnd();
202
271
  }
203
- /** White space before the first word counts for nothing, so no amount of it can push the field's name out. */
204
272
  text(t) {
205
- const top = this.stack[this.stack.length - 1];
206
- if (this.capture && top?.inCode && top.code.length < MAX_FIELD_CODE)
207
- top.code = top.code ? top.code + t : t.trimStart();
273
+ if (this.capture)
274
+ this.stack.text(t);
275
+ else if (this.shown)
276
+ this.stack.result(t);
208
277
  }
209
- /** A part can end inside a field. Word still reads the code it has. */
210
278
  finish() {
211
- for (let f = this.stack.pop(); f; f = this.stack.pop())
212
- this.evaluate(f.code, f.computed);
213
- }
214
- /**
215
- * Reports a field by its code. A field whose name holds the content of an external entity has a type the file does
216
- * not say, as one whose name another field builds does.
217
- */
218
- evaluate(code, computed) {
219
- const data = { code: (0, context_1.clip)(code.trim()) };
220
- const words = (0, formulas_1.fieldWords)(code);
221
- const first = words[0]?.text ?? '';
222
- const entity = (0, xml_1.unknownEntity)(first);
223
- if (computed || entity !== undefined)
224
- this.ctx.report(types_1.OfficeCategory.Field, types_1.OfficeDetail.Computed, this.part, entity === undefined ? data : { ...data, entity: (0, context_1.clip)(entity) });
225
- const name = first.toUpperCase();
226
- // An argument comes before any switch, and a switch is an unquoted word starting with a backslash.
227
- const switchAt = words.findIndex(w => !w.quoted && w.text.startsWith('\\'));
228
- const arg = (n) => (words[n] !== undefined && (switchAt < 0 || n < switchAt) ? words[n].text : undefined);
229
- if (name === 'DDE' || name === 'DDEAUTO')
230
- this.ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Field, this.part, data);
231
- else if (name === 'MACROBUTTON')
232
- this.ctx.report(types_1.OfficeCategory.Field, types_1.OfficeDetail.Macrobutton, this.part, { ...data, macro: (0, context_1.clip)(words[1]?.text ?? '') });
233
- else if (name === 'INCLUDETEXT' || name === 'INCLUDEPICTURE' || name === 'IMPORT' || name === 'LINK') {
234
- const target = arg(name === 'LINK' ? 2 : 1);
235
- if (target === undefined)
236
- this.ctx.report(types_1.OfficeCategory.External, types_1.OfficeDetail.Include, this.part, data);
237
- else
238
- this.ctx.external(types_1.OfficeDetail.Include, target, this.part, data, 'field');
239
- }
240
- else if (name === 'HYPERLINK') {
241
- const target = arg(1);
242
- if (target !== undefined && !(0, links_1.isFragment)(target))
243
- this.ctx.link(target, this.part, { ...data, source: 'HYPERLINK field' });
244
- }
279
+ this.stack.finish();
245
280
  }
246
281
  }
247
282
  /** Text that may run long. Detection needs only whether it holds more than white space; the report shows its start. */
@@ -266,6 +301,19 @@ class PartRules {
266
301
  */
267
302
  formula;
268
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;
269
317
  /** The last namespace seen and its lowercase form. Neighboring elements share one, and this spares a copy each. */
270
318
  ns = '';
271
319
  lower = '';
@@ -274,6 +322,11 @@ class PartRules {
274
322
  this.part = part;
275
323
  this.fields = new Fields(ctx, part);
276
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
+ }
277
330
  lowerNs(el) {
278
331
  if (el.ns !== this.ns) {
279
332
  this.ns = el.ns;
@@ -281,38 +334,88 @@ class PartRules {
281
334
  }
282
335
  return this.lower;
283
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
+ }
284
356
  start(el) {
285
357
  const ns = this.lowerNs(el);
286
358
  if (CUSTOM_UI_NS.has(ns)) {
287
359
  if (el.depth === 1)
288
360
  this.ctx.report(types_1.OfficeCategory.Ui, types_1.OfficeDetail.Ribbon, this.part);
289
361
  for (const a of el.attributes) {
290
- if (a.ns === '' && a.value.trim() && /^(on[A-Z]|get[A-Z]|loadImage$)/.test(a.local))
291
- 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
+ }
292
368
  }
293
369
  return;
294
370
  }
295
- if (WORD_NS.has(ns))
371
+ if (el.depth === 1 && el.local === 'macrosheet')
372
+ this.macroSheet = true;
373
+ if (exports.WORD_NS.has(ns)) {
296
374
  this.fields.start(el);
297
- 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')]);
298
381
  if (el.local === 'f' || el.local === 'definedName')
299
- 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() };
383
+ if (el.local === 'definedName') {
384
+ // Excel runs the macro a name like _xlnm.Auto_Open refers to, as it does in a binary workbook.
385
+ const name = (0, xml_1.attr)(el, '', 'name') ?? '';
386
+ const auto = (0, biff_1.autoName)(name);
387
+ if (auto)
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);
389
+ }
300
390
  else if (el.local === 'ddeLink')
301
- 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);
302
394
  }
303
395
  else if (XDR_NS.has(ns)) {
304
396
  const macro = (0, xml_1.attr)(el, '', 'macro');
305
- if (macro?.trim())
306
- 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
+ }
307
401
  }
308
402
  else if (ns === VML_EXCEL_NS && el.local === 'FmlaMacro')
309
- this.macroText = new Shown();
403
+ this.macroText = { shown: new Shown(), index: el.index };
310
404
  else if (ns === VISIO_NS && el.local === 'Cell')
311
405
  this.visioCell(el);
312
- 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') {
313
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
+ }
314
415
  }
315
416
  text(t) {
417
+ if (this.label && this.label.text.length < 4096)
418
+ this.label.text += t;
316
419
  this.fields.text(t);
317
420
  const f = this.formula;
318
421
  if (f) {
@@ -321,58 +424,120 @@ class PartRules {
321
424
  for (const c of t)
322
425
  if (f.quotes.outside(c) && c === '|')
323
426
  f.ddeBar = true;
427
+ f.calls.write(t);
324
428
  if (f.entity === undefined && t.includes(xml_1.UNKNOWN))
325
429
  f.entity = (0, xml_1.unknownEntity)(t);
326
430
  }
327
- this.macroText?.add(t);
431
+ this.macroText?.shown.add(t);
432
+ this.hyperlinkBase?.shown.add(t);
328
433
  }
329
434
  end(el) {
330
435
  const ns = this.lowerNs(el);
331
- 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))
332
441
  this.fields.end(el);
333
- if (SHEET_NS.has(ns) && (el.local === 'f' || el.local === 'definedName') && this.formula) {
334
- 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;
335
445
  if (ddeBar)
336
- 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);
337
447
  else if (entity !== undefined)
338
- 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);
339
452
  this.formula = undefined;
340
453
  }
341
454
  if (ns === VML_EXCEL_NS && el.local === 'FmlaMacro' && this.macroText) {
342
- if (this.macroText.content)
343
- 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);
344
457
  this.macroText = undefined;
345
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
+ }
346
465
  if (el.depth === 1)
347
466
  this.fields.finish();
348
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
+ }
349
485
  /** PowerPoint action settings, in a DrawingML hyperlink's action attribute. The relationship it names is reported on its own. */
350
486
  drawingAction(el) {
351
487
  const written = ((0, xml_1.attr)(el, '', 'action') ?? '').trim();
352
488
  const action = written.toLowerCase();
353
489
  if (!action)
354
490
  return;
355
- 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;
356
492
  const data = { trigger: el.local, action: (0, context_1.clip)(written), ...(id === undefined ? {} : { relationship: (0, context_1.clip)(id) }) };
357
493
  if (action.startsWith('ppaction://program'))
358
- 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);
359
495
  else if (action.startsWith('ppaction://macro'))
360
- 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);
361
497
  else if (action.startsWith('ppaction://ole'))
362
- 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);
363
499
  }
364
500
  visioCell(el) {
365
501
  const n = (0, xml_1.attr)(el, '', 'N') ?? '';
366
502
  const formula = (0, xml_1.attr)(el, '', 'F') ?? '';
367
503
  if (VISIO_CODE.test(formula)) {
368
504
  if (n.startsWith('Event'))
369
- 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);
370
506
  else
371
- 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);
372
508
  }
373
509
  const v = (0, xml_1.attr)(el, '', 'V');
374
510
  if (n === 'Address' && v?.trim() && !(0, links_1.isFragment)(v))
375
- 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
+ }
376
541
  }
377
542
  }
378
543
  /**
@@ -384,6 +549,7 @@ async function inspectOoxml(ctx, contentTypes) {
384
549
  const { defaults, overrides } = await readContentTypes(ctx, contentTypes);
385
550
  const parts = new Map();
386
551
  const relsOf = new Map();
552
+ const plan = ctx.plan;
387
553
  for (const entry of ctx.zip.entries) {
388
554
  if (entry === contentTypes)
389
555
  continue;
@@ -392,7 +558,7 @@ async function inspectOoxml(ctx, contentTypes) {
392
558
  await ctx.drain(entry);
393
559
  continue;
394
560
  }
395
- const source = relsSource(entry.name);
561
+ const source = (0, exports.relsSource)(entry.name);
396
562
  if (source !== undefined) {
397
563
  relsOf.set(source, entry);
398
564
  continue;
@@ -403,30 +569,45 @@ async function inspectOoxml(ctx, contentTypes) {
403
569
  }
404
570
  const roots = new Set();
405
571
  const mains = [];
572
+ /** The labels each part gives its hyperlinks, by part key and relationship Id. */
573
+ const labelsOf = new Map();
406
574
  const relate = (rels, from, r) => {
407
575
  const location = `${rels.name}, ${(0, context_1.clip)(r.id)}`;
408
576
  const part = r.external ? undefined : parts.get(resolveTarget(rels.name, r.target));
409
577
  if (part) {
410
578
  part.reachedAs |= REACHED_AS.get(r.type) ?? 0;
411
- if (TYPE.control.has(from?.contentType ?? ''))
579
+ if (exports.TYPE.control.has(from?.contentType ?? ''))
412
580
  part.claimed = true;
413
581
  (from?.targets ?? roots).add(part);
414
582
  }
415
- // A hyperlink to a part of the package, or to a place in the document, goes nowhere else.
416
- if (REL.hyperlink.has(r.type)) {
417
- if (!part && !(0, links_1.isFragment)(r.target))
418
- 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
+ }
419
593
  }
420
594
  else if (r.external) {
421
595
  const kind = EXTERNAL_KINDS.find(([types]) => types.has(r.type))?.[1] ?? types_1.OfficeDetail.Other;
422
- 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)));
423
597
  }
598
+ if (reported?.action === 'strip')
599
+ plan.dropRelationship(rels.name, r.id);
424
600
  };
425
601
  const packageRels = relsOf.get('');
602
+ let strict = false;
603
+ let mainType = '';
426
604
  if (packageRels) {
427
605
  await readRelationships(ctx, packageRels, r => {
428
- if (REL.main.has(r.type))
606
+ if (exports.REL.main.has(r.type)) {
429
607
  mains.push(r.external ? undefined : parts.get(resolveTarget(packageRels.name, r.target)));
608
+ strict = r.type.startsWith(S);
609
+ mainType = r.type;
610
+ }
430
611
  relate(packageRels, undefined, r);
431
612
  });
432
613
  }
@@ -435,25 +616,38 @@ async function inspectOoxml(ctx, contentTypes) {
435
616
  const main = mains[0];
436
617
  if (!main)
437
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' });
438
- 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 ?? '');
439
622
  if (!format)
440
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 ?? '') });
441
624
  ctx.format = format;
442
625
  for (const [key, p] of parts) {
443
626
  const ct = p.contentType;
444
627
  if (ct === undefined) {
445
- 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);
446
630
  p.ole = await ctx.drain(p.entry);
447
631
  }
448
- else if (ct.endsWith('+xml') || ct === 'application/xml' || ct === 'text/xml' || ct === TYPE.vml) {
449
- 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
+ }
450
642
  }
451
- else if (ct === TYPE.xlsbExternalLink) {
643
+ else if (ct === exports.TYPE.xlsbExternalLink) {
452
644
  // The binary form of an Excel ddeLink.
453
645
  const links = new xlsb_1.XlsbLinks();
454
646
  p.ole = await ctx.drain(p.entry, c => links.write(c));
647
+ // The binary part is the link, so it goes whole.
455
648
  for (const l of links.dde)
456
- 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);
457
651
  }
458
652
  else
459
653
  p.ole = await ctx.drain(p.entry);
@@ -466,32 +660,50 @@ async function inspectOoxml(ctx, contentTypes) {
466
660
  if (source !== '' && !parts.has(source))
467
661
  await readRelationships(ctx, rels, r => relate(rels, undefined, r));
468
662
  for (const p of parts.values())
469
- partRules(ctx, p);
663
+ await partRules(ctx, p);
470
664
  unreferenced(ctx, parts, relsOf, roots);
665
+ plan.graph = { contentTypes, parts, relsOf, roots, strict, xps };
471
666
  }
472
- /** Findings about a whole part, from its content type and the relationships that reach it. */
473
- 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) {
474
673
  const ct = p.contentType ?? '';
475
674
  const loc = p.entry.name;
675
+ const key = (0, zip_1.nameKey)(loc);
476
676
  const data = { part: loc, contentType: (0, context_1.clip)(ct) };
477
677
  const as = (bit) => (p.reachedAs & bit) !== 0;
478
- const vba = TYPE.vba.has(ct) || as(AS.vba);
479
- const ole = TYPE.ole.has(ct) || as(AS.ole);
480
- const control = TYPE.control.has(ct) || as(AS.control);
481
- 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);
482
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 = [];
483
690
  if (vba)
484
- ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.VbaProject, loc, data);
485
- if (TYPE.macrosheet.has(ct) || as(AS.macrosheet))
486
- 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);
487
695
  if (ole)
488
- 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));
489
697
  if (pkg)
490
- 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));
491
699
  if (control || (controlBinary && !p.claimed))
492
- 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));
493
701
  if (p.ole && !vba && !ole && !control && !controlBinary && !pkg)
494
- 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);
495
707
  }
496
708
  /** Parts that no chain of internal relationships reaches from the package. */
497
709
  function unreferenced(ctx, parts, relsOf, roots) {
@@ -505,12 +717,13 @@ function unreferenced(ctx, parts, relsOf, roots) {
505
717
  queue.push(t);
506
718
  }
507
719
  }
508
- for (const p of parts.values())
509
- if (!reached.has(p))
510
- 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
+ }
511
724
  for (const [source, rels] of relsOf) {
512
725
  const owner = parts.get(source);
513
- if (source !== '' && !(owner && reached.has(owner)))
514
- 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));
515
728
  }
516
729
  }