@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/dist/odf.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.EntryRules = exports.packagePath = exports.hrefOf = exports.odfNamespace = exports.NS = void 0;
|
|
4
4
|
exports.oattr = oattr;
|
|
5
5
|
exports.folderOf = folderOf;
|
|
6
|
+
exports.nameOdf = nameOdf;
|
|
6
7
|
exports.inspectOdf = inspectOdf;
|
|
7
8
|
const contained_1 = require("./contained");
|
|
8
9
|
const context_1 = require("./context");
|
|
@@ -286,12 +287,16 @@ function folderOf(name) {
|
|
|
286
287
|
}
|
|
287
288
|
/** The most of the mimetype entry that is kept. The longest media type in the table is 56 characters. */
|
|
288
289
|
const MAX_MIMETYPE = 256;
|
|
289
|
-
/**
|
|
290
|
-
|
|
290
|
+
/**
|
|
291
|
+
* Names an OpenDocument package's format by the media type in its mimetype entry or, without one, its manifest's root
|
|
292
|
+
* entry. Throws when the two name different formats, or the media type is not one this package reads. `layout` hears,
|
|
293
|
+
* with the entry it concerns, of each way the mimetype entry and the manifest leave the layout OpenDocument asks for.
|
|
294
|
+
*/
|
|
295
|
+
async function nameOdf(ctx, layout = () => undefined) {
|
|
291
296
|
const zip = ctx.zip;
|
|
292
297
|
const mimetype = zip.entries.find(e => e.name === 'mimetype');
|
|
293
298
|
const manifestEntry = zip.get('META-INF/manifest.xml');
|
|
294
|
-
const
|
|
299
|
+
const note = (reason) => layout(mimetype?.name ?? 'META-INF/manifest.xml', reason);
|
|
295
300
|
let declared;
|
|
296
301
|
if (mimetype) {
|
|
297
302
|
let head = Buffer.alloc(0);
|
|
@@ -303,28 +308,34 @@ async function inspectOdf(ctx) {
|
|
|
303
308
|
declared = head.toString('latin1') + (mimetype.size > MAX_MIMETYPE ? '...' : '');
|
|
304
309
|
const first = zip.entries.reduce((a, b) => (b.localOffset < a.localOffset ? b : a));
|
|
305
310
|
if (first !== mimetype)
|
|
306
|
-
|
|
311
|
+
note('mimetype is not the first entry');
|
|
307
312
|
if (mimetype.method !== 0)
|
|
308
|
-
|
|
313
|
+
note('mimetype is compressed');
|
|
309
314
|
if (mimetype.localExtra)
|
|
310
|
-
|
|
315
|
+
note('mimetype has an extra field');
|
|
311
316
|
if (declared !== declared.trim())
|
|
312
|
-
|
|
317
|
+
note('mimetype has white space around the media type');
|
|
313
318
|
declared = declared.trim();
|
|
314
319
|
}
|
|
315
320
|
else
|
|
316
|
-
|
|
321
|
+
note('there is no mimetype entry');
|
|
317
322
|
const manifest = manifestEntry ? await readManifest(ctx, manifestEntry) : undefined;
|
|
318
323
|
if (!manifest)
|
|
319
|
-
|
|
324
|
+
note('there is no manifest');
|
|
320
325
|
const root = manifest?.root;
|
|
321
326
|
if (declared !== undefined && root !== undefined && declared !== root && formats_1.ODF_FORMATS.has(declared) && formats_1.ODF_FORMATS.has(root)) {
|
|
322
327
|
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.AmbiguousFormat, 'The mimetype entry and the manifest name different formats', { mimetype: (0, context_1.clip)(declared), manifest: (0, context_1.clip)(root) });
|
|
323
328
|
}
|
|
324
|
-
const
|
|
325
|
-
const format = formats_1.ODF_FORMATS.get(
|
|
329
|
+
const mediaType = declared ?? root ?? '';
|
|
330
|
+
const format = formats_1.ODF_FORMATS.get(mediaType);
|
|
326
331
|
if (!format)
|
|
327
|
-
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)(
|
|
332
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The OpenDocument media type is not one this package reads', { mediaType: (0, context_1.clip)(mediaType) });
|
|
333
|
+
return { format, mediaType, mimetype, manifestEntry, manifest };
|
|
334
|
+
}
|
|
335
|
+
/** Inspects an OpenDocument package. */
|
|
336
|
+
async function inspectOdf(ctx) {
|
|
337
|
+
const zip = ctx.zip;
|
|
338
|
+
const { format, mediaType: type, mimetype, manifestEntry, manifest } = await nameOdf(ctx, (location, reason) => ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.OdfLayout, location, { reason }));
|
|
328
339
|
ctx.format = format;
|
|
329
340
|
if (manifest?.encrypted)
|
|
330
341
|
throw new errors_1.StopError(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.PasswordRequired, 'The manifest lists encrypted entries');
|
package/dist/ole.d.ts
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
import type { CompoundFile } from './cfb';
|
|
2
2
|
import type { RunContext } from './context';
|
|
3
|
+
import type { Format } from './formats';
|
|
4
|
+
import { ZipArchive } from './zip';
|
|
3
5
|
/** The families a compound file can hold, by the stream or storage that marks each. */
|
|
4
|
-
type Family = 'word' | 'excel' | 'powerpoint' | 'visio' | 'outlook' | 'publisher' | 'project' | 'encrypted';
|
|
6
|
+
export type Family = 'word' | 'excel' | 'powerpoint' | 'visio' | 'outlook' | 'publisher' | 'project' | 'encrypted';
|
|
5
7
|
/** What the root of a compound file holds, read by the names of its children, ignoring case as MS-CFB does. */
|
|
6
8
|
export declare function families(cfb: CompoundFile): Family[];
|
|
9
|
+
/**
|
|
10
|
+
* The family whose format a compound file holds, by the streams and storages at its root. Throws for a root that
|
|
11
|
+
* holds no Office document, or the documents of two families.
|
|
12
|
+
*/
|
|
13
|
+
export declare function mainFamily(cfb: CompoundFile): Family;
|
|
14
|
+
/**
|
|
15
|
+
* The format of a compound file of `family`. A Word document is a template when its FIB says so, and a workbook a
|
|
16
|
+
* template or an add-in when its globals do; both are read without a password. An encrypted package names no format
|
|
17
|
+
* until it is decrypted, so for one this is undefined.
|
|
18
|
+
*/
|
|
19
|
+
export declare function compoundFormat(cfb: CompoundFile, family: Family, deadline?: number): Promise<Format | undefined>;
|
|
20
|
+
/**
|
|
21
|
+
* Decrypts the package of an encrypted compound file with the run's passwords, then VelvetSweatshop, and reads it as
|
|
22
|
+
* a ZIP archive. Throws ENCRYPTED/PASSWORD_REQUIRED when no password opens it.
|
|
23
|
+
*/
|
|
24
|
+
export declare function openEncryptedPackage(ctx: RunContext, cfb: CompoundFile): Promise<ZipArchive>;
|
|
7
25
|
/**
|
|
8
26
|
* Inspects a compound file: the rules every compound file gets, over its whole tree, and then those of the format
|
|
9
27
|
* its root names.
|
|
10
28
|
*/
|
|
11
29
|
export declare function inspectCompound(ctx: RunContext, cfb: CompoundFile): Promise<void>;
|
|
12
|
-
export {};
|
package/dist/ole.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.families = families;
|
|
4
|
+
exports.mainFamily = mainFamily;
|
|
5
|
+
exports.compoundFormat = compoundFormat;
|
|
6
|
+
exports.openEncryptedPackage = openEncryptedPackage;
|
|
4
7
|
exports.inspectCompound = inspectCompound;
|
|
5
8
|
const errors_1 = require("./errors");
|
|
6
9
|
const msg_1 = require("./msg");
|
|
@@ -43,10 +46,10 @@ function families(cfb) {
|
|
|
43
46
|
return out;
|
|
44
47
|
}
|
|
45
48
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
49
|
+
* The family whose format a compound file holds, by the streams and storages at its root. Throws for a root that
|
|
50
|
+
* holds no Office document, or the documents of two families.
|
|
48
51
|
*/
|
|
49
|
-
|
|
52
|
+
function mainFamily(cfb) {
|
|
50
53
|
const found = families(cfb);
|
|
51
54
|
// Word 2007 writes a Word document beside an encrypted package, for readers that cannot decrypt it.
|
|
52
55
|
const main = found.filter(x => !(x === 'word' && found.includes('encrypted')));
|
|
@@ -54,40 +57,88 @@ async function inspectCompound(ctx, cfb) {
|
|
|
54
57
|
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.AmbiguousFormat, `The compound file holds ${main.join(' and ')} streams`, { reason: `${main.join(' and ')} streams` });
|
|
55
58
|
if (main.length === 0)
|
|
56
59
|
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The compound file holds no Office document stream', { reason: 'a compound file of another kind' });
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
return main[0];
|
|
61
|
+
}
|
|
62
|
+
/** A stream at the root of a compound file, by name, ignoring case. */
|
|
63
|
+
const rootStream = (cfb, name) => {
|
|
64
|
+
const e = cfb.child(cfb.root, name);
|
|
65
|
+
return e?.kind === 'stream' ? e : undefined;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* The format of a compound file of `family`. A Word document is a template when its FIB says so, and a workbook a
|
|
69
|
+
* template or an add-in when its globals do; both are read without a password. An encrypted package names no format
|
|
70
|
+
* until it is decrypted, so for one this is undefined.
|
|
71
|
+
*/
|
|
72
|
+
async function compoundFormat(cfb, family, deadline) {
|
|
73
|
+
switch (family) {
|
|
74
|
+
case 'word': {
|
|
75
|
+
const word = rootStream(cfb, 'WordDocument');
|
|
76
|
+
return word && (await (0, word_1.isWordTemplate)(cfb, word)) ? f('dot', 'application/msword') : f('doc', 'application/msword');
|
|
77
|
+
}
|
|
78
|
+
case 'excel': {
|
|
79
|
+
// Excel 97 to 2003 reads Workbook, and Excel 5.0 and 95 read Book. A file can hold both.
|
|
80
|
+
let template = false;
|
|
81
|
+
let addIn = false;
|
|
82
|
+
for (const name of ['Workbook', 'Book']) {
|
|
83
|
+
const s = rootStream(cfb, name);
|
|
84
|
+
if (!s)
|
|
85
|
+
continue;
|
|
86
|
+
const kind = await (0, xls_1.workbookKind)(cfb, s, deadline);
|
|
87
|
+
template ||= kind.template;
|
|
88
|
+
addIn ||= kind.addIn;
|
|
89
|
+
}
|
|
90
|
+
return addIn ? f('xla', 'application/vnd.ms-excel') : template ? f('xlt', 'application/vnd.ms-excel') : f('xls', 'application/vnd.ms-excel');
|
|
91
|
+
}
|
|
92
|
+
case 'powerpoint':
|
|
93
|
+
return f('ppt', 'application/vnd.ms-powerpoint');
|
|
94
|
+
case 'visio':
|
|
95
|
+
return f('vsd', 'application/vnd.visio');
|
|
96
|
+
case 'outlook':
|
|
97
|
+
return cfb.root.clsid === '0006F046-0000-0000-C000-000000000046' ? f('oft', 'application/vnd.ms-outlook') : f('msg', 'application/vnd.ms-outlook');
|
|
98
|
+
case 'publisher':
|
|
99
|
+
return f('pub', 'application/x-mspublisher');
|
|
100
|
+
case 'project':
|
|
101
|
+
return f('mpp', 'application/vnd.ms-project');
|
|
102
|
+
default:
|
|
103
|
+
// An encrypted package names its format only once it is decrypted.
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Decrypts the package of an encrypted compound file with the run's passwords, then VelvetSweatshop, and reads it as
|
|
109
|
+
* a ZIP archive. Throws ENCRYPTED/PASSWORD_REQUIRED when no password opens it.
|
|
110
|
+
*/
|
|
111
|
+
async function openEncryptedPackage(ctx, cfb) {
|
|
112
|
+
const info = rootStream(cfb, 'EncryptionInfo');
|
|
113
|
+
const pkg = rootStream(cfb, 'EncryptedPackage');
|
|
114
|
+
if (!info || !pkg)
|
|
115
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The compound file holds no encrypted package', { reason: 'no EncryptionInfo and EncryptedPackage streams' });
|
|
116
|
+
const { source } = await (0, offcrypto_1.openPackage)(ctx, cfb, info, pkg);
|
|
117
|
+
return zip_1.ZipArchive.open(source, ctx.settings, ctx.settings.deadline);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Inspects a compound file: the rules every compound file gets, over its whole tree, and then those of the format
|
|
121
|
+
* its root names.
|
|
122
|
+
*/
|
|
123
|
+
async function inspectCompound(ctx, cfb) {
|
|
124
|
+
const family = mainFamily(cfb);
|
|
125
|
+
ctx.format = await compoundFormat(cfb, family, ctx.settings.deadline);
|
|
59
126
|
const plan = new plan_1.CompoundPlan();
|
|
60
127
|
if (family !== 'encrypted')
|
|
61
128
|
ctx.run.output = { kind: 'cfb', cfb, plan };
|
|
62
129
|
await (0, objects_1.compoundRules)(ctx, cfb, '', false, plan);
|
|
63
|
-
const stream = (name) =>
|
|
64
|
-
const e = cfb.child(cfb.root, name);
|
|
65
|
-
return e?.kind === 'stream' ? e : undefined;
|
|
66
|
-
};
|
|
130
|
+
const stream = (name) => rootStream(cfb, name);
|
|
67
131
|
// Beside an encrypted package, Word 2007 writes a WordDocument stream for older readers, of zeros in the files at
|
|
68
132
|
// hand. Those readers read it if it holds a document, so it gets the Word rules when it does.
|
|
69
133
|
const word = stream('WordDocument');
|
|
70
|
-
if (word)
|
|
71
|
-
|
|
72
|
-
if (r?.template && family === 'word')
|
|
73
|
-
ctx.format = f('dot', 'application/msword');
|
|
74
|
-
}
|
|
134
|
+
if (word)
|
|
135
|
+
await (0, word_1.wordRules)(ctx, cfb, word, family === 'encrypted');
|
|
75
136
|
if (family === 'excel') {
|
|
76
|
-
// Excel 97 to 2003 reads Workbook, and Excel 5.0 and 95 read Book. A file can hold both.
|
|
77
|
-
let template = false;
|
|
78
|
-
let addIn = false;
|
|
79
137
|
for (const name of ['Workbook', 'Book']) {
|
|
80
138
|
const s = stream(name);
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
const r = await (0, xls_1.excelRules)(ctx, cfb, s, offcrypto_1.unlockWorkbook);
|
|
84
|
-
template ||= r.template;
|
|
85
|
-
addIn ||= r.addIn;
|
|
139
|
+
if (s)
|
|
140
|
+
await (0, xls_1.excelRules)(ctx, cfb, s, offcrypto_1.unlockWorkbook);
|
|
86
141
|
}
|
|
87
|
-
if (addIn)
|
|
88
|
-
ctx.format = f('xla', 'application/vnd.ms-excel');
|
|
89
|
-
else if (template)
|
|
90
|
-
ctx.format = f('xlt', 'application/vnd.ms-excel');
|
|
91
142
|
// Excel keeps the data of a workbook's ActiveX controls in one Ctls stream.
|
|
92
143
|
const controls = stream('Ctls');
|
|
93
144
|
if (controls && controls.size > 0 && ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, controls.name, { stream: controls.name }).action === 'strip')
|
|
@@ -101,14 +152,12 @@ async function inspectCompound(ctx, cfb) {
|
|
|
101
152
|
const visio = stream('VisioDocument');
|
|
102
153
|
if (family === 'visio' && visio)
|
|
103
154
|
await (0, vsd_1.visioRules)(ctx, cfb, visio);
|
|
104
|
-
|
|
105
|
-
const pkg = stream('EncryptedPackage');
|
|
106
|
-
if (family === 'encrypted' && info && pkg) {
|
|
155
|
+
if (family === 'encrypted') {
|
|
107
156
|
// The package goes through the OOXML rules once a password opens it, and names the format.
|
|
108
|
-
const
|
|
109
|
-
const
|
|
157
|
+
const zip = await openEncryptedPackage(ctx, cfb);
|
|
158
|
+
const location = stream('EncryptedPackage')?.name ?? 'EncryptedPackage';
|
|
110
159
|
for (const n of zip.notes)
|
|
111
|
-
ctx.report(types_1.OfficeCategory.Container, n.detail,
|
|
160
|
+
ctx.report(types_1.OfficeCategory.Container, n.detail, location, { bytes: n.bytes });
|
|
112
161
|
// The rewrite writes the package decrypted, since office-defuse does not encrypt.
|
|
113
162
|
const zctx = ctx.withZip(zip);
|
|
114
163
|
await (0, package_1.inspectPackage)(zctx);
|
|
@@ -116,24 +165,3 @@ async function inspectCompound(ctx, cfb) {
|
|
|
116
165
|
ctx.run.output = { kind: 'zip', zip, plan: zctx.plan, format: ctx.format };
|
|
117
166
|
}
|
|
118
167
|
}
|
|
119
|
-
function formatOf(family, cfb) {
|
|
120
|
-
switch (family) {
|
|
121
|
-
case 'word':
|
|
122
|
-
return f('doc', 'application/msword');
|
|
123
|
-
case 'excel':
|
|
124
|
-
return f('xls', 'application/vnd.ms-excel');
|
|
125
|
-
case 'powerpoint':
|
|
126
|
-
return f('ppt', 'application/vnd.ms-powerpoint');
|
|
127
|
-
case 'visio':
|
|
128
|
-
return f('vsd', 'application/vnd.visio');
|
|
129
|
-
case 'outlook':
|
|
130
|
-
return cfb.root.clsid === '0006F046-0000-0000-C000-000000000046' ? f('oft', 'application/vnd.ms-outlook') : f('msg', 'application/vnd.ms-outlook');
|
|
131
|
-
case 'publisher':
|
|
132
|
-
return f('pub', 'application/x-mspublisher');
|
|
133
|
-
case 'project':
|
|
134
|
-
return f('mpp', 'application/vnd.ms-project');
|
|
135
|
-
default:
|
|
136
|
-
// An encrypted package names its format only once it is decrypted.
|
|
137
|
-
return undefined;
|
|
138
|
-
}
|
|
139
|
-
}
|
package/dist/ooxml.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ZipContext } from './context';
|
|
2
|
+
import { type Format } from './formats';
|
|
2
3
|
import { type XmlElement } from './xml';
|
|
3
4
|
import { type ZipEntry } from './zip';
|
|
4
5
|
export declare const CONTENT_TYPES_NS = "http://schemas.openxmlformats.org/package/2006/content-types";
|
|
@@ -81,9 +82,33 @@ export declare function resolvePartRef(part: string, ref: string): string;
|
|
|
81
82
|
export declare function xpsResources(el: XmlElement): Array<[string, string]>;
|
|
82
83
|
/** Reads a relationships part and hands each relationship to `each` as it is read. Ids given twice are reported. */
|
|
83
84
|
export declare function readRelationships(ctx: ZipContext, entry: ZipEntry, each: (r: Relationship) => void): Promise<void>;
|
|
85
|
+
/** An OOXML package's parts, typed by its [Content_Types].xml, and its relationships parts by the key of the part each belongs to. */
|
|
86
|
+
export interface OoxmlParts {
|
|
87
|
+
parts: Map<string, Part>;
|
|
88
|
+
relsOf: Map<string, ZipEntry>;
|
|
89
|
+
}
|
|
90
|
+
/** Reads [Content_Types].xml and types each part by it. Directory entries are neither parts nor relationships parts. */
|
|
91
|
+
export declare function readParts(ctx: ZipContext, contentTypes: ZipEntry): Promise<OoxmlParts>;
|
|
92
|
+
/** The format the package relationships name, and the relationship types they use. */
|
|
93
|
+
export interface OoxmlMain {
|
|
94
|
+
format: Format;
|
|
95
|
+
/** The package uses the relationship types of Strict OOXML. */
|
|
96
|
+
strict: boolean;
|
|
97
|
+
/** The package is XPS or OpenXPS. */
|
|
98
|
+
xps: boolean;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Reads the package relationships and names the format by the one main part they name: by its content type, or for
|
|
102
|
+
* XPS by the relationship type too, since XPS and OpenXPS share their content types. `each` sees every package
|
|
103
|
+
* relationship as it is read. Throws when no main part, or more than one, is named, or its format is unknown.
|
|
104
|
+
*/
|
|
105
|
+
export declare function readMain(ctx: ZipContext, { parts, relsOf }: OoxmlParts, each?: (rels: ZipEntry, r: Relationship) => void): Promise<OoxmlMain>;
|
|
106
|
+
/** Names an OOXML package's format as its inspection does, reading only [Content_Types].xml and the package relationships. */
|
|
107
|
+
export declare function nameOoxml(ctx: ZipContext, contentTypes: ZipEntry): Promise<Format>;
|
|
84
108
|
/**
|
|
85
109
|
* Inspects an OOXML package. The package relationships come first, since they name the main part and so the format.
|
|
86
110
|
* Then each part is read in archive order, followed by its relationships, which are reported as they are read and
|
|
87
|
-
* leave behind only which parts they reach and how.
|
|
111
|
+
* leave behind only which parts they reach and how. The run names `outer` as the format when given, as for a Flat
|
|
112
|
+
* OPC package unpacked into a ZIP archive, and the package's own format is returned.
|
|
88
113
|
*/
|
|
89
|
-
export declare function inspectOoxml(ctx: ZipContext, contentTypes: ZipEntry): Promise<
|
|
114
|
+
export declare function inspectOoxml(ctx: ZipContext, contentTypes: ZipEntry, outer?: Format): Promise<Format>;
|
package/dist/ooxml.js
CHANGED
|
@@ -5,6 +5,9 @@ exports.resolveTarget = resolveTarget;
|
|
|
5
5
|
exports.resolvePartRef = resolvePartRef;
|
|
6
6
|
exports.xpsResources = xpsResources;
|
|
7
7
|
exports.readRelationships = readRelationships;
|
|
8
|
+
exports.readParts = readParts;
|
|
9
|
+
exports.readMain = readMain;
|
|
10
|
+
exports.nameOoxml = nameOoxml;
|
|
8
11
|
exports.inspectOoxml = inspectOoxml;
|
|
9
12
|
const biff_1 = require("./biff");
|
|
10
13
|
const contained_1 = require("./contained");
|
|
@@ -540,24 +543,14 @@ class PartRules {
|
|
|
540
543
|
}
|
|
541
544
|
}
|
|
542
545
|
}
|
|
543
|
-
/**
|
|
544
|
-
|
|
545
|
-
* Then each part is read in archive order, followed by its relationships, which are reported as they are read and
|
|
546
|
-
* leave behind only which parts they reach and how.
|
|
547
|
-
*/
|
|
548
|
-
async function inspectOoxml(ctx, contentTypes) {
|
|
546
|
+
/** Reads [Content_Types].xml and types each part by it. Directory entries are neither parts nor relationships parts. */
|
|
547
|
+
async function readParts(ctx, contentTypes) {
|
|
549
548
|
const { defaults, overrides } = await readContentTypes(ctx, contentTypes);
|
|
550
549
|
const parts = new Map();
|
|
551
550
|
const relsOf = new Map();
|
|
552
|
-
const plan = ctx.plan;
|
|
553
551
|
for (const entry of ctx.zip.entries) {
|
|
554
|
-
if (entry === contentTypes)
|
|
555
|
-
continue;
|
|
556
|
-
// OPC has no folders. A directory entry is still inflated once, so its bytes are checked like any other.
|
|
557
|
-
if (entry.directory) {
|
|
558
|
-
await ctx.drain(entry);
|
|
552
|
+
if (entry === contentTypes || entry.directory)
|
|
559
553
|
continue;
|
|
560
|
-
}
|
|
561
554
|
const source = (0, exports.relsSource)(entry.name);
|
|
562
555
|
if (source !== undefined) {
|
|
563
556
|
relsOf.set(source, entry);
|
|
@@ -567,8 +560,54 @@ async function inspectOoxml(ctx, contentTypes) {
|
|
|
567
560
|
const ext = /\.([^./]*)$/.exec(entry.name)?.[1]?.toLowerCase();
|
|
568
561
|
parts.set(key, { entry, contentType: overrides.get(key) ?? (ext === undefined ? undefined : defaults.get(ext)), reachedAs: 0, claimed: false, targets: new Set(), ole: false });
|
|
569
562
|
}
|
|
570
|
-
|
|
563
|
+
return { parts, relsOf };
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Reads the package relationships and names the format by the one main part they name: by its content type, or for
|
|
567
|
+
* XPS by the relationship type too, since XPS and OpenXPS share their content types. `each` sees every package
|
|
568
|
+
* relationship as it is read. Throws when no main part, or more than one, is named, or its format is unknown.
|
|
569
|
+
*/
|
|
570
|
+
async function readMain(ctx, { parts, relsOf }, each) {
|
|
571
|
+
const packageRels = relsOf.get('');
|
|
571
572
|
const mains = [];
|
|
573
|
+
let strict = false;
|
|
574
|
+
let mainType = '';
|
|
575
|
+
if (packageRels) {
|
|
576
|
+
await readRelationships(ctx, packageRels, r => {
|
|
577
|
+
if (exports.REL.main.has(r.type)) {
|
|
578
|
+
mains.push(r.external ? undefined : parts.get(resolveTarget(packageRels.name, r.target)));
|
|
579
|
+
strict = r.type.startsWith(S);
|
|
580
|
+
mainType = r.type;
|
|
581
|
+
}
|
|
582
|
+
each?.(packageRels, r);
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
if (mains.length > 1)
|
|
586
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.AmbiguousFormat, 'The package names more than one main part', { reason: 'more than one main part' });
|
|
587
|
+
const main = mains[0];
|
|
588
|
+
if (!main)
|
|
589
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The package has no main part', { reason: 'no main document part' });
|
|
590
|
+
const xps = mainType === exports.XPS_MAIN.xps || mainType === exports.XPS_MAIN.oxps;
|
|
591
|
+
const format = xps ? (main.contentType === formats_1.XPS_SEQUENCE ? formats_1.XPS_FORMATS[mainType === exports.XPS_MAIN.oxps ? 'oxps' : 'xps'] : undefined) : formats_1.OOXML_FORMATS.get(main.contentType ?? '');
|
|
592
|
+
if (!format)
|
|
593
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The main part has an unknown content type', { contentType: (0, context_1.clip)(main.contentType ?? '') });
|
|
594
|
+
return { format, strict, xps };
|
|
595
|
+
}
|
|
596
|
+
/** Names an OOXML package's format as its inspection does, reading only [Content_Types].xml and the package relationships. */
|
|
597
|
+
async function nameOoxml(ctx, contentTypes) {
|
|
598
|
+
return (await readMain(ctx, await readParts(ctx, contentTypes))).format;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Inspects an OOXML package. The package relationships come first, since they name the main part and so the format.
|
|
602
|
+
* Then each part is read in archive order, followed by its relationships, which are reported as they are read and
|
|
603
|
+
* leave behind only which parts they reach and how. The run names `outer` as the format when given, as for a Flat
|
|
604
|
+
* OPC package unpacked into a ZIP archive, and the package's own format is returned.
|
|
605
|
+
*/
|
|
606
|
+
async function inspectOoxml(ctx, contentTypes, outer) {
|
|
607
|
+
const typed = await readParts(ctx, contentTypes);
|
|
608
|
+
const { parts, relsOf } = typed;
|
|
609
|
+
const plan = ctx.plan;
|
|
610
|
+
const roots = new Set();
|
|
572
611
|
/** The labels each part gives its hyperlinks, by part key and relationship Id. */
|
|
573
612
|
const labelsOf = new Map();
|
|
574
613
|
const relate = (rels, from, r) => {
|
|
@@ -598,30 +637,12 @@ async function inspectOoxml(ctx, contentTypes) {
|
|
|
598
637
|
if (reported?.action === 'strip')
|
|
599
638
|
plan.dropRelationship(rels.name, r.id);
|
|
600
639
|
};
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
mains.push(r.external ? undefined : parts.get(resolveTarget(packageRels.name, r.target)));
|
|
608
|
-
strict = r.type.startsWith(S);
|
|
609
|
-
mainType = r.type;
|
|
610
|
-
}
|
|
611
|
-
relate(packageRels, undefined, r);
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
if (mains.length > 1)
|
|
615
|
-
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.AmbiguousFormat, 'The package names more than one main part', { reason: 'more than one main part' });
|
|
616
|
-
const main = mains[0];
|
|
617
|
-
if (!main)
|
|
618
|
-
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The package has no main part', { reason: 'no main document part' });
|
|
619
|
-
const xps = mainType === exports.XPS_MAIN.xps || mainType === exports.XPS_MAIN.oxps;
|
|
620
|
-
// XPS and OpenXPS share their content types, so the relationship type tells them apart.
|
|
621
|
-
const format = xps ? (main.contentType === formats_1.XPS_SEQUENCE ? formats_1.XPS_FORMATS[mainType === exports.XPS_MAIN.oxps ? 'oxps' : 'xps'] : undefined) : formats_1.OOXML_FORMATS.get(main.contentType ?? '');
|
|
622
|
-
if (!format)
|
|
623
|
-
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The main part has an unknown content type', { contentType: (0, context_1.clip)(main.contentType ?? '') });
|
|
624
|
-
ctx.format = format;
|
|
640
|
+
const { format, strict, xps } = await readMain(ctx, typed, (rels, r) => relate(rels, undefined, r));
|
|
641
|
+
ctx.format = outer ?? format;
|
|
642
|
+
// OPC has no folders. A directory entry is still inflated once, so its bytes are checked like any other.
|
|
643
|
+
for (const entry of ctx.zip.entries)
|
|
644
|
+
if (entry.directory)
|
|
645
|
+
await ctx.drain(entry);
|
|
625
646
|
for (const [key, p] of parts) {
|
|
626
647
|
const ct = p.contentType;
|
|
627
648
|
if (ct === undefined) {
|
|
@@ -663,6 +684,7 @@ async function inspectOoxml(ctx, contentTypes) {
|
|
|
663
684
|
await partRules(ctx, p);
|
|
664
685
|
unreferenced(ctx, parts, relsOf, roots);
|
|
665
686
|
plan.graph = { contentTypes, parts, relsOf, roots, strict, xps };
|
|
687
|
+
return format;
|
|
666
688
|
}
|
|
667
689
|
/**
|
|
668
690
|
* Findings about a whole part, from its content type and the relationships that reach it. The files inside the
|
package/dist/package.d.ts
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
1
|
import type { ZipContext } from './context';
|
|
2
|
+
import type { Format } from './formats';
|
|
3
|
+
import type { ZipArchive, ZipEntry } from './zip';
|
|
4
|
+
/**
|
|
5
|
+
* The package a ZIP archive's markers name: OOXML by [Content_Types].xml, OpenDocument by a mimetype entry or a
|
|
6
|
+
* manifest. Throws for an archive with both or neither.
|
|
7
|
+
*/
|
|
8
|
+
export declare function packageKind(zip: ZipArchive): {
|
|
9
|
+
kind: 'ooxml';
|
|
10
|
+
contentTypes: ZipEntry;
|
|
11
|
+
} | {
|
|
12
|
+
kind: 'odf';
|
|
13
|
+
};
|
|
2
14
|
/** Inspects an OOXML or OpenDocument package, which its markers name. */
|
|
3
15
|
export declare function inspectPackage(ctx: ZipContext): Promise<void>;
|
|
16
|
+
/** Names an OOXML or OpenDocument package's format as its inspection does, without the rules. */
|
|
17
|
+
export declare function namePackage(ctx: ZipContext): Promise<Format>;
|
package/dist/package.js
CHANGED
|
@@ -1,21 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.packageKind = packageKind;
|
|
3
4
|
exports.inspectPackage = inspectPackage;
|
|
5
|
+
exports.namePackage = namePackage;
|
|
4
6
|
const errors_1 = require("./errors");
|
|
5
7
|
const odf_1 = require("./odf");
|
|
6
8
|
const ooxml_1 = require("./ooxml");
|
|
7
9
|
const types_1 = require("./types");
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The package a ZIP archive's markers name: OOXML by [Content_Types].xml, OpenDocument by a mimetype entry or a
|
|
12
|
+
* manifest. Throws for an archive with both or neither.
|
|
13
|
+
*/
|
|
14
|
+
function packageKind(zip) {
|
|
11
15
|
const contentTypes = zip.get('[Content_Types].xml');
|
|
12
16
|
const odf = zip.entries.some(e => e.name === 'mimetype') || zip.get('META-INF/manifest.xml') !== undefined;
|
|
13
17
|
if (contentTypes && odf)
|
|
14
18
|
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.AmbiguousFormat, 'The file has both [Content_Types].xml and an OpenDocument mimetype or manifest', { reason: 'OOXML and OpenDocument markers' });
|
|
15
19
|
if (contentTypes)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
return { kind: 'ooxml', contentTypes };
|
|
21
|
+
if (odf)
|
|
22
|
+
return { kind: 'odf' };
|
|
23
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The archive has neither [Content_Types].xml nor an OpenDocument mimetype or manifest', { reason: 'a plain ZIP archive' });
|
|
24
|
+
}
|
|
25
|
+
/** Inspects an OOXML or OpenDocument package, which its markers name. */
|
|
26
|
+
async function inspectPackage(ctx) {
|
|
27
|
+
const k = packageKind(ctx.zip);
|
|
28
|
+
if (k.kind === 'ooxml')
|
|
29
|
+
await (0, ooxml_1.inspectOoxml)(ctx, k.contentTypes);
|
|
19
30
|
else
|
|
20
|
-
|
|
31
|
+
await (0, odf_1.inspectOdf)(ctx);
|
|
32
|
+
}
|
|
33
|
+
/** Names an OOXML or OpenDocument package's format as its inspection does, without the rules. */
|
|
34
|
+
async function namePackage(ctx) {
|
|
35
|
+
const k = packageKind(ctx.zip);
|
|
36
|
+
return k.kind === 'ooxml' ? (0, ooxml_1.nameOoxml)(ctx, k.contentTypes) : (await (0, odf_1.nameOdf)(ctx)).format;
|
|
21
37
|
}
|
package/dist/plugins.js
CHANGED
|
@@ -2,34 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.officePlugin = officePlugin;
|
|
4
4
|
const contained_1 = require("./contained");
|
|
5
|
+
const detect_1 = require("./detect");
|
|
5
6
|
const engine_1 = require("./engine");
|
|
6
|
-
const flat_1 = require("./flat");
|
|
7
7
|
const score_1 = require("./score");
|
|
8
|
-
const zip_1 = require("./zip");
|
|
9
|
-
/** What the ZIP reader needs to look for the markers of an Office package, with the default limits. */
|
|
10
|
-
const PROBE = { entries: 10_000, decompressedBytes: 512 * 1024 * 1024, totalDecompressedBytes: 2 * 1024 * 1024 * 1024 };
|
|
11
|
-
/**
|
|
12
|
-
* Whether the content is a file office-defuse reads: a compound file, RTF, a ZIP archive with the marker of an Office
|
|
13
|
-
* Open XML or OpenDocument package, or XML whose root is a flat OpenDocument document or a Flat OPC package. A ZIP
|
|
14
|
-
* archive without a marker, or one the ZIP reader refuses, is not.
|
|
15
|
-
*/
|
|
16
|
-
async function isOffice(source, sniffed) {
|
|
17
|
-
if (sniffed === 'application/x-ole-storage' || sniffed === 'application/rtf')
|
|
18
|
-
return true;
|
|
19
|
-
if (sniffed === 'application/xml') {
|
|
20
|
-
const head = await source.read(0, Math.min(16384, await source.size()));
|
|
21
|
-
return (0, flat_1.isFlatHead)(Buffer.from(head).toString('utf8'));
|
|
22
|
-
}
|
|
23
|
-
if (sniffed !== 'application/zip')
|
|
24
|
-
return false;
|
|
25
|
-
try {
|
|
26
|
-
const zip = await zip_1.ZipArchive.open(source, PROBE);
|
|
27
|
-
return zip.get('[Content_Types].xml') !== undefined || zip.entries.some(e => e.name === 'mimetype') || zip.get('META-INF/manifest.xml') !== undefined;
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
8
|
/**
|
|
34
9
|
* Defuses Office and OpenDocument files inside other files, with `options` when given. Without them, it takes the
|
|
35
10
|
* options of the office-defuse run that found the file, or none when another package found it. `limits.nestingDepth`
|
|
@@ -40,8 +15,8 @@ function officePlugin(options) {
|
|
|
40
15
|
return {
|
|
41
16
|
kind: 'file',
|
|
42
17
|
name: 'office',
|
|
43
|
-
// It
|
|
44
|
-
accepts: f =>
|
|
18
|
+
// It takes what detectOffice names: the content decides, whatever the name says.
|
|
19
|
+
accepts: async (f) => (await (0, detect_1.detectOfficeSource)(f.source, { name: f.name, declaredType: f.declaredType })) !== undefined,
|
|
45
20
|
async process(file, sink, context) {
|
|
46
21
|
const run = options ?? context[contained_1.RUN_OPTIONS] ?? {};
|
|
47
22
|
const r = await (0, engine_1.disarmAtDepth)(file.source, sink, run, context.depth, context.deadline);
|
package/dist/rtf.js
CHANGED
|
@@ -6,6 +6,7 @@ const contained_1 = require("./contained");
|
|
|
6
6
|
const context_1 = require("./context");
|
|
7
7
|
const errors_1 = require("./errors");
|
|
8
8
|
const fields_1 = require("./fields");
|
|
9
|
+
const formats_1 = require("./formats");
|
|
9
10
|
const io_1 = require("./io");
|
|
10
11
|
const links_1 = require("./links");
|
|
11
12
|
const objects_1 = require("./objects");
|
|
@@ -102,7 +103,7 @@ const isDigit = (c) => c >= 0x30 && c <= 0x39;
|
|
|
102
103
|
* text after the document's last brace is still read, as rtfobj reads it, and reported as trailing data.
|
|
103
104
|
*/
|
|
104
105
|
async function inspectRtf(ctx, source, size) {
|
|
105
|
-
ctx.format =
|
|
106
|
+
ctx.format = formats_1.RTF_FORMAT;
|
|
106
107
|
const cuts = [];
|
|
107
108
|
ctx.run.output = { kind: 'rtf', size, cuts };
|
|
108
109
|
const reader = new RtfReader(ctx, size, cuts);
|
package/dist/types.d.ts
CHANGED
|
@@ -234,6 +234,29 @@ export interface OfficeLimits {
|
|
|
234
234
|
/** Milliseconds for the whole run. Not enforced unless set. */
|
|
235
235
|
timeMs?: number;
|
|
236
236
|
}
|
|
237
|
+
export interface FormatDetection {
|
|
238
|
+
/** The format's MIME type, such as application/vnd.openxmlformats-officedocument.wordprocessingml.document. */
|
|
239
|
+
mimeType: string;
|
|
240
|
+
/** The format's usual extension, lowercase and without the dot. */
|
|
241
|
+
extension: string;
|
|
242
|
+
/** Where the format starts in the file: 0, or the offset of its data when other data comes first. */
|
|
243
|
+
offset: number;
|
|
244
|
+
/** Set when the match rests on more than the content, and says what it rests on. */
|
|
245
|
+
heuristic?: string;
|
|
246
|
+
}
|
|
247
|
+
/** What a detect function may know of a file besides its content. */
|
|
248
|
+
export interface DetectOptions {
|
|
249
|
+
/** The file's name, as the upload or the container that holds it gives it. */
|
|
250
|
+
name?: string;
|
|
251
|
+
/** The MIME type the upload or the container declares. */
|
|
252
|
+
declaredType?: string;
|
|
253
|
+
/** The Date.now() value detection must finish by, as `PluginContext.deadline`. */
|
|
254
|
+
deadline?: number;
|
|
255
|
+
/** Folder for temporary files. Defaults to `os.tmpdir()`. */
|
|
256
|
+
tempDir?: string;
|
|
257
|
+
/** Bytes held in memory before a temporary file takes over. */
|
|
258
|
+
memoryThreshold?: number;
|
|
259
|
+
}
|
|
237
260
|
/** What a contained file plugin learns of the run, with nothing format-specific, so any package of the family can make one. */
|
|
238
261
|
export interface PluginContext {
|
|
239
262
|
/** The depth of the file itself: 0 for the upload, 1 for a file inside it, and so on, whatever the formats. */
|
package/dist/word.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { CfbEntry, CompoundFile } from './cfb';
|
|
2
2
|
import type { RunContext } from './context';
|
|
3
|
+
/** Whether a WordDocument stream holds a template: its FIB has fDot set. */
|
|
4
|
+
export declare function isWordTemplate(cfb: CompoundFile, stream: CfbEntry): Promise<boolean>;
|
|
3
5
|
/**
|
|
4
6
|
* Inspects a Word 6.0 to 2003 document: its FIB, whether it is encrypted, the fields in its text and the template it
|
|
5
|
-
* attaches.
|
|
7
|
+
* attaches. An `optional` stream that holds no FIB is not read.
|
|
6
8
|
*/
|
|
7
|
-
export declare function wordRules(ctx: RunContext, cfb: CompoundFile, stream: CfbEntry, optional?: boolean): Promise<
|
|
8
|
-
template: boolean;
|
|
9
|
-
} | undefined>;
|
|
9
|
+
export declare function wordRules(ctx: RunContext, cfb: CompoundFile, stream: CfbEntry, optional?: boolean): Promise<void>;
|