@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/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
- /** Inspects an OpenDocument package. */
290
- async function inspectOdf(ctx) {
290
+ /**
291
+ * Names an OpenDocument package's format by the media type in its mimetype entry or, without one, its manifest's root
292
+ * entry. Throws when the two name different formats, or the media type is not one this package reads. `layout` hears,
293
+ * with the entry it concerns, of each way the mimetype entry and the manifest leave the layout OpenDocument asks for.
294
+ */
295
+ async function nameOdf(ctx, layout = () => undefined) {
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 layout = (reason) => ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.OdfLayout, mimetype?.name ?? 'META-INF/manifest.xml', { reason });
299
+ const note = (reason) => layout(mimetype?.name ?? 'META-INF/manifest.xml', reason);
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
- layout('mimetype is not the first entry');
311
+ note('mimetype is not the first entry');
307
312
  if (mimetype.method !== 0)
308
- layout('mimetype is compressed');
313
+ note('mimetype is compressed');
309
314
  if (mimetype.localExtra)
310
- layout('mimetype has an extra field');
315
+ note('mimetype has an extra field');
311
316
  if (declared !== declared.trim())
312
- layout('mimetype has white space around the media type');
317
+ note('mimetype has white space around the media type');
313
318
  declared = declared.trim();
314
319
  }
315
320
  else
316
- layout('there is no mimetype entry');
321
+ note('there is no mimetype entry');
317
322
  const manifest = manifestEntry ? await readManifest(ctx, manifestEntry) : undefined;
318
323
  if (!manifest)
319
- layout('there is no manifest');
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 type = declared ?? root ?? '';
325
- const format = formats_1.ODF_FORMATS.get(type);
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)(type) });
332
+ throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The OpenDocument media type is not one this package reads', { mediaType: (0, context_1.clip)(mediaType) });
333
+ return { format, mediaType, mimetype, manifestEntry, manifest };
334
+ }
335
+ /** Inspects an OpenDocument package. */
336
+ async function inspectOdf(ctx) {
337
+ const zip = ctx.zip;
338
+ const { format, mediaType: type, mimetype, manifestEntry, manifest } = await nameOdf(ctx, (location, reason) => ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.OdfLayout, location, { reason }));
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
- * Inspects a compound file: the rules every compound file gets, over its whole tree, and then those of the format
47
- * its root names.
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
- async function inspectCompound(ctx, cfb) {
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
- const family = main[0];
58
- ctx.format = formatOf(family, cfb);
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
- const r = await (0, word_1.wordRules)(ctx, cfb, word, family === 'encrypted');
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 (!s)
82
- continue;
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
- const info = stream('EncryptionInfo');
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 { source } = await (0, offcrypto_1.openPackage)(ctx, cfb, info, pkg);
109
- const zip = await zip_1.ZipArchive.open(source, ctx.settings, ctx.settings.deadline);
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, pkg.name, { bytes: n.bytes });
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<void>;
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
- * Inspects an OOXML package. The package relationships come first, since they name the main part and so the format.
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
- const roots = new Set();
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 packageRels = relsOf.get('');
602
- let strict = false;
603
- let mainType = '';
604
- if (packageRels) {
605
- await readRelationships(ctx, packageRels, r => {
606
- if (exports.REL.main.has(r.type)) {
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
- /** Inspects an OOXML or OpenDocument package, which its markers name. */
9
- async function inspectPackage(ctx) {
10
- const zip = ctx.zip;
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
- await (0, ooxml_1.inspectOoxml)(ctx, contentTypes);
17
- else if (odf)
18
- await (0, odf_1.inspectOdf)(ctx);
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
- 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' });
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 can only read what it can parse, so the content decides, whatever the name says.
44
- accepts: f => isOffice(f.source, f.sniffedType),
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 = { format: 'rtf', mimeType: 'application/rtf' };
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. Returns whether it is a template, or undefined for an `optional` stream that holds no FIB.
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>;