@patrickdk77/office-defuse 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +230 -2
- package/dist/context.d.ts +38 -0
- package/dist/context.js +100 -0
- package/dist/crc32.d.ts +2 -0
- package/dist/crc32.js +18 -0
- package/dist/engine.d.ts +5 -0
- package/dist/engine.js +151 -0
- package/dist/errors.d.ts +19 -0
- package/dist/errors.js +33 -0
- package/dist/findings.d.ts +25 -0
- package/dist/findings.js +169 -0
- package/dist/formats.d.ts +10 -0
- package/dist/formats.js +46 -0
- package/dist/formulas.d.ts +22 -0
- package/dist/formulas.js +77 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +30 -0
- package/dist/index.mjs +3 -0
- package/dist/io.d.ts +23 -0
- package/dist/io.js +139 -0
- package/dist/links.d.ts +26 -0
- package/dist/links.js +145 -0
- package/dist/odf.d.ts +3 -0
- package/dist/odf.js +297 -0
- package/dist/ooxml.d.ts +8 -0
- package/dist/ooxml.js +516 -0
- package/dist/score.d.ts +17 -0
- package/dist/score.js +63 -0
- package/dist/types.d.ts +247 -0
- package/dist/types.js +133 -0
- package/dist/xlsb.d.ts +19 -0
- package/dist/xlsb.js +101 -0
- package/dist/xml.d.ts +256 -0
- package/dist/xml.js +1105 -0
- package/dist/zip-writer.d.ts +38 -0
- package/dist/zip-writer.js +182 -0
- package/dist/zip.d.ts +55 -0
- package/dist/zip.js +535 -0
- package/package.json +96 -4
package/dist/links.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isAbsoluteTarget = exports.isInsidePackage = exports.isLocalTarget = exports.isFragment = void 0;
|
|
4
|
+
exports.checkLink = checkLink;
|
|
5
|
+
const types_1 = require("./types");
|
|
6
|
+
const xml_1 = require("./xml");
|
|
7
|
+
const IPV4 = /^\d{1,3}(\.\d{1,3}){3}$/;
|
|
8
|
+
const SCHEME = /^([a-zA-Z][a-zA-Z0-9+.-]*):/;
|
|
9
|
+
/** A browser drops control characters from a URL, and so does this before reading one. */
|
|
10
|
+
const clean = (s) => s.replace(/\p{Cc}/gu, '').trim();
|
|
11
|
+
const networkPrefix = (s) => s.startsWith('\\\\') || s.startsWith('//') || s.startsWith('\\/') || s.startsWith('/\\');
|
|
12
|
+
/** A target that names the same document or a place in it. RFC 3986 reads an empty reference as the document itself. */
|
|
13
|
+
const isFragment = (target) => {
|
|
14
|
+
const t = clean(target);
|
|
15
|
+
return t === '' || t.startsWith('#');
|
|
16
|
+
};
|
|
17
|
+
exports.isFragment = isFragment;
|
|
18
|
+
/**
|
|
19
|
+
* Classifies a hyperlink target with pdf-defuse's checks, without the comparison of the target with the link's text.
|
|
20
|
+
* Call it only for targets that are more than a fragment. A target that holds the content of an external entity
|
|
21
|
+
* cannot be read, and reads as the empty string: a relative link, which is what pdf-defuse makes of a URI it cannot
|
|
22
|
+
* resolve.
|
|
23
|
+
*/
|
|
24
|
+
function checkLink(rawTarget) {
|
|
25
|
+
const uri = rawTarget.includes(xml_1.UNKNOWN) ? '' : clean(rawTarget);
|
|
26
|
+
const link = (detail, site) => ({ category: types_1.OfficeCategory.Link, detail, host: site });
|
|
27
|
+
if (networkPrefix(uri))
|
|
28
|
+
return link(types_1.OfficeDetail.NetworkPath);
|
|
29
|
+
const m = SCHEME.exec(uri);
|
|
30
|
+
if (!m)
|
|
31
|
+
return link(types_1.OfficeDetail.Relative);
|
|
32
|
+
const scheme = m[1].toLowerCase();
|
|
33
|
+
// C:\path is a drive, not a scheme.
|
|
34
|
+
if (scheme.length === 1)
|
|
35
|
+
return link(types_1.OfficeDetail.FileUrl);
|
|
36
|
+
if (scheme === 'javascript' || scheme === 'vbscript')
|
|
37
|
+
return link(types_1.OfficeDetail.ScriptUrl);
|
|
38
|
+
if (scheme === 'vnd.sun.star.script' || scheme === 'macro')
|
|
39
|
+
return { category: types_1.OfficeCategory.Macro, detail: types_1.OfficeDetail.Link };
|
|
40
|
+
if (scheme === 'data')
|
|
41
|
+
return link(types_1.OfficeDetail.DataUrl);
|
|
42
|
+
if (scheme === 'smb' || scheme === 'cifs' || scheme === 'afp' || scheme === 'nfs')
|
|
43
|
+
return link(types_1.OfficeDetail.NetworkPath);
|
|
44
|
+
if (scheme === 'file') {
|
|
45
|
+
let host = '';
|
|
46
|
+
try {
|
|
47
|
+
host = new URL(uri).hostname;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// A file: URL the parser refuses is read as a local path.
|
|
51
|
+
}
|
|
52
|
+
return link(host && host.toLowerCase() !== 'localhost' ? types_1.OfficeDetail.NetworkPath : types_1.OfficeDetail.FileUrl);
|
|
53
|
+
}
|
|
54
|
+
if (scheme === 'mailto') {
|
|
55
|
+
let addr = uri.slice(7).split('?')[0];
|
|
56
|
+
try {
|
|
57
|
+
addr = decodeURIComponent(addr);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// The address stays as written.
|
|
61
|
+
}
|
|
62
|
+
const domain = addr.includes('@') ? addr.slice(addr.lastIndexOf('@') + 1).toLowerCase() : '';
|
|
63
|
+
return link(hostVerdict(domain, domain) ?? types_1.OfficeDetail.Safe, domain);
|
|
64
|
+
}
|
|
65
|
+
if (scheme !== 'http' && scheme !== 'https')
|
|
66
|
+
return link(types_1.OfficeDetail.OtherScheme);
|
|
67
|
+
// The authority as written, before the URL parser normalizes it.
|
|
68
|
+
const rawAuth = uri
|
|
69
|
+
.slice(m[0].length)
|
|
70
|
+
.replace(/^[\\/]*/, '')
|
|
71
|
+
.split(/[/\\?#]/, 1)[0];
|
|
72
|
+
let parsed;
|
|
73
|
+
try {
|
|
74
|
+
parsed = new URL(uri);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return link(types_1.OfficeDetail.OtherScheme);
|
|
78
|
+
}
|
|
79
|
+
if (parsed.username || parsed.password || rawAuth.includes('@'))
|
|
80
|
+
return link(types_1.OfficeDetail.Credentials);
|
|
81
|
+
// The parser lowercases, punycodes and turns every IPv4 shorthand into dotted form, as browsers do.
|
|
82
|
+
const host = parsed.hostname.replace(/\.$/, '');
|
|
83
|
+
return link(hostVerdict(host, rawAuth) ?? types_1.OfficeDetail.Safe, host);
|
|
84
|
+
}
|
|
85
|
+
function hostVerdict(host, raw) {
|
|
86
|
+
if (!host)
|
|
87
|
+
return types_1.OfficeDetail.OtherScheme;
|
|
88
|
+
if (raw.includes('%'))
|
|
89
|
+
return types_1.OfficeDetail.EncodedHost;
|
|
90
|
+
if (host.startsWith('[') || IPV4.test(host))
|
|
91
|
+
return types_1.OfficeDetail.IpHost;
|
|
92
|
+
if (/^\d+$/.test(host) || /^0x[0-9a-f]+$/i.test(host))
|
|
93
|
+
return types_1.OfficeDetail.IpHost;
|
|
94
|
+
if (/\P{ASCII}/u.test(host) || host.split('.').some(label => label.startsWith('xn--')))
|
|
95
|
+
return types_1.OfficeDetail.LookalikeHost;
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The target with every %XX turned into its character. A target can percent-encode its scheme, as `%73cript:` does in
|
|
100
|
+
* a CVE-2017-0199 sample, so the checks below hold a target local or inside the package only when both forms are.
|
|
101
|
+
*/
|
|
102
|
+
const decoded = (s) => s.replace(/%([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)));
|
|
103
|
+
function local(t) {
|
|
104
|
+
if (networkPrefix(t))
|
|
105
|
+
return false;
|
|
106
|
+
const m = SCHEME.exec(t);
|
|
107
|
+
if (!m || m[1].length === 1)
|
|
108
|
+
return true;
|
|
109
|
+
if (m[1].toLowerCase() !== 'file')
|
|
110
|
+
return false;
|
|
111
|
+
try {
|
|
112
|
+
const host = new URL(t).hostname;
|
|
113
|
+
return !host || host.toLowerCase() === 'localhost';
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Whether a target outside the file is on this machine: a drive path, a file: URL without a host, an absolute path or
|
|
121
|
+
* a relative one. A UNC path, a file: URL with a host and every other scheme reach elsewhere, and so can a target that
|
|
122
|
+
* holds the content of an external entity.
|
|
123
|
+
*/
|
|
124
|
+
const isLocalTarget = (rawTarget) => !rawTarget.includes(xml_1.UNKNOWN) && local(clean(rawTarget)) && local(clean(decoded(rawTarget)));
|
|
125
|
+
exports.isLocalTarget = isLocalTarget;
|
|
126
|
+
function inside(t) {
|
|
127
|
+
if (t === '' || networkPrefix(t) || SCHEME.test(t) || t.startsWith('/') || t.startsWith('\\'))
|
|
128
|
+
return false;
|
|
129
|
+
let depth = 0;
|
|
130
|
+
for (const seg of t.split(/[/\\]/)) {
|
|
131
|
+
if (seg === '..')
|
|
132
|
+
depth--;
|
|
133
|
+
else if (seg !== '.' && seg !== '')
|
|
134
|
+
depth++;
|
|
135
|
+
if (depth < 0)
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
/** Whether an ODF reference names an entry inside the package: a relative path that does not climb out of it. */
|
|
141
|
+
const isInsidePackage = (rawHref) => inside(clean(rawHref)) && inside(clean(decoded(rawHref)));
|
|
142
|
+
exports.isInsidePackage = isInsidePackage;
|
|
143
|
+
/** Whether an OOXML relationship target is not a part name, whatever TargetMode says: a scheme, a drive or a network path. */
|
|
144
|
+
const isAbsoluteTarget = (rawTarget) => [rawTarget, decoded(rawTarget)].some(t => SCHEME.test(clean(t)) || t.startsWith('\\') || t.startsWith('//'));
|
|
145
|
+
exports.isAbsoluteTarget = isAbsoluteTarget;
|
package/dist/odf.d.ts
ADDED
package/dist/odf.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.inspectOdf = inspectOdf;
|
|
4
|
+
const context_1 = require("./context");
|
|
5
|
+
const errors_1 = require("./errors");
|
|
6
|
+
const formats_1 = require("./formats");
|
|
7
|
+
const formulas_1 = require("./formulas");
|
|
8
|
+
const links_1 = require("./links");
|
|
9
|
+
const types_1 = require("./types");
|
|
10
|
+
const zip_1 = require("./zip");
|
|
11
|
+
const NS = {
|
|
12
|
+
manifest: 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0',
|
|
13
|
+
office: 'urn:oasis:names:tc:opendocument:xmlns:office:1.0',
|
|
14
|
+
script: 'urn:oasis:names:tc:opendocument:xmlns:script:1.0',
|
|
15
|
+
presentation: 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0',
|
|
16
|
+
text: 'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
|
|
17
|
+
draw: 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0',
|
|
18
|
+
table: 'urn:oasis:names:tc:opendocument:xmlns:table:1.0',
|
|
19
|
+
form: 'urn:oasis:names:tc:opendocument:xmlns:form:1.0',
|
|
20
|
+
meta: 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
|
|
21
|
+
style: 'urn:oasis:names:tc:opendocument:xmlns:style:1.0',
|
|
22
|
+
database: 'urn:oasis:names:tc:opendocument:xmlns:database:1.0',
|
|
23
|
+
xlink: 'http://www.w3.org/1999/xlink',
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Folders of code and settings. Each is reported by name, as a whole, so the files in them are inflated and checked
|
|
27
|
+
* but not parsed.
|
|
28
|
+
*/
|
|
29
|
+
const FOLDERS = [
|
|
30
|
+
['basic', types_1.OfficeDetail.OdfBasic, types_1.OfficeCategory.Macro],
|
|
31
|
+
['scripts', types_1.OfficeDetail.OdfScripts, types_1.OfficeCategory.Macro],
|
|
32
|
+
['dialogs', types_1.OfficeDetail.OdfDialogs, types_1.OfficeCategory.Macro],
|
|
33
|
+
['configurations2', types_1.OfficeDetail.OdfConfiguration, types_1.OfficeCategory.Ui],
|
|
34
|
+
];
|
|
35
|
+
/** Event names that fire as the document opens. Focus and activation count only for the document's own events. */
|
|
36
|
+
const ON_OPEN = new Set(['load', 'new', 'create', 'loadfinished', 'viewcreated']);
|
|
37
|
+
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`]);
|
|
39
|
+
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],
|
|
50
|
+
]);
|
|
51
|
+
/** 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`]);
|
|
53
|
+
/**
|
|
54
|
+
* LibreOffice reads urn:oasis:names:tc:<any>:xmlns:<name>:1.<any> as the OpenDocument namespace <name> 1.0, so this
|
|
55
|
+
* does too. The comparison ignores case, which can only find more.
|
|
56
|
+
*/
|
|
57
|
+
function odfNs(ns) {
|
|
58
|
+
const m = /^urn:oasis:names:tc:[^:]*:xmlns:([^:]*):1\.[^:]+$/i.exec(ns);
|
|
59
|
+
return m ? `urn:oasis:names:tc:opendocument:xmlns:${m[1].toLowerCase()}:1.0` : ns;
|
|
60
|
+
}
|
|
61
|
+
/** An attribute by normalized namespace and local name. */
|
|
62
|
+
function oattr(el, ns, local) {
|
|
63
|
+
for (const a of el.attributes)
|
|
64
|
+
if (a.local === local && odfNs(a.ns) === ns)
|
|
65
|
+
return a.value;
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
/** A path inside the package as the manifest and the zip name it: without "./" and leading slashes. */
|
|
69
|
+
const packagePath = (href) => (0, zip_1.nameKey)(href.trim().replace(/^(\.\/)+/, ''));
|
|
70
|
+
/** The rules that read inside one XML entry of an ODF package. */
|
|
71
|
+
class EntryRules {
|
|
72
|
+
ctx;
|
|
73
|
+
entry;
|
|
74
|
+
oleRefs;
|
|
75
|
+
/** Depth of the office:scripts element being read, 0 outside one. */
|
|
76
|
+
scriptsDepth = 0;
|
|
77
|
+
/** An office:script element being read, with whether it holds anything. */
|
|
78
|
+
script;
|
|
79
|
+
constructor(ctx, entry,
|
|
80
|
+
/** Whether an object refers to each entry, by key. Only entries are kept, so a part full of references costs no memory. */
|
|
81
|
+
oleRefs) {
|
|
82
|
+
this.ctx = ctx;
|
|
83
|
+
this.entry = entry;
|
|
84
|
+
this.oleRefs = oleRefs;
|
|
85
|
+
}
|
|
86
|
+
report(category, detail, data) {
|
|
87
|
+
this.ctx.report(category, detail, this.entry, data);
|
|
88
|
+
}
|
|
89
|
+
start(raw) {
|
|
90
|
+
if (this.script)
|
|
91
|
+
this.script.content = true;
|
|
92
|
+
const el = { ...raw, ns: odfNs(raw.ns) };
|
|
93
|
+
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)
|
|
96
|
+
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')
|
|
100
|
+
this.scriptEvent(el, href);
|
|
101
|
+
else if (el.ns === NS.presentation && el.local === 'event-listener')
|
|
102
|
+
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') ?? '') });
|
|
105
|
+
}
|
|
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) } : {}) });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
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) } : {}) });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
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);
|
|
118
|
+
}
|
|
119
|
+
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) });
|
|
122
|
+
}
|
|
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'))
|
|
124
|
+
return;
|
|
125
|
+
// 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()))
|
|
127
|
+
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));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
text(t) {
|
|
135
|
+
if (this.script && t.trim())
|
|
136
|
+
this.script.content = true;
|
|
137
|
+
}
|
|
138
|
+
end(el) {
|
|
139
|
+
if (this.scriptsDepth === el.depth && odfNs(el.ns) === NS.office && el.local === 'scripts')
|
|
140
|
+
this.scriptsDepth = 0;
|
|
141
|
+
if (this.script && this.script.depth === el.depth) {
|
|
142
|
+
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) });
|
|
144
|
+
this.script = undefined;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
scriptEvent(el, href) {
|
|
148
|
+
const event = oattr(el, NS.script, 'event-name') ?? '';
|
|
149
|
+
// LibreOffice runs nothing for a listener whose script URL and macro name are both empty. An empty href does not
|
|
150
|
+
// hide a macro name.
|
|
151
|
+
const target = [href, oattr(el, NS.script, 'macro-name')].find(v => v !== undefined && v !== '');
|
|
152
|
+
if (target === undefined)
|
|
153
|
+
return;
|
|
154
|
+
const name = event
|
|
155
|
+
.slice(event.lastIndexOf(':') + 1)
|
|
156
|
+
.toLowerCase()
|
|
157
|
+
.replace(/^on/, '')
|
|
158
|
+
.replace(/-/g, '');
|
|
159
|
+
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);
|
|
165
|
+
}
|
|
166
|
+
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) } : {}) };
|
|
169
|
+
if (action === 'execute')
|
|
170
|
+
this.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.RunProgram, data);
|
|
171
|
+
else if (action === 'verb')
|
|
172
|
+
this.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.OleVerb, data);
|
|
173
|
+
else if (href !== undefined && !(0, links_1.isFragment)(href))
|
|
174
|
+
this.ctx.link(href, this.entry, data);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/** The manifest. Only paths that name an entry of the archive are kept, so a manifest of millions costs no memory. */
|
|
178
|
+
async function readManifest(ctx, entry) {
|
|
179
|
+
const keys = new Set(ctx.zip.entries.map(e => (0, zip_1.nameKey)(e.name)));
|
|
180
|
+
const m = { paths: new Set(), encrypted: false };
|
|
181
|
+
await ctx.parse(entry, {
|
|
182
|
+
start(el) {
|
|
183
|
+
if (odfNs(el.ns) !== NS.manifest)
|
|
184
|
+
return;
|
|
185
|
+
if (el.local === 'encryption-data')
|
|
186
|
+
m.encrypted = true;
|
|
187
|
+
if (el.local !== 'file-entry')
|
|
188
|
+
return;
|
|
189
|
+
const path = oattr(el, NS.manifest, 'full-path');
|
|
190
|
+
if (path === undefined)
|
|
191
|
+
return;
|
|
192
|
+
if (path === '/') {
|
|
193
|
+
if (m.root !== undefined)
|
|
194
|
+
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') ?? '';
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const key = packagePath(path);
|
|
199
|
+
if (!keys.has(key))
|
|
200
|
+
return;
|
|
201
|
+
if (m.paths.has(key))
|
|
202
|
+
ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.DuplicateEntry, entry.name, { kind: 'manifest path', name: (0, context_1.clip)(path) });
|
|
203
|
+
m.paths.add(key);
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
return m;
|
|
207
|
+
}
|
|
208
|
+
/** The code folder an entry sits in, as the folder's path and its finding. */
|
|
209
|
+
function folderOf(name) {
|
|
210
|
+
const segs = name.split('/');
|
|
211
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
212
|
+
const hit = FOLDERS.find(([folder]) => folder === segs[i].toLowerCase());
|
|
213
|
+
if (hit)
|
|
214
|
+
return { prefix: `${segs.slice(0, i + 1).join('/')}/`, detail: hit[1], category: hit[2] };
|
|
215
|
+
}
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
/** The most of the mimetype entry that is kept. The longest media type in the table is 56 characters. */
|
|
219
|
+
const MAX_MIMETYPE = 256;
|
|
220
|
+
/** Inspects an OpenDocument package. */
|
|
221
|
+
async function inspectOdf(ctx) {
|
|
222
|
+
const zip = ctx.zip;
|
|
223
|
+
const mimetype = zip.entries.find(e => e.name === 'mimetype');
|
|
224
|
+
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 });
|
|
226
|
+
let declared;
|
|
227
|
+
if (mimetype) {
|
|
228
|
+
let head = Buffer.alloc(0);
|
|
229
|
+
await ctx.drain(mimetype, c => {
|
|
230
|
+
if (head.length < MAX_MIMETYPE)
|
|
231
|
+
head = Buffer.concat([head, c.subarray(0, MAX_MIMETYPE - head.length)]);
|
|
232
|
+
});
|
|
233
|
+
// A longer entry holds no media type this package reads, whatever its start says.
|
|
234
|
+
declared = head.toString('latin1') + (mimetype.size > MAX_MIMETYPE ? '...' : '');
|
|
235
|
+
const first = zip.entries.reduce((a, b) => (b.localOffset < a.localOffset ? b : a));
|
|
236
|
+
if (first !== mimetype)
|
|
237
|
+
layout('mimetype is not the first entry');
|
|
238
|
+
if (mimetype.method !== 0)
|
|
239
|
+
layout('mimetype is compressed');
|
|
240
|
+
if (mimetype.localExtra)
|
|
241
|
+
layout('mimetype has an extra field');
|
|
242
|
+
if (declared !== declared.trim())
|
|
243
|
+
layout('mimetype has white space around the media type');
|
|
244
|
+
declared = declared.trim();
|
|
245
|
+
}
|
|
246
|
+
else
|
|
247
|
+
layout('there is no mimetype entry');
|
|
248
|
+
const manifest = manifestEntry ? await readManifest(ctx, manifestEntry) : undefined;
|
|
249
|
+
if (!manifest)
|
|
250
|
+
layout('there is no manifest');
|
|
251
|
+
const root = manifest?.root;
|
|
252
|
+
if (declared !== undefined && root !== undefined && declared !== root && formats_1.ODF_FORMATS.has(declared) && formats_1.ODF_FORMATS.has(root)) {
|
|
253
|
+
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
|
+
}
|
|
255
|
+
const type = declared ?? root ?? '';
|
|
256
|
+
const format = formats_1.ODF_FORMATS.get(type);
|
|
257
|
+
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) });
|
|
259
|
+
ctx.format = format;
|
|
260
|
+
if (manifest?.encrypted)
|
|
261
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.PasswordRequired, 'The manifest lists encrypted entries');
|
|
262
|
+
const folders = new Map();
|
|
263
|
+
const oleRefs = new Map(zip.entries.map(e => [(0, zip_1.nameKey)(e.name), false]));
|
|
264
|
+
const oleEntries = [];
|
|
265
|
+
for (const entry of zip.entries) {
|
|
266
|
+
if (entry === mimetype || entry === manifestEntry)
|
|
267
|
+
continue;
|
|
268
|
+
const folder = folderOf(entry.name);
|
|
269
|
+
if (folder) {
|
|
270
|
+
// LibreOffice writes an empty Configurations2/accelerator/current.xml into the documents it saves. An empty
|
|
271
|
+
// file holds no code or setting.
|
|
272
|
+
if (entry.size > 0) {
|
|
273
|
+
const f = folders.get(folder.prefix) ?? { detail: folder.detail, category: folder.category, entries: 0 };
|
|
274
|
+
f.entries++;
|
|
275
|
+
folders.set(folder.prefix, f);
|
|
276
|
+
}
|
|
277
|
+
await ctx.drain(entry);
|
|
278
|
+
}
|
|
279
|
+
else if (!entry.directory && /\.xml$/i.test(entry.name))
|
|
280
|
+
await ctx.parse(entry, new EntryRules(ctx, entry.name, oleRefs));
|
|
281
|
+
else if (await ctx.drain(entry))
|
|
282
|
+
oleEntries.push(entry);
|
|
283
|
+
}
|
|
284
|
+
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 });
|
|
289
|
+
if (manifest) {
|
|
290
|
+
for (const e of zip.entries) {
|
|
291
|
+
if (e.directory || e === mimetype || e === manifestEntry || /^meta-inf\//i.test(e.name))
|
|
292
|
+
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 });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
package/dist/ooxml.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type RunContext } from './context';
|
|
2
|
+
import { type ZipEntry } from './zip';
|
|
3
|
+
/**
|
|
4
|
+
* Inspects an OOXML package. The package relationships come first, since they name the main part and so the format.
|
|
5
|
+
* Then each part is read in archive order, followed by its relationships, which are reported as they are read and
|
|
6
|
+
* leave behind only which parts they reach and how.
|
|
7
|
+
*/
|
|
8
|
+
export declare function inspectOoxml(ctx: RunContext, contentTypes: ZipEntry): Promise<void>;
|