@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/odf.js CHANGED
@@ -1,14 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EntryRules = exports.packagePath = exports.hrefOf = exports.odfNamespace = exports.NS = void 0;
4
+ exports.oattr = oattr;
5
+ exports.folderOf = folderOf;
6
+ exports.nameOdf = nameOdf;
3
7
  exports.inspectOdf = inspectOdf;
8
+ const contained_1 = require("./contained");
4
9
  const context_1 = require("./context");
5
10
  const errors_1 = require("./errors");
6
11
  const formats_1 = require("./formats");
7
12
  const formulas_1 = require("./formulas");
8
13
  const links_1 = require("./links");
14
+ const plan_1 = require("./plan");
9
15
  const types_1 = require("./types");
10
16
  const zip_1 = require("./zip");
11
- const NS = {
17
+ exports.NS = {
12
18
  manifest: 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0',
13
19
  office: 'urn:oasis:names:tc:opendocument:xmlns:office:1.0',
14
20
  script: 'urn:oasis:names:tc:opendocument:xmlns:script:1.0',
@@ -20,6 +26,7 @@ const NS = {
20
26
  meta: 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
21
27
  style: 'urn:oasis:names:tc:opendocument:xmlns:style:1.0',
22
28
  database: 'urn:oasis:names:tc:opendocument:xmlns:database:1.0',
29
+ chart: 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0',
23
30
  xlink: 'http://www.w3.org/1999/xlink',
24
31
  };
25
32
  /**
@@ -35,21 +42,21 @@ const FOLDERS = [
35
42
  /** Event names that fire as the document opens. Focus and activation count only for the document's own events. */
36
43
  const ON_OPEN = new Set(['load', 'new', 'create', 'loadfinished', 'viewcreated']);
37
44
  const ON_OPEN_DOCUMENT = new Set(['focus', 'activate']);
38
- const LINK_ELEMENTS = new Set([`${NS.text} a`, `${NS.draw} a`, `${NS.draw} area-rectangle`, `${NS.draw} area-circle`, `${NS.draw} area-polygon`]);
45
+ const LINK_ELEMENTS = new Set([`${exports.NS.text} a`, `${exports.NS.draw} a`, `${exports.NS.draw} area-rectangle`, `${exports.NS.draw} area-circle`, `${exports.NS.draw} area-polygon`]);
39
46
  const EXTERNAL_KIND = new Map([
40
- [`${NS.meta} template`, types_1.OfficeDetail.Template],
41
- [`${NS.meta} auto-reload`, types_1.OfficeDetail.Reload],
42
- [`${NS.draw} image`, types_1.OfficeDetail.Image],
43
- [`${NS.draw} fill-image`, types_1.OfficeDetail.Image],
44
- [`${NS.style} background-image`, types_1.OfficeDetail.Image],
45
- [`${NS.text} section-source`, types_1.OfficeDetail.Include],
46
- [`${NS.table} table-source`, types_1.OfficeDetail.Include],
47
- [`${NS.draw} object`, types_1.OfficeDetail.Object],
48
- [`${NS.draw} object-ole`, types_1.OfficeDetail.Object],
49
- [`${NS.draw} floating-frame`, types_1.OfficeDetail.Frame],
47
+ [`${exports.NS.meta} template`, types_1.OfficeDetail.Template],
48
+ [`${exports.NS.meta} auto-reload`, types_1.OfficeDetail.Reload],
49
+ [`${exports.NS.draw} image`, types_1.OfficeDetail.Image],
50
+ [`${exports.NS.draw} fill-image`, types_1.OfficeDetail.Image],
51
+ [`${exports.NS.style} background-image`, types_1.OfficeDetail.Image],
52
+ [`${exports.NS.text} section-source`, types_1.OfficeDetail.Include],
53
+ [`${exports.NS.table} table-source`, types_1.OfficeDetail.Include],
54
+ [`${exports.NS.draw} object`, types_1.OfficeDetail.Object],
55
+ [`${exports.NS.draw} object-ole`, types_1.OfficeDetail.Object],
56
+ [`${exports.NS.draw} floating-frame`, types_1.OfficeDetail.Frame],
50
57
  ]);
51
58
  /** Elements whose reference always leaves the package: it resolves against the document's own location. */
52
- const ALWAYS_OUTSIDE = new Set([`${NS.meta} template`, `${NS.meta} auto-reload`, `${NS.text} section-source`, `${NS.table} table-source`]);
59
+ const ALWAYS_OUTSIDE = new Set([`${exports.NS.meta} template`, `${exports.NS.meta} auto-reload`, `${exports.NS.text} section-source`, `${exports.NS.table} table-source`]);
53
60
  /**
54
61
  * LibreOffice reads urn:oasis:names:tc:<any>:xmlns:<name>:1.<any> as the OpenDocument namespace <name> 1.0, so this
55
62
  * does too. The comparison ignores case, which can only find more.
@@ -65,90 +72,155 @@ function oattr(el, ns, local) {
65
72
  return a.value;
66
73
  return undefined;
67
74
  }
75
+ /** The ODF_NS-normalized namespace of `ns`, exported for the rewrite. */
76
+ const odfNamespace = (ns) => odfNs(ns);
77
+ exports.odfNamespace = odfNamespace;
78
+ /** The edit that takes an element's xlink:href away, under whatever namespace URI the attribute has. */
79
+ function stripHref(el) {
80
+ return { strip: new Set(el.attributes.filter(a => a.local === 'href' && odfNs(a.ns) === exports.NS.xlink).map(a => (0, plan_1.attributeKey)(a.ns, a.local))) };
81
+ }
82
+ /** The xlink:href of an element, for the rewrite. */
83
+ const hrefOf = (el) => oattr(el, exports.NS.xlink, 'href');
84
+ exports.hrefOf = hrefOf;
68
85
  /** A path inside the package as the manifest and the zip name it: without "./" and leading slashes. */
69
86
  const packagePath = (href) => (0, zip_1.nameKey)(href.trim().replace(/^(\.\/)+/, ''));
70
- /** The rules that read inside one XML entry of an ODF package. */
87
+ exports.packagePath = packagePath;
88
+ /**
89
+ * The rules that read inside one XML entry of an ODF package, or inside a flat OpenDocument file. A flat file has no
90
+ * entries, so in one every relative reference names a file beside the document, which loads from outside it.
91
+ */
71
92
  class EntryRules {
72
93
  ctx;
73
94
  entry;
74
95
  oleRefs;
96
+ flat;
75
97
  /** Depth of the office:scripts element being read, 0 outside one. */
76
98
  scriptsDepth = 0;
77
99
  /** An office:script element being read, with whether it holds anything. */
78
100
  script;
101
+ /** A link element being read, whose text and title go into the label check at its end. */
102
+ label;
79
103
  constructor(ctx, entry,
80
104
  /** Whether an object refers to each entry, by key. Only entries are kept, so a part full of references costs no memory. */
81
- oleRefs) {
105
+ oleRefs, flat = false) {
82
106
  this.ctx = ctx;
83
107
  this.entry = entry;
84
108
  this.oleRefs = oleRefs;
109
+ this.flat = flat;
110
+ }
111
+ /**
112
+ * Whether a reference names an entry of the package. LibreOffice's vnd.sun.star.Package: URLs name one too. In a
113
+ * flat file no entry exists, so only such a URL stays inside, naming nothing.
114
+ */
115
+ inside(href) {
116
+ const own = /^\s*vnd\.sun\.star\.package:(.*)$/is.exec(href);
117
+ if (own)
118
+ return this.flat || (0, links_1.isInsidePackage)(own[1]);
119
+ return !this.flat && (0, links_1.isInsidePackage)(href);
85
120
  }
86
121
  report(category, detail, data) {
87
- this.ctx.report(category, detail, this.entry, data);
122
+ return this.ctx.report(category, detail, this.entry, data);
123
+ }
124
+ /** Records the edit for the element when the finding strips. */
125
+ edit(r, el, edit = { remove: true }) {
126
+ if (r.action === 'strip')
127
+ this.ctx.plan.element(this.entry, el.index, edit);
88
128
  }
89
129
  start(raw) {
90
130
  if (this.script)
91
131
  this.script.content = true;
92
132
  const el = { ...raw, ns: odfNs(raw.ns) };
93
133
  const key = `${el.ns} ${el.local}`;
94
- const href = oattr(el, NS.xlink, 'href');
95
- if (el.ns === NS.office && el.local === 'scripts' && !this.scriptsDepth)
134
+ const href = oattr(el, exports.NS.xlink, 'href');
135
+ if (el.ns === exports.NS.office && el.local === 'scripts' && !this.scriptsDepth)
96
136
  this.scriptsDepth = el.depth;
97
- else if (el.ns === NS.office && el.local === 'script')
98
- this.script = { depth: el.depth, language: oattr(el, NS.script, 'language') ?? '', content: false };
99
- else if (el.ns === NS.script && el.local === 'event-listener')
137
+ else if (el.ns === exports.NS.office && el.local === 'script')
138
+ this.script = { depth: el.depth, language: oattr(el, exports.NS.script, 'language') ?? '', content: false, index: el.index };
139
+ else if (el.ns === exports.NS.script && el.local === 'event-listener')
100
140
  this.scriptEvent(el, href);
101
- else if (el.ns === NS.presentation && el.local === 'event-listener')
141
+ else if (el.ns === exports.NS.presentation && el.local === 'event-listener')
102
142
  this.presentationEvent(el, href);
103
- else if ((el.ns === NS.text && el.local === 'dde-connection-decl') || (el.ns === NS.office && el.local === 'dde-source')) {
104
- this.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Link, { application: (0, context_1.clip)(oattr(el, NS.office, 'dde-application') ?? ''), topic: (0, context_1.clip)(oattr(el, NS.office, 'dde-topic') ?? '') });
143
+ else if ((el.ns === exports.NS.text && el.local === 'dde-connection-decl') || (el.ns === exports.NS.office && el.local === 'dde-source')) {
144
+ this.edit(this.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Link, { application: (0, context_1.clip)(oattr(el, exports.NS.office, 'dde-application') ?? ''), topic: (0, context_1.clip)(oattr(el, exports.NS.office, 'dde-topic') ?? '') }), raw);
105
145
  }
106
- else if (el.ns === NS.draw && el.local === 'applet') {
107
- this.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Applet, { code: (0, context_1.clip)(oattr(el, NS.draw, 'code') ?? ''), ...(href ? { target: (0, context_1.clip)(href) } : {}) });
146
+ else if (el.ns === exports.NS.draw && el.local === 'applet') {
147
+ this.edit(this.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Applet, { code: (0, context_1.clip)(oattr(el, exports.NS.draw, 'code') ?? ''), ...(href ? { target: (0, context_1.clip)(href) } : {}) }), raw);
108
148
  return;
109
149
  }
110
- else if (el.ns === NS.draw && el.local === 'plugin') {
111
- this.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Plugin, { mimeType: (0, context_1.clip)(oattr(el, NS.draw, 'mime-type') ?? ''), ...(href ? { target: (0, context_1.clip)(href) } : {}) });
150
+ else if (el.ns === exports.NS.draw && el.local === 'plugin') {
151
+ this.edit(this.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Plugin, { mimeType: (0, context_1.clip)(oattr(el, exports.NS.draw, 'mime-type') ?? ''), ...(href ? { target: (0, context_1.clip)(href) } : {}) }), raw);
112
152
  return;
113
153
  }
114
- else if (el.ns === NS.draw && el.local === 'object-ole' && (href === undefined || (0, links_1.isInsidePackage)(href))) {
115
- this.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, href ? { target: (0, context_1.clip)(href) } : undefined);
116
- if (href && this.oleRefs.has(packagePath(href)))
117
- this.oleRefs.set(packagePath(href), true);
154
+ else if (el.ns === exports.NS.draw && el.local === 'object-ole' && (href === undefined || this.inside(href))) {
155
+ // The object's entry may still be ahead in the archive, so its plugin verdict decides this element later. The
156
+ // preview image beside the element stays either way.
157
+ const r = this.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, href ? { target: (0, context_1.clip)(href) } : undefined);
158
+ this.ctx.plan.objects.push({ part: this.entry, index: el.index, target: href ? (0, exports.packagePath)(href) : '', finding: r.finding, strips: r.action === 'strip' });
159
+ if (href && this.oleRefs.has((0, exports.packagePath)(href)))
160
+ this.oleRefs.set((0, exports.packagePath)(href), true);
118
161
  }
119
162
  for (const a of el.attributes) {
120
- if (odfNs(a.ns) === NS.table && (a.local === 'formula' || a.local === 'expression') && (0, formulas_1.callsDde)(a.value))
121
- this.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Formula, { formula: (0, context_1.clip)(a.value) });
163
+ // The cell keeps its value without the formula.
164
+ if (odfNs(a.ns) !== exports.NS.table || (a.local !== 'formula' && a.local !== 'expression'))
165
+ continue;
166
+ const strip = { strip: new Set([(0, plan_1.attributeKey)(a.ns, a.local)]) };
167
+ if ((0, formulas_1.callsDde)(a.value))
168
+ this.edit(this.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Formula, { formula: (0, context_1.clip)(a.value) }), raw, strip);
169
+ for (const [fn, kind] of (0, formulas_1.formulaCalls)(a.value))
170
+ this.edit(this.report(kind === 'fetch' ? types_1.OfficeCategory.External : types_1.OfficeCategory.Macro, types_1.OfficeDetail.Formula, { function: (0, context_1.clip)(fn), formula: (0, context_1.clip)(a.value) }), raw, strip);
122
171
  }
123
- if (href === undefined || (0, links_1.isFragment)(href) || (el.ns === NS.script && el.local === 'event-listener') || (el.ns === NS.presentation && el.local === 'event-listener'))
172
+ if (href === undefined || (0, links_1.isFragment)(href) || (el.ns === exports.NS.script && el.local === 'event-listener') || (el.ns === exports.NS.presentation && el.local === 'event-listener'))
124
173
  return;
125
174
  // A database document names its own embedded database this way.
126
- if (el.ns === NS.database && el.local === 'connection-resource' && /^sdbc:embedded:/i.test(href.trim()))
175
+ if (el.ns === exports.NS.database && el.local === 'connection-resource' && /^sdbc:embedded:/i.test(href.trim()))
176
+ return;
177
+ // A chart names the document that holds it, its data source, as "..".
178
+ if (el.ns === exports.NS.chart && el.local === 'chart' && /^\.\.\/?$/.test(href.trim()))
127
179
  return;
128
- if (LINK_ELEMENTS.has(key) || (el.ns === NS.form && el.local !== 'form'))
129
- this.ctx.link(href, this.entry, { element: (0, context_1.clip)(el.qname) });
130
- else if ((ALWAYS_OUTSIDE.has(key) || !(0, links_1.isInsidePackage)(href)) && !(el.ns === NS.form && el.local === 'form')) {
131
- this.ctx.external(EXTERNAL_KIND.get(key) ?? types_1.OfficeDetail.Other, href, this.entry, { element: (0, context_1.clip)(el.qname) }, (0, context_1.clip)(el.local));
180
+ if (LINK_ELEMENTS.has(key)) {
181
+ // The text of a link stays without it. An image map area is nothing but its link.
182
+ const r = this.ctx.link(href, this.entry, { element: (0, context_1.clip)(el.qname) });
183
+ this.edit(r, raw, el.local === 'a' ? { unwrap: true } : { remove: true });
184
+ if (r.action !== 'strip' && !this.label)
185
+ this.label = { index: el.index, href, text: '', title: oattr(el, exports.NS.office, 'title') ?? oattr(el, exports.NS.xlink, 'title'), unwrap: el.local === 'a' };
186
+ }
187
+ else if (el.ns === exports.NS.form && el.local === 'form') {
188
+ // A form's own href is where it sends its fields, which goes as pdf-defuse's form submit actions go.
189
+ this.edit(this.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.SubmitForm, { element: (0, context_1.clip)(el.qname), target: (0, context_1.clip)(href, 2000) }), raw, stripHref(raw));
190
+ }
191
+ else if (el.ns === exports.NS.form)
192
+ this.edit(this.ctx.link(href, this.entry, { element: (0, context_1.clip)(el.qname) }), raw, stripHref(raw));
193
+ else if (ALWAYS_OUTSIDE.has(key) || !this.inside(href)) {
194
+ this.edit(this.ctx.external(EXTERNAL_KIND.get(key) ?? types_1.OfficeDetail.Other, href, this.entry, { element: (0, context_1.clip)(el.qname) }, (0, context_1.clip)(el.local)), raw);
132
195
  }
133
196
  }
134
197
  text(t) {
135
198
  if (this.script && t.trim())
136
199
  this.script.content = true;
200
+ if (this.label && this.label.text.length < 4096)
201
+ this.label.text += t;
137
202
  }
138
203
  end(el) {
139
- if (this.scriptsDepth === el.depth && odfNs(el.ns) === NS.office && el.local === 'scripts')
204
+ const label = this.label;
205
+ if (label && label.index === el.index) {
206
+ this.label = undefined;
207
+ const r = this.ctx.labelMismatch(label.href, [label.text, label.title], this.entry, { element: (0, context_1.clip)(el.qname) });
208
+ if (r)
209
+ this.edit(r, el, label.unwrap ? { unwrap: true } : { remove: true });
210
+ }
211
+ if (this.scriptsDepth === el.depth && odfNs(el.ns) === exports.NS.office && el.local === 'scripts')
140
212
  this.scriptsDepth = 0;
141
213
  if (this.script && this.script.depth === el.depth) {
142
214
  if (this.script.content)
143
- this.report(types_1.OfficeCategory.Macro, /basic/i.test(this.script.language) ? types_1.OfficeDetail.OdfBasic : types_1.OfficeDetail.OdfScripts, { language: (0, context_1.clip)(this.script.language) });
215
+ this.edit(this.report(types_1.OfficeCategory.Macro, /basic/i.test(this.script.language) ? types_1.OfficeDetail.OdfBasic : types_1.OfficeDetail.OdfScripts, { language: (0, context_1.clip)(this.script.language) }), el, { remove: true });
144
216
  this.script = undefined;
145
217
  }
146
218
  }
147
219
  scriptEvent(el, href) {
148
- const event = oattr(el, NS.script, 'event-name') ?? '';
220
+ const event = oattr(el, exports.NS.script, 'event-name') ?? '';
149
221
  // LibreOffice runs nothing for a listener whose script URL and macro name are both empty. An empty href does not
150
222
  // hide a macro name.
151
- const target = [href, oattr(el, NS.script, 'macro-name')].find(v => v !== undefined && v !== '');
223
+ const target = [href, oattr(el, exports.NS.script, 'macro-name')].find(v => v !== undefined && v !== '');
152
224
  if (target === undefined)
153
225
  return;
154
226
  const name = event
@@ -157,45 +229,43 @@ class EntryRules {
157
229
  .replace(/^on/, '')
158
230
  .replace(/-/g, '');
159
231
  const onOpen = ON_OPEN.has(name) || (this.scriptsDepth > 0 && ON_OPEN_DOCUMENT.has(name));
160
- const data = { event: (0, context_1.clip)(event), target: (0, context_1.clip)(target), language: (0, context_1.clip)(oattr(el, NS.script, 'language') ?? '') };
161
- if (onOpen)
162
- this.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.EventOnOpen, data);
163
- else
164
- this.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Event, data);
232
+ const data = { event: (0, context_1.clip)(event), target: (0, context_1.clip)(target), language: (0, context_1.clip)(oattr(el, exports.NS.script, 'language') ?? '') };
233
+ this.edit(this.report(types_1.OfficeCategory.Macro, onOpen ? types_1.OfficeDetail.EventOnOpen : types_1.OfficeDetail.Event, data), el);
165
234
  }
166
235
  presentationEvent(el, href) {
167
- const action = oattr(el, NS.presentation, 'action') ?? '';
168
- const data = { event: (0, context_1.clip)(oattr(el, NS.script, 'event-name') ?? ''), action: (0, context_1.clip)(action), ...(href ? { target: (0, context_1.clip)(href) } : {}) };
236
+ const action = oattr(el, exports.NS.presentation, 'action') ?? '';
237
+ const data = { event: (0, context_1.clip)(oattr(el, exports.NS.script, 'event-name') ?? ''), action: (0, context_1.clip)(action), ...(href ? { target: (0, context_1.clip)(href) } : {}) };
169
238
  if (action === 'execute')
170
- this.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.RunProgram, data);
239
+ this.edit(this.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.RunProgram, data), el);
171
240
  else if (action === 'verb')
172
- this.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.OleVerb, data);
241
+ this.edit(this.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.OleVerb, data), el);
173
242
  else if (href !== undefined && !(0, links_1.isFragment)(href))
174
- this.ctx.link(href, this.entry, data);
243
+ this.edit(this.ctx.link(href, this.entry, data), el);
175
244
  }
176
245
  }
246
+ exports.EntryRules = EntryRules;
177
247
  /** The manifest. Only paths that name an entry of the archive are kept, so a manifest of millions costs no memory. */
178
248
  async function readManifest(ctx, entry) {
179
249
  const keys = new Set(ctx.zip.entries.map(e => (0, zip_1.nameKey)(e.name)));
180
250
  const m = { paths: new Set(), encrypted: false };
181
251
  await ctx.parse(entry, {
182
252
  start(el) {
183
- if (odfNs(el.ns) !== NS.manifest)
253
+ if (odfNs(el.ns) !== exports.NS.manifest)
184
254
  return;
185
255
  if (el.local === 'encryption-data')
186
256
  m.encrypted = true;
187
257
  if (el.local !== 'file-entry')
188
258
  return;
189
- const path = oattr(el, NS.manifest, 'full-path');
259
+ const path = oattr(el, exports.NS.manifest, 'full-path');
190
260
  if (path === undefined)
191
261
  return;
192
262
  if (path === '/') {
193
263
  if (m.root !== undefined)
194
264
  ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.DuplicateEntry, entry.name, { kind: 'manifest path', name: '/' });
195
- m.root = oattr(el, NS.manifest, 'media-type') ?? '';
265
+ m.root = oattr(el, exports.NS.manifest, 'media-type') ?? '';
196
266
  return;
197
267
  }
198
- const key = packagePath(path);
268
+ const key = (0, exports.packagePath)(path);
199
269
  if (!keys.has(key))
200
270
  return;
201
271
  if (m.paths.has(key))
@@ -217,12 +287,16 @@ function folderOf(name) {
217
287
  }
218
288
  /** The most of the mimetype entry that is kept. The longest media type in the table is 56 characters. */
219
289
  const MAX_MIMETYPE = 256;
220
- /** Inspects an OpenDocument package. */
221
- async function inspectOdf(ctx) {
290
+ /**
291
+ * Names an OpenDocument package's format by the media type in its mimetype entry or, without one, its manifest's root
292
+ * entry. Throws when the two name different formats, or the media type is not one this package reads. `layout` hears,
293
+ * with the entry it concerns, of each way the mimetype entry and the manifest leave the layout OpenDocument asks for.
294
+ */
295
+ async function nameOdf(ctx, layout = () => undefined) {
222
296
  const zip = ctx.zip;
223
297
  const mimetype = zip.entries.find(e => e.name === 'mimetype');
224
298
  const manifestEntry = zip.get('META-INF/manifest.xml');
225
- const layout = (reason) => ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.OdfLayout, mimetype?.name ?? 'META-INF/manifest.xml', { reason });
299
+ const note = (reason) => layout(mimetype?.name ?? 'META-INF/manifest.xml', reason);
226
300
  let declared;
227
301
  if (mimetype) {
228
302
  let head = Buffer.alloc(0);
@@ -234,34 +308,42 @@ async function inspectOdf(ctx) {
234
308
  declared = head.toString('latin1') + (mimetype.size > MAX_MIMETYPE ? '...' : '');
235
309
  const first = zip.entries.reduce((a, b) => (b.localOffset < a.localOffset ? b : a));
236
310
  if (first !== mimetype)
237
- layout('mimetype is not the first entry');
311
+ note('mimetype is not the first entry');
238
312
  if (mimetype.method !== 0)
239
- layout('mimetype is compressed');
313
+ note('mimetype is compressed');
240
314
  if (mimetype.localExtra)
241
- layout('mimetype has an extra field');
315
+ note('mimetype has an extra field');
242
316
  if (declared !== declared.trim())
243
- layout('mimetype has white space around the media type');
317
+ note('mimetype has white space around the media type');
244
318
  declared = declared.trim();
245
319
  }
246
320
  else
247
- layout('there is no mimetype entry');
321
+ note('there is no mimetype entry');
248
322
  const manifest = manifestEntry ? await readManifest(ctx, manifestEntry) : undefined;
249
323
  if (!manifest)
250
- layout('there is no manifest');
324
+ note('there is no manifest');
251
325
  const root = manifest?.root;
252
326
  if (declared !== undefined && root !== undefined && declared !== root && formats_1.ODF_FORMATS.has(declared) && formats_1.ODF_FORMATS.has(root)) {
253
327
  throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.AmbiguousFormat, 'The mimetype entry and the manifest name different formats', { mimetype: (0, context_1.clip)(declared), manifest: (0, context_1.clip)(root) });
254
328
  }
255
- const type = declared ?? root ?? '';
256
- const format = formats_1.ODF_FORMATS.get(type);
329
+ const mediaType = declared ?? root ?? '';
330
+ const format = formats_1.ODF_FORMATS.get(mediaType);
257
331
  if (!format)
258
- throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The OpenDocument media type is not one this package reads', { mediaType: (0, context_1.clip)(type) });
332
+ throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The OpenDocument media type is not one this package reads', { mediaType: (0, context_1.clip)(mediaType) });
333
+ return { format, mediaType, mimetype, manifestEntry, manifest };
334
+ }
335
+ /** Inspects an OpenDocument package. */
336
+ async function inspectOdf(ctx) {
337
+ const zip = ctx.zip;
338
+ const { format, mediaType: type, mimetype, manifestEntry, manifest } = await nameOdf(ctx, (location, reason) => ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.OdfLayout, location, { reason }));
259
339
  ctx.format = format;
260
340
  if (manifest?.encrypted)
261
341
  throw new errors_1.StopError(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.PasswordRequired, 'The manifest lists encrypted entries');
262
342
  const folders = new Map();
263
343
  const oleRefs = new Map(zip.entries.map(e => [(0, zip_1.nameKey)(e.name), false]));
264
344
  const oleEntries = [];
345
+ /** Entries that are whole files in formats of their own, such as pictures, which go to the plugins. */
346
+ const wholeFiles = [];
265
347
  for (const entry of zip.entries) {
266
348
  if (entry === mimetype || entry === manifestEntry)
267
349
  continue;
@@ -280,18 +362,64 @@ async function inspectOdf(ctx) {
280
362
  await ctx.parse(entry, new EntryRules(ctx, entry.name, oleRefs));
281
363
  else if (await ctx.drain(entry))
282
364
  oleEntries.push(entry);
365
+ else if (!entry.directory && !/^meta-inf\//i.test(entry.name))
366
+ wholeFiles.push(entry);
283
367
  }
368
+ const plan = ctx.plan;
369
+ // A folder goes whole, its empty files with it.
284
370
  for (const [prefix, f] of folders)
285
- ctx.report(f.category, f.detail, prefix, { entries: f.entries });
286
- for (const e of oleEntries)
287
- if (!oleRefs.get((0, zip_1.nameKey)(e.name)))
288
- ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleFile, e.name, { part: e.name });
371
+ if (ctx.report(f.category, f.detail, prefix, { entries: f.entries }).action === 'strip')
372
+ plan.folders.add(`${(0, zip_1.nameKey)(prefix)}/`);
373
+ // OLE entries go to the plugins. When none takes one, the elements that refer to it decide, or EMBEDDED/OLE_FILE.
374
+ const decided = new Map();
375
+ for (const e of oleEntries) {
376
+ const key = (0, zip_1.nameKey)(e.name);
377
+ const decision = await (0, contained_1.decideContained)(ctx, { name: e.name, location: e.name, chunks: () => zip.read(e) });
378
+ if (decision) {
379
+ decided.set(key, decision.keep);
380
+ if (!decision.keep)
381
+ plan.drop.add(key);
382
+ else if (decision.replacement)
383
+ plan.replace.set(key, decision.replacement);
384
+ }
385
+ if (!oleRefs.get(key)) {
386
+ const r = ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleFile, e.name, { part: e.name }, (0, contained_1.presence)(decision));
387
+ if (!decision && r.action === 'strip')
388
+ plan.drop.add(key);
389
+ }
390
+ }
391
+ // A whole file no plugin takes stays, since no rule names it.
392
+ for (const e of wholeFiles) {
393
+ const decision = await (0, contained_1.decideContained)(ctx, { name: e.name, location: e.name, chunks: () => zip.read(e) });
394
+ if (decision && !decision.keep)
395
+ plan.drop.add((0, zip_1.nameKey)(e.name));
396
+ else if (decision?.replacement)
397
+ plan.replace.set((0, zip_1.nameKey)(e.name), decision.replacement);
398
+ }
399
+ const files = new Set(zip.entries.filter(e => !e.directory).map(e => (0, zip_1.nameKey)(e.name)));
400
+ for (const o of plan.objects) {
401
+ const kept = decided.get(o.target);
402
+ if (kept === true) {
403
+ // A plugin kept the object's file, so the object stays.
404
+ if (o.finding) {
405
+ o.finding.action = 'info';
406
+ ctx.run.superseded.add(o.finding);
407
+ }
408
+ continue;
409
+ }
410
+ if (kept === false || o.strips) {
411
+ plan.element(o.part, o.index, { remove: true });
412
+ if (files.has(o.target))
413
+ plan.drop.add(o.target);
414
+ }
415
+ }
289
416
  if (manifest) {
290
417
  for (const e of zip.entries) {
291
418
  if (e.directory || e === mimetype || e === manifestEntry || /^meta-inf\//i.test(e.name))
292
419
  continue;
293
- if (!manifest.paths.has((0, zip_1.nameKey)(e.name)))
294
- ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.UnreferencedPart, e.name, { part: e.name });
420
+ if (!manifest.paths.has((0, zip_1.nameKey)(e.name)) && ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.UnreferencedPart, e.name, { part: e.name }).action === 'strip')
421
+ plan.drop.add((0, zip_1.nameKey)(e.name));
295
422
  }
296
423
  }
424
+ plan.odf = { mimetype, manifest: manifestEntry, mediaType: type };
297
425
  }
@@ -5,6 +5,8 @@ import { type ByteSource } from './types';
5
5
  import type { Decrypt } from './xls';
6
6
  /** The password Excel opens a workbook with when nobody set one to open it (MS-OFFCRYPTO 2.4.3). */
7
7
  export declare const DEFAULT_PASSWORD = "VelvetSweatshop";
8
+ /** The password PowerPoint 97 to 2003 opens a presentation with when nobody set one to open it. */
9
+ export declare const POWERPOINT_PASSWORD = "/01Hannes Ruescher/01";
8
10
  /** RC4, which OpenSSL 3 leaves out of its default provider. */
9
11
  export declare class Rc4 {
10
12
  private readonly s;
@@ -17,9 +19,30 @@ export declare class Rc4 {
17
19
  /**
18
20
  * Opens a workbook that its FILEPASS record (MS-XLS 2.4.117) encrypts with RC4 or RC4 CryptoAPI, with the caller's
19
21
  * password or VelvetSweatshop. The records after FILEPASS are encrypted in 1024-byte blocks of the stream, each with its
20
- * own key. Their headers are not, but take keystream bytes, and some records are never encrypted.
22
+ * own key. Their headers are not, but take keystream bytes, and some records are never encrypted. XOR obfuscation,
23
+ * the only kind an Excel 5.0 or 95 workbook has, is undone the same way record by record.
21
24
  */
22
25
  export declare function unlockWorkbook(ctx: RunContext, filePass: BiffRecord, stream: string): Decrypt;
26
+ /** A stream decrypted where it is read. */
27
+ export type StreamDecrypt = (offset: number, bytes: Uint8Array) => Uint8Array;
28
+ /** What opens an encrypted Word 97 to 2003 document: a decryption for each of its three encrypted streams. */
29
+ export interface WordKey {
30
+ word: StreamDecrypt;
31
+ table: StreamDecrypt;
32
+ data: StreamDecrypt;
33
+ }
34
+ /**
35
+ * Opens an encrypted Word 97 to 2003 document (MS-DOC 2.2.6) with the caller's passwords. XOR obfuscation keeps its
36
+ * verifier in FibBase.lKey. RC4 and RC4 CryptoAPI keep their EncryptionHeader in the table stream's first lKey bytes.
37
+ * The WordDocument stream stays plain for its first 68 bytes, and Word has no default password.
38
+ */
39
+ export declare function unlockDocument(ctx: RunContext, fib: Uint8Array, table: ByteSource, stream: string): Promise<WordKey>;
40
+ /**
41
+ * Opens an encrypted PowerPoint 97 to 2003 presentation (MS-PPT 2.3.7): `session` is the CryptSession10Container's
42
+ * data, an RC4 CryptoAPI EncryptionHeader. Returns the key of each persist object, whose block number is its persist
43
+ * id, after the caller's passwords and PowerPoint's own default have been tried.
44
+ */
45
+ export declare function unlockPresentation(ctx: RunContext, session: Uint8Array, stream: string): (id: number) => Rc4;
23
46
  /**
24
47
  * Inspects an encrypted OOXML package: EncryptionInfo (MS-OFFCRYPTO 2.3.4) names the cipher, and the caller's password
25
48
  * or VelvetSweatshop gives the key. The package it decrypts goes through the OOXML rules, read in place: Agile