@patrickdk77/office-defuse 0.1.1 → 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 +197 -42
- package/dist/cfb-writer.d.ts +25 -0
- package/dist/cfb-writer.js +314 -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 +107 -12
- package/dist/context.js +101 -24
- package/dist/engine.d.ts +12 -1
- package/dist/engine.js +276 -31
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +27 -1
- package/dist/fields.d.ts +20 -13
- package/dist/fields.js +65 -23
- package/dist/findings.d.ts +2 -1
- package/dist/findings.js +17 -1
- 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 +3 -2
- package/dist/msg.js +172 -18
- package/dist/objects.d.ts +8 -5
- package/dist/objects.js +90 -20
- package/dist/odf.d.ts +65 -1
- package/dist/odf.js +181 -64
- package/dist/offcrypto.d.ts +24 -1
- package/dist/offcrypto.js +308 -69
- package/dist/ole.js +17 -5
- package/dist/oleobj.d.ts +11 -0
- package/dist/oleobj.js +20 -3
- package/dist/ooxml.d.ts +81 -0
- package/dist/ooxml.js +342 -81
- 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.js +45 -5
- 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.js +214 -41
- 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 +43 -6
- package/dist/types.js +14 -0
- package/dist/vsd.d.ts +13 -0
- package/dist/vsd.js +689 -0
- package/dist/word.js +51 -17
- package/dist/xls.js +38 -4
- 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 +4 -1
package/dist/word.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.wordRules = wordRules;
|
|
|
4
4
|
const errors_1 = require("./errors");
|
|
5
5
|
const fields_1 = require("./fields");
|
|
6
6
|
const io_1 = require("./io");
|
|
7
|
+
const offcrypto_1 = require("./offcrypto");
|
|
7
8
|
const oleobj_1 = require("./oleobj");
|
|
8
9
|
const types_1 = require("./types");
|
|
9
10
|
/** wIdent of Word 97 and later, and of Word 6.0 and 95 (MS-DOC 2.5.2). */
|
|
@@ -14,17 +15,24 @@ const FC_STTBF_ASSOC = 32;
|
|
|
14
15
|
const FC_CLX = 33;
|
|
15
16
|
/** In a Word 6.0 or 95 FIB, the length of the macro table that holds WordBasic macros. Word writes 2 for an empty one. */
|
|
16
17
|
const WORD6_MACROS = 0x11c;
|
|
18
|
+
/** In a Word 6.0 or 95 FIB, fcClx and lcbClx: where the piece table of a file saved fast lies in the WordDocument stream. */
|
|
19
|
+
const WORD6_CLX = 0x160;
|
|
17
20
|
/** Text is read in pieces of at most this many characters. */
|
|
18
21
|
const PIECE = 65536;
|
|
19
22
|
const u16 = (b, at) => b[at] | (b[at + 1] << 8);
|
|
23
|
+
/** A stream read through its decryption. */
|
|
24
|
+
const decrypted = (src, decrypt) => ({
|
|
25
|
+
size: () => src.size(),
|
|
26
|
+
read: async (offset, length) => decrypt(offset, await src.read(offset, length)),
|
|
27
|
+
});
|
|
20
28
|
const u32 = (b, at) => (b[at] | (b[at + 1] << 8) | (b[at + 2] << 16) | (b[at + 3] << 24)) >>> 0;
|
|
21
29
|
/**
|
|
22
30
|
* Inspects a Word 6.0 to 2003 document: its FIB, whether it is encrypted, the fields in its text and the template it
|
|
23
31
|
* attaches. Returns whether it is a template, or undefined for an `optional` stream that holds no FIB.
|
|
24
32
|
*/
|
|
25
33
|
async function wordRules(ctx, cfb, stream, optional = false) {
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
let source = cfb.source(stream);
|
|
35
|
+
let fib = await source.read(0, 1024);
|
|
28
36
|
const ident = fib.length >= 32 ? u16(fib, 0) : 0;
|
|
29
37
|
if (ident !== WORD8 && ident !== WORD6) {
|
|
30
38
|
if (optional)
|
|
@@ -32,9 +40,18 @@ async function wordRules(ctx, cfb, stream, optional = false) {
|
|
|
32
40
|
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The WordDocument stream does not start with a Word FIB', { reason: 'a WordDocument stream without a FIB' });
|
|
33
41
|
}
|
|
34
42
|
const flags = u16(fib, 10);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
const tableEntry = cfb.child(cfb.root, flags & 0x0200 ? '1Table' : '0Table');
|
|
44
|
+
let tableSource = tableEntry?.kind === 'stream' ? cfb.source(tableEntry) : undefined;
|
|
45
|
+
// fEncrypted, set for RC4, RC4 CryptoAPI and XOR obfuscation alike. Word 97 to 2003 documents are read through the
|
|
46
|
+
// decryption. A Word 6.0 or 95 document's encryption is another, which this reader does not undo.
|
|
47
|
+
if (flags & 0x0100) {
|
|
48
|
+
if (ident === WORD6 || !tableSource)
|
|
49
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.PasswordRequired, 'The document is encrypted', { stream: stream.name });
|
|
50
|
+
const key = await (0, offcrypto_1.unlockDocument)(ctx, fib, tableSource, stream.name);
|
|
51
|
+
source = decrypted(source, key.word);
|
|
52
|
+
tableSource = decrypted(tableSource, key.table);
|
|
53
|
+
fib = await source.read(0, 1024);
|
|
54
|
+
}
|
|
38
55
|
const location = stream.name;
|
|
39
56
|
const fields = new fields_1.FieldStack(ctx, location);
|
|
40
57
|
// Word marks a field's start, separator and end with 0x13, 0x14 and 0x15 in the text. Other control characters in
|
|
@@ -47,28 +64,45 @@ async function wordRules(ctx, cfb, stream, optional = false) {
|
|
|
47
64
|
fields.separate();
|
|
48
65
|
else if (ch === '\x15')
|
|
49
66
|
fields.end();
|
|
50
|
-
else if (
|
|
51
|
-
fields.
|
|
67
|
+
else if (ch >= ' ' || ch === '\t') {
|
|
68
|
+
if (fields.inCode)
|
|
69
|
+
fields.text(ch);
|
|
70
|
+
else
|
|
71
|
+
fields.result(ch);
|
|
72
|
+
}
|
|
52
73
|
}
|
|
53
74
|
};
|
|
54
75
|
const reader = new io_1.Reader(source, stream.size, 65536, 8);
|
|
55
76
|
if (ident === WORD6) {
|
|
56
|
-
// Word 6.0 and 95 keep their text as 8-bit characters
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
77
|
+
// Word 6.0 and 95 keep their text as 8-bit characters. A file saved fast (fComplex) adds pieces to the end, and its
|
|
78
|
+
// piece table, in the WordDocument stream itself at fcClx (FIB offset 0x160), gives their order. Any other file,
|
|
79
|
+
// or one whose piece table does not read, is read from fcMin to fcMac, every byte between, as msodde does.
|
|
80
|
+
const clxAt = u32(fib, WORD6_CLX);
|
|
81
|
+
const clxSize = u32(fib, WORD6_CLX + 4);
|
|
82
|
+
const list = flags & 0x0004 && clxSize > 0 && clxAt + clxSize <= stream.size ? pieces(await reader.read(clxAt, clxSize)).filter(p => !p.compressed && p.fc + p.length <= stream.size) : [];
|
|
83
|
+
if (list.length) {
|
|
84
|
+
for (const p of list) {
|
|
85
|
+
for (let cp = 0; cp < p.length; cp += PIECE) {
|
|
86
|
+
(0, errors_1.checkTime)(ctx.settings.deadline);
|
|
87
|
+
text(latin(await reader.read(p.fc + cp, Math.min(PIECE, p.length - cp))));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const from = u32(fib, 0x18);
|
|
93
|
+
const to = Math.min(u32(fib, 0x1c), stream.size);
|
|
94
|
+
for (let at = from; at < to; at += PIECE) {
|
|
95
|
+
(0, errors_1.checkTime)(ctx.settings.deadline);
|
|
96
|
+
text(latin(await reader.read(at, Math.min(PIECE, to - at))));
|
|
97
|
+
}
|
|
63
98
|
}
|
|
64
99
|
if (fib.length >= WORD6_MACROS + 4 && u32(fib, WORD6_MACROS) > 2)
|
|
65
100
|
ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.WordBasic, location, { stream: location, bytes: u32(fib, WORD6_MACROS) });
|
|
66
101
|
}
|
|
67
102
|
else {
|
|
68
103
|
const pairs = fcLcb(fib);
|
|
69
|
-
const table =
|
|
70
|
-
if (table?.kind === 'stream') {
|
|
71
|
-
const tableSource = cfb.source(table);
|
|
104
|
+
const table = tableEntry;
|
|
105
|
+
if (table?.kind === 'stream' && tableSource) {
|
|
72
106
|
const clx = pairs[FC_CLX];
|
|
73
107
|
if (clx && clx.lcb > 0 && clx.fc + clx.lcb <= table.size) {
|
|
74
108
|
for (const p of pieces(await tableSource.read(clx.fc, clx.lcb))) {
|
package/dist/xls.js
CHANGED
|
@@ -6,10 +6,21 @@ exports.hyperlink = hyperlink;
|
|
|
6
6
|
const biff_1 = require("./biff");
|
|
7
7
|
const context_1 = require("./context");
|
|
8
8
|
const errors_1 = require("./errors");
|
|
9
|
+
const formulas_1 = require("./formulas");
|
|
9
10
|
const links_1 = require("./links");
|
|
10
11
|
const oleobj_1 = require("./oleobj");
|
|
11
12
|
const types_1 = require("./types");
|
|
12
13
|
const STATES = ['visible', 'hidden', 'very hidden', 'hidden'];
|
|
14
|
+
/** Record types of cell formulas (MS-XLS 2.4.127, 2.4.260 and 2.4.4): FORMULA, SHRFMLA and ARRAY. */
|
|
15
|
+
const FORMULA = 0x0006;
|
|
16
|
+
const SHRFMLA = 0x04bc;
|
|
17
|
+
const ARRAY = 0x0221;
|
|
18
|
+
/** Where the parsed formula starts in each: its cce, then its rgce. */
|
|
19
|
+
const FORMULA_AT = new Map([
|
|
20
|
+
[FORMULA, 20],
|
|
21
|
+
[SHRFMLA, 8],
|
|
22
|
+
[ARRAY, 12],
|
|
23
|
+
]);
|
|
13
24
|
/**
|
|
14
25
|
* Inspects an Excel 5.0 to 2003 workbook stream. Excel reads the globals substream from the start of the stream,
|
|
15
26
|
* then each sheet from where its BoundSheet record says the sheet starts. The globals give the macro sheets, the
|
|
@@ -26,6 +37,9 @@ async function excelRules(ctx, cfb, stream, unlock) {
|
|
|
26
37
|
let decrypt = r => r.body;
|
|
27
38
|
const sheets = [];
|
|
28
39
|
const books = [];
|
|
40
|
+
const names = [];
|
|
41
|
+
/** The SupBook each XTI of the ExternSheet record names. */
|
|
42
|
+
const xti = [];
|
|
29
43
|
const flags = { template: false, addIn: false };
|
|
30
44
|
for (let r = await records.next(); r && r.type !== biff_1.R.eof; r = await records.next()) {
|
|
31
45
|
if (r.type === biff_1.R.filePass) {
|
|
@@ -41,15 +55,23 @@ async function excelRules(ctx, cfb, stream, unlock) {
|
|
|
41
55
|
flags.addIn = true;
|
|
42
56
|
else if (r.type === biff_1.R.lbl) {
|
|
43
57
|
const name = (0, biff_1.parseLbl)(body, biff8);
|
|
58
|
+
names.push(name.name);
|
|
44
59
|
const auto = (0, biff_1.autoName)(name.name);
|
|
45
60
|
if (auto)
|
|
46
61
|
ctx.report(types_1.OfficeCategory.Macro, auto.onOpen ? types_1.OfficeDetail.EventOnOpen : types_1.OfficeDetail.Event, location, { event: auto.event, name: (0, context_1.clip)(name.name) });
|
|
47
62
|
}
|
|
48
63
|
else if (r.type === biff_1.R.supBook && body.length >= 4) {
|
|
49
64
|
const cch = body[2] | (body[3] << 8);
|
|
50
|
-
books.push({ ctab: body[0] | (body[1] << 8), cch, path: cch > 0 && cch <= 0xff ? (0, biff_1.xlString)(body, 4, cch).text : '', ole: false, dde: false });
|
|
65
|
+
books.push({ ctab: body[0] | (body[1] << 8), cch, path: cch > 0 && cch <= 0xff ? (0, biff_1.xlString)(body, 4, cch).text : '', ole: false, dde: false, names: [] });
|
|
66
|
+
}
|
|
67
|
+
else if (r.type === biff_1.R.externSheet && biff8 && body.length >= 2) {
|
|
68
|
+
const n = body[0] | (body[1] << 8);
|
|
69
|
+
for (let k = 0; k < n && 2 + k * 6 + 2 <= body.length; k++)
|
|
70
|
+
xti.push(body[2 + k * 6] | (body[3 + k * 6] << 8));
|
|
51
71
|
}
|
|
52
72
|
else if (r.type === biff_1.R.externName && body.length >= 2 && books.length) {
|
|
73
|
+
if (biff8 && body.length >= 7)
|
|
74
|
+
books[books.length - 1].names.push((0, biff_1.xlString)(body, 7, body[6]).text);
|
|
53
75
|
// fOle and fOleLink mark an OLE object's name. Excel writes StdDocumentName with fOle beside the items of a DDE
|
|
54
76
|
// link, so a link with any DDE item is a DDE link.
|
|
55
77
|
if ((body[0] | (body[1] << 8)) & 0x18)
|
|
@@ -60,8 +82,9 @@ async function excelRules(ctx, cfb, stream, unlock) {
|
|
|
60
82
|
}
|
|
61
83
|
for (const b of books)
|
|
62
84
|
linkRules(ctx, b, location);
|
|
85
|
+
const nameOf = (ref) => (ref.kind === 'name' ? names[ref.index - 1] : books[xti[ref.ixti] ?? -1]?.names[ref.index - 1]);
|
|
63
86
|
for (const s of sheets)
|
|
64
|
-
await sheetRules(ctx, cfb, stream, s, decrypt, location);
|
|
87
|
+
await sheetRules(ctx, cfb, stream, s, decrypt, location, biff8 ? nameOf : undefined);
|
|
65
88
|
return flags;
|
|
66
89
|
}
|
|
67
90
|
/** A SupBook record, read as MS-XLS 2.4.271 and oletools tell its kinds apart. */
|
|
@@ -115,14 +138,15 @@ function virtualPath(p) {
|
|
|
115
138
|
return out;
|
|
116
139
|
}
|
|
117
140
|
/** One sheet's substream, with any chart substreams inside it: Obj records that run macros, and hyperlinks. */
|
|
118
|
-
async function sheetRules(ctx, cfb, stream, s, decrypt, location) {
|
|
141
|
+
async function sheetRules(ctx, cfb, stream, s, decrypt, location, nameOf) {
|
|
119
142
|
const records = new biff_1.RecordReader(cfb, stream, s.position, ctx.settings.deadline);
|
|
120
143
|
const bof = await records.next();
|
|
121
144
|
if (bof?.type !== biff_1.R.bof || bof.body.length < 4)
|
|
122
145
|
return;
|
|
123
146
|
const bofType = bof.body[2] | (bof.body[3] << 8);
|
|
124
147
|
// The sheet's own BOF says what it is, and so does its BoundSheet record. Either one makes a macro sheet.
|
|
125
|
-
|
|
148
|
+
const macroSheet = s.type === 1 || bofType === 0x0040;
|
|
149
|
+
if (macroSheet)
|
|
126
150
|
ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.XlmMacrosheet, location, { sheet: (0, context_1.clip)(s.name), state: STATES[s.state] });
|
|
127
151
|
let depth = 1;
|
|
128
152
|
for (let r = await records.next(); r && depth > 0; r = await records.next()) {
|
|
@@ -139,6 +163,16 @@ async function sheetRules(ctx, cfb, stream, s, decrypt, location) {
|
|
|
139
163
|
if (target.trim() && !(0, links_1.isFragment)(target))
|
|
140
164
|
ctx.link(target, location, { sheet: (0, context_1.clip)(s.name), source: 'HLink record' });
|
|
141
165
|
}
|
|
166
|
+
else if (nameOf && !macroSheet && FORMULA_AT.has(r.type)) {
|
|
167
|
+
// A macro sheet's formulas are its macros, which its own finding covers.
|
|
168
|
+
const b = decrypt(r);
|
|
169
|
+
const at = FORMULA_AT.get(r.type) ?? 0;
|
|
170
|
+
if (b.length < at + 2)
|
|
171
|
+
continue;
|
|
172
|
+
const cce = b[at] | (b[at + 1] << 8);
|
|
173
|
+
for (const [fn, kind] of (0, formulas_1.biffCalls)(b.subarray(at + 2, at + 2 + cce), nameOf))
|
|
174
|
+
ctx.report(kind === 'fetch' ? types_1.OfficeCategory.External : types_1.OfficeCategory.Macro, types_1.OfficeDetail.Formula, location, { sheet: (0, context_1.clip)(s.name), function: (0, context_1.clip)(fn) });
|
|
175
|
+
}
|
|
142
176
|
}
|
|
143
177
|
}
|
|
144
178
|
/** Whether an Obj record (MS-XLS 2.4.181) holds an FtMacro subrecord, the macro a click runs. */
|
package/dist/xml.d.ts
CHANGED
|
@@ -32,6 +32,17 @@ export interface XmlElement {
|
|
|
32
32
|
*/
|
|
33
33
|
start: number;
|
|
34
34
|
end: number;
|
|
35
|
+
/** The element's position among the elements of the part, from 0, counting those in entity text. */
|
|
36
|
+
index: number;
|
|
37
|
+
/** The namespace declarations of the start tag, as prefix ('' for the default namespace) and URI, defaults included. */
|
|
38
|
+
namespaces: Array<[string, string]>;
|
|
39
|
+
/** Written as an empty-element tag. */
|
|
40
|
+
empty: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* The start tag does not say on its own what the attributes are: a value refers to an entity the internal subset
|
|
43
|
+
* declares, or an attribute-list declaration added a default or normalized a value.
|
|
44
|
+
*/
|
|
45
|
+
rewritten: boolean;
|
|
35
46
|
}
|
|
36
47
|
export interface XmlHandler {
|
|
37
48
|
start?(el: XmlElement): void;
|
|
@@ -87,6 +98,12 @@ export interface XmlExternalReference {
|
|
|
87
98
|
export interface XmlNotes {
|
|
88
99
|
doctype?(d: XmlDoctype): void;
|
|
89
100
|
externalEntity?(r: XmlExternalReference): void;
|
|
101
|
+
/**
|
|
102
|
+
* A reference to an internal entity in content, outside any other entity's text, at these offsets. The events of
|
|
103
|
+
* its text come between this call and `referenceEnd`.
|
|
104
|
+
*/
|
|
105
|
+
referenceStart?(start: number, end: number): void;
|
|
106
|
+
referenceEnd?(): void;
|
|
90
107
|
}
|
|
91
108
|
export interface XmlOptions {
|
|
92
109
|
/** Reads the dialect Office writes in VML drawing parts. See XmlTokenizer. */
|
|
@@ -99,6 +116,15 @@ export interface XmlOptions {
|
|
|
99
116
|
export declare function attr(el: XmlElement, ns: string, local: string): string | undefined;
|
|
100
117
|
/** An XML name without a colon. */
|
|
101
118
|
export declare const isNcName: (s: string) => boolean;
|
|
119
|
+
export type XmlEncoding = 'utf-8' | 'utf-16le' | 'utf-16be';
|
|
120
|
+
/**
|
|
121
|
+
* The encoding of an XML part from its first four bytes, as XML 1.0 Appendix F tells it, and the length of its byte
|
|
122
|
+
* order mark. Undefined for UTF-32, which no Office application reads.
|
|
123
|
+
*/
|
|
124
|
+
export declare function detectEncoding(b: Uint8Array): {
|
|
125
|
+
encoding: XmlEncoding;
|
|
126
|
+
bom: number;
|
|
127
|
+
} | undefined;
|
|
102
128
|
/**
|
|
103
129
|
* A streaming XML 1.0 tokenizer with namespaces. It refuses references to entities nothing declares, encodings other
|
|
104
130
|
* than UTF-8 and UTF-16, and anything not well-formed. It holds one markup token at a time; text goes to the handler
|
|
@@ -133,6 +159,10 @@ export declare class XmlTokenizer {
|
|
|
133
159
|
/** The XML declaration says standalone="yes". */
|
|
134
160
|
private standalone;
|
|
135
161
|
private encoding;
|
|
162
|
+
/** Start tags read so far, which numbers the elements. */
|
|
163
|
+
private elements;
|
|
164
|
+
/** An attribute value of the tag being read referred to an entity of the internal subset. */
|
|
165
|
+
private generalReference;
|
|
136
166
|
/** The last text piece ended in CR, so an LF that starts the next piece belongs to the same line end. */
|
|
137
167
|
private lastCr;
|
|
138
168
|
private scan?;
|
package/dist/xml.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.XmlTokenizer = exports.isNcName = exports.unknownEntity = exports.showReferences = exports.UNKNOWN = exports.XMLNS_NS = exports.XML_NS = void 0;
|
|
4
4
|
exports.attr = attr;
|
|
5
|
+
exports.detectEncoding = detectEncoding;
|
|
5
6
|
const node_util_1 = require("node:util");
|
|
6
7
|
const errors_1 = require("./errors");
|
|
7
8
|
const types_1 = require("./types");
|
|
@@ -53,6 +54,26 @@ const NAME_END = /[\s>[\]"'%;&<]/;
|
|
|
53
54
|
/** Attribute types other than CDATA, whose values are normalized further. NOTATION and enumerations are read apart. */
|
|
54
55
|
const TOKENIZED_TYPES = new Set(['ID', 'IDREF', 'IDREFS', 'ENTITY', 'ENTITIES', 'NMTOKEN', 'NMTOKENS']);
|
|
55
56
|
const LIMIT_DETAILS = new Set([types_1.OfficeDetail.XmlDepth, types_1.OfficeDetail.XmlTokenSize, types_1.OfficeDetail.XmlEntityDepth, types_1.OfficeDetail.XmlEntityExpansions, types_1.OfficeDetail.XmlEntitySize]);
|
|
57
|
+
/**
|
|
58
|
+
* The encoding of an XML part from its first four bytes, as XML 1.0 Appendix F tells it, and the length of its byte
|
|
59
|
+
* order mark. Undefined for UTF-32, which no Office application reads.
|
|
60
|
+
*/
|
|
61
|
+
function detectEncoding(b) {
|
|
62
|
+
const at = (i) => (i < b.length ? b[i] : -1);
|
|
63
|
+
if ((at(0) === 0 && at(1) === 0) || (at(0) === 0xff && at(1) === 0xfe && at(2) === 0 && at(3) === 0) || (at(0) === 0x3c && at(1) === 0 && at(2) === 0 && at(3) === 0))
|
|
64
|
+
return undefined;
|
|
65
|
+
if (at(0) === 0xef && at(1) === 0xbb && at(2) === 0xbf)
|
|
66
|
+
return { encoding: 'utf-8', bom: 3 };
|
|
67
|
+
if (at(0) === 0xff && at(1) === 0xfe)
|
|
68
|
+
return { encoding: 'utf-16le', bom: 2 };
|
|
69
|
+
if (at(0) === 0xfe && at(1) === 0xff)
|
|
70
|
+
return { encoding: 'utf-16be', bom: 2 };
|
|
71
|
+
if (at(0) === 0x3c && at(1) === 0 && at(2) === 0x3f && at(3) === 0)
|
|
72
|
+
return { encoding: 'utf-16le', bom: 0 };
|
|
73
|
+
if (at(0) === 0 && at(1) === 0x3c && at(2) === 0 && at(3) === 0x3f)
|
|
74
|
+
return { encoding: 'utf-16be', bom: 0 };
|
|
75
|
+
return { encoding: 'utf-8', bom: 0 };
|
|
76
|
+
}
|
|
56
77
|
const isSpace = (c) => c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d;
|
|
57
78
|
/** A character XML 1.0 allows. The decoders leave no lone surrogates, so the halves of a pair pass here. */
|
|
58
79
|
const isXmlChar = (c) => c === 0x09 || c === 0x0a || c === 0x0d || (c >= 0x20 && c !== 0xfffe && c !== 0xffff);
|
|
@@ -169,6 +190,10 @@ class XmlTokenizer {
|
|
|
169
190
|
/** The XML declaration says standalone="yes". */
|
|
170
191
|
standalone = false;
|
|
171
192
|
encoding = 'utf-8';
|
|
193
|
+
/** Start tags read so far, which numbers the elements. */
|
|
194
|
+
elements = 0;
|
|
195
|
+
/** An attribute value of the tag being read referred to an entity of the internal subset. */
|
|
196
|
+
generalReference = false;
|
|
172
197
|
/** The last text piece ended in CR, so an LF that starts the next piece belongs to the same line end. */
|
|
173
198
|
lastCr = false;
|
|
174
199
|
scan;
|
|
@@ -227,29 +252,14 @@ class XmlTokenizer {
|
|
|
227
252
|
/** Picks the encoding from the first bytes, as XML 1.0 Appendix F does, and reads the bytes held so far. */
|
|
228
253
|
begin() {
|
|
229
254
|
const b = this.head;
|
|
230
|
-
const
|
|
231
|
-
if (
|
|
232
|
-
this.stop(types_1.OfficeDetail.Encoding, 'UTF-32 is not an encoding Office reads');
|
|
233
|
-
|
|
234
|
-
let skip = 0;
|
|
235
|
-
if (at(0) === 0xef && at(1) === 0xbb && at(2) === 0xbf)
|
|
236
|
-
skip = 3;
|
|
237
|
-
else if (at(0) === 0xff && at(1) === 0xfe) {
|
|
238
|
-
this.encoding = 'utf-16le';
|
|
239
|
-
skip = 2;
|
|
240
|
-
}
|
|
241
|
-
else if (at(0) === 0xfe && at(1) === 0xff) {
|
|
242
|
-
this.encoding = 'utf-16be';
|
|
243
|
-
skip = 2;
|
|
244
|
-
}
|
|
245
|
-
else if (at(0) === 0x3c && at(1) === 0 && at(2) === 0x3f && at(3) === 0)
|
|
246
|
-
this.encoding = 'utf-16le';
|
|
247
|
-
else if (at(0) === 0 && at(1) === 0x3c && at(2) === 0 && at(3) === 0x3f)
|
|
248
|
-
this.encoding = 'utf-16be';
|
|
255
|
+
const found = detectEncoding(b);
|
|
256
|
+
if (!found)
|
|
257
|
+
return this.stop(types_1.OfficeDetail.Encoding, 'UTF-32 is not an encoding Office reads');
|
|
258
|
+
this.encoding = found.encoding;
|
|
249
259
|
const decoder = new node_util_1.TextDecoder(this.encoding, { fatal: true, ignoreBOM: true });
|
|
250
260
|
this.decoder = decoder;
|
|
251
261
|
this.head = new Uint8Array(0);
|
|
252
|
-
this.feed(this.decode(decoder, b.subarray(
|
|
262
|
+
this.feed(this.decode(decoder, b.subarray(found.bom), true), false);
|
|
253
263
|
return decoder;
|
|
254
264
|
}
|
|
255
265
|
decode(decoder, bytes, more) {
|
|
@@ -467,6 +477,8 @@ class XmlTokenizer {
|
|
|
467
477
|
return;
|
|
468
478
|
}
|
|
469
479
|
this.enter(name, e.text);
|
|
480
|
+
if (!this.span)
|
|
481
|
+
this.notes.referenceStart?.(start, end);
|
|
470
482
|
const { buf, pos, base, floor, span: outer } = this;
|
|
471
483
|
this.buf = e.text;
|
|
472
484
|
this.pos = 0;
|
|
@@ -482,6 +494,8 @@ class XmlTokenizer {
|
|
|
482
494
|
this.floor = floor;
|
|
483
495
|
this.span = outer;
|
|
484
496
|
this.active.pop();
|
|
497
|
+
if (!outer)
|
|
498
|
+
this.notes.referenceEnd?.();
|
|
485
499
|
}
|
|
486
500
|
/** A reference to a general entity in an attribute value. Its text is normalized into the value. */
|
|
487
501
|
attributeReference(name) {
|
|
@@ -489,6 +503,7 @@ class XmlTokenizer {
|
|
|
489
503
|
if (!('text' in e))
|
|
490
504
|
this.malformed(`A reference to external entity "${name}" in an attribute value`);
|
|
491
505
|
this.enter(name, e.text);
|
|
506
|
+
this.generalReference = true;
|
|
492
507
|
const value = this.attributeValue(e.text, true);
|
|
493
508
|
this.active.pop();
|
|
494
509
|
return value;
|
|
@@ -981,6 +996,7 @@ class XmlTokenizer {
|
|
|
981
996
|
const qname = body.slice(0, i);
|
|
982
997
|
const raw = [];
|
|
983
998
|
let selfClosing = this.vml && qname.toLowerCase() === 'br';
|
|
999
|
+
this.generalReference = false;
|
|
984
1000
|
for (;;) {
|
|
985
1001
|
const before = i;
|
|
986
1002
|
while (i < n && isSpace(body.charCodeAt(i)))
|
|
@@ -1017,18 +1033,24 @@ class XmlTokenizer {
|
|
|
1017
1033
|
// Attribute-list declarations normalize the attributes they type, and add the defaults the tag leaves out. Each
|
|
1018
1034
|
// default added counts against the entity limits, so declarations cannot multiply the work of a long part.
|
|
1019
1035
|
const declared = this.declared.get(qname);
|
|
1036
|
+
let rewritten = this.generalReference;
|
|
1020
1037
|
if (declared) {
|
|
1021
1038
|
const given = new Set();
|
|
1022
1039
|
for (const a of raw) {
|
|
1023
1040
|
given.add(a[0]);
|
|
1024
|
-
if (declared.tokenized.has(a[0]))
|
|
1025
|
-
|
|
1041
|
+
if (!declared.tokenized.has(a[0]))
|
|
1042
|
+
continue;
|
|
1043
|
+
const v = tokenize(a[1]);
|
|
1044
|
+
if (v !== a[1])
|
|
1045
|
+
rewritten = true;
|
|
1046
|
+
a[1] = v;
|
|
1026
1047
|
}
|
|
1027
1048
|
for (const [name, value] of declared.defaults) {
|
|
1028
1049
|
if (given.has(name))
|
|
1029
1050
|
continue;
|
|
1030
1051
|
this.charge(value.length);
|
|
1031
1052
|
raw.push([name, value]);
|
|
1053
|
+
rewritten = true;
|
|
1032
1054
|
}
|
|
1033
1055
|
}
|
|
1034
1056
|
const scope = new Map();
|
|
@@ -1061,7 +1083,7 @@ class XmlTokenizer {
|
|
|
1061
1083
|
const depth = this.stack.length + 1;
|
|
1062
1084
|
if (depth > this.limits.depth)
|
|
1063
1085
|
this.stop(types_1.OfficeDetail.XmlDepth, `Elements nest deeper than ${this.limits.depth}`);
|
|
1064
|
-
const el = { ns: '', local: '', qname, attributes: [], depth, start, end };
|
|
1086
|
+
const el = { ns: '', local: '', qname, attributes: [], depth, start, end, index: this.elements++, namespaces: [...scope], empty: selfClosing, rewritten };
|
|
1065
1087
|
const open = { el, scope: scope.size ? scope : undefined };
|
|
1066
1088
|
this.stack.push(open);
|
|
1067
1089
|
const [prefix, local] = this.split(qname);
|
package/dist/zip-writer.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export declare class ZipWriter {
|
|
|
24
24
|
constructor(sink: ByteSink, options?: {
|
|
25
25
|
forceZip64?: boolean;
|
|
26
26
|
});
|
|
27
|
+
/** Bytes written so far: where the next entry's local header goes. */
|
|
28
|
+
get position(): number;
|
|
27
29
|
private put;
|
|
28
30
|
/** Adds an entry from bytes in memory, deflated unless `compress` is false. A name ending in "/" is a directory. */
|
|
29
31
|
add(name: string, data: Uint8Array, options?: {
|
|
@@ -31,6 +33,8 @@ export declare class ZipWriter {
|
|
|
31
33
|
dosTime?: number;
|
|
32
34
|
dosDate?: number;
|
|
33
35
|
}): Promise<void>;
|
|
36
|
+
/** The local header `copy` writes for an entry. */
|
|
37
|
+
localHeader(info: CopiedEntry): Uint8Array;
|
|
34
38
|
/** Adds an entry whose compressed bytes come from `chunks` unchanged, such as an entry of the archive being rewritten. */
|
|
35
39
|
copy(info: CopiedEntry, chunks: AsyncIterable<Uint8Array>): Promise<void>;
|
|
36
40
|
/** Writes the central directory and the end records. The caller closes the sink. */
|
package/dist/zip-writer.js
CHANGED
|
@@ -76,6 +76,10 @@ class ZipWriter {
|
|
|
76
76
|
this.sink = sink;
|
|
77
77
|
this.options = options;
|
|
78
78
|
}
|
|
79
|
+
/** Bytes written so far: where the next entry's local header goes. */
|
|
80
|
+
get position() {
|
|
81
|
+
return this.offset;
|
|
82
|
+
}
|
|
79
83
|
async put(b) {
|
|
80
84
|
await this.sink.write(b);
|
|
81
85
|
this.offset += b.length;
|
|
@@ -89,15 +93,11 @@ class ZipWriter {
|
|
|
89
93
|
yield body;
|
|
90
94
|
})());
|
|
91
95
|
}
|
|
92
|
-
/**
|
|
93
|
-
|
|
94
|
-
if (this.names.has(info.name))
|
|
95
|
-
throw new Error(`Entry ${info.name} is already in the archive`);
|
|
96
|
-
this.names.add(info.name);
|
|
96
|
+
/** The local header `copy` writes for an entry. */
|
|
97
|
+
localHeader(info) {
|
|
97
98
|
const name = Buffer.from(info.name, 'utf8');
|
|
98
99
|
const flags = /^[\x20-\x7e]*$/.test(info.name) ? 0 : 0x0800;
|
|
99
100
|
const big = this.options.forceZip64 === true || info.size >= MAX32 || info.compressedSize >= MAX32;
|
|
100
|
-
const localOffset = this.offset;
|
|
101
101
|
const time = info.dosTime ?? 0;
|
|
102
102
|
const date = info.dosDate ?? DOS_DATE;
|
|
103
103
|
const local = new Builder()
|
|
@@ -115,7 +115,20 @@ class ZipWriter {
|
|
|
115
115
|
.bytes(name);
|
|
116
116
|
if (big)
|
|
117
117
|
local.u16(0x0001).u16(16).u64(info.size).u64(info.compressedSize);
|
|
118
|
-
|
|
118
|
+
return local.build();
|
|
119
|
+
}
|
|
120
|
+
/** Adds an entry whose compressed bytes come from `chunks` unchanged, such as an entry of the archive being rewritten. */
|
|
121
|
+
async copy(info, chunks) {
|
|
122
|
+
if (this.names.has(info.name))
|
|
123
|
+
throw new Error(`Entry ${info.name} is already in the archive`);
|
|
124
|
+
this.names.add(info.name);
|
|
125
|
+
const name = Buffer.from(info.name, 'utf8');
|
|
126
|
+
const flags = /^[\x20-\x7e]*$/.test(info.name) ? 0 : 0x0800;
|
|
127
|
+
const big = this.options.forceZip64 === true || info.size >= MAX32 || info.compressedSize >= MAX32;
|
|
128
|
+
const localOffset = this.offset;
|
|
129
|
+
const time = info.dosTime ?? 0;
|
|
130
|
+
const date = info.dosDate ?? DOS_DATE;
|
|
131
|
+
await this.put(this.localHeader(info));
|
|
119
132
|
let written = 0;
|
|
120
133
|
for await (const c of chunks) {
|
|
121
134
|
written += c.length;
|
package/dist/zip.d.ts
CHANGED
|
@@ -27,10 +27,17 @@ export interface ZipEntry {
|
|
|
27
27
|
}
|
|
28
28
|
/** Something the reader saw that it does not refuse. Each is a strip finding, since the rewrite drops the bytes. */
|
|
29
29
|
export interface ZipNote {
|
|
30
|
-
detail: D.LeadingBytes | D.HiddenData | D.TrailingBytes;
|
|
30
|
+
detail: D.LeadingBytes | D.HiddenData | D.HiddenEntry | D.TrailingBytes;
|
|
31
31
|
bytes: number;
|
|
32
|
+
/** For a hidden entry, where its local header sits. */
|
|
33
|
+
offset?: number;
|
|
32
34
|
}
|
|
33
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* The key two names collide on: percent-decoded, NFKC and case-folded, without a directory's slash, and without the
|
|
37
|
+
* dots and spaces Windows drops from the end of each segment. Upper then lower case is the full folding, which takes a
|
|
38
|
+
* sharp s to ss as macOS's case-insensitive file system does. NFKC also joins compatibility forms such as ligatures
|
|
39
|
+
* and full-width letters.
|
|
40
|
+
*/
|
|
34
41
|
export declare function nameKey(name: string): string;
|
|
35
42
|
/**
|
|
36
43
|
* A ZIP archive read from its central directory, after every local header was checked against it. Reading an entry
|
package/dist/zip.js
CHANGED
|
@@ -56,6 +56,22 @@ const END_SEARCH = 22 + MAX16;
|
|
|
56
56
|
const FLAG_MASK = 0x0001 | 0x0008 | 0x0040 | 0x0800 | 0x2000;
|
|
57
57
|
const ENCRYPTION_FLAGS = 0x0001 | 0x0040 | 0x2000;
|
|
58
58
|
const MAX_NAME_BYTES = 1024;
|
|
59
|
+
const LOCAL_SIGNATURE = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
|
|
60
|
+
/** The offset of the first local file header signature in [start, end) of the file, or undefined. */
|
|
61
|
+
async function findLocalHeader(reader, start, end, deadline) {
|
|
62
|
+
let carry = Buffer.alloc(0);
|
|
63
|
+
let at = start;
|
|
64
|
+
for await (const c of reader.chunks(start, end - start)) {
|
|
65
|
+
(0, errors_1.checkTime)(deadline);
|
|
66
|
+
const b = Buffer.concat([carry, Buffer.from(c.buffer, c.byteOffset, c.byteLength)]);
|
|
67
|
+
const i = b.indexOf(LOCAL_SIGNATURE);
|
|
68
|
+
if (i >= 0)
|
|
69
|
+
return at - carry.length + i;
|
|
70
|
+
carry = b.subarray(Math.max(0, b.length - 3));
|
|
71
|
+
at += c.length;
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
59
75
|
const fail = (detail, message, data) => {
|
|
60
76
|
throw new errors_1.StopError(types_1.OfficeCategory.Container, detail, message, data);
|
|
61
77
|
};
|
|
@@ -137,6 +153,20 @@ function checkUnicodePath(extras, name) {
|
|
|
137
153
|
if (other !== name)
|
|
138
154
|
fail(types_1.OfficeDetail.NameEncoding, 'The Unicode path field names a different entry', { name });
|
|
139
155
|
}
|
|
156
|
+
/** True for a segment of nothing but dots and spaces, the empty one included. */
|
|
157
|
+
function onlyDotsAndSpaces(seg) {
|
|
158
|
+
for (const c of seg)
|
|
159
|
+
if (c !== '.' && c !== ' ')
|
|
160
|
+
return false;
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
/** A segment without the dots and spaces Windows drops from its end. A loop, since /[. ]+$/ backtracks. */
|
|
164
|
+
function trimSegment(seg) {
|
|
165
|
+
let end = seg.length;
|
|
166
|
+
while (end > 0 && (seg[end - 1] === '.' || seg[end - 1] === ' '))
|
|
167
|
+
end--;
|
|
168
|
+
return seg.slice(0, end);
|
|
169
|
+
}
|
|
140
170
|
function checkName(name, directory) {
|
|
141
171
|
const why = (reason) => fail(types_1.OfficeDetail.UnsafeName, `Entry name ${reason}`, { name, reason });
|
|
142
172
|
if (name === '')
|
|
@@ -150,11 +180,17 @@ function checkName(name, directory) {
|
|
|
150
180
|
if (/^[A-Za-z]:/.test(name))
|
|
151
181
|
why('starts with a drive letter');
|
|
152
182
|
for (const seg of (directory ? name.slice(0, -1) : name).split('/')) {
|
|
153
|
-
|
|
154
|
-
|
|
183
|
+
// Windows drops trailing dots and spaces from each segment, so ".. " and "..." climb out as ".." does.
|
|
184
|
+
if (onlyDotsAndSpaces(seg))
|
|
185
|
+
why('has an empty segment, or one of only dots and spaces');
|
|
155
186
|
}
|
|
156
187
|
}
|
|
157
|
-
/**
|
|
188
|
+
/**
|
|
189
|
+
* The key two names collide on: percent-decoded, NFKC and case-folded, without a directory's slash, and without the
|
|
190
|
+
* dots and spaces Windows drops from the end of each segment. Upper then lower case is the full folding, which takes a
|
|
191
|
+
* sharp s to ss as macOS's case-insensitive file system does. NFKC also joins compatibility forms such as ligatures
|
|
192
|
+
* and full-width letters.
|
|
193
|
+
*/
|
|
158
194
|
function nameKey(name) {
|
|
159
195
|
let n = name;
|
|
160
196
|
try {
|
|
@@ -163,7 +199,7 @@ function nameKey(name) {
|
|
|
163
199
|
catch {
|
|
164
200
|
// Not valid percent-encoding, so it stays as written.
|
|
165
201
|
}
|
|
166
|
-
return n.normalize('
|
|
202
|
+
return n.normalize('NFKC').toUpperCase().toLowerCase().replace(/\/$/, '').split('/').map(trimSegment).join('/');
|
|
167
203
|
}
|
|
168
204
|
/** ZIP64 values the central directory defers to its extra field, in the order the format fixes. */
|
|
169
205
|
function zip64Central(extras, fields, name) {
|
|
@@ -371,25 +407,46 @@ class ZipArchive {
|
|
|
371
407
|
(0, errors_1.checkTime)(deadline);
|
|
372
408
|
await ZipArchive.checkLocal(reader, e, raws[i], dirOffset);
|
|
373
409
|
}
|
|
410
|
+
// A reader that walks the file cannot know where a stored entry with a data descriptor ends. It looks for the
|
|
411
|
+
// next signature, so a local header inside the data is an entry to it, and part of this entry here.
|
|
412
|
+
for (const e of entries) {
|
|
413
|
+
if (e.method !== 0 || !(e.flags & 0x0008) || e.compressedSize === 0)
|
|
414
|
+
continue;
|
|
415
|
+
const at = await findLocalHeader(reader, e.dataOffset, e.dataOffset + e.compressedSize, deadline);
|
|
416
|
+
if (at !== undefined)
|
|
417
|
+
notes.push({ detail: types_1.OfficeDetail.HiddenEntry, bytes: e.dataOffset + e.compressedSize - at, offset: at });
|
|
418
|
+
}
|
|
374
419
|
// Entries and the central directory must tile the file. Bytes no entry covers are reported, overlaps refused.
|
|
375
420
|
const sorted = [...entries].sort((a, b) => a.localOffset - b.localOffset);
|
|
421
|
+
const gaps = [];
|
|
376
422
|
let covered = 0;
|
|
377
423
|
let hidden = 0;
|
|
378
424
|
for (const e of sorted) {
|
|
379
425
|
if (e.localOffset < covered)
|
|
380
426
|
fail(types_1.OfficeDetail.Overlap, 'Two entries share bytes', { name: e.name });
|
|
427
|
+
if (e.localOffset > covered)
|
|
428
|
+
gaps.push([covered, e.localOffset]);
|
|
381
429
|
if (covered === 0 && e.localOffset > 0)
|
|
382
430
|
notes.push({ detail: types_1.OfficeDetail.LeadingBytes, bytes: e.localOffset });
|
|
383
431
|
else
|
|
384
432
|
hidden += e.localOffset - covered;
|
|
385
433
|
covered = e.end;
|
|
386
434
|
}
|
|
435
|
+
if (dirOffset > covered)
|
|
436
|
+
gaps.push([covered, dirOffset]);
|
|
387
437
|
if (sorted.length === 0 && dirOffset > 0)
|
|
388
438
|
notes.push({ detail: types_1.OfficeDetail.LeadingBytes, bytes: dirOffset });
|
|
389
439
|
else
|
|
390
440
|
hidden += dirOffset - covered;
|
|
391
441
|
if (hidden > 0)
|
|
392
442
|
notes.push({ detail: types_1.OfficeDetail.HiddenData, bytes: hidden });
|
|
443
|
+
// A local header in a gap is an entry the central directory does not list. A reader that walks the file from the
|
|
444
|
+
// start, as streaming unzippers do, extracts it; a reader of the central directory never sees it.
|
|
445
|
+
for (const [from, to] of gaps) {
|
|
446
|
+
const at = await findLocalHeader(reader, from, to, deadline);
|
|
447
|
+
if (at !== undefined)
|
|
448
|
+
notes.push({ detail: types_1.OfficeDetail.HiddenEntry, bytes: to - from, offset: at });
|
|
449
|
+
}
|
|
393
450
|
return new ZipArchive(reader, entries, notes, deadline);
|
|
394
451
|
}
|
|
395
452
|
/** Checks a local header and any data descriptor against the central directory, and fills in where the entry's data and the entry end. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patrickdk77/office-defuse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Inspect Office and OpenDocument files for VBA and Excel 4.0 macros, LibreOffice Basic and scripts, DDE, external templates and links, embedded OLE objects and ActiveX controls, and score the risk. Strict ZIP and XML readers, bounded memory, no runtime dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "patrickdk77",
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
},
|
|
17
17
|
"./package.json": "./package.json"
|
|
18
18
|
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"office-defuse": "dist/cli.js"
|
|
21
|
+
},
|
|
19
22
|
"files": [
|
|
20
23
|
"dist",
|
|
21
24
|
"README.md",
|