@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/odf.d.ts CHANGED
@@ -1,3 +1,67 @@
1
- import { type RunContext } from './context';
1
+ import { type PlanContext, type ZipContext } from './context';
2
+ import { OfficeCategory as C, OfficeDetail as D } from './types';
3
+ import type { XmlElement, XmlHandler } from './xml';
4
+ export declare const NS: {
5
+ manifest: string;
6
+ office: string;
7
+ script: string;
8
+ presentation: string;
9
+ text: string;
10
+ draw: string;
11
+ table: string;
12
+ form: string;
13
+ meta: string;
14
+ style: string;
15
+ database: string;
16
+ chart: string;
17
+ xlink: string;
18
+ };
19
+ /** An attribute by normalized namespace and local name. */
20
+ export declare function oattr(el: XmlElement, ns: string, local: string): string | undefined;
21
+ /** The ODF_NS-normalized namespace of `ns`, exported for the rewrite. */
22
+ export declare const odfNamespace: (ns: string) => string;
23
+ /** The xlink:href of an element, for the rewrite. */
24
+ export declare const hrefOf: (el: XmlElement) => string | undefined;
25
+ /** A path inside the package as the manifest and the zip name it: without "./" and leading slashes. */
26
+ export declare const packagePath: (href: string) => string;
27
+ /**
28
+ * The rules that read inside one XML entry of an ODF package, or inside a flat OpenDocument file. A flat file has no
29
+ * entries, so in one every relative reference names a file beside the document, which loads from outside it.
30
+ */
31
+ export declare class EntryRules implements XmlHandler {
32
+ private readonly ctx;
33
+ private readonly entry;
34
+ /** Whether an object refers to each entry, by key. Only entries are kept, so a part full of references costs no memory. */
35
+ private readonly oleRefs;
36
+ private readonly flat;
37
+ /** Depth of the office:scripts element being read, 0 outside one. */
38
+ private scriptsDepth;
39
+ /** An office:script element being read, with whether it holds anything. */
40
+ private script?;
41
+ /** A link element being read, whose text and title go into the label check at its end. */
42
+ private label?;
43
+ constructor(ctx: PlanContext, entry: string,
44
+ /** Whether an object refers to each entry, by key. Only entries are kept, so a part full of references costs no memory. */
45
+ oleRefs: Map<string, boolean>, flat?: boolean);
46
+ /**
47
+ * Whether a reference names an entry of the package. LibreOffice's vnd.sun.star.Package: URLs name one too. In a
48
+ * flat file no entry exists, so only such a URL stays inside, naming nothing.
49
+ */
50
+ private inside;
51
+ private report;
52
+ /** Records the edit for the element when the finding strips. */
53
+ private edit;
54
+ start(raw: XmlElement): void;
55
+ text(t: string): void;
56
+ end(el: XmlElement): void;
57
+ private scriptEvent;
58
+ private presentationEvent;
59
+ }
60
+ /** The code folder an entry sits in, as the folder's path and its finding. */
61
+ export declare function folderOf(name: string): {
62
+ prefix: string;
63
+ detail: D;
64
+ category: C;
65
+ } | undefined;
2
66
  /** Inspects an OpenDocument package. */
3
- export declare function inspectOdf(ctx: RunContext): Promise<void>;
67
+ export declare function inspectOdf(ctx: ZipContext): Promise<void>;
package/dist/odf.js CHANGED
@@ -1,14 +1,19 @@
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;
3
6
  exports.inspectOdf = inspectOdf;
7
+ const contained_1 = require("./contained");
4
8
  const context_1 = require("./context");
5
9
  const errors_1 = require("./errors");
6
10
  const formats_1 = require("./formats");
7
11
  const formulas_1 = require("./formulas");
8
12
  const links_1 = require("./links");
13
+ const plan_1 = require("./plan");
9
14
  const types_1 = require("./types");
10
15
  const zip_1 = require("./zip");
11
- const NS = {
16
+ exports.NS = {
12
17
  manifest: 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0',
13
18
  office: 'urn:oasis:names:tc:opendocument:xmlns:office:1.0',
14
19
  script: 'urn:oasis:names:tc:opendocument:xmlns:script:1.0',
@@ -20,6 +25,7 @@ const NS = {
20
25
  meta: 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
21
26
  style: 'urn:oasis:names:tc:opendocument:xmlns:style:1.0',
22
27
  database: 'urn:oasis:names:tc:opendocument:xmlns:database:1.0',
28
+ chart: 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0',
23
29
  xlink: 'http://www.w3.org/1999/xlink',
24
30
  };
25
31
  /**
@@ -35,21 +41,21 @@ const FOLDERS = [
35
41
  /** Event names that fire as the document opens. Focus and activation count only for the document's own events. */
36
42
  const ON_OPEN = new Set(['load', 'new', 'create', 'loadfinished', 'viewcreated']);
37
43
  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`]);
44
+ 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
45
  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],
46
+ [`${exports.NS.meta} template`, types_1.OfficeDetail.Template],
47
+ [`${exports.NS.meta} auto-reload`, types_1.OfficeDetail.Reload],
48
+ [`${exports.NS.draw} image`, types_1.OfficeDetail.Image],
49
+ [`${exports.NS.draw} fill-image`, types_1.OfficeDetail.Image],
50
+ [`${exports.NS.style} background-image`, types_1.OfficeDetail.Image],
51
+ [`${exports.NS.text} section-source`, types_1.OfficeDetail.Include],
52
+ [`${exports.NS.table} table-source`, types_1.OfficeDetail.Include],
53
+ [`${exports.NS.draw} object`, types_1.OfficeDetail.Object],
54
+ [`${exports.NS.draw} object-ole`, types_1.OfficeDetail.Object],
55
+ [`${exports.NS.draw} floating-frame`, types_1.OfficeDetail.Frame],
50
56
  ]);
51
57
  /** 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`]);
58
+ 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
59
  /**
54
60
  * LibreOffice reads urn:oasis:names:tc:<any>:xmlns:<name>:1.<any> as the OpenDocument namespace <name> 1.0, so this
55
61
  * does too. The comparison ignores case, which can only find more.
@@ -65,90 +71,155 @@ function oattr(el, ns, local) {
65
71
  return a.value;
66
72
  return undefined;
67
73
  }
74
+ /** The ODF_NS-normalized namespace of `ns`, exported for the rewrite. */
75
+ const odfNamespace = (ns) => odfNs(ns);
76
+ exports.odfNamespace = odfNamespace;
77
+ /** The edit that takes an element's xlink:href away, under whatever namespace URI the attribute has. */
78
+ function stripHref(el) {
79
+ 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))) };
80
+ }
81
+ /** The xlink:href of an element, for the rewrite. */
82
+ const hrefOf = (el) => oattr(el, exports.NS.xlink, 'href');
83
+ exports.hrefOf = hrefOf;
68
84
  /** A path inside the package as the manifest and the zip name it: without "./" and leading slashes. */
69
85
  const packagePath = (href) => (0, zip_1.nameKey)(href.trim().replace(/^(\.\/)+/, ''));
70
- /** The rules that read inside one XML entry of an ODF package. */
86
+ exports.packagePath = packagePath;
87
+ /**
88
+ * The rules that read inside one XML entry of an ODF package, or inside a flat OpenDocument file. A flat file has no
89
+ * entries, so in one every relative reference names a file beside the document, which loads from outside it.
90
+ */
71
91
  class EntryRules {
72
92
  ctx;
73
93
  entry;
74
94
  oleRefs;
95
+ flat;
75
96
  /** Depth of the office:scripts element being read, 0 outside one. */
76
97
  scriptsDepth = 0;
77
98
  /** An office:script element being read, with whether it holds anything. */
78
99
  script;
100
+ /** A link element being read, whose text and title go into the label check at its end. */
101
+ label;
79
102
  constructor(ctx, entry,
80
103
  /** Whether an object refers to each entry, by key. Only entries are kept, so a part full of references costs no memory. */
81
- oleRefs) {
104
+ oleRefs, flat = false) {
82
105
  this.ctx = ctx;
83
106
  this.entry = entry;
84
107
  this.oleRefs = oleRefs;
108
+ this.flat = flat;
109
+ }
110
+ /**
111
+ * Whether a reference names an entry of the package. LibreOffice's vnd.sun.star.Package: URLs name one too. In a
112
+ * flat file no entry exists, so only such a URL stays inside, naming nothing.
113
+ */
114
+ inside(href) {
115
+ const own = /^\s*vnd\.sun\.star\.package:(.*)$/is.exec(href);
116
+ if (own)
117
+ return this.flat || (0, links_1.isInsidePackage)(own[1]);
118
+ return !this.flat && (0, links_1.isInsidePackage)(href);
85
119
  }
86
120
  report(category, detail, data) {
87
- this.ctx.report(category, detail, this.entry, data);
121
+ return this.ctx.report(category, detail, this.entry, data);
122
+ }
123
+ /** Records the edit for the element when the finding strips. */
124
+ edit(r, el, edit = { remove: true }) {
125
+ if (r.action === 'strip')
126
+ this.ctx.plan.element(this.entry, el.index, edit);
88
127
  }
89
128
  start(raw) {
90
129
  if (this.script)
91
130
  this.script.content = true;
92
131
  const el = { ...raw, ns: odfNs(raw.ns) };
93
132
  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)
133
+ const href = oattr(el, exports.NS.xlink, 'href');
134
+ if (el.ns === exports.NS.office && el.local === 'scripts' && !this.scriptsDepth)
96
135
  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')
136
+ else if (el.ns === exports.NS.office && el.local === 'script')
137
+ this.script = { depth: el.depth, language: oattr(el, exports.NS.script, 'language') ?? '', content: false, index: el.index };
138
+ else if (el.ns === exports.NS.script && el.local === 'event-listener')
100
139
  this.scriptEvent(el, href);
101
- else if (el.ns === NS.presentation && el.local === 'event-listener')
140
+ else if (el.ns === exports.NS.presentation && el.local === 'event-listener')
102
141
  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') ?? '') });
142
+ else if ((el.ns === exports.NS.text && el.local === 'dde-connection-decl') || (el.ns === exports.NS.office && el.local === 'dde-source')) {
143
+ 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
144
  }
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) } : {}) });
145
+ else if (el.ns === exports.NS.draw && el.local === 'applet') {
146
+ 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
147
  return;
109
148
  }
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) } : {}) });
149
+ else if (el.ns === exports.NS.draw && el.local === 'plugin') {
150
+ 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
151
  return;
113
152
  }
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);
153
+ else if (el.ns === exports.NS.draw && el.local === 'object-ole' && (href === undefined || this.inside(href))) {
154
+ // The object's entry may still be ahead in the archive, so its plugin verdict decides this element later. The
155
+ // preview image beside the element stays either way.
156
+ const r = this.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, href ? { target: (0, context_1.clip)(href) } : undefined);
157
+ this.ctx.plan.objects.push({ part: this.entry, index: el.index, target: href ? (0, exports.packagePath)(href) : '', finding: r.finding, strips: r.action === 'strip' });
158
+ if (href && this.oleRefs.has((0, exports.packagePath)(href)))
159
+ this.oleRefs.set((0, exports.packagePath)(href), true);
118
160
  }
119
161
  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) });
162
+ // The cell keeps its value without the formula.
163
+ if (odfNs(a.ns) !== exports.NS.table || (a.local !== 'formula' && a.local !== 'expression'))
164
+ continue;
165
+ const strip = { strip: new Set([(0, plan_1.attributeKey)(a.ns, a.local)]) };
166
+ if ((0, formulas_1.callsDde)(a.value))
167
+ this.edit(this.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Formula, { formula: (0, context_1.clip)(a.value) }), raw, strip);
168
+ for (const [fn, kind] of (0, formulas_1.formulaCalls)(a.value))
169
+ 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
170
  }
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'))
171
+ 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
172
  return;
125
173
  // 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()))
174
+ if (el.ns === exports.NS.database && el.local === 'connection-resource' && /^sdbc:embedded:/i.test(href.trim()))
175
+ return;
176
+ // A chart names the document that holds it, its data source, as "..".
177
+ if (el.ns === exports.NS.chart && el.local === 'chart' && /^\.\.\/?$/.test(href.trim()))
127
178
  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));
179
+ if (LINK_ELEMENTS.has(key)) {
180
+ // The text of a link stays without it. An image map area is nothing but its link.
181
+ const r = this.ctx.link(href, this.entry, { element: (0, context_1.clip)(el.qname) });
182
+ this.edit(r, raw, el.local === 'a' ? { unwrap: true } : { remove: true });
183
+ if (r.action !== 'strip' && !this.label)
184
+ this.label = { index: el.index, href, text: '', title: oattr(el, exports.NS.office, 'title') ?? oattr(el, exports.NS.xlink, 'title'), unwrap: el.local === 'a' };
185
+ }
186
+ else if (el.ns === exports.NS.form && el.local === 'form') {
187
+ // A form's own href is where it sends its fields, which goes as pdf-defuse's form submit actions go.
188
+ 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));
189
+ }
190
+ else if (el.ns === exports.NS.form)
191
+ this.edit(this.ctx.link(href, this.entry, { element: (0, context_1.clip)(el.qname) }), raw, stripHref(raw));
192
+ else if (ALWAYS_OUTSIDE.has(key) || !this.inside(href)) {
193
+ 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
194
  }
133
195
  }
134
196
  text(t) {
135
197
  if (this.script && t.trim())
136
198
  this.script.content = true;
199
+ if (this.label && this.label.text.length < 4096)
200
+ this.label.text += t;
137
201
  }
138
202
  end(el) {
139
- if (this.scriptsDepth === el.depth && odfNs(el.ns) === NS.office && el.local === 'scripts')
203
+ const label = this.label;
204
+ if (label && label.index === el.index) {
205
+ this.label = undefined;
206
+ const r = this.ctx.labelMismatch(label.href, [label.text, label.title], this.entry, { element: (0, context_1.clip)(el.qname) });
207
+ if (r)
208
+ this.edit(r, el, label.unwrap ? { unwrap: true } : { remove: true });
209
+ }
210
+ if (this.scriptsDepth === el.depth && odfNs(el.ns) === exports.NS.office && el.local === 'scripts')
140
211
  this.scriptsDepth = 0;
141
212
  if (this.script && this.script.depth === el.depth) {
142
213
  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) });
214
+ 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
215
  this.script = undefined;
145
216
  }
146
217
  }
147
218
  scriptEvent(el, href) {
148
- const event = oattr(el, NS.script, 'event-name') ?? '';
219
+ const event = oattr(el, exports.NS.script, 'event-name') ?? '';
149
220
  // LibreOffice runs nothing for a listener whose script URL and macro name are both empty. An empty href does not
150
221
  // hide a macro name.
151
- const target = [href, oattr(el, NS.script, 'macro-name')].find(v => v !== undefined && v !== '');
222
+ const target = [href, oattr(el, exports.NS.script, 'macro-name')].find(v => v !== undefined && v !== '');
152
223
  if (target === undefined)
153
224
  return;
154
225
  const name = event
@@ -157,45 +228,43 @@ class EntryRules {
157
228
  .replace(/^on/, '')
158
229
  .replace(/-/g, '');
159
230
  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);
231
+ 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') ?? '') };
232
+ this.edit(this.report(types_1.OfficeCategory.Macro, onOpen ? types_1.OfficeDetail.EventOnOpen : types_1.OfficeDetail.Event, data), el);
165
233
  }
166
234
  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) } : {}) };
235
+ const action = oattr(el, exports.NS.presentation, 'action') ?? '';
236
+ 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
237
  if (action === 'execute')
170
- this.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.RunProgram, data);
238
+ this.edit(this.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.RunProgram, data), el);
171
239
  else if (action === 'verb')
172
- this.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.OleVerb, data);
240
+ this.edit(this.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.OleVerb, data), el);
173
241
  else if (href !== undefined && !(0, links_1.isFragment)(href))
174
- this.ctx.link(href, this.entry, data);
242
+ this.edit(this.ctx.link(href, this.entry, data), el);
175
243
  }
176
244
  }
245
+ exports.EntryRules = EntryRules;
177
246
  /** The manifest. Only paths that name an entry of the archive are kept, so a manifest of millions costs no memory. */
178
247
  async function readManifest(ctx, entry) {
179
248
  const keys = new Set(ctx.zip.entries.map(e => (0, zip_1.nameKey)(e.name)));
180
249
  const m = { paths: new Set(), encrypted: false };
181
250
  await ctx.parse(entry, {
182
251
  start(el) {
183
- if (odfNs(el.ns) !== NS.manifest)
252
+ if (odfNs(el.ns) !== exports.NS.manifest)
184
253
  return;
185
254
  if (el.local === 'encryption-data')
186
255
  m.encrypted = true;
187
256
  if (el.local !== 'file-entry')
188
257
  return;
189
- const path = oattr(el, NS.manifest, 'full-path');
258
+ const path = oattr(el, exports.NS.manifest, 'full-path');
190
259
  if (path === undefined)
191
260
  return;
192
261
  if (path === '/') {
193
262
  if (m.root !== undefined)
194
263
  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') ?? '';
264
+ m.root = oattr(el, exports.NS.manifest, 'media-type') ?? '';
196
265
  return;
197
266
  }
198
- const key = packagePath(path);
267
+ const key = (0, exports.packagePath)(path);
199
268
  if (!keys.has(key))
200
269
  return;
201
270
  if (m.paths.has(key))
@@ -262,6 +331,8 @@ async function inspectOdf(ctx) {
262
331
  const folders = new Map();
263
332
  const oleRefs = new Map(zip.entries.map(e => [(0, zip_1.nameKey)(e.name), false]));
264
333
  const oleEntries = [];
334
+ /** Entries that are whole files in formats of their own, such as pictures, which go to the plugins. */
335
+ const wholeFiles = [];
265
336
  for (const entry of zip.entries) {
266
337
  if (entry === mimetype || entry === manifestEntry)
267
338
  continue;
@@ -280,18 +351,64 @@ async function inspectOdf(ctx) {
280
351
  await ctx.parse(entry, new EntryRules(ctx, entry.name, oleRefs));
281
352
  else if (await ctx.drain(entry))
282
353
  oleEntries.push(entry);
354
+ else if (!entry.directory && !/^meta-inf\//i.test(entry.name))
355
+ wholeFiles.push(entry);
283
356
  }
357
+ const plan = ctx.plan;
358
+ // A folder goes whole, its empty files with it.
284
359
  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 });
360
+ if (ctx.report(f.category, f.detail, prefix, { entries: f.entries }).action === 'strip')
361
+ plan.folders.add(`${(0, zip_1.nameKey)(prefix)}/`);
362
+ // OLE entries go to the plugins. When none takes one, the elements that refer to it decide, or EMBEDDED/OLE_FILE.
363
+ const decided = new Map();
364
+ for (const e of oleEntries) {
365
+ const key = (0, zip_1.nameKey)(e.name);
366
+ const decision = await (0, contained_1.decideContained)(ctx, { name: e.name, location: e.name, chunks: () => zip.read(e) });
367
+ if (decision) {
368
+ decided.set(key, decision.keep);
369
+ if (!decision.keep)
370
+ plan.drop.add(key);
371
+ else if (decision.replacement)
372
+ plan.replace.set(key, decision.replacement);
373
+ }
374
+ if (!oleRefs.get(key)) {
375
+ const r = ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleFile, e.name, { part: e.name }, (0, contained_1.presence)(decision));
376
+ if (!decision && r.action === 'strip')
377
+ plan.drop.add(key);
378
+ }
379
+ }
380
+ // A whole file no plugin takes stays, since no rule names it.
381
+ for (const e of wholeFiles) {
382
+ const decision = await (0, contained_1.decideContained)(ctx, { name: e.name, location: e.name, chunks: () => zip.read(e) });
383
+ if (decision && !decision.keep)
384
+ plan.drop.add((0, zip_1.nameKey)(e.name));
385
+ else if (decision?.replacement)
386
+ plan.replace.set((0, zip_1.nameKey)(e.name), decision.replacement);
387
+ }
388
+ const files = new Set(zip.entries.filter(e => !e.directory).map(e => (0, zip_1.nameKey)(e.name)));
389
+ for (const o of plan.objects) {
390
+ const kept = decided.get(o.target);
391
+ if (kept === true) {
392
+ // A plugin kept the object's file, so the object stays.
393
+ if (o.finding) {
394
+ o.finding.action = 'info';
395
+ ctx.run.superseded.add(o.finding);
396
+ }
397
+ continue;
398
+ }
399
+ if (kept === false || o.strips) {
400
+ plan.element(o.part, o.index, { remove: true });
401
+ if (files.has(o.target))
402
+ plan.drop.add(o.target);
403
+ }
404
+ }
289
405
  if (manifest) {
290
406
  for (const e of zip.entries) {
291
407
  if (e.directory || e === mimetype || e === manifestEntry || /^meta-inf\//i.test(e.name))
292
408
  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 });
409
+ 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')
410
+ plan.drop.add((0, zip_1.nameKey)(e.name));
295
411
  }
296
412
  }
413
+ plan.odf = { mimetype, manifest: manifestEntry, mediaType: type };
297
414
  }
@@ -0,0 +1,54 @@
1
+ import type { BiffRecord } from './biff';
2
+ import type { CfbEntry, CompoundFile } from './cfb';
3
+ import type { RunContext } from './context';
4
+ import { type ByteSource } from './types';
5
+ import type { Decrypt } from './xls';
6
+ /** The password Excel opens a workbook with when nobody set one to open it (MS-OFFCRYPTO 2.4.3). */
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";
10
+ /** RC4, which OpenSSL 3 leaves out of its default provider. */
11
+ export declare class Rc4 {
12
+ private readonly s;
13
+ private i;
14
+ private j;
15
+ constructor(key: Uint8Array);
16
+ /** XORs the next keystream bytes into a copy of `data`. */
17
+ crypt(data: Uint8Array): Uint8Array;
18
+ }
19
+ /**
20
+ * Opens a workbook that its FILEPASS record (MS-XLS 2.4.117) encrypts with RC4 or RC4 CryptoAPI, with the caller's
21
+ * password or VelvetSweatshop. The records after FILEPASS are encrypted in 1024-byte blocks of the stream, each with its
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.
24
+ */
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;
46
+ /**
47
+ * Inspects an encrypted OOXML package: EncryptionInfo (MS-OFFCRYPTO 2.3.4) names the cipher, and the caller's password
48
+ * or VelvetSweatshop gives the key. The package it decrypts goes through the OOXML rules, read in place: Agile
49
+ * encryption works in 4,096-byte segments and Standard in 16-byte blocks, so nothing is decrypted that is not read.
50
+ */
51
+ export declare function openPackage(ctx: RunContext, cfb: CompoundFile, info: CfbEntry, pkg: CfbEntry): Promise<{
52
+ source: ByteSource;
53
+ size: number;
54
+ }>;