@patrickdk77/office-defuse 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +278 -36
- package/dist/biff.d.ts +62 -0
- package/dist/biff.js +95 -0
- package/dist/cfb-writer.d.ts +25 -0
- package/dist/cfb-writer.js +314 -0
- package/dist/cfb.d.ts +58 -0
- package/dist/cfb.js +456 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +357 -0
- package/dist/contained.d.ts +46 -0
- package/dist/contained.js +152 -0
- package/dist/context.d.ts +140 -17
- package/dist/context.js +135 -37
- package/dist/crc32.d.ts +8 -2
- package/dist/crc32.js +44 -4
- package/dist/engine.d.ts +12 -1
- package/dist/engine.js +336 -42
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +27 -1
- package/dist/fields.d.ts +36 -0
- package/dist/fields.js +139 -0
- package/dist/findings.d.ts +2 -1
- package/dist/findings.js +44 -9
- package/dist/flat.d.ts +33 -0
- package/dist/flat.js +486 -0
- package/dist/formats.d.ts +7 -0
- package/dist/formats.js +5 -1
- package/dist/formulas.d.ts +27 -0
- package/dist/formulas.js +157 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +8 -1
- package/dist/index.mjs +1 -1
- package/dist/io.d.ts +62 -1
- package/dist/io.js +245 -1
- package/dist/links.d.ts +27 -0
- package/dist/links.js +106 -1
- package/dist/msg.d.ts +8 -0
- package/dist/msg.js +251 -0
- package/dist/objects.d.ts +45 -0
- package/dist/objects.js +204 -0
- package/dist/odf.d.ts +66 -2
- package/dist/odf.js +181 -64
- package/dist/offcrypto.d.ts +54 -0
- package/dist/offcrypto.js +647 -0
- package/dist/ole.d.ts +12 -0
- package/dist/ole.js +139 -0
- package/dist/oleobj.d.ts +85 -0
- package/dist/oleobj.js +215 -0
- package/dist/ooxml.d.ts +83 -2
- package/dist/ooxml.js +358 -145
- package/dist/package.d.ts +3 -0
- package/dist/package.js +21 -0
- package/dist/plan.d.ts +104 -0
- package/dist/plan.js +73 -0
- package/dist/plugins.d.ts +8 -0
- package/dist/plugins.js +60 -0
- package/dist/ppt.d.ts +8 -0
- package/dist/ppt.js +281 -0
- package/dist/rewrite-cfb.d.ts +14 -0
- package/dist/rewrite-cfb.js +52 -0
- package/dist/rewrite-rtf.d.ts +9 -0
- package/dist/rewrite-rtf.js +45 -0
- package/dist/rewrite-xml.d.ts +36 -0
- package/dist/rewrite-xml.js +341 -0
- package/dist/rewrite-zip.d.ts +8 -0
- package/dist/rewrite-zip.js +498 -0
- package/dist/rtf.d.ts +8 -0
- package/dist/rtf.js +712 -0
- package/dist/rtfcomp.d.ts +7 -0
- package/dist/rtfcomp.js +80 -0
- package/dist/score.js +1 -1
- package/dist/sniff.d.ts +5 -0
- package/dist/sniff.js +38 -0
- package/dist/types.d.ts +66 -2
- package/dist/types.js +33 -0
- package/dist/vsd.d.ts +13 -0
- package/dist/vsd.js +689 -0
- package/dist/word.d.ts +9 -0
- package/dist/word.js +194 -0
- package/dist/xls.d.ts +27 -0
- package/dist/xls.js +208 -0
- package/dist/xml.d.ts +30 -0
- package/dist/xml.js +45 -23
- package/dist/zip-writer.d.ts +4 -0
- package/dist/zip-writer.js +20 -7
- package/dist/zip.d.ts +9 -2
- package/dist/zip.js +61 -4
- package/package.json +6 -3
package/dist/ole.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.families = families;
|
|
4
|
+
exports.inspectCompound = inspectCompound;
|
|
5
|
+
const errors_1 = require("./errors");
|
|
6
|
+
const msg_1 = require("./msg");
|
|
7
|
+
const objects_1 = require("./objects");
|
|
8
|
+
const offcrypto_1 = require("./offcrypto");
|
|
9
|
+
const package_1 = require("./package");
|
|
10
|
+
const plan_1 = require("./plan");
|
|
11
|
+
const ppt_1 = require("./ppt");
|
|
12
|
+
const types_1 = require("./types");
|
|
13
|
+
const vsd_1 = require("./vsd");
|
|
14
|
+
const word_1 = require("./word");
|
|
15
|
+
const xls_1 = require("./xls");
|
|
16
|
+
const zip_1 = require("./zip");
|
|
17
|
+
const f = (format, mimeType) => ({ format, mimeType });
|
|
18
|
+
const upper = (e) => e.name.toUpperCase();
|
|
19
|
+
const PUBLISHER_CLSIDS = new Set(['00021200-0000-0000-00C0-000000000046', '00021201-0000-0000-00C0-000000000046']);
|
|
20
|
+
/** What the root of a compound file holds, read by the names of its children, ignoring case as MS-CFB does. */
|
|
21
|
+
function families(cfb) {
|
|
22
|
+
const names = new Map(cfb.root.children.map(c => [upper(c), c]));
|
|
23
|
+
const stream = (n) => names.get(n)?.kind === 'stream';
|
|
24
|
+
const out = [];
|
|
25
|
+
if (stream('ENCRYPTIONINFO') && stream('ENCRYPTEDPACKAGE'))
|
|
26
|
+
out.push('encrypted');
|
|
27
|
+
if (stream('WORDDOCUMENT'))
|
|
28
|
+
out.push('word');
|
|
29
|
+
if (stream('WORKBOOK') || stream('BOOK'))
|
|
30
|
+
out.push('excel');
|
|
31
|
+
if (stream('POWERPOINT DOCUMENT'))
|
|
32
|
+
out.push('powerpoint');
|
|
33
|
+
if (stream('VISIODOCUMENT'))
|
|
34
|
+
out.push('visio');
|
|
35
|
+
if (stream('__PROPERTIES_VERSION1.0'))
|
|
36
|
+
out.push('outlook');
|
|
37
|
+
// Publisher 98 and later keep their content in a Quill storage. Publisher 95 has none, and names itself by class id.
|
|
38
|
+
if (names.get('QUILL')?.kind === 'storage' || (PUBLISHER_CLSIDS.has(cfb.root.clsid) && stream('CONTENTS')))
|
|
39
|
+
out.push('publisher');
|
|
40
|
+
// Project keeps its data in a storage named by three spaces and a number, beside a Props stream.
|
|
41
|
+
if ([...names.keys()].some(n => /^PROPS\d*$/.test(n)) && cfb.root.children.some(c => c.kind === 'storage' && /^ {3}\d+$/.test(c.name)))
|
|
42
|
+
out.push('project');
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
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.
|
|
48
|
+
*/
|
|
49
|
+
async function inspectCompound(ctx, cfb) {
|
|
50
|
+
const found = families(cfb);
|
|
51
|
+
// Word 2007 writes a Word document beside an encrypted package, for readers that cannot decrypt it.
|
|
52
|
+
const main = found.filter(x => !(x === 'word' && found.includes('encrypted')));
|
|
53
|
+
if (main.length > 1)
|
|
54
|
+
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
|
+
if (main.length === 0)
|
|
56
|
+
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);
|
|
59
|
+
const plan = new plan_1.CompoundPlan();
|
|
60
|
+
if (family !== 'encrypted')
|
|
61
|
+
ctx.run.output = { kind: 'cfb', cfb, plan };
|
|
62
|
+
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
|
+
};
|
|
67
|
+
// Beside an encrypted package, Word 2007 writes a WordDocument stream for older readers, of zeros in the files at
|
|
68
|
+
// hand. Those readers read it if it holds a document, so it gets the Word rules when it does.
|
|
69
|
+
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
|
+
}
|
|
75
|
+
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
|
+
for (const name of ['Workbook', 'Book']) {
|
|
80
|
+
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;
|
|
86
|
+
}
|
|
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
|
+
// Excel keeps the data of a workbook's ActiveX controls in one Ctls stream.
|
|
92
|
+
const controls = stream('Ctls');
|
|
93
|
+
if (controls && controls.size > 0 && ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, controls.name, { stream: controls.name }).action === 'strip')
|
|
94
|
+
plan.drop.add(controls);
|
|
95
|
+
}
|
|
96
|
+
if (family === 'outlook')
|
|
97
|
+
await (0, msg_1.messageRules)(ctx, cfb, cfb.root, 0, plan);
|
|
98
|
+
const ppt = stream('PowerPoint Document');
|
|
99
|
+
if (family === 'powerpoint' && ppt)
|
|
100
|
+
await (0, ppt_1.powerPointRules)(ctx, cfb, ppt);
|
|
101
|
+
const visio = stream('VisioDocument');
|
|
102
|
+
if (family === 'visio' && visio)
|
|
103
|
+
await (0, vsd_1.visioRules)(ctx, cfb, visio);
|
|
104
|
+
const info = stream('EncryptionInfo');
|
|
105
|
+
const pkg = stream('EncryptedPackage');
|
|
106
|
+
if (family === 'encrypted' && info && pkg) {
|
|
107
|
+
// 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);
|
|
110
|
+
for (const n of zip.notes)
|
|
111
|
+
ctx.report(types_1.OfficeCategory.Container, n.detail, pkg.name, { bytes: n.bytes });
|
|
112
|
+
// The rewrite writes the package decrypted, since office-defuse does not encrypt.
|
|
113
|
+
const zctx = ctx.withZip(zip);
|
|
114
|
+
await (0, package_1.inspectPackage)(zctx);
|
|
115
|
+
if (ctx.format)
|
|
116
|
+
ctx.run.output = { kind: 'zip', zip, plan: zctx.plan, format: ctx.format };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
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/oleobj.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** Thrown by ByteReader when a structure runs past the end of its bytes. Parsers catch it and keep what they read. */
|
|
2
|
+
export declare class Truncated extends Error {
|
|
3
|
+
}
|
|
4
|
+
/** Runs `read`. A structure that runs past its data gives what `partial` returns instead, and any other error goes up. */
|
|
5
|
+
export declare function bounded<T>(read: () => T, partial: () => T): T;
|
|
6
|
+
/** Little-endian reads over bytes in memory, bounded by their end. */
|
|
7
|
+
export declare class ByteReader {
|
|
8
|
+
readonly b: Uint8Array;
|
|
9
|
+
pos: number;
|
|
10
|
+
constructor(b: Uint8Array);
|
|
11
|
+
get left(): number;
|
|
12
|
+
private need;
|
|
13
|
+
u8(): number;
|
|
14
|
+
u16(): number;
|
|
15
|
+
u32(): number;
|
|
16
|
+
bytes(n: number): Uint8Array;
|
|
17
|
+
skip(n: number): void;
|
|
18
|
+
/** An ANSI string ended by a NUL within `max` bytes, read as Latin-1. */
|
|
19
|
+
ansiz(max?: number): string;
|
|
20
|
+
/** A 32-bit byte count, then that many bytes of ANSI text whose last is a NUL. */
|
|
21
|
+
lengthPrefixedAnsi(max?: number): string;
|
|
22
|
+
/** A 32-bit count of UTF-16 code units, then the units, the last of which may be a NUL. */
|
|
23
|
+
lengthPrefixedUnicode(max?: number): string;
|
|
24
|
+
}
|
|
25
|
+
export declare const latin1: (b: Uint8Array) => string;
|
|
26
|
+
export declare const utf16: (b: Uint8Array) => string;
|
|
27
|
+
/** The class an OLE object names in its CompObj stream (MS-OLEDS 2.3.8): the user type and the ProgID. */
|
|
28
|
+
export interface CompObj {
|
|
29
|
+
userType?: string;
|
|
30
|
+
progId?: string;
|
|
31
|
+
}
|
|
32
|
+
export declare function parseCompObj(b: Uint8Array): CompObj;
|
|
33
|
+
/** What a moniker names: a URL moniker's URL, or a file moniker's path. */
|
|
34
|
+
export interface MonikerTarget {
|
|
35
|
+
target: string;
|
|
36
|
+
/** A URL moniker, whose target loads from wherever it names, however it is spelled. */
|
|
37
|
+
url: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The address a moniker names: a URL moniker's URL, a file moniker's path, or the first of a composite's that names
|
|
41
|
+
* one. Other monikers name nothing this reads, and give ''. The reader stops after the moniker it reads.
|
|
42
|
+
*/
|
|
43
|
+
export declare function readMoniker(r: ByteReader, depth?: number): MonikerTarget;
|
|
44
|
+
/** What an OLE stream (MS-OLEDS 2.3.3) says: whether the object is a link, and what it links to. */
|
|
45
|
+
export interface OleStreamInfo extends MonikerTarget {
|
|
46
|
+
linked: boolean;
|
|
47
|
+
}
|
|
48
|
+
export declare function parseOleStream(b: Uint8Array): OleStreamInfo;
|
|
49
|
+
/** An embedded file in Packager's native data, which oleobj reads the same way. */
|
|
50
|
+
export interface PackagedFile {
|
|
51
|
+
fileName: string;
|
|
52
|
+
sourcePath: string;
|
|
53
|
+
tempPath: string;
|
|
54
|
+
/** False when the data names a file it does not hold, a link. */
|
|
55
|
+
embedded: boolean;
|
|
56
|
+
size: number;
|
|
57
|
+
/** Where the file's bytes start in the native data. */
|
|
58
|
+
dataAt: number;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Packager's native data, as oleobj reads it: a 16-bit type, the label and source path ended by NULs, eight bytes,
|
|
62
|
+
* the temporary path ended by a NUL, and the size of the file's data. Without the size it is a link. Undefined when it
|
|
63
|
+
* does not read that far.
|
|
64
|
+
*/
|
|
65
|
+
export declare function parsePackage(b: Uint8Array): PackagedFile | undefined;
|
|
66
|
+
/** An OLE 1.0 object (MS-OLEDS 2.2), as RTF's objdata holds one. */
|
|
67
|
+
export interface Ole1Object {
|
|
68
|
+
/** 1 linked, 2 embedded, 5 a static picture. */
|
|
69
|
+
formatId: number;
|
|
70
|
+
className: string;
|
|
71
|
+
topicName: string;
|
|
72
|
+
itemName: string;
|
|
73
|
+
/** The native data of an embedded object. */
|
|
74
|
+
data?: Uint8Array;
|
|
75
|
+
/** Where the native data starts in the object's bytes, after the size before it. */
|
|
76
|
+
dataAt?: number;
|
|
77
|
+
}
|
|
78
|
+
export declare function parseOle1(b: Uint8Array): Ole1Object | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* Data that holds a 32-bit size at `dataAt - 4` and that many bytes at `dataAt`, with `data` in their place and the
|
|
81
|
+
* size set to match. What came before and after stays. Packager's native data and an OLE 1.0 object have this layout.
|
|
82
|
+
*/
|
|
83
|
+
export declare function replaceSized(bytes: Uint8Array, dataAt: number, oldSize: number, data: Uint8Array): Uint8Array;
|
|
84
|
+
/** A stream that starts with the size of what follows, such as Ole10Native, around `data`. */
|
|
85
|
+
export declare const sizedStream: (data: Uint8Array) => Buffer<ArrayBuffer>;
|
package/dist/oleobj.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sizedStream = exports.utf16 = exports.latin1 = exports.ByteReader = exports.Truncated = void 0;
|
|
4
|
+
exports.bounded = bounded;
|
|
5
|
+
exports.parseCompObj = parseCompObj;
|
|
6
|
+
exports.readMoniker = readMoniker;
|
|
7
|
+
exports.parseOleStream = parseOleStream;
|
|
8
|
+
exports.parsePackage = parsePackage;
|
|
9
|
+
exports.parseOle1 = parseOle1;
|
|
10
|
+
exports.replaceSized = replaceSized;
|
|
11
|
+
const cfb_1 = require("./cfb");
|
|
12
|
+
/** Thrown by ByteReader when a structure runs past the end of its bytes. Parsers catch it and keep what they read. */
|
|
13
|
+
class Truncated extends Error {
|
|
14
|
+
}
|
|
15
|
+
exports.Truncated = Truncated;
|
|
16
|
+
/** Runs `read`. A structure that runs past its data gives what `partial` returns instead, and any other error goes up. */
|
|
17
|
+
function bounded(read, partial) {
|
|
18
|
+
try {
|
|
19
|
+
return read();
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
if (e instanceof Truncated)
|
|
23
|
+
return partial();
|
|
24
|
+
throw e;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Little-endian reads over bytes in memory, bounded by their end. */
|
|
28
|
+
class ByteReader {
|
|
29
|
+
b;
|
|
30
|
+
pos = 0;
|
|
31
|
+
constructor(b) {
|
|
32
|
+
this.b = b;
|
|
33
|
+
}
|
|
34
|
+
get left() {
|
|
35
|
+
return this.b.length - this.pos;
|
|
36
|
+
}
|
|
37
|
+
need(n) {
|
|
38
|
+
if (n < 0 || this.pos + n > this.b.length)
|
|
39
|
+
throw new Truncated('The structure runs past its data');
|
|
40
|
+
const at = this.pos;
|
|
41
|
+
this.pos += n;
|
|
42
|
+
return at;
|
|
43
|
+
}
|
|
44
|
+
u8() {
|
|
45
|
+
return this.b[this.need(1)];
|
|
46
|
+
}
|
|
47
|
+
u16() {
|
|
48
|
+
const at = this.need(2);
|
|
49
|
+
return this.b[at] | (this.b[at + 1] << 8);
|
|
50
|
+
}
|
|
51
|
+
u32() {
|
|
52
|
+
const at = this.need(4);
|
|
53
|
+
return (this.b[at] | (this.b[at + 1] << 8) | (this.b[at + 2] << 16) | (this.b[at + 3] << 24)) >>> 0;
|
|
54
|
+
}
|
|
55
|
+
bytes(n) {
|
|
56
|
+
const at = this.need(n);
|
|
57
|
+
return this.b.subarray(at, at + n);
|
|
58
|
+
}
|
|
59
|
+
skip(n) {
|
|
60
|
+
this.need(n);
|
|
61
|
+
}
|
|
62
|
+
/** An ANSI string ended by a NUL within `max` bytes, read as Latin-1. */
|
|
63
|
+
ansiz(max = 1024) {
|
|
64
|
+
const end = this.b.indexOf(0, this.pos);
|
|
65
|
+
if (end < 0 || end - this.pos >= max)
|
|
66
|
+
throw new Truncated('No NUL ends the string');
|
|
67
|
+
const s = (0, exports.latin1)(this.b.subarray(this.pos, end));
|
|
68
|
+
this.pos = end + 1;
|
|
69
|
+
return s;
|
|
70
|
+
}
|
|
71
|
+
/** A 32-bit byte count, then that many bytes of ANSI text whose last is a NUL. */
|
|
72
|
+
lengthPrefixedAnsi(max = 65536) {
|
|
73
|
+
const n = this.u32();
|
|
74
|
+
if (n > max)
|
|
75
|
+
throw new Truncated('A string length past any real string');
|
|
76
|
+
if (n === 0)
|
|
77
|
+
return '';
|
|
78
|
+
const s = this.bytes(n);
|
|
79
|
+
return (0, exports.latin1)(s[n - 1] === 0 ? s.subarray(0, n - 1) : s);
|
|
80
|
+
}
|
|
81
|
+
/** A 32-bit count of UTF-16 code units, then the units, the last of which may be a NUL. */
|
|
82
|
+
lengthPrefixedUnicode(max = 32768) {
|
|
83
|
+
const n = this.u32();
|
|
84
|
+
if (n > max)
|
|
85
|
+
throw new Truncated('A string length past any real string');
|
|
86
|
+
return (0, exports.utf16)(this.bytes(n * 2)).replace(/\0+$/, '');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
exports.ByteReader = ByteReader;
|
|
90
|
+
const latin1 = (b) => Buffer.from(b.buffer, b.byteOffset, b.byteLength).toString('latin1');
|
|
91
|
+
exports.latin1 = latin1;
|
|
92
|
+
const utf16 = (b) => Buffer.from(b.buffer, b.byteOffset, b.byteLength - (b.byteLength % 2)).toString('utf16le');
|
|
93
|
+
exports.utf16 = utf16;
|
|
94
|
+
function parseCompObj(b) {
|
|
95
|
+
const r = new ByteReader(b);
|
|
96
|
+
const out = {};
|
|
97
|
+
return bounded(() => {
|
|
98
|
+
r.skip(28);
|
|
99
|
+
out.userType = r.lengthPrefixedAnsi();
|
|
100
|
+
const marker = r.u32();
|
|
101
|
+
if (marker === 0xffffffff || marker === 0xfffffffe)
|
|
102
|
+
r.skip(4);
|
|
103
|
+
else if (marker !== 0)
|
|
104
|
+
r.skip(marker);
|
|
105
|
+
// MS-OLEDS calls it Reserved1. Windows writes the ProgID there.
|
|
106
|
+
out.progId = r.lengthPrefixedAnsi();
|
|
107
|
+
return out;
|
|
108
|
+
}, () => out);
|
|
109
|
+
}
|
|
110
|
+
/** URL moniker, file moniker and composite moniker class ids (MS-OSHARED 2.3.7). */
|
|
111
|
+
const URL_MONIKER = '79EAC9E0-BAF9-11CE-8C82-00AA004BA90B';
|
|
112
|
+
const FILE_MONIKER = '00000303-0000-0000-C000-000000000046';
|
|
113
|
+
const COMPOSITE_MONIKER = '00000309-0000-0000-C000-000000000046';
|
|
114
|
+
/**
|
|
115
|
+
* The address a moniker names: a URL moniker's URL, a file moniker's path, or the first of a composite's that names
|
|
116
|
+
* one. Other monikers name nothing this reads, and give ''. The reader stops after the moniker it reads.
|
|
117
|
+
*/
|
|
118
|
+
function readMoniker(r, depth = 0) {
|
|
119
|
+
const clsid = (0, cfb_1.guid)(r.bytes(16));
|
|
120
|
+
if (clsid === URL_MONIKER) {
|
|
121
|
+
const n = r.u32();
|
|
122
|
+
const raw = r.bytes(Math.min(n, r.left));
|
|
123
|
+
return { target: (0, exports.utf16)(raw).split('\0')[0], url: true };
|
|
124
|
+
}
|
|
125
|
+
if (clsid === FILE_MONIKER) {
|
|
126
|
+
const anti = r.u16();
|
|
127
|
+
const ansi = r.lengthPrefixedAnsi();
|
|
128
|
+
r.skip(2 + 2 + 16);
|
|
129
|
+
const unicodeSize = r.u32();
|
|
130
|
+
let path = ansi;
|
|
131
|
+
if (unicodeSize > 0) {
|
|
132
|
+
const bytes = r.u32();
|
|
133
|
+
r.skip(2);
|
|
134
|
+
path = (0, exports.utf16)(r.bytes(bytes));
|
|
135
|
+
}
|
|
136
|
+
return { target: '..\\'.repeat(Math.min(anti, 64)) + path, url: false };
|
|
137
|
+
}
|
|
138
|
+
if (clsid === COMPOSITE_MONIKER && depth < 8) {
|
|
139
|
+
const count = r.u32();
|
|
140
|
+
for (let i = 0; i < Math.min(count, 64); i++) {
|
|
141
|
+
const found = readMoniker(r, depth + 1);
|
|
142
|
+
if (found.target)
|
|
143
|
+
return found;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { target: '', url: false };
|
|
147
|
+
}
|
|
148
|
+
function parseOleStream(b) {
|
|
149
|
+
const r = new ByteReader(b);
|
|
150
|
+
const out = { linked: false, target: '', url: false };
|
|
151
|
+
return bounded(() => {
|
|
152
|
+
r.skip(4);
|
|
153
|
+
out.linked = (r.u32() & 1) === 1;
|
|
154
|
+
if (!out.linked)
|
|
155
|
+
return out;
|
|
156
|
+
r.skip(8);
|
|
157
|
+
r.skip(r.u32());
|
|
158
|
+
// The relative moniker comes first, then the absolute one, which Office prefers when both resolve.
|
|
159
|
+
const monikers = [];
|
|
160
|
+
for (let i = 0; i < 2; i++) {
|
|
161
|
+
const size = r.u32();
|
|
162
|
+
if (size > 0)
|
|
163
|
+
monikers.push(readMoniker(new ByteReader(r.bytes(size))));
|
|
164
|
+
}
|
|
165
|
+
return Object.assign(out, monikers.reverse().find(m => m.target));
|
|
166
|
+
}, () => out);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Packager's native data, as oleobj reads it: a 16-bit type, the label and source path ended by NULs, eight bytes,
|
|
170
|
+
* the temporary path ended by a NUL, and the size of the file's data. Without the size it is a link. Undefined when it
|
|
171
|
+
* does not read that far.
|
|
172
|
+
*/
|
|
173
|
+
function parsePackage(b) {
|
|
174
|
+
const r = new ByteReader(b);
|
|
175
|
+
return bounded(() => {
|
|
176
|
+
r.skip(2);
|
|
177
|
+
const fileName = r.ansiz();
|
|
178
|
+
const sourcePath = r.ansiz();
|
|
179
|
+
r.skip(8);
|
|
180
|
+
const tempPath = r.ansiz();
|
|
181
|
+
return bounded(() => ({ fileName, sourcePath, tempPath, embedded: true, size: r.u32(), dataAt: r.pos }), () => ({ fileName, sourcePath, tempPath, embedded: false, size: 0, dataAt: r.pos }));
|
|
182
|
+
}, () => undefined);
|
|
183
|
+
}
|
|
184
|
+
function parseOle1(b) {
|
|
185
|
+
const r = new ByteReader(b);
|
|
186
|
+
return bounded(() => {
|
|
187
|
+
r.skip(4);
|
|
188
|
+
const formatId = r.u32();
|
|
189
|
+
if (formatId !== 1 && formatId !== 2)
|
|
190
|
+
return { formatId, className: '', topicName: '', itemName: '' };
|
|
191
|
+
const className = r.lengthPrefixedAnsi();
|
|
192
|
+
const topicName = r.lengthPrefixedAnsi();
|
|
193
|
+
const itemName = r.lengthPrefixedAnsi();
|
|
194
|
+
if (formatId === 1)
|
|
195
|
+
return { formatId, className, topicName, itemName };
|
|
196
|
+
const size = r.u32();
|
|
197
|
+
const dataAt = r.pos;
|
|
198
|
+
return { formatId, className, topicName, itemName, data: r.bytes(Math.min(size, r.left)), dataAt };
|
|
199
|
+
}, () => undefined);
|
|
200
|
+
}
|
|
201
|
+
const le32 = (n) => {
|
|
202
|
+
const b = Buffer.alloc(4);
|
|
203
|
+
b.writeUInt32LE(n >>> 0);
|
|
204
|
+
return b;
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
207
|
+
* Data that holds a 32-bit size at `dataAt - 4` and that many bytes at `dataAt`, with `data` in their place and the
|
|
208
|
+
* size set to match. What came before and after stays. Packager's native data and an OLE 1.0 object have this layout.
|
|
209
|
+
*/
|
|
210
|
+
function replaceSized(bytes, dataAt, oldSize, data) {
|
|
211
|
+
return Buffer.concat([bytes.subarray(0, dataAt - 4), le32(data.length), data, bytes.subarray(Math.min(bytes.length, dataAt + oldSize))]);
|
|
212
|
+
}
|
|
213
|
+
/** A stream that starts with the size of what follows, such as Ole10Native, around `data`. */
|
|
214
|
+
const sizedStream = (data) => Buffer.concat([le32(data.length), data]);
|
|
215
|
+
exports.sizedStream = sizedStream;
|
package/dist/ooxml.d.ts
CHANGED
|
@@ -1,8 +1,89 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ZipContext } from './context';
|
|
2
|
+
import { type XmlElement } from './xml';
|
|
2
3
|
import { type ZipEntry } from './zip';
|
|
4
|
+
export declare const CONTENT_TYPES_NS = "http://schemas.openxmlformats.org/package/2006/content-types";
|
|
5
|
+
export declare const RELS_NS = "http://schemas.openxmlformats.org/package/2006/relationships";
|
|
6
|
+
/** The relationship types that name an XPS package's FixedDocumentSequence, lowercased. */
|
|
7
|
+
export declare const XPS_MAIN: {
|
|
8
|
+
xps: string;
|
|
9
|
+
oxps: string;
|
|
10
|
+
};
|
|
11
|
+
/** The XPS and OpenXPS markup namespaces, lowercased. */
|
|
12
|
+
export declare const XPS_NS: Set<string>;
|
|
13
|
+
/** Relationship types, lowercased, as OPC compares them without regard to case. */
|
|
14
|
+
export declare const REL: {
|
|
15
|
+
main: Set<string>;
|
|
16
|
+
hyperlink: Set<string>;
|
|
17
|
+
template: Set<string>;
|
|
18
|
+
image: Set<string>;
|
|
19
|
+
frame: Set<string>;
|
|
20
|
+
subDocument: Set<string>;
|
|
21
|
+
workbook: Set<string>;
|
|
22
|
+
};
|
|
23
|
+
/** Content types, lowercased. */
|
|
24
|
+
export declare const TYPE: {
|
|
25
|
+
vba: Set<string>;
|
|
26
|
+
macrosheet: Set<string>;
|
|
27
|
+
ole: Set<string>;
|
|
28
|
+
control: Set<string>;
|
|
29
|
+
controlBinary: Set<string>;
|
|
30
|
+
vml: string;
|
|
31
|
+
xlsbExternalLink: string;
|
|
32
|
+
};
|
|
33
|
+
/** Whether the rules read a part of this content type as XML. */
|
|
34
|
+
export declare const isXmlType: (ct: string | undefined) => boolean;
|
|
35
|
+
export declare const WORD_NS: Set<string>;
|
|
36
|
+
export declare const SHEET_NS: Set<string>;
|
|
37
|
+
export declare const R_NS: Set<string>;
|
|
38
|
+
export interface Relationship {
|
|
39
|
+
id: string;
|
|
40
|
+
type: string;
|
|
41
|
+
target: string;
|
|
42
|
+
external: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface Part {
|
|
45
|
+
entry: ZipEntry;
|
|
46
|
+
contentType?: string;
|
|
47
|
+
/** The AS bits of the relationship types that reach the part. */
|
|
48
|
+
reachedAs: number;
|
|
49
|
+
/** A relationship of an ActiveX control part reaches it, so it is that control's binary. */
|
|
50
|
+
claimed: boolean;
|
|
51
|
+
/** The parts its own relationships reach. */
|
|
52
|
+
targets: Set<Part>;
|
|
53
|
+
ole: boolean;
|
|
54
|
+
}
|
|
55
|
+
/** What the rewrite needs of an OOXML package: its parts, which part each relationships part belongs to, and the roots. */
|
|
56
|
+
export interface OoxmlGraph {
|
|
57
|
+
contentTypes: ZipEntry;
|
|
58
|
+
parts: Map<string, Part>;
|
|
59
|
+
relsOf: Map<string, ZipEntry>;
|
|
60
|
+
/** The parts the package relationships reach. */
|
|
61
|
+
roots: Set<Part>;
|
|
62
|
+
/** The package uses the relationship types of Strict OOXML. */
|
|
63
|
+
strict: boolean;
|
|
64
|
+
/** The package is XPS or OpenXPS, whose parts name the parts they use in attributes. */
|
|
65
|
+
xps: boolean;
|
|
66
|
+
}
|
|
67
|
+
/** The key of the part a relationships part belongs to, '' for the package, or undefined for any other part. */
|
|
68
|
+
export declare const relsSource: (relsName: string) => string | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* Resolves an internal target against the folder of the part it belongs to, as RFC 3986 resolves a relative
|
|
71
|
+
* reference: a ".." at the root of the package stays at the root. LibreOffice writes "../customXml/item1.xml" in the
|
|
72
|
+
* package relationships of workbooks it saves.
|
|
73
|
+
*/
|
|
74
|
+
export declare function resolveTarget(relsName: string, target: string): string;
|
|
75
|
+
/** A reference in an XPS part to another part: an absolute part name, or a path relative to the part's folder. */
|
|
76
|
+
export declare function resolvePartRef(part: string, ref: string): string;
|
|
77
|
+
/**
|
|
78
|
+
* The resources an XPS element names in its attributes, as the attribute and the URI: a document, page or resource
|
|
79
|
+
* dictionary's Source, a font's FontUri, an image brush's image and profile, and a ContextColor's profile.
|
|
80
|
+
*/
|
|
81
|
+
export declare function xpsResources(el: XmlElement): Array<[string, string]>;
|
|
82
|
+
/** Reads a relationships part and hands each relationship to `each` as it is read. Ids given twice are reported. */
|
|
83
|
+
export declare function readRelationships(ctx: ZipContext, entry: ZipEntry, each: (r: Relationship) => void): Promise<void>;
|
|
3
84
|
/**
|
|
4
85
|
* Inspects an OOXML package. The package relationships come first, since they name the main part and so the format.
|
|
5
86
|
* Then each part is read in archive order, followed by its relationships, which are reported as they are read and
|
|
6
87
|
* leave behind only which parts they reach and how.
|
|
7
88
|
*/
|
|
8
|
-
export declare function inspectOoxml(ctx:
|
|
89
|
+
export declare function inspectOoxml(ctx: ZipContext, contentTypes: ZipEntry): Promise<void>;
|