@patrickdk77/office-defuse 0.2.0 → 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.
- package/README.md +15 -2
- package/dist/detect.d.ts +13 -0
- package/dist/detect.js +83 -0
- package/dist/engine.d.ts +55 -1
- package/dist/engine.js +22 -12
- package/dist/flat.d.ts +5 -5
- package/dist/flat.js +110 -41
- package/dist/formats.d.ts +2 -0
- package/dist/formats.js +3 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -1
- package/dist/index.mjs +1 -1
- package/dist/odf.d.ts +22 -0
- package/dist/odf.js +23 -12
- package/dist/ole.d.ts +19 -2
- package/dist/ole.js +81 -53
- package/dist/ooxml.d.ts +27 -2
- package/dist/ooxml.js +60 -38
- package/dist/package.d.ts +14 -0
- package/dist/package.js +23 -7
- package/dist/plugins.js +3 -28
- package/dist/rtf.js +2 -1
- package/dist/types.d.ts +23 -0
- package/dist/word.d.ts +4 -4
- package/dist/word.js +19 -6
- package/dist/xls.d.ts +10 -4
- package/dist/xls.js +20 -6
- package/dist/zip.d.ts +2 -0
- package/dist/zip.js +6 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -91,7 +91,7 @@ A ZIP with both markers is rejected as PACKAGE/AMBIGUOUS_FORMAT, since Office co
|
|
|
91
91
|
|
|
92
92
|
A .xlsb gets the package checks. Its sheets are binary records, and the formulas in them go unread. Its external link parts are binary too, and office-defuse reads their link records for DDE links. A .odb gets every ODF check, and the database under `database/` goes unread.
|
|
93
93
|
|
|
94
|
-
A file that starts with the compound file signature is a compound file, MS-CFB. The streams and storages at its root name the format: `WordDocument` for Word, `Workbook` or `Book` for Excel, `PowerPoint Document`, `VisioDocument`, `__properties_version1.0` for an Outlook message, a `Quill` storage or Publisher's class id beside a `Contents` stream for Publisher, and a `Props` stream beside a storage named by a number for Project. Word's FIB says whether a document is a template, and Excel's records whether a workbook is a template or an add-in. An Outlook template has a class id of its own. A PowerPoint 97 to 2003 template or show reports its format as .ppt, and a Visio stencil or template as .vsd.
|
|
94
|
+
A file that starts with the compound file signature is a compound file, MS-CFB. The streams and storages at its root name the format: `WordDocument` for Word, `Workbook` or `Book` for Excel, `PowerPoint Document`, `VisioDocument`, `__properties_version1.0` for an Outlook message, a `Quill` storage or Publisher's class id beside a `Contents` stream for Publisher, and a `Props` stream beside a storage named by a number for Project. Word's FIB says whether a document is a template, and Excel's records whether a workbook is a template or an add-in. Neither needs the password of an encrypted file. An Outlook template has a class id of its own. A PowerPoint 97 to 2003 template or show reports its format as .ppt, and a Visio stencil or template as .vsd.
|
|
95
95
|
|
|
96
96
|
A compound file with two of these is PACKAGE/AMBIGUOUS_FORMAT. One with none is PACKAGE/UNRECOGNIZED, and so is a `WordDocument` stream that holds no Word 6.0 to 2003 FIB. A PowerPoint 4.0 file, a bare OLE object and a `vbaProject.bin` on its own all land there.
|
|
97
97
|
|
|
@@ -101,6 +101,19 @@ A file that starts as XML, after any byte order mark and white space, is a flat
|
|
|
101
101
|
|
|
102
102
|
A file that starts with `{\rt` is RTF, as Word takes it. A compound file or RTF file in which the ZIP reader also finds an archive is PACKAGE/AMBIGUOUS_FORMAT. A file with the compound file signature whose compound file does not read, but whose ZIP archive does, is that ZIP archive with LEADING_BYTES.
|
|
103
103
|
|
|
104
|
+
## Detection
|
|
105
|
+
|
|
106
|
+
`detectOffice(bytes, options)` and `detectOfficeSource(source, options)` tell a router whether office-defuse reads a file. They return `{ mimeType, extension, offset }`, or undefined. `extension` and `mimeType` are the `format` and `mimeType` an inspection reports. The inspection and detection name a file with the same functions. Detection opens the container and reads only what names the format, without the rules, the plugins or the rewrite:
|
|
107
|
+
|
|
108
|
+
- the streams at a compound file's root, and for Word and Excel the FIB or the workbook's globals, which say whether it is a template or an add-in
|
|
109
|
+
- a package's `[Content_Types].xml` and package relationships, or its `mimetype` entry and manifest
|
|
110
|
+
- a flat file's root element, and for Flat OPC each part's name and content type and the package relationships
|
|
111
|
+
- the first bytes of RTF
|
|
112
|
+
|
|
113
|
+
Detection does not claim a file whose format office-defuse cannot name. That covers a ZIP archive with no Office marker and a package of another kind, such as a NuGet package or a StarOffice file. It covers a compound file with no Office document at its root, such as an installer or a bare OLE object, and a file that is a ZIP archive and a compound file at once. An encrypted OOXML package names its format only once it is decrypted, and detection has no password. When VelvetSweatshop opens the package, detection names the format inside. Otherwise it claims the package as a compound file, application/x-ole-storage with an empty extension, since the file is office-defuse's whatever the password turns out to be. The inspection then decrypts it with the run's password, or rejects it as encrypted. Detection claims a file it can name even when the file is damaged further on, and the inspection then rejects it.
|
|
114
|
+
|
|
115
|
+
`offset` is where the ZIP data starts in an archive with other data in front, as the ZIP reader finds it, and 0 for anything else. The name and declared type play no part, so `heuristic` is never set. `detectOfficeSource` leaves the source open and throws with the `DEFUSE_LIMIT` code once `options.deadline` passes. An I/O error from the source comes back unchanged. Naming a Flat OPC package packs its parts into a ZIP archive, which spills into `options.tempDir` past `options.memoryThreshold`. `officePlugin` accepts exactly what detection claims.
|
|
116
|
+
|
|
104
117
|
## What it reports
|
|
105
118
|
|
|
106
119
|
In a ZIP package:
|
|
@@ -241,7 +254,7 @@ The findings a plugin returns go into the report with their `location` after the
|
|
|
241
254
|
|
|
242
255
|
The plugin context has `depth`, the file's depth in the upload, and `deadline`, which every file of the upload shares. An error with `code: 'DEFUSE_LIMIT'` or `code: 'DEFUSE_IO'` from a plugin stops the whole upload. At depth 0, a limit becomes LIMIT/TIME or LIMIT/NESTING_DEPTH, and an I/O error is thrown as its cause.
|
|
243
256
|
|
|
244
|
-
`officePlugin(options)` defuses Office and OpenDocument files inside other files. It accepts
|
|
257
|
+
`officePlugin(options)` defuses Office and OpenDocument files inside other files. It accepts the files [`detectOffice`](#detection) claims, by their content alone, an encrypted OOXML package included, which it decrypts with the run's password. It runs disarm at the context's depth with the context's deadline. Without `options` it takes the options of the office-defuse run that found the file, or none when another package found it. A clean file comes back `passed`, a defused one `scrubbed`, both with the rewritten bytes, and a rejected one `removed`. The findings it returns carry their `weight`.
|
|
245
258
|
|
|
246
259
|
```js
|
|
247
260
|
const { disarmOffice, officePlugin } = require('@patrickdk77/office-defuse');
|
package/dist/detect.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ByteSource, type DetectOptions, type FormatDetection } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* The format office-defuse reads `source` as, when it can name it from the content: the format and MIME type an
|
|
4
|
+
* inspection reports, read the way the inspection reads them, without the rules, the plugins or the rewrite. A ZIP
|
|
5
|
+
* archive, compound file or XML file whose format it cannot name, such as a plain archive, an installer or a package
|
|
6
|
+
* of another kind, is not claimed. A file it names but cannot read past that point is claimed, and an inspection then
|
|
7
|
+
* rejects it. So is an encrypted Office Open XML package no password at hand opens, as application/x-ole-storage with
|
|
8
|
+
* no extension, since only the password says which format is inside. The offset is that of the ZIP data in an archive with data in front, and 0 otherwise. The name and the
|
|
9
|
+
* declared type play no part. The source stays open.
|
|
10
|
+
*/
|
|
11
|
+
export declare function detectOfficeSource(source: ByteSource, options?: DetectOptions): Promise<FormatDetection | undefined>;
|
|
12
|
+
/** `detectOfficeSource` over bytes in memory. */
|
|
13
|
+
export declare function detectOffice(bytes: Uint8Array, options?: DetectOptions): Promise<FormatDetection | undefined>;
|
package/dist/detect.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.detectOfficeSource = detectOfficeSource;
|
|
4
|
+
exports.detectOffice = detectOffice;
|
|
5
|
+
const engine_1 = require("./engine");
|
|
6
|
+
const errors_1 = require("./errors");
|
|
7
|
+
const flat_1 = require("./flat");
|
|
8
|
+
const formats_1 = require("./formats");
|
|
9
|
+
const io_1 = require("./io");
|
|
10
|
+
const ole_1 = require("./ole");
|
|
11
|
+
const package_1 = require("./package");
|
|
12
|
+
const types_1 = require("./types");
|
|
13
|
+
/**
|
|
14
|
+
* The format of a file whose container `openContainer` read, named by the steps the inspection names it by, without
|
|
15
|
+
* the rules: the root of a compound file, and for Word and Excel whether it is a template or an add-in; the markers,
|
|
16
|
+
* [Content_Types].xml and package relationships of a package; the root of a flat file; or RTF. An encrypted package
|
|
17
|
+
* names its format when the passwords the run tries, which without a password is VelvetSweatshop alone, open it.
|
|
18
|
+
*/
|
|
19
|
+
async function nameContainer(ctx, container, src, size, limits) {
|
|
20
|
+
if (container.kind === 'rtf')
|
|
21
|
+
return formats_1.RTF_FORMAT;
|
|
22
|
+
if (container.kind === 'xml')
|
|
23
|
+
return (0, flat_1.nameFlat)(ctx, src, size, limits);
|
|
24
|
+
if (container.kind === 'zip')
|
|
25
|
+
return (0, package_1.namePackage)(ctx.withZip(container.zip));
|
|
26
|
+
const cfb = container.cfb;
|
|
27
|
+
const family = (0, ole_1.mainFamily)(cfb);
|
|
28
|
+
const format = await (0, ole_1.compoundFormat)(cfb, family, ctx.settings.deadline);
|
|
29
|
+
if (format)
|
|
30
|
+
return format;
|
|
31
|
+
try {
|
|
32
|
+
return await (0, package_1.namePackage)(ctx.withZip(await (0, ole_1.openEncryptedPackage)(ctx, cfb)));
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
// An encrypted package that no password at hand opens is still office-defuse's. Only the password says which
|
|
36
|
+
// format is inside, and the inspection decrypts it with the run's password or rejects it as encrypted.
|
|
37
|
+
if (family === 'encrypted' && e instanceof errors_1.StopError && e.category === types_1.OfficeCategory.Encrypted)
|
|
38
|
+
return ENCRYPTED_PACKAGE;
|
|
39
|
+
throw e;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** An encrypted Office Open XML package whose format no password at hand reveals: a compound file, with no extension of its own. */
|
|
43
|
+
const ENCRYPTED_PACKAGE = { format: '', mimeType: 'application/x-ole-storage' };
|
|
44
|
+
/**
|
|
45
|
+
* The format office-defuse reads `source` as, when it can name it from the content: the format and MIME type an
|
|
46
|
+
* inspection reports, read the way the inspection reads them, without the rules, the plugins or the rewrite. A ZIP
|
|
47
|
+
* archive, compound file or XML file whose format it cannot name, such as a plain archive, an installer or a package
|
|
48
|
+
* of another kind, is not claimed. A file it names but cannot read past that point is claimed, and an inspection then
|
|
49
|
+
* rejects it. So is an encrypted Office Open XML package no password at hand opens, as application/x-ole-storage with
|
|
50
|
+
* no extension, since only the password says which format is inside. The offset is that of the ZIP data in an archive with data in front, and 0 otherwise. The name and the
|
|
51
|
+
* declared type play no part. The source stays open.
|
|
52
|
+
*/
|
|
53
|
+
async function detectOfficeSource(source, options = {}) {
|
|
54
|
+
const run = (0, engine_1.newRun)({ tempDir: options.tempDir, memoryThreshold: options.memoryThreshold }, 0, options.deadline);
|
|
55
|
+
const src = (0, engine_1.guarded)(source);
|
|
56
|
+
try {
|
|
57
|
+
const { limits, settings, ctx } = (0, engine_1.runContext)(run);
|
|
58
|
+
const size = await src.size();
|
|
59
|
+
(0, errors_1.checkTime)(run.deadline);
|
|
60
|
+
const container = await (0, engine_1.openContainer)(src, size, limits, settings);
|
|
61
|
+
const format = await nameContainer(ctx, container, src, size, limits);
|
|
62
|
+
if (run.temp.ioError !== undefined)
|
|
63
|
+
throw run.temp.ioError;
|
|
64
|
+
return { mimeType: format.mimeType, extension: format.format, offset: container.kind === 'zip' ? container.zip.start : 0 };
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
if (src.failure !== undefined)
|
|
68
|
+
throw src.failure;
|
|
69
|
+
if (run.temp.ioError !== undefined)
|
|
70
|
+
throw run.temp.ioError;
|
|
71
|
+
if ((0, errors_1.stopsTree)(e) !== undefined || (0, errors_1.isIoError)(e))
|
|
72
|
+
throw e;
|
|
73
|
+
// Any other failure, as in an inspection, means office-defuse cannot name the file's format.
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
await (0, engine_1.endRun)(run);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** `detectOfficeSource` over bytes in memory. */
|
|
81
|
+
function detectOffice(bytes, options = {}) {
|
|
82
|
+
return detectOfficeSource((0, io_1.bufferSource)(bytes), options);
|
|
83
|
+
}
|
package/dist/engine.d.ts
CHANGED
|
@@ -1,4 +1,57 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CompoundFile } from './cfb';
|
|
2
|
+
import { RunContext, type RunSettings, type RunState } from './context';
|
|
3
|
+
import { type ByteSink, type ByteSource, type OfficeDisarmResult, type OfficeInspection, type OfficeLimits, type OfficeOptions } from './types';
|
|
4
|
+
import { ZipArchive } from './zip';
|
|
5
|
+
declare const DEFAULT_LIMITS: {
|
|
6
|
+
entries: number;
|
|
7
|
+
decompressedBytes: number;
|
|
8
|
+
totalDecompressedBytes: number;
|
|
9
|
+
xmlDepth: number;
|
|
10
|
+
xmlTokenSize: number;
|
|
11
|
+
xmlEntityDepth: number;
|
|
12
|
+
xmlEntityExpansions: number;
|
|
13
|
+
xmlEntitySize: number;
|
|
14
|
+
rtfDepth: number;
|
|
15
|
+
};
|
|
16
|
+
export type Limits = typeof DEFAULT_LIMITS & Pick<OfficeLimits, 'fileSize' | 'timeMs' | 'nestingDepth'>;
|
|
17
|
+
/**
|
|
18
|
+
* Wraps the source for one run. A read that returns more or fewer bytes than the source's size allows fails with
|
|
19
|
+
* code EIO, since the reader would take the shifted bytes for the file. Any error the source throws is kept, and the
|
|
20
|
+
* engine throws it instead of a verdict on the file.
|
|
21
|
+
*/
|
|
22
|
+
export declare function guarded(source: ByteSource): ByteSource & {
|
|
23
|
+
failure?: unknown;
|
|
24
|
+
};
|
|
25
|
+
export type Container = {
|
|
26
|
+
kind: 'zip';
|
|
27
|
+
zip: ZipArchive;
|
|
28
|
+
} | {
|
|
29
|
+
kind: 'cfb';
|
|
30
|
+
cfb: CompoundFile;
|
|
31
|
+
} | {
|
|
32
|
+
kind: 'rtf';
|
|
33
|
+
} | {
|
|
34
|
+
kind: 'xml';
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Reads the file's container. A file that starts with the compound file signature is read as one, and one that
|
|
38
|
+
* starts with {\rt is RTF, which is how Word tells them. The ZIP reader runs too, from the end: a file both read is
|
|
39
|
+
* ambiguous, since Office goes by the first bytes and a ZIP reader by the last. A compound file that does not read is
|
|
40
|
+
* a ZIP archive with data in front, if the ZIP reader read one. Word reads any file that starts with {\rt as RTF, so
|
|
41
|
+
* that does not fail. A file that starts as XML is a flat OpenDocument file or a Flat OPC package, and one that is a
|
|
42
|
+
* ZIP archive as well is ambiguous.
|
|
43
|
+
*/
|
|
44
|
+
export declare function openContainer(src: ByteSource, size: number, limits: Limits, settings: RunSettings): Promise<Container>;
|
|
45
|
+
/** A run of the engine over one file at `depth`, with the deadline the whole upload shares. */
|
|
46
|
+
export declare function newRun(options: OfficeOptions, depth: number, deadline: number | undefined, verify?: RunState['verify']): RunState;
|
|
47
|
+
/** Releases what a run holds: the copies plugins returned and the temporary directory. */
|
|
48
|
+
export declare function endRun(run: RunState): Promise<void>;
|
|
49
|
+
/** The limits, settings and context a run reads its file with, from the run's options. */
|
|
50
|
+
export declare function runContext(run: RunState): {
|
|
51
|
+
limits: Limits;
|
|
52
|
+
settings: RunSettings;
|
|
53
|
+
ctx: RunContext;
|
|
54
|
+
};
|
|
2
55
|
/** Inspects an Office or OpenDocument file without changing it. The source is closed when the run ends. */
|
|
3
56
|
export declare function inspectOfficeSource(source: ByteSource, options?: OfficeOptions): Promise<OfficeInspection>;
|
|
4
57
|
/** Inspects Office or OpenDocument bytes. */
|
|
@@ -14,3 +67,4 @@ export declare function disarmAtDepth(source: ByteSource, sink: ByteSink, option
|
|
|
14
67
|
export declare function disarmOffice(bytes: Uint8Array, options?: OfficeOptions): Promise<OfficeDisarmResult & {
|
|
15
68
|
bytes?: Uint8Array;
|
|
16
69
|
}>;
|
|
70
|
+
export {};
|
package/dist/engine.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.guarded = guarded;
|
|
4
|
+
exports.openContainer = openContainer;
|
|
5
|
+
exports.newRun = newRun;
|
|
6
|
+
exports.endRun = endRun;
|
|
7
|
+
exports.runContext = runContext;
|
|
3
8
|
exports.inspectOfficeSource = inspectOfficeSource;
|
|
4
9
|
exports.inspectOffice = inspectOffice;
|
|
5
10
|
exports.disarmOfficeSource = disarmOfficeSource;
|
|
@@ -178,6 +183,21 @@ async function endRun(run) {
|
|
|
178
183
|
await run.temp.cleanup();
|
|
179
184
|
}
|
|
180
185
|
}
|
|
186
|
+
/** The limits, settings and context a run reads its file with, from the run's options. */
|
|
187
|
+
function runContext(run) {
|
|
188
|
+
const options = run.options;
|
|
189
|
+
const limits = resolveLimits(options.limits);
|
|
190
|
+
const password = options.password;
|
|
191
|
+
const settings = {
|
|
192
|
+
entries: limits.entries,
|
|
193
|
+
decompressedBytes: limits.decompressedBytes,
|
|
194
|
+
totalDecompressedBytes: limits.totalDecompressedBytes,
|
|
195
|
+
rtfDepth: limits.rtfDepth,
|
|
196
|
+
deadline: run.deadline,
|
|
197
|
+
passwords: typeof password === 'string' ? [password] : (password ?? []),
|
|
198
|
+
};
|
|
199
|
+
return { limits, settings, ctx: new context_1.RunContext(new findings_1.FindingFactory(options.actionOverrides), xmlLimitsOf(limits), settings, run) };
|
|
200
|
+
}
|
|
181
201
|
/** The finding a limit error from this package or any other gives at depth 0. */
|
|
182
202
|
function limitFinding(factory, e) {
|
|
183
203
|
return factory.make(types_1.OfficeCategory.Limit, e.limit === 'nesting' ? types_1.OfficeDetail.NestingDepth : types_1.OfficeDetail.Time);
|
|
@@ -188,21 +208,11 @@ function limitFinding(factory, e) {
|
|
|
188
208
|
*/
|
|
189
209
|
async function analyze(run, source) {
|
|
190
210
|
const options = run.options;
|
|
191
|
-
const limits = resolveLimits(options.limits);
|
|
192
211
|
const deadline = run.deadline;
|
|
193
|
-
const
|
|
212
|
+
const { limits, settings, ctx } = runContext(run);
|
|
213
|
+
const factory = ctx.factory;
|
|
194
214
|
const findings = [];
|
|
195
215
|
const src = guarded(source);
|
|
196
|
-
const password = options.password;
|
|
197
|
-
const settings = {
|
|
198
|
-
entries: limits.entries,
|
|
199
|
-
decompressedBytes: limits.decompressedBytes,
|
|
200
|
-
totalDecompressedBytes: limits.totalDecompressedBytes,
|
|
201
|
-
rtfDepth: limits.rtfDepth,
|
|
202
|
-
deadline,
|
|
203
|
-
passwords: typeof password === 'string' ? [password] : (password ?? []),
|
|
204
|
-
};
|
|
205
|
-
const ctx = new context_1.RunContext(factory, xmlLimitsOf(limits), settings, run);
|
|
206
216
|
const isOutput = run.verify !== undefined;
|
|
207
217
|
try {
|
|
208
218
|
if (limits.nestingDepth !== undefined && run.depth > limits.nestingDepth)
|
package/dist/flat.d.ts
CHANGED
|
@@ -12,16 +12,16 @@ export declare const FLAT_ODF_PART = "office:document";
|
|
|
12
12
|
export declare const FLAT_OPC_FORMAT: Format;
|
|
13
13
|
/** Whether the first bytes of a file read as XML: a byte order mark, white space and then "<". */
|
|
14
14
|
export declare function startsAsXml(head: Uint8Array): boolean;
|
|
15
|
-
/**
|
|
16
|
-
* Whether the start of an XML file names a flat OpenDocument document or a Flat OPC package as its root element. The
|
|
17
|
-
* reading of the whole file decides; this tells a router which package to try.
|
|
18
|
-
*/
|
|
19
|
-
export declare function isFlatHead(text: string): boolean;
|
|
20
15
|
/**
|
|
21
16
|
* Inspects a flat XML file: flat OpenDocument, read with the OpenDocument rules over its one part, or Flat OPC,
|
|
22
17
|
* which is unpacked into a package of its parts and read with the OOXML rules. Returns what the rewrite needs.
|
|
23
18
|
*/
|
|
24
19
|
export declare function inspectFlat(ctx: RunContext, src: ByteSource, size: number, zipLimits: ZipLimits): Promise<Output | undefined>;
|
|
20
|
+
/**
|
|
21
|
+
* Names a flat file's format as its inspection does, without the rules: flat OpenDocument by its root element, and a
|
|
22
|
+
* Flat OPC package once the package its parts make names an OOXML format.
|
|
23
|
+
*/
|
|
24
|
+
export declare function nameFlat(ctx: RunContext, src: ByteSource, size: number, zipLimits: ZipLimits): Promise<Format>;
|
|
25
25
|
/** Writes a flat OpenDocument file without what the plan removes. */
|
|
26
26
|
export declare function writeFlatOdf(run: RunState, src: ByteSource, plan: PackagePlan, limits: XmlLimits, sink: ByteSink): Promise<void>;
|
|
27
27
|
/**
|
package/dist/flat.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.FLAT_OPC_FORMAT = exports.FLAT_ODF_PART = exports.PKG_NS = void 0;
|
|
4
4
|
exports.startsAsXml = startsAsXml;
|
|
5
|
-
exports.isFlatHead = isFlatHead;
|
|
6
5
|
exports.inspectFlat = inspectFlat;
|
|
6
|
+
exports.nameFlat = nameFlat;
|
|
7
7
|
exports.writeFlatOdf = writeFlatOdf;
|
|
8
8
|
exports.writeFlatOpc = writeFlatOpc;
|
|
9
9
|
const node_util_1 = require("node:util");
|
|
@@ -51,16 +51,6 @@ function startsAsXml(head) {
|
|
|
51
51
|
const text = new node_util_1.TextDecoder(found.encoding, { fatal: false }).decode(head.subarray(found.bom));
|
|
52
52
|
return text.trimStart().startsWith('<');
|
|
53
53
|
}
|
|
54
|
-
/**
|
|
55
|
-
* Whether the start of an XML file names a flat OpenDocument document or a Flat OPC package as its root element. The
|
|
56
|
-
* reading of the whole file decides; this tells a router which package to try.
|
|
57
|
-
*/
|
|
58
|
-
function isFlatHead(text) {
|
|
59
|
-
const body = text.replace(/^\uFEFF/, '').replace(/^(?:\s|<\?[\s\S]*?\?>|<!--[\s\S]*?-->|<!DOCTYPE[^>]*>)*/, '');
|
|
60
|
-
const qname = /^<([^\s/>]+)/.exec(body)?.[1] ?? '';
|
|
61
|
-
const local = qname.slice(qname.indexOf(':') + 1);
|
|
62
|
-
return (local === 'document' && /urn:oasis:names:tc:opendocument:xmlns:office:1\.\d/i.test(text)) || (local === 'package' && text.includes(exports.PKG_NS));
|
|
63
|
-
}
|
|
64
54
|
/** Decodes base64 that arrives in pieces of any size, white space and line ends included. */
|
|
65
55
|
class Base64 {
|
|
66
56
|
rest = '';
|
|
@@ -141,6 +131,7 @@ class FlatOdf {
|
|
|
141
131
|
class FlatOpc {
|
|
142
132
|
run;
|
|
143
133
|
pending;
|
|
134
|
+
keep;
|
|
144
135
|
parts = [];
|
|
145
136
|
/** Where the pkg:package start tag begins, which is where the prolog ends. */
|
|
146
137
|
rootStart = 0;
|
|
@@ -148,9 +139,12 @@ class FlatOpc {
|
|
|
148
139
|
xmlDepth = 0;
|
|
149
140
|
b64;
|
|
150
141
|
scopes = [];
|
|
151
|
-
constructor(run, pending
|
|
142
|
+
constructor(run, pending,
|
|
143
|
+
/** The parts whose content is kept, by name. The others keep only their name, content type and attributes. */
|
|
144
|
+
keep = () => true) {
|
|
152
145
|
this.run = run;
|
|
153
146
|
this.pending = pending;
|
|
147
|
+
this.keep = keep;
|
|
154
148
|
}
|
|
155
149
|
start(el) {
|
|
156
150
|
this.scopes.push(el.namespaces);
|
|
@@ -182,7 +176,7 @@ class FlatOpc {
|
|
|
182
176
|
this.part.xml = { start: el.end, end: el.end, inherited };
|
|
183
177
|
this.xmlDepth = el.depth;
|
|
184
178
|
}
|
|
185
|
-
else if (el.depth === 3 && this.part && el.local === 'binaryData') {
|
|
179
|
+
else if (el.depth === 3 && this.part && el.local === 'binaryData' && this.keep(this.part.name)) {
|
|
186
180
|
const sink = new io_1.SpillSink(this.run.temp, this.run.threshold);
|
|
187
181
|
this.run.held.push(sink);
|
|
188
182
|
this.part.binary = sink;
|
|
@@ -210,6 +204,23 @@ class FlatOpc {
|
|
|
210
204
|
}
|
|
211
205
|
}
|
|
212
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* The format a flat file's root element names: a flat OpenDocument format by its office:mimetype, or 'opc' for a Flat
|
|
209
|
+
* OPC package, whose format the package its parts make names. Throws for any other root, or a flat OpenDocument media
|
|
210
|
+
* type this package does not read.
|
|
211
|
+
*/
|
|
212
|
+
function flatRoot(el) {
|
|
213
|
+
if ((0, odf_1.odfNamespace)(el.ns) === odf_1.NS.office && el.local === 'document') {
|
|
214
|
+
const type = (0, odf_1.oattr)(el, odf_1.NS.office, 'mimetype') ?? '';
|
|
215
|
+
const format = FLAT_ODF.get(type);
|
|
216
|
+
if (!format)
|
|
217
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The flat OpenDocument media type is not one this package reads', { mediaType: (0, context_1.clip)(type) });
|
|
218
|
+
return format;
|
|
219
|
+
}
|
|
220
|
+
if (el.ns === exports.PKG_NS && el.local === 'package')
|
|
221
|
+
return 'opc';
|
|
222
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The XML file is neither flat OpenDocument nor Flat OPC', { root: (0, context_1.clip)(el.qname), namespace: (0, context_1.clip)(el.ns) });
|
|
223
|
+
}
|
|
213
224
|
/** Reads the root element and hands everything to the rules of the flat format it names. */
|
|
214
225
|
class FlatReader {
|
|
215
226
|
ctx;
|
|
@@ -223,19 +234,14 @@ class FlatReader {
|
|
|
223
234
|
}
|
|
224
235
|
start(el) {
|
|
225
236
|
if (el.depth === 1) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
this.format = FLAT_ODF.get(type);
|
|
229
|
-
if (!this.format)
|
|
230
|
-
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The flat OpenDocument media type is not one this package reads', { mediaType: (0, context_1.clip)(type) });
|
|
231
|
-
this.ctx.format = this.format;
|
|
232
|
-
this.odf = new FlatOdf(this.ctx, this.ctx.run, this.pending);
|
|
233
|
-
}
|
|
234
|
-
else if (el.ns === exports.PKG_NS && el.local === 'package') {
|
|
237
|
+
const format = flatRoot(el);
|
|
238
|
+
if (format === 'opc')
|
|
235
239
|
this.opc = new FlatOpc(this.ctx.run, this.pending);
|
|
240
|
+
else {
|
|
241
|
+
this.format = format;
|
|
242
|
+
this.ctx.format = format;
|
|
243
|
+
this.odf = new FlatOdf(this.ctx, this.ctx.run, this.pending);
|
|
236
244
|
}
|
|
237
|
-
else
|
|
238
|
-
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The XML file is neither flat OpenDocument nor Flat OPC', { root: (0, context_1.clip)(el.qname), namespace: (0, context_1.clip)(el.ns) });
|
|
239
245
|
}
|
|
240
246
|
this.odf?.start(el);
|
|
241
247
|
this.opc?.start(el);
|
|
@@ -350,7 +356,6 @@ function partXml(text, inherited) {
|
|
|
350
356
|
* which is unpacked into a package of its parts and read with the OOXML rules. Returns what the rewrite needs.
|
|
351
357
|
*/
|
|
352
358
|
async function inspectFlat(ctx, src, size, zipLimits) {
|
|
353
|
-
const run = ctx.run;
|
|
354
359
|
const pctx = ctx.withPlan();
|
|
355
360
|
const pending = new Pending();
|
|
356
361
|
const reader = new FlatReader(pctx, pending);
|
|
@@ -366,7 +371,25 @@ async function inspectFlat(ctx, src, size, zipLimits) {
|
|
|
366
371
|
const opc = reader.opc;
|
|
367
372
|
if (!opc)
|
|
368
373
|
return undefined;
|
|
369
|
-
const
|
|
374
|
+
const { zip, prolog } = await packFlatOpc(ctx, src, size, opc, zipLimits);
|
|
375
|
+
const zctx = ctx.withZip(zip);
|
|
376
|
+
const contentTypes = zip.get('[Content_Types].xml');
|
|
377
|
+
if (!contentTypes)
|
|
378
|
+
return undefined;
|
|
379
|
+
const inner = await (0, ooxml_1.inspectOoxml)(zctx, contentTypes, exports.FLAT_OPC_FORMAT);
|
|
380
|
+
const layout = {
|
|
381
|
+
instructions: [...prolog.matchAll(/<\?mso-application\s[^?]*\?>/g)].map(m => m[0]),
|
|
382
|
+
extra: new Map(opc.parts.map(p => [(0, zip_1.nameKey)(p.name), p.extra])),
|
|
383
|
+
};
|
|
384
|
+
return { kind: 'flatopc', zip, plan: zctx.plan, format: inner, layout };
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Packs the parts of a Flat OPC package into a ZIP archive, with a [Content_Types].xml that gives each its content
|
|
388
|
+
* type, and returns it with the text before the package element. A part the reader did not keep goes in empty.
|
|
389
|
+
*/
|
|
390
|
+
async function packFlatOpc(ctx, src, size, opc, zipLimits) {
|
|
391
|
+
const run = ctx.run;
|
|
392
|
+
const xmlParts = opc.parts.filter(p => p.xml && opc.keep(p.name));
|
|
370
393
|
const cut = await cutText(src, size, ctx.settings.deadline, opc.rootStart, xmlParts.map(p => p.xml ?? { start: 0, end: 0 }));
|
|
371
394
|
// The parts go into a package of their own, with a [Content_Types].xml that gives each its content type.
|
|
372
395
|
const packed = new io_1.SpillSink(run.temp, run.threshold);
|
|
@@ -384,8 +407,9 @@ async function inspectFlat(ctx, src, size, zipLimits) {
|
|
|
384
407
|
for (const p of opc.parts) {
|
|
385
408
|
(0, errors_1.checkTime)(ctx.settings.deadline);
|
|
386
409
|
let data = new Uint8Array(0);
|
|
387
|
-
|
|
388
|
-
|
|
410
|
+
const cutAt = xmlParts.indexOf(p);
|
|
411
|
+
if (p.xml && cutAt >= 0)
|
|
412
|
+
data = partXml(cut.texts[cutAt], p.xml.inherited);
|
|
389
413
|
else if (p.binary) {
|
|
390
414
|
const chunks = [];
|
|
391
415
|
for await (const c of p.binary.read())
|
|
@@ -398,21 +422,66 @@ async function inspectFlat(ctx, src, size, zipLimits) {
|
|
|
398
422
|
await packed.close();
|
|
399
423
|
const packedSource = packed.source();
|
|
400
424
|
run.sources.push(packedSource);
|
|
401
|
-
|
|
402
|
-
|
|
425
|
+
return { zip: await zip_1.ZipArchive.open(packedSource, zipLimits, ctx.settings.deadline), prolog: cut.prolog };
|
|
426
|
+
}
|
|
427
|
+
/** Stops the reading of a flat file once its root has named a flat OpenDocument format. */
|
|
428
|
+
const ROOT_NAMED = new Error('The root names the format');
|
|
429
|
+
/**
|
|
430
|
+
* Reads a flat file as far as naming its format needs: its root element, and for a Flat OPC package, every part's
|
|
431
|
+
* name, content type and attributes, with the content of the package relationships alone.
|
|
432
|
+
*/
|
|
433
|
+
class FlatNamer {
|
|
434
|
+
run;
|
|
435
|
+
pending;
|
|
436
|
+
format;
|
|
437
|
+
opc;
|
|
438
|
+
constructor(run, pending) {
|
|
439
|
+
this.run = run;
|
|
440
|
+
this.pending = pending;
|
|
441
|
+
}
|
|
442
|
+
start(el) {
|
|
443
|
+
if (el.depth === 1) {
|
|
444
|
+
const format = flatRoot(el);
|
|
445
|
+
if (format !== 'opc') {
|
|
446
|
+
this.format = format;
|
|
447
|
+
throw ROOT_NAMED;
|
|
448
|
+
}
|
|
449
|
+
this.opc = new FlatOpc(this.run, this.pending, name => (0, ooxml_1.relsSource)(name) === '');
|
|
450
|
+
}
|
|
451
|
+
this.opc?.start(el);
|
|
452
|
+
}
|
|
453
|
+
text(t) {
|
|
454
|
+
this.opc?.text(t);
|
|
455
|
+
}
|
|
456
|
+
end(el, start) {
|
|
457
|
+
this.opc?.end(el, start);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Names a flat file's format as its inspection does, without the rules: flat OpenDocument by its root element, and a
|
|
462
|
+
* Flat OPC package once the package its parts make names an OOXML format.
|
|
463
|
+
*/
|
|
464
|
+
async function nameFlat(ctx, src, size, zipLimits) {
|
|
465
|
+
const pending = new Pending();
|
|
466
|
+
const namer = new FlatNamer(ctx.run, pending);
|
|
467
|
+
try {
|
|
468
|
+
await ctx.parseXml(FLAT_OPC_PART, chunksOf(src, size, ctx.settings.deadline), namer, false, undefined, () => pending.flush());
|
|
469
|
+
}
|
|
470
|
+
catch (e) {
|
|
471
|
+
if (e !== ROOT_NAMED)
|
|
472
|
+
throw e;
|
|
473
|
+
}
|
|
474
|
+
if (namer.format)
|
|
475
|
+
return namer.format;
|
|
476
|
+
const opc = namer.opc;
|
|
477
|
+
if (!opc)
|
|
478
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The XML file has no root element', { reason: 'no root element' });
|
|
479
|
+
const { zip } = await packFlatOpc(ctx, src, size, opc, zipLimits);
|
|
403
480
|
const contentTypes = zip.get('[Content_Types].xml');
|
|
404
481
|
if (!contentTypes)
|
|
405
|
-
|
|
406
|
-
await (0, ooxml_1.
|
|
407
|
-
|
|
408
|
-
ctx.format = exports.FLAT_OPC_FORMAT;
|
|
409
|
-
if (!inner)
|
|
410
|
-
return undefined;
|
|
411
|
-
const layout = {
|
|
412
|
-
instructions: [...cut.prolog.matchAll(/<\?mso-application\s[^?]*\?>/g)].map(m => m[0]),
|
|
413
|
-
extra: new Map(opc.parts.map(p => [(0, zip_1.nameKey)(p.name), p.extra])),
|
|
414
|
-
};
|
|
415
|
-
return { kind: 'flatopc', zip, plan: zctx.plan, format: inner, layout };
|
|
482
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The Flat OPC package has no [Content_Types].xml', { reason: 'no [Content_Types].xml' });
|
|
483
|
+
await (0, ooxml_1.nameOoxml)(ctx.withZip(zip), contentTypes);
|
|
484
|
+
return exports.FLAT_OPC_FORMAT;
|
|
416
485
|
}
|
|
417
486
|
/** Writes a flat OpenDocument file without what the plan removes. */
|
|
418
487
|
async function writeFlatOdf(run, src, plan, limits, sink) {
|
package/dist/formats.d.ts
CHANGED
|
@@ -15,3 +15,5 @@ export declare const XPS_FORMATS: {
|
|
|
15
15
|
};
|
|
16
16
|
/** OpenDocument formats by the media type in the mimetype entry or the manifest, exactly as written. */
|
|
17
17
|
export declare const ODF_FORMATS: Map<string, Format>;
|
|
18
|
+
/** RTF, which Word reads in any file that starts with {\rt. */
|
|
19
|
+
export declare const RTF_FORMAT: Format;
|
package/dist/formats.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ODF_FORMATS = exports.XPS_FORMATS = exports.XPS_SEQUENCE = exports.OOXML_FORMATS = void 0;
|
|
3
|
+
exports.RTF_FORMAT = exports.ODF_FORMATS = exports.XPS_FORMATS = exports.XPS_SEQUENCE = exports.OOXML_FORMATS = void 0;
|
|
4
4
|
const f = (format, mimeType) => ({ format, mimeType });
|
|
5
5
|
/** OOXML formats by the content type of the main part, lowercased. */
|
|
6
6
|
exports.OOXML_FORMATS = new Map([
|
|
@@ -48,3 +48,5 @@ exports.ODF_FORMATS = new Map([
|
|
|
48
48
|
['text-master', 'odm'],
|
|
49
49
|
['base', 'odb'],
|
|
50
50
|
].map(([kind, ext]) => [`application/vnd.oasis.opendocument.${kind}`, f(ext, `application/vnd.oasis.opendocument.${kind}`)]));
|
|
51
|
+
/** RTF, which Word reads in any file that starts with {\rt. */
|
|
52
|
+
exports.RTF_FORMAT = f('rtf', 'application/rtf');
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { detectOffice, detectOfficeSource } from './detect';
|
|
1
2
|
export { disarmOffice, disarmOfficeSource, inspectOffice, inspectOfficeSource } from './engine';
|
|
2
3
|
export { allFindingSpecs } from './findings';
|
|
3
4
|
export { bufferSink, bufferSource, fileSink, fileSource, writableSink } from './io';
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,10 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.scoreFindings = exports.DEFAULT_BANDS = exports.COMBINATIONS = exports.officePlugin = exports.writableSink = exports.fileSource = exports.fileSink = exports.bufferSource = exports.bufferSink = exports.allFindingSpecs = exports.inspectOfficeSource = exports.inspectOffice = exports.disarmOfficeSource = exports.disarmOffice = void 0;
|
|
17
|
+
exports.scoreFindings = exports.DEFAULT_BANDS = exports.COMBINATIONS = exports.officePlugin = exports.writableSink = exports.fileSource = exports.fileSink = exports.bufferSource = exports.bufferSink = exports.allFindingSpecs = exports.inspectOfficeSource = exports.inspectOffice = exports.disarmOfficeSource = exports.disarmOffice = exports.detectOfficeSource = exports.detectOffice = void 0;
|
|
18
|
+
var detect_1 = require("./detect");
|
|
19
|
+
Object.defineProperty(exports, "detectOffice", { enumerable: true, get: function () { return detect_1.detectOffice; } });
|
|
20
|
+
Object.defineProperty(exports, "detectOfficeSource", { enumerable: true, get: function () { return detect_1.detectOfficeSource; } });
|
|
18
21
|
var engine_1 = require("./engine");
|
|
19
22
|
Object.defineProperty(exports, "disarmOffice", { enumerable: true, get: function () { return engine_1.disarmOffice; } });
|
|
20
23
|
Object.defineProperty(exports, "disarmOfficeSource", { enumerable: true, get: function () { return engine_1.disarmOfficeSource; } });
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import cjs from './index.js';
|
|
2
|
-
export const { disarmOffice, disarmOfficeSource, inspectOffice, inspectOfficeSource, allFindingSpecs, bufferSink, bufferSource, fileSink, fileSource, writableSink, officePlugin, COMBINATIONS, DEFAULT_BANDS, scoreFindings, OfficeCategory, OfficeDetail, OfficeRisk } = cjs;
|
|
2
|
+
export const { detectOffice, detectOfficeSource, disarmOffice, disarmOfficeSource, inspectOffice, inspectOfficeSource, allFindingSpecs, bufferSink, bufferSource, fileSink, fileSource, writableSink, officePlugin, COMBINATIONS, DEFAULT_BANDS, scoreFindings, OfficeCategory, OfficeDetail, OfficeRisk } = cjs;
|
|
3
3
|
export default cjs;
|
package/dist/odf.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type PlanContext, type ZipContext } from './context';
|
|
2
|
+
import { type Format } from './formats';
|
|
2
3
|
import { OfficeCategory as C, OfficeDetail as D } from './types';
|
|
3
4
|
import type { XmlElement, XmlHandler } from './xml';
|
|
5
|
+
import { type ZipEntry } from './zip';
|
|
4
6
|
export declare const NS: {
|
|
5
7
|
manifest: string;
|
|
6
8
|
office: string;
|
|
@@ -57,11 +59,31 @@ export declare class EntryRules implements XmlHandler {
|
|
|
57
59
|
private scriptEvent;
|
|
58
60
|
private presentationEvent;
|
|
59
61
|
}
|
|
62
|
+
export interface Manifest {
|
|
63
|
+
paths: Set<string>;
|
|
64
|
+
root?: string;
|
|
65
|
+
encrypted: boolean;
|
|
66
|
+
}
|
|
60
67
|
/** The code folder an entry sits in, as the folder's path and its finding. */
|
|
61
68
|
export declare function folderOf(name: string): {
|
|
62
69
|
prefix: string;
|
|
63
70
|
detail: D;
|
|
64
71
|
category: C;
|
|
65
72
|
} | undefined;
|
|
73
|
+
/** What names an OpenDocument package: the media type its format comes from, and the entries that give it. */
|
|
74
|
+
export interface OdfNaming {
|
|
75
|
+
format: Format;
|
|
76
|
+
/** The media type the format was named by. */
|
|
77
|
+
mediaType: string;
|
|
78
|
+
mimetype?: ZipEntry;
|
|
79
|
+
manifestEntry?: ZipEntry;
|
|
80
|
+
manifest?: Manifest;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Names an OpenDocument package's format by the media type in its mimetype entry or, without one, its manifest's root
|
|
84
|
+
* entry. Throws when the two name different formats, or the media type is not one this package reads. `layout` hears,
|
|
85
|
+
* with the entry it concerns, of each way the mimetype entry and the manifest leave the layout OpenDocument asks for.
|
|
86
|
+
*/
|
|
87
|
+
export declare function nameOdf(ctx: ZipContext, layout?: (location: string, reason: string) => void): Promise<OdfNaming>;
|
|
66
88
|
/** Inspects an OpenDocument package. */
|
|
67
89
|
export declare function inspectOdf(ctx: ZipContext): Promise<void>;
|