@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
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.startTag = startTag;
|
|
4
|
+
exports.partEdits = partEdits;
|
|
5
|
+
exports.splice = splice;
|
|
6
|
+
const node_util_1 = require("node:util");
|
|
7
|
+
const plan_1 = require("./plan");
|
|
8
|
+
const xml_1 = require("./xml");
|
|
9
|
+
/** Word elements that a field's code holds whole, which go when the field does. */
|
|
10
|
+
const IN_CODE = new Set(['r', 'fldSimple', 'hyperlink', 'smartTag']);
|
|
11
|
+
/** Word elements that make up a field's code and marks. */
|
|
12
|
+
const CODE_MARKS = new Set(['instrText', 'delInstrText', 'fldChar']);
|
|
13
|
+
const escapeText = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\r/g, ' ');
|
|
14
|
+
const escapeAttribute = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"').replace(/\t/g, '	').replace(/\n/g, ' ').replace(/\r/g, ' ');
|
|
15
|
+
/** A start tag written out from what the tokenizer read, with the edit's attributes left out or changed. */
|
|
16
|
+
function startTag(el, edit) {
|
|
17
|
+
let s = `<${el.qname}`;
|
|
18
|
+
for (const [prefix, uri] of el.namespaces)
|
|
19
|
+
s += ` ${prefix ? `xmlns:${prefix}` : 'xmlns'}="${escapeAttribute(uri)}"`;
|
|
20
|
+
for (const a of el.attributes) {
|
|
21
|
+
if (edit?.strip?.has((0, plan_1.attributeKey)(a.ns, a.local)))
|
|
22
|
+
continue;
|
|
23
|
+
s += ` ${a.qname}="${escapeAttribute(edit?.set?.get(a.qname) ?? a.value)}"`;
|
|
24
|
+
}
|
|
25
|
+
return s + (el.empty ? '/>' : '>');
|
|
26
|
+
}
|
|
27
|
+
/** Two edits of one element as one. Removing it covers everything else. */
|
|
28
|
+
function merge(a, b) {
|
|
29
|
+
if (!a || !b)
|
|
30
|
+
return a ?? b;
|
|
31
|
+
if (a.remove || b.remove)
|
|
32
|
+
return { remove: true };
|
|
33
|
+
return {
|
|
34
|
+
unwrap: a.unwrap || b.unwrap,
|
|
35
|
+
strip: new Set([...(a.strip ?? []), ...(b.strip ?? [])]),
|
|
36
|
+
set: new Map([...(a.set ?? []), ...(b.set ?? [])]),
|
|
37
|
+
content: b.content ?? a.content,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Finds the edits of one part as the tokenizer reads it: the elements the plan names and the elements that refer to
|
|
42
|
+
* something that goes, the Word fields the plan names, and with an internal subset that goes, the document type
|
|
43
|
+
* declaration, every entity reference expanded and every start tag whose attributes the declarations changed.
|
|
44
|
+
*/
|
|
45
|
+
class Rewriter {
|
|
46
|
+
plan;
|
|
47
|
+
rules;
|
|
48
|
+
edits = [];
|
|
49
|
+
rootRemoved = false;
|
|
50
|
+
stack = [];
|
|
51
|
+
/** The depth of the element being skipped, 0 when none is. */
|
|
52
|
+
skipping = 0;
|
|
53
|
+
/** The entity reference being written out, at the top level of the part. */
|
|
54
|
+
expansion;
|
|
55
|
+
/** A reference inside a removed element, whose text needs no writing out. */
|
|
56
|
+
ignored = false;
|
|
57
|
+
marks = new Map();
|
|
58
|
+
active = new Set();
|
|
59
|
+
/**
|
|
60
|
+
* Whether the internal subset goes. References to external entities that stay need their declarations, so then the
|
|
61
|
+
* subset stays, and the check of the output reports it.
|
|
62
|
+
*/
|
|
63
|
+
subset;
|
|
64
|
+
constructor(plan, rules) {
|
|
65
|
+
this.plan = plan;
|
|
66
|
+
this.rules = rules;
|
|
67
|
+
this.subset = plan?.subset === true && !(plan.hasExternal && !plan.external);
|
|
68
|
+
for (const f of plan?.fields ?? []) {
|
|
69
|
+
if (f.simple) {
|
|
70
|
+
this.marks.set(f.begin, { field: f, role: 'begin' });
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
// The code runs from the begin mark to the separator, or to the end mark when there is none.
|
|
74
|
+
const close = f.separate ?? f.end;
|
|
75
|
+
this.marks.set(f.begin, { field: f, role: 'begin' });
|
|
76
|
+
if (f.separate !== undefined)
|
|
77
|
+
this.marks.set(f.separate, { field: f, role: f.separate === close ? 'close' : 'other' });
|
|
78
|
+
if (f.end !== undefined && !this.marks.has(f.end))
|
|
79
|
+
this.marks.set(f.end, { field: f, role: f.end === close ? 'close' : 'other' });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
get top() {
|
|
83
|
+
return this.stack[this.stack.length - 1];
|
|
84
|
+
}
|
|
85
|
+
/** Where text written out from an entity goes: the element it is in, or the reference itself. */
|
|
86
|
+
sink() {
|
|
87
|
+
const top = this.top;
|
|
88
|
+
if (top?.expanded)
|
|
89
|
+
return top.body;
|
|
90
|
+
return this.expansion?.out;
|
|
91
|
+
}
|
|
92
|
+
fieldEdit(el, edit) {
|
|
93
|
+
const mark = this.marks.get(el.index);
|
|
94
|
+
if (mark) {
|
|
95
|
+
if (mark.role === 'close') {
|
|
96
|
+
// The separator ends the code. Elements still open hold it, so they are not wholly code.
|
|
97
|
+
this.active.delete(mark.field);
|
|
98
|
+
for (const f of this.stack)
|
|
99
|
+
f.regions?.delete(mark.field);
|
|
100
|
+
}
|
|
101
|
+
return mark.field.simple ? { ...edit, unwrap: true } : { remove: true };
|
|
102
|
+
}
|
|
103
|
+
if (this.active.size && this.rules.word?.(el) && CODE_MARKS.has(el.local))
|
|
104
|
+
return { remove: true };
|
|
105
|
+
return edit;
|
|
106
|
+
}
|
|
107
|
+
start(el) {
|
|
108
|
+
const parent = this.top;
|
|
109
|
+
if (this.skipping) {
|
|
110
|
+
this.stack.push({ el, dropped: true, expanded: false, kept: 0, removed: 0 });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const edit = this.fieldEdit(el, merge(this.plan?.elements.get(el.index), this.rules.element(el)));
|
|
114
|
+
const expanded = this.expansion !== undefined;
|
|
115
|
+
const frame = { el, edit, dropped: edit?.remove === true, expanded, kept: 0, removed: 0 };
|
|
116
|
+
if (!frame.dropped && this.active.size && this.rules.word?.(el) && IN_CODE.has(el.local))
|
|
117
|
+
frame.regions = new Set(this.active);
|
|
118
|
+
const mark = this.marks.get(el.index);
|
|
119
|
+
if (mark?.role === 'begin' && !mark.field.simple)
|
|
120
|
+
this.active.add(mark.field);
|
|
121
|
+
if (parent) {
|
|
122
|
+
if (frame.dropped)
|
|
123
|
+
parent.removed++;
|
|
124
|
+
else
|
|
125
|
+
parent.kept++;
|
|
126
|
+
}
|
|
127
|
+
this.stack.push(frame);
|
|
128
|
+
if (frame.dropped) {
|
|
129
|
+
if (el.depth === 1 && !expanded)
|
|
130
|
+
this.rootRemoved = true;
|
|
131
|
+
this.skipping = el.depth;
|
|
132
|
+
if (this.expansion)
|
|
133
|
+
this.expansion.changed = true;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (this.expansion) {
|
|
137
|
+
frame.head = edit?.unwrap ? '' : startTag(el, edit);
|
|
138
|
+
frame.body = [];
|
|
139
|
+
if (edit)
|
|
140
|
+
this.expansion.changed = true;
|
|
141
|
+
}
|
|
142
|
+
else if (edit?.unwrap)
|
|
143
|
+
this.edits.push({ start: el.start, end: el.end });
|
|
144
|
+
else if (edit?.strip?.size || edit?.set?.size || (this.subset && el.rewritten))
|
|
145
|
+
this.edits.push({ start: el.start, end: el.end, text: startTag(el, edit) });
|
|
146
|
+
}
|
|
147
|
+
end(el, start, end) {
|
|
148
|
+
const frame = this.stack.pop();
|
|
149
|
+
if (!frame)
|
|
150
|
+
return;
|
|
151
|
+
const parent = this.top;
|
|
152
|
+
if (frame.dropped) {
|
|
153
|
+
if (this.skipping !== el.depth)
|
|
154
|
+
return;
|
|
155
|
+
this.skipping = 0;
|
|
156
|
+
if (!frame.expanded)
|
|
157
|
+
this.edits.push({ start: el.start, end });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const cut = (frame.regions?.size ?? 0) > 0 || (frame.kept === 0 && frame.removed > 0 && this.rules.collapses?.(el) === true);
|
|
161
|
+
if (cut) {
|
|
162
|
+
if (parent) {
|
|
163
|
+
parent.kept--;
|
|
164
|
+
parent.removed++;
|
|
165
|
+
}
|
|
166
|
+
if (frame.expanded) {
|
|
167
|
+
if (this.expansion)
|
|
168
|
+
this.expansion.changed = true;
|
|
169
|
+
}
|
|
170
|
+
else if (el.depth === 1)
|
|
171
|
+
this.rootRemoved = true;
|
|
172
|
+
else
|
|
173
|
+
this.edits.push({ start: el.start, end });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (frame.expanded) {
|
|
177
|
+
const body = frame.edit?.content !== undefined && !frame.edit.unwrap ? escapeText(frame.edit.content) : (frame.body ?? []).join('');
|
|
178
|
+
const text = (frame.head ?? '') + body + (el.empty || frame.edit?.unwrap ? '' : `</${el.qname}>`);
|
|
179
|
+
this.sink()?.push(text);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (el.depth === 1 && !el.empty) {
|
|
183
|
+
const added = this.rules.append?.();
|
|
184
|
+
if (added)
|
|
185
|
+
this.edits.push({ start, end: start, text: added });
|
|
186
|
+
}
|
|
187
|
+
if (frame.edit?.unwrap && !el.empty)
|
|
188
|
+
this.edits.push({ start, end });
|
|
189
|
+
// New content goes between the tags. Text in the element, which the edit replaces, is never written out.
|
|
190
|
+
if (frame.edit?.content !== undefined && !el.empty && !frame.edit.unwrap)
|
|
191
|
+
this.edits.push({ start: el.end, end: start, text: escapeText(frame.edit.content) });
|
|
192
|
+
}
|
|
193
|
+
text(t) {
|
|
194
|
+
if (this.skipping || !this.expansion)
|
|
195
|
+
return;
|
|
196
|
+
let s = escapeText(t);
|
|
197
|
+
if (s.includes(xml_1.UNKNOWN)) {
|
|
198
|
+
// A reference to an external entity inside the text being written out.
|
|
199
|
+
const drop = this.plan?.external === true;
|
|
200
|
+
if (drop)
|
|
201
|
+
this.expansion.changed = true;
|
|
202
|
+
s = s.replace(new RegExp(`${xml_1.UNKNOWN}([^${xml_1.UNKNOWN}]*)${xml_1.UNKNOWN}`, 'g'), (_m, name) => (drop ? '' : `&${name};`));
|
|
203
|
+
}
|
|
204
|
+
this.sink()?.push(s);
|
|
205
|
+
}
|
|
206
|
+
doctype(d) {
|
|
207
|
+
if ((this.subset && d.internalSubset) || (this.plan?.doctype && !d.internalSubset))
|
|
208
|
+
this.edits.push({ start: d.start, end: d.end });
|
|
209
|
+
}
|
|
210
|
+
externalEntity(r) {
|
|
211
|
+
if (this.skipping || this.expansion || !this.plan?.external)
|
|
212
|
+
return;
|
|
213
|
+
this.edits.push({ start: r.start, end: r.end });
|
|
214
|
+
}
|
|
215
|
+
referenceStart(start, end) {
|
|
216
|
+
if (this.skipping) {
|
|
217
|
+
this.ignored = true;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
this.expansion = { start, end, out: [], changed: false };
|
|
221
|
+
}
|
|
222
|
+
referenceEnd() {
|
|
223
|
+
if (this.ignored) {
|
|
224
|
+
this.ignored = false;
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const x = this.expansion;
|
|
228
|
+
this.expansion = undefined;
|
|
229
|
+
// A reference is written out as its text when the subset goes, or when something in its text goes.
|
|
230
|
+
if (x && (this.subset || x.changed))
|
|
231
|
+
this.edits.push({ start: x.start, end: x.end, text: x.out.join('') });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Reads a part again with the tokenizer and returns the edits its rewrite makes. The part was read once already, by
|
|
236
|
+
* the same tokenizer with the same limits, so this reading sees the same elements in the same order.
|
|
237
|
+
*/
|
|
238
|
+
async function partEdits(chunks, part, limits, vml, plan, rules) {
|
|
239
|
+
const r = new Rewriter(plan, rules);
|
|
240
|
+
const t = new xml_1.XmlTokenizer(r, limits, part, {
|
|
241
|
+
vml,
|
|
242
|
+
notes: { doctype: d => r.doctype(d), externalEntity: e => r.externalEntity(e), referenceStart: (s, e) => r.referenceStart(s, e), referenceEnd: () => r.referenceEnd() },
|
|
243
|
+
});
|
|
244
|
+
for await (const c of chunks)
|
|
245
|
+
t.write(c);
|
|
246
|
+
t.end();
|
|
247
|
+
return { edits: r.edits, rootRemoved: r.rootRemoved };
|
|
248
|
+
}
|
|
249
|
+
/** Sorts edits and drops each one that lies inside another, so they apply in one pass. */
|
|
250
|
+
function normalize(edits) {
|
|
251
|
+
const sorted = [...edits].sort((a, b) => a.start - b.start || Number(b.start === b.end) - Number(a.start === a.end) || b.end - a.end);
|
|
252
|
+
const out = [];
|
|
253
|
+
let reach = 0;
|
|
254
|
+
for (const e of sorted) {
|
|
255
|
+
if (e.start < reach)
|
|
256
|
+
continue;
|
|
257
|
+
out.push(e);
|
|
258
|
+
reach = Math.max(reach, e.end);
|
|
259
|
+
}
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
/** Encodes text in the part's own encoding. */
|
|
263
|
+
function encode(s, encoding) {
|
|
264
|
+
if (encoding === 'utf-8')
|
|
265
|
+
return Buffer.from(s, 'utf8');
|
|
266
|
+
const b = Buffer.from(s, 'utf16le');
|
|
267
|
+
return encoding === 'utf-16be' ? b.swap16() : b;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Applies edits to a part as its bytes arrive, and yields the result in the part's encoding, its byte order mark
|
|
271
|
+
* included. Text the edits do not touch keeps its characters, so it keeps its bytes.
|
|
272
|
+
*/
|
|
273
|
+
async function* splice(chunks, edits) {
|
|
274
|
+
const list = normalize(edits);
|
|
275
|
+
let k = 0;
|
|
276
|
+
let skipTo = 0;
|
|
277
|
+
let pos = 0;
|
|
278
|
+
let head = new Uint8Array(0);
|
|
279
|
+
let decoder;
|
|
280
|
+
let encoding = 'utf-8';
|
|
281
|
+
// The decoder hands over whole characters, and edits start and end at markup, so no piece ends inside a pair.
|
|
282
|
+
const apply = (text, last) => {
|
|
283
|
+
let out = '';
|
|
284
|
+
let i = 0;
|
|
285
|
+
const base = pos;
|
|
286
|
+
while (i < text.length || (last && k < list.length && list[k].start === base + i)) {
|
|
287
|
+
const at = base + i;
|
|
288
|
+
if (at < skipTo) {
|
|
289
|
+
const to = Math.min(text.length, skipTo - base);
|
|
290
|
+
if (to <= i)
|
|
291
|
+
break;
|
|
292
|
+
i = to;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const next = list[k];
|
|
296
|
+
if (next && next.start === at) {
|
|
297
|
+
out += next.text ?? '';
|
|
298
|
+
skipTo = next.end;
|
|
299
|
+
k++;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const stop = next ? Math.min(text.length, next.start - base) : text.length;
|
|
303
|
+
if (stop <= i)
|
|
304
|
+
break;
|
|
305
|
+
out += text.slice(i, stop);
|
|
306
|
+
i = stop;
|
|
307
|
+
}
|
|
308
|
+
pos += text.length;
|
|
309
|
+
return encode(out, encoding);
|
|
310
|
+
};
|
|
311
|
+
const begin = () => {
|
|
312
|
+
const found = (0, xml_1.detectEncoding)(head) ?? { encoding: 'utf-8', bom: 0 };
|
|
313
|
+
encoding = found.encoding;
|
|
314
|
+
decoder = new node_util_1.TextDecoder(encoding, { fatal: true, ignoreBOM: true });
|
|
315
|
+
const bom = head.subarray(0, found.bom);
|
|
316
|
+
const rest = head.subarray(found.bom);
|
|
317
|
+
head = new Uint8Array(0);
|
|
318
|
+
return [bom, apply(decoder.decode(rest, { stream: true }), false)];
|
|
319
|
+
};
|
|
320
|
+
for await (const c of chunks) {
|
|
321
|
+
if (!decoder) {
|
|
322
|
+
head = head.length ? Buffer.concat([head, c]) : c;
|
|
323
|
+
if (head.length < 4)
|
|
324
|
+
continue;
|
|
325
|
+
for (const b of begin())
|
|
326
|
+
if (b.length)
|
|
327
|
+
yield b;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const b = apply(decoder.decode(c, { stream: true }), false);
|
|
331
|
+
if (b.length)
|
|
332
|
+
yield b;
|
|
333
|
+
}
|
|
334
|
+
if (!decoder)
|
|
335
|
+
for (const b of begin())
|
|
336
|
+
if (b.length)
|
|
337
|
+
yield b;
|
|
338
|
+
const tail = apply(decoder.decode(new Uint8Array(0)), true);
|
|
339
|
+
if (tail.length)
|
|
340
|
+
yield tail;
|
|
341
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RunState } from './context';
|
|
2
|
+
import type { Format } from './formats';
|
|
3
|
+
import type { PackagePlan } from './plan';
|
|
4
|
+
import type { ByteSink } from './types';
|
|
5
|
+
import { type XmlLimits } from './xml';
|
|
6
|
+
import { type ZipArchive } from './zip';
|
|
7
|
+
/** Writes the rewritten package to `sink`, which the caller closes. */
|
|
8
|
+
export declare function writePackage(run: RunState, zip: ZipArchive, plan: PackagePlan, format: Format, limits: XmlLimits, sink: ByteSink): Promise<void>;
|