@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.
Files changed (75) hide show
  1. package/README.md +197 -42
  2. package/dist/cfb-writer.d.ts +25 -0
  3. package/dist/cfb-writer.js +314 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +357 -0
  6. package/dist/contained.d.ts +46 -0
  7. package/dist/contained.js +152 -0
  8. package/dist/context.d.ts +107 -12
  9. package/dist/context.js +101 -24
  10. package/dist/engine.d.ts +12 -1
  11. package/dist/engine.js +276 -31
  12. package/dist/errors.d.ts +17 -0
  13. package/dist/errors.js +27 -1
  14. package/dist/fields.d.ts +20 -13
  15. package/dist/fields.js +65 -23
  16. package/dist/findings.d.ts +2 -1
  17. package/dist/findings.js +17 -1
  18. package/dist/flat.d.ts +33 -0
  19. package/dist/flat.js +486 -0
  20. package/dist/formats.d.ts +7 -0
  21. package/dist/formats.js +5 -1
  22. package/dist/formulas.d.ts +27 -0
  23. package/dist/formulas.js +157 -1
  24. package/dist/index.d.ts +3 -2
  25. package/dist/index.js +8 -1
  26. package/dist/index.mjs +1 -1
  27. package/dist/io.d.ts +62 -1
  28. package/dist/io.js +245 -1
  29. package/dist/links.d.ts +27 -0
  30. package/dist/links.js +106 -1
  31. package/dist/msg.d.ts +3 -2
  32. package/dist/msg.js +172 -18
  33. package/dist/objects.d.ts +8 -5
  34. package/dist/objects.js +90 -20
  35. package/dist/odf.d.ts +65 -1
  36. package/dist/odf.js +181 -64
  37. package/dist/offcrypto.d.ts +24 -1
  38. package/dist/offcrypto.js +308 -69
  39. package/dist/ole.js +17 -5
  40. package/dist/oleobj.d.ts +11 -0
  41. package/dist/oleobj.js +20 -3
  42. package/dist/ooxml.d.ts +81 -0
  43. package/dist/ooxml.js +342 -81
  44. package/dist/plan.d.ts +104 -0
  45. package/dist/plan.js +73 -0
  46. package/dist/plugins.d.ts +8 -0
  47. package/dist/plugins.js +60 -0
  48. package/dist/ppt.js +45 -5
  49. package/dist/rewrite-cfb.d.ts +14 -0
  50. package/dist/rewrite-cfb.js +52 -0
  51. package/dist/rewrite-rtf.d.ts +9 -0
  52. package/dist/rewrite-rtf.js +45 -0
  53. package/dist/rewrite-xml.d.ts +36 -0
  54. package/dist/rewrite-xml.js +341 -0
  55. package/dist/rewrite-zip.d.ts +8 -0
  56. package/dist/rewrite-zip.js +498 -0
  57. package/dist/rtf.js +214 -41
  58. package/dist/rtfcomp.d.ts +7 -0
  59. package/dist/rtfcomp.js +80 -0
  60. package/dist/score.js +1 -1
  61. package/dist/sniff.d.ts +5 -0
  62. package/dist/sniff.js +38 -0
  63. package/dist/types.d.ts +43 -6
  64. package/dist/types.js +14 -0
  65. package/dist/vsd.d.ts +13 -0
  66. package/dist/vsd.js +689 -0
  67. package/dist/word.js +51 -17
  68. package/dist/xls.js +38 -4
  69. package/dist/xml.d.ts +30 -0
  70. package/dist/xml.js +45 -23
  71. package/dist/zip-writer.d.ts +4 -0
  72. package/dist/zip-writer.js +20 -7
  73. package/dist/zip.d.ts +9 -2
  74. package/dist/zip.js +61 -4
  75. package/package.json +4 -1
package/dist/flat.js ADDED
@@ -0,0 +1,486 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FLAT_OPC_FORMAT = exports.FLAT_ODF_PART = exports.PKG_NS = void 0;
4
+ exports.startsAsXml = startsAsXml;
5
+ exports.isFlatHead = isFlatHead;
6
+ exports.inspectFlat = inspectFlat;
7
+ exports.writeFlatOdf = writeFlatOdf;
8
+ exports.writeFlatOpc = writeFlatOpc;
9
+ const node_util_1 = require("node:util");
10
+ const contained_1 = require("./contained");
11
+ const context_1 = require("./context");
12
+ const errors_1 = require("./errors");
13
+ const io_1 = require("./io");
14
+ const odf_1 = require("./odf");
15
+ const ooxml_1 = require("./ooxml");
16
+ const rewrite_xml_1 = require("./rewrite-xml");
17
+ const rewrite_zip_1 = require("./rewrite-zip");
18
+ const types_1 = require("./types");
19
+ const xml_1 = require("./xml");
20
+ const zip_1 = require("./zip");
21
+ const zip_writer_1 = require("./zip-writer");
22
+ /** The namespace of a Flat OPC package, the single-file form of OOXML that Word, Excel and PowerPoint save as XML. */
23
+ exports.PKG_NS = 'http://schemas.microsoft.com/office/2006/xmlPackage';
24
+ /** The name findings give the one part of a flat OpenDocument file. */
25
+ exports.FLAT_ODF_PART = 'office:document';
26
+ /** The name findings give the wrapper of a Flat OPC package. */
27
+ const FLAT_OPC_PART = 'pkg:package';
28
+ const LOEXT_NS = 'urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0';
29
+ /** Flat OpenDocument formats, by the media type in office:mimetype. */
30
+ const FLAT_ODF = new Map([
31
+ ['text', 'fodt'],
32
+ ['spreadsheet', 'fods'],
33
+ ['presentation', 'fodp'],
34
+ ['graphics', 'fodg'],
35
+ ].map(([kind, ext]) => [`application/vnd.oasis.opendocument.${kind}`, { format: ext, mimeType: `application/vnd.oasis.opendocument.${kind}-flat-xml` }]));
36
+ /** Flat OPC has no media type of its own, and Office saves it with the .xml extension. */
37
+ exports.FLAT_OPC_FORMAT = { format: 'xml', mimeType: 'application/xml' };
38
+ const CHUNK = 65536;
39
+ /** A source's bytes in chunks, checking the deadline before each. */
40
+ async function* chunksOf(src, size, deadline) {
41
+ for (let at = 0; at < size; at += CHUNK) {
42
+ (0, errors_1.checkTime)(deadline);
43
+ yield await src.read(at, Math.min(CHUNK, size - at));
44
+ }
45
+ }
46
+ /** Whether the first bytes of a file read as XML: a byte order mark, white space and then "<". */
47
+ function startsAsXml(head) {
48
+ const found = (0, xml_1.detectEncoding)(head);
49
+ if (!found)
50
+ return false;
51
+ const text = new node_util_1.TextDecoder(found.encoding, { fatal: false }).decode(head.subarray(found.bom));
52
+ return text.trimStart().startsWith('<');
53
+ }
54
+ /**
55
+ * Whether the start of an XML file names a flat OpenDocument document or a Flat OPC package as its root element. The
56
+ * reading of the whole file decides; this tells a router which package to try.
57
+ */
58
+ function isFlatHead(text) {
59
+ const body = text.replace(/^\uFEFF/, '').replace(/^(?:\s|<\?[\s\S]*?\?>|<!--[\s\S]*?-->|<!DOCTYPE[^>]*>)*/, '');
60
+ const qname = /^<([^\s/>]+)/.exec(body)?.[1] ?? '';
61
+ const local = qname.slice(qname.indexOf(':') + 1);
62
+ return (local === 'document' && /urn:oasis:names:tc:opendocument:xmlns:office:1\.\d/i.test(text)) || (local === 'package' && text.includes(exports.PKG_NS));
63
+ }
64
+ /** Decodes base64 that arrives in pieces of any size, white space and line ends included. */
65
+ class Base64 {
66
+ rest = '';
67
+ push(t) {
68
+ const s = this.rest + t.replace(/\s+/g, '');
69
+ const n = s.length - (s.length % 4);
70
+ this.rest = s.slice(n);
71
+ return Buffer.from(s.slice(0, n), 'base64');
72
+ }
73
+ end() {
74
+ const r = this.rest;
75
+ this.rest = '';
76
+ return Buffer.from(r, 'base64');
77
+ }
78
+ }
79
+ /** Decoded bytes the handlers gather while the tokenizer runs, which go to their sinks between chunks. */
80
+ class Pending {
81
+ queue = [];
82
+ closing = [];
83
+ add(sink, b) {
84
+ if (b.length)
85
+ this.queue.push([sink, b]);
86
+ }
87
+ close(sink) {
88
+ this.closing.push(sink);
89
+ }
90
+ async flush() {
91
+ for (const [sink, b] of this.queue.splice(0))
92
+ await sink.write(b);
93
+ for (const sink of this.closing.splice(0))
94
+ await sink.close();
95
+ }
96
+ }
97
+ /**
98
+ * The rules of a flat OpenDocument file: the OpenDocument rules over its one part, and the files it holds in
99
+ * office:binary-data, such as pictures and OLE objects, which go to the plugins.
100
+ */
101
+ class FlatOdf {
102
+ run;
103
+ pending;
104
+ rules;
105
+ binaries = [];
106
+ stack = [];
107
+ current;
108
+ constructor(ctx, run, pending) {
109
+ this.run = run;
110
+ this.pending = pending;
111
+ this.rules = new odf_1.EntryRules(ctx, exports.FLAT_ODF_PART, new Map(), true);
112
+ }
113
+ start(el) {
114
+ this.rules.start(el);
115
+ if (!this.current && (0, odf_1.odfNamespace)(el.ns) === odf_1.NS.office && el.local === 'binary-data') {
116
+ const sink = new io_1.SpillSink(this.run.temp, this.run.threshold);
117
+ this.run.held.push(sink);
118
+ this.current = { binary: { parent: this.stack[this.stack.length - 1], data: el, sink }, b64: new Base64() };
119
+ }
120
+ this.stack.push(el);
121
+ }
122
+ text(t) {
123
+ if (this.current)
124
+ this.pending.add(this.current.binary.sink, this.current.b64.push(t));
125
+ else
126
+ this.rules.text(t);
127
+ }
128
+ end(el) {
129
+ this.stack.pop();
130
+ const c = this.current;
131
+ if (c && c.binary.data.index === el.index) {
132
+ this.pending.add(c.binary.sink, c.b64.end());
133
+ this.pending.close(c.binary.sink);
134
+ this.binaries.push(c.binary);
135
+ this.current = undefined;
136
+ }
137
+ this.rules.end(el);
138
+ }
139
+ }
140
+ /** The parts of a Flat OPC package. XML parts are cut from the text in a second reading, by their offsets. */
141
+ class FlatOpc {
142
+ run;
143
+ pending;
144
+ parts = [];
145
+ /** Where the pkg:package start tag begins, which is where the prolog ends. */
146
+ rootStart = 0;
147
+ part;
148
+ xmlDepth = 0;
149
+ b64;
150
+ scopes = [];
151
+ constructor(run, pending) {
152
+ this.run = run;
153
+ this.pending = pending;
154
+ }
155
+ start(el) {
156
+ this.scopes.push(el.namespaces);
157
+ if (el.depth === 1)
158
+ this.rootStart = el.start;
159
+ if (this.xmlDepth) {
160
+ const xml = this.part?.xml;
161
+ // The part's root inherits the namespaces in scope, and the ones it declares itself it does not need.
162
+ if (xml && el.depth === this.xmlDepth + 1)
163
+ for (const [prefix] of el.namespaces)
164
+ xml.inherited.delete(prefix);
165
+ return;
166
+ }
167
+ if (el.ns !== exports.PKG_NS)
168
+ return;
169
+ if (el.depth === 2 && el.local === 'part') {
170
+ const name = el.attributes.find(a => a.ns === exports.PKG_NS && a.local === 'name')?.value;
171
+ if (name === undefined || name.replace(/^\//, '') === '')
172
+ throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'A Flat OPC part has no name', { reason: 'a part without a name' });
173
+ const contentType = el.attributes.find(a => a.ns === exports.PKG_NS && a.local === 'contentType')?.value ?? '';
174
+ const extra = el.attributes.filter(a => !(a.ns === exports.PKG_NS && (a.local === 'name' || a.local === 'contentType'))).map((a) => [a.qname, a.value]);
175
+ this.part = { name: name.replace(/^\//, ''), contentType, extra };
176
+ }
177
+ else if (el.depth === 3 && this.part && el.local === 'xmlData') {
178
+ const inherited = new Map();
179
+ for (const scope of this.scopes)
180
+ for (const [prefix, uri] of scope)
181
+ inherited.set(prefix, uri);
182
+ this.part.xml = { start: el.end, end: el.end, inherited };
183
+ this.xmlDepth = el.depth;
184
+ }
185
+ else if (el.depth === 3 && this.part && el.local === 'binaryData') {
186
+ const sink = new io_1.SpillSink(this.run.temp, this.run.threshold);
187
+ this.run.held.push(sink);
188
+ this.part.binary = sink;
189
+ this.b64 = new Base64();
190
+ }
191
+ }
192
+ text(t) {
193
+ if (this.b64 && this.part?.binary)
194
+ this.pending.add(this.part.binary, this.b64.push(t));
195
+ }
196
+ end(el, start) {
197
+ this.scopes.pop();
198
+ if (this.xmlDepth === el.depth && this.part?.xml) {
199
+ this.part.xml.end = start;
200
+ this.xmlDepth = 0;
201
+ }
202
+ else if (this.b64 && el.depth === 3 && this.part?.binary) {
203
+ this.pending.add(this.part.binary, this.b64.end());
204
+ this.pending.close(this.part.binary);
205
+ this.b64 = undefined;
206
+ }
207
+ else if (el.depth === 2 && this.part) {
208
+ this.parts.push(this.part);
209
+ this.part = undefined;
210
+ }
211
+ }
212
+ }
213
+ /** Reads the root element and hands everything to the rules of the flat format it names. */
214
+ class FlatReader {
215
+ ctx;
216
+ pending;
217
+ odf;
218
+ opc;
219
+ format;
220
+ constructor(ctx, pending) {
221
+ this.ctx = ctx;
222
+ this.pending = pending;
223
+ }
224
+ start(el) {
225
+ if (el.depth === 1) {
226
+ if ((0, odf_1.odfNamespace)(el.ns) === odf_1.NS.office && el.local === 'document') {
227
+ const type = (0, odf_1.oattr)(el, odf_1.NS.office, 'mimetype') ?? '';
228
+ this.format = FLAT_ODF.get(type);
229
+ if (!this.format)
230
+ throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The flat OpenDocument media type is not one this package reads', { mediaType: (0, context_1.clip)(type) });
231
+ this.ctx.format = this.format;
232
+ this.odf = new FlatOdf(this.ctx, this.ctx.run, this.pending);
233
+ }
234
+ else if (el.ns === exports.PKG_NS && el.local === 'package') {
235
+ this.opc = new FlatOpc(this.ctx.run, this.pending);
236
+ }
237
+ else
238
+ throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The XML file is neither flat OpenDocument nor Flat OPC', { root: (0, context_1.clip)(el.qname), namespace: (0, context_1.clip)(el.ns) });
239
+ }
240
+ this.odf?.start(el);
241
+ this.opc?.start(el);
242
+ }
243
+ text(t) {
244
+ this.odf?.text(t);
245
+ this.opc?.text(t);
246
+ }
247
+ end(el, start) {
248
+ this.odf?.end(el);
249
+ this.opc?.end(el, start);
250
+ }
251
+ }
252
+ /** Base64 of everything a sink holds. */
253
+ async function base64Of(sink) {
254
+ const chunks = [];
255
+ for await (const c of sink.read())
256
+ chunks.push(Buffer.from(c));
257
+ return Buffer.concat(chunks).toString('base64');
258
+ }
259
+ /**
260
+ * The plugins' verdicts on the files a flat OpenDocument file holds. A file a plugin removed goes with the element
261
+ * that holds it, and one a plugin rewrote goes back in as base64. An OLE object a plugin kept stays, and one no plugin
262
+ * took gets the rule EMBEDDED/OLE_OBJECT gives it. A picture no plugin took stays, since no rule names it.
263
+ */
264
+ async function decideBinaries(ctx, odf) {
265
+ const plan = ctx.plan;
266
+ const decided = new Set();
267
+ let n = 0;
268
+ for (const b of odf.binaries) {
269
+ n++;
270
+ const parent = b.parent;
271
+ if (!parent)
272
+ continue;
273
+ const declaredType = (0, odf_1.oattr)(parent, odf_1.NS.draw, 'mime-type') ?? parent.attributes.find(a => a.ns === LOEXT_NS && a.local === 'mime-type')?.value;
274
+ const decision = await (0, contained_1.decideContained)(ctx, { location: `${exports.FLAT_ODF_PART} > ${parent.qname} ${n}`, declaredType, chunks: () => b.sink.read() });
275
+ const object = plan.objects.find(o => o.index === parent.index);
276
+ if (object) {
277
+ decided.add(parent.index);
278
+ if (decision?.keep) {
279
+ if (object.finding) {
280
+ object.finding.action = 'info';
281
+ ctx.run.superseded.add(object.finding);
282
+ }
283
+ }
284
+ else if (decision || object.strips)
285
+ plan.element(exports.FLAT_ODF_PART, parent.index, { remove: true });
286
+ }
287
+ else if (decision && !decision.keep)
288
+ plan.element(exports.FLAT_ODF_PART, parent.index, { remove: true });
289
+ if (decision?.keep && decision.replacement)
290
+ plan.element(exports.FLAT_ODF_PART, b.data.index, { content: await base64Of(decision.replacement) });
291
+ }
292
+ // An object with no data of its own gets its rule alone.
293
+ for (const o of plan.objects)
294
+ if (!decided.has(o.index) && o.strips)
295
+ plan.element(exports.FLAT_ODF_PART, o.index, { remove: true });
296
+ }
297
+ /** The ranges of the decoded text that `wanted` names, cut in one reading in order, and the text before `prolog`. */
298
+ async function cutText(src, size, deadline, prolog, wanted) {
299
+ const order = wanted.map((w, i) => ({ ...w, i })).sort((a, b) => a.start - b.start);
300
+ const texts = wanted.map(() => '');
301
+ let head = '';
302
+ let pos = 0;
303
+ let decoder;
304
+ let k = 0;
305
+ const take = (s) => {
306
+ if (pos < prolog)
307
+ head += s.slice(0, Math.max(0, prolog - pos));
308
+ for (let j = k; j < order.length && order[j].start < pos + s.length; j++) {
309
+ const w = order[j];
310
+ const from = Math.max(w.start, pos) - pos;
311
+ const to = Math.min(w.end, pos + s.length) - pos;
312
+ if (to > from)
313
+ texts[w.i] += s.slice(from, to);
314
+ }
315
+ pos += s.length;
316
+ while (k < order.length && order[k].end <= pos)
317
+ k++;
318
+ };
319
+ let first = true;
320
+ for await (const c of chunksOf(src, size, deadline)) {
321
+ if (first) {
322
+ first = false;
323
+ const found = (0, xml_1.detectEncoding)(c) ?? { encoding: 'utf-8', bom: 0 };
324
+ decoder = new node_util_1.TextDecoder(found.encoding, { fatal: true, ignoreBOM: true });
325
+ take(decoder.decode(c.subarray(found.bom), { stream: true }));
326
+ continue;
327
+ }
328
+ take(decoder.decode(c, { stream: true }));
329
+ }
330
+ if (decoder)
331
+ take(decoder.decode());
332
+ return { prolog: head, texts };
333
+ }
334
+ const escapeAttribute = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/"/g, '&quot;');
335
+ /** A part's XML, cut from a Flat OPC package, as a part of its own: with an XML declaration and the namespaces it inherited. */
336
+ function partXml(text, inherited) {
337
+ let body = text.trim();
338
+ if (inherited.size) {
339
+ // The root's start tag is the first "<" that does not open a comment or a processing instruction.
340
+ const m = /^(?:\s|<!--[\s\S]*?-->|<\?[\s\S]*?\?>)*<[^\s/>]+/.exec(body);
341
+ if (m) {
342
+ const decls = [...inherited].map(([prefix, uri]) => ` ${prefix ? `xmlns:${prefix}` : 'xmlns'}="${escapeAttribute(uri)}"`).join('');
343
+ body = body.slice(0, m[0].length) + decls + body.slice(m[0].length);
344
+ }
345
+ }
346
+ return Buffer.from(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${body}`, 'utf8');
347
+ }
348
+ /**
349
+ * Inspects a flat XML file: flat OpenDocument, read with the OpenDocument rules over its one part, or Flat OPC,
350
+ * which is unpacked into a package of its parts and read with the OOXML rules. Returns what the rewrite needs.
351
+ */
352
+ async function inspectFlat(ctx, src, size, zipLimits) {
353
+ const run = ctx.run;
354
+ const pctx = ctx.withPlan();
355
+ const pending = new Pending();
356
+ const reader = new FlatReader(pctx, pending);
357
+ // A finding about the document type comes before the root element, so the first bytes pick the name it gives.
358
+ const head = await src.read(0, Math.min(CHUNK, size));
359
+ const found = (0, xml_1.detectEncoding)(head) ?? { encoding: 'utf-8', bom: 0 };
360
+ const opcHead = new node_util_1.TextDecoder(found.encoding, { fatal: false }).decode(head.subarray(found.bom)).includes(exports.PKG_NS);
361
+ await pctx.parseXml(opcHead ? FLAT_OPC_PART : exports.FLAT_ODF_PART, chunksOf(src, size, ctx.settings.deadline), reader, false, pctx.plan, () => pending.flush());
362
+ if (reader.odf) {
363
+ await decideBinaries(pctx, reader.odf);
364
+ return { kind: 'flatodf', plan: pctx.plan };
365
+ }
366
+ const opc = reader.opc;
367
+ if (!opc)
368
+ return undefined;
369
+ const xmlParts = opc.parts.filter(p => p.xml);
370
+ const cut = await cutText(src, size, ctx.settings.deadline, opc.rootStart, xmlParts.map(p => p.xml ?? { start: 0, end: 0 }));
371
+ // The parts go into a package of their own, with a [Content_Types].xml that gives each its content type.
372
+ const packed = new io_1.SpillSink(run.temp, run.threshold);
373
+ run.held.push(packed);
374
+ const writer = new zip_writer_1.ZipWriter(packed);
375
+ const seen = new Set();
376
+ for (const p of opc.parts) {
377
+ const k = (0, zip_1.nameKey)(p.name);
378
+ if (seen.has(k) || k === (0, zip_1.nameKey)('[Content_Types].xml'))
379
+ throw new errors_1.StopError(types_1.OfficeCategory.Container, types_1.OfficeDetail.DuplicateName, `Two Flat OPC parts are named ${(0, context_1.clip)(p.name)}`, { name: (0, context_1.clip)(p.name) });
380
+ seen.add(k);
381
+ }
382
+ const types = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<Types xmlns="${ooxml_1.CONTENT_TYPES_NS}">${opc.parts.map(p => `<Override PartName="/${escapeAttribute(p.name)}" ContentType="${escapeAttribute(p.contentType)}"/>`).join('')}</Types>`;
383
+ await writer.add('[Content_Types].xml', Buffer.from(types, 'utf8'), { compress: false });
384
+ for (const p of opc.parts) {
385
+ (0, errors_1.checkTime)(ctx.settings.deadline);
386
+ let data = new Uint8Array(0);
387
+ if (p.xml)
388
+ data = partXml(cut.texts[xmlParts.indexOf(p)], p.xml.inherited);
389
+ else if (p.binary) {
390
+ const chunks = [];
391
+ for await (const c of p.binary.read())
392
+ chunks.push(Buffer.from(c));
393
+ data = Buffer.concat(chunks);
394
+ }
395
+ await writer.add(p.name, data, { compress: false });
396
+ }
397
+ await writer.finish();
398
+ await packed.close();
399
+ const packedSource = packed.source();
400
+ run.sources.push(packedSource);
401
+ const zip = await zip_1.ZipArchive.open(packedSource, zipLimits, ctx.settings.deadline);
402
+ const zctx = ctx.withZip(zip);
403
+ const contentTypes = zip.get('[Content_Types].xml');
404
+ if (!contentTypes)
405
+ return undefined;
406
+ await (0, ooxml_1.inspectOoxml)(zctx, contentTypes);
407
+ const inner = ctx.format;
408
+ ctx.format = exports.FLAT_OPC_FORMAT;
409
+ if (!inner)
410
+ return undefined;
411
+ const layout = {
412
+ instructions: [...cut.prolog.matchAll(/<\?mso-application\s[^?]*\?>/g)].map(m => m[0]),
413
+ extra: new Map(opc.parts.map(p => [(0, zip_1.nameKey)(p.name), p.extra])),
414
+ };
415
+ return { kind: 'flatopc', zip, plan: zctx.plan, format: inner, layout };
416
+ }
417
+ /** Writes a flat OpenDocument file without what the plan removes. */
418
+ async function writeFlatOdf(run, src, plan, limits, sink) {
419
+ const size = await src.size();
420
+ const r = await (0, rewrite_xml_1.partEdits)(chunksOf(src, size, run.deadline), exports.FLAT_ODF_PART, limits, false, plan.parts.get(exports.FLAT_ODF_PART), { element: () => undefined });
421
+ for await (const c of (0, rewrite_xml_1.splice)(chunksOf(src, size, run.deadline), r.edits))
422
+ await sink.write(c);
423
+ }
424
+ /** The content types of a package's parts, by key, from its [Content_Types].xml. */
425
+ async function contentTypesOf(zip, limits) {
426
+ const defaults = new Map();
427
+ const overrides = new Map();
428
+ const entry = zip.get('[Content_Types].xml');
429
+ if (entry) {
430
+ const t = new xml_1.XmlTokenizer({
431
+ start(el) {
432
+ if (el.ns.toLowerCase() !== ooxml_1.CONTENT_TYPES_NS)
433
+ return;
434
+ const type = (0, xml_1.attr)(el, '', 'ContentType') ?? '';
435
+ if (el.local === 'Default')
436
+ defaults.set(((0, xml_1.attr)(el, '', 'Extension') ?? '').toLowerCase(), type);
437
+ else if (el.local === 'Override')
438
+ overrides.set((0, zip_1.nameKey)(((0, xml_1.attr)(el, '', 'PartName') ?? '').replace(/^\//, '')), type);
439
+ },
440
+ }, limits, entry.name);
441
+ for await (const c of zip.read(entry))
442
+ t.write(c);
443
+ t.end();
444
+ }
445
+ return name => overrides.get((0, zip_1.nameKey)(name)) ?? defaults.get(/\.([^./]*)$/.exec(name)?.[1]?.toLowerCase() ?? '') ?? '';
446
+ }
447
+ /** A part's XML as it goes inside pkg:xmlData: decoded, without its byte order mark and XML declaration. */
448
+ function embeddedXml(bytes) {
449
+ const found = (0, xml_1.detectEncoding)(bytes) ?? { encoding: 'utf-8', bom: 0 };
450
+ const text = new node_util_1.TextDecoder(found.encoding, { fatal: true }).decode(bytes.subarray(found.bom));
451
+ return text.replace(/^\s*<\?xml[\s\S]*?\?>\s*/, '');
452
+ }
453
+ /**
454
+ * Writes a Flat OPC package without what the plan removes: the package is rewritten as any OOXML package is, and its
455
+ * parts are written back into one XML file, each with its name, content type and attributes.
456
+ */
457
+ async function writeFlatOpc(run, output, limits, zipLimits, sink) {
458
+ const packed = new io_1.SpillSink(run.temp, run.threshold);
459
+ run.held.push(packed);
460
+ await (0, rewrite_zip_1.writePackage)(run, output.zip, output.plan, output.format, limits, packed);
461
+ await packed.close();
462
+ const packedSource = packed.source();
463
+ run.sources.push(packedSource);
464
+ const zip = await zip_1.ZipArchive.open(packedSource, zipLimits, run.deadline);
465
+ const typeOf = await contentTypesOf(zip, limits);
466
+ const put = (s) => sink.write(Buffer.from(s, 'utf8'));
467
+ await put(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${output.layout.instructions.map(i => `${i}\n`).join('')}<pkg:package xmlns:pkg="${exports.PKG_NS}">`);
468
+ for (const e of zip.entries) {
469
+ (0, errors_1.checkTime)(run.deadline);
470
+ if (e.directory || e.name === '[Content_Types].xml')
471
+ continue;
472
+ const chunks = [];
473
+ for await (const c of zip.read(e))
474
+ chunks.push(Buffer.from(c));
475
+ const bytes = Buffer.concat(chunks);
476
+ const type = typeOf(e.name);
477
+ const extra = (output.layout.extra.get((0, zip_1.nameKey)(e.name)) ?? []).map(([q, v]) => ` ${q}="${escapeAttribute(v)}"`).join('');
478
+ await put(`<pkg:part pkg:name="/${escapeAttribute(e.name)}" pkg:contentType="${escapeAttribute(type)}"${extra}>`);
479
+ if ((0, ooxml_1.isXmlType)(type.toLowerCase()) || type.toLowerCase().endsWith('xml'))
480
+ await put(`<pkg:xmlData>${embeddedXml(bytes)}</pkg:xmlData>`);
481
+ else
482
+ await put(`<pkg:binaryData>${(bytes.toString('base64').match(/.{1,76}/g) ?? []).join('\n')}</pkg:binaryData>`);
483
+ await put('</pkg:part>');
484
+ }
485
+ await put('</pkg:package>');
486
+ }
package/dist/formats.d.ts CHANGED
@@ -6,5 +6,12 @@ export interface Format {
6
6
  }
7
7
  /** OOXML formats by the content type of the main part, lowercased. */
8
8
  export declare const OOXML_FORMATS: Map<string, Format>;
9
+ /** The content type of an XPS package's FixedDocumentSequence, which XPS and OpenXPS share. */
10
+ export declare const XPS_SEQUENCE = "application/vnd.ms-package.xps-fixeddocumentsequence+xml";
11
+ /** XPS formats, told apart by the relationship type that names the FixedDocumentSequence. */
12
+ export declare const XPS_FORMATS: {
13
+ xps: Format;
14
+ oxps: Format;
15
+ };
9
16
  /** OpenDocument formats by the media type in the mimetype entry or the manifest, exactly as written. */
10
17
  export declare const ODF_FORMATS: Map<string, Format>;
package/dist/formats.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ODF_FORMATS = exports.OOXML_FORMATS = void 0;
3
+ exports.ODF_FORMATS = exports.XPS_FORMATS = exports.XPS_SEQUENCE = exports.OOXML_FORMATS = void 0;
4
4
  const f = (format, mimeType) => ({ format, mimeType });
5
5
  /** OOXML formats by the content type of the main part, lowercased. */
6
6
  exports.OOXML_FORMATS = new Map([
@@ -30,6 +30,10 @@ exports.OOXML_FORMATS = new Map([
30
30
  ['application/vnd.ms-visio.stencil.main+xml', f('vssx', 'application/vnd.ms-visio.stencil')],
31
31
  ['application/vnd.ms-visio.stencil.macroenabled.main+xml', f('vssm', 'application/vnd.ms-visio.stencil.macroEnabled.12')],
32
32
  ]);
33
+ /** The content type of an XPS package's FixedDocumentSequence, which XPS and OpenXPS share. */
34
+ exports.XPS_SEQUENCE = 'application/vnd.ms-package.xps-fixeddocumentsequence+xml';
35
+ /** XPS formats, told apart by the relationship type that names the FixedDocumentSequence. */
36
+ exports.XPS_FORMATS = { xps: f('xps', 'application/vnd.ms-xpsdocument'), oxps: f('oxps', 'application/oxps') };
33
37
  /** OpenDocument formats by the media type in the mimetype entry or the manifest, exactly as written. */
34
38
  exports.ODF_FORMATS = new Map([
35
39
  ['text', 'odt'],
@@ -8,6 +8,33 @@ export declare class Quotes {
8
8
  /** Takes the next character and tells whether it stands outside quotes. */
9
9
  outside(c: string): boolean;
10
10
  }
11
+ /** What a function a formula calls does outside the file, or undefined when it does nothing there. */
12
+ export declare function riskyFunction(name: string): 'fetch' | 'run' | undefined;
13
+ /** The functions, outside quotes, that a formula's text calls and that reach outside the file, as its text arrives. */
14
+ export declare class FunctionCalls {
15
+ private readonly quotes;
16
+ private word;
17
+ private spaced;
18
+ readonly found: Map<string, "fetch" | "run">;
19
+ write(t: string): void;
20
+ }
21
+ /** The functions a formula's whole text calls that reach outside the file. */
22
+ export declare function formulaCalls(formula: string): Map<string, 'fetch' | 'run'>;
23
+ /** A name a BIFF formula token refers to: a defined name by index, or an external name through an XTI. */
24
+ export type BiffNameRef = {
25
+ kind: 'name';
26
+ index: number;
27
+ } | {
28
+ kind: 'namex';
29
+ ixti: number;
30
+ index: number;
31
+ };
32
+ /**
33
+ * The functions a BIFF8 formula (MS-XLS 2.5.198) calls that reach outside the file: built-in ones by index, and
34
+ * add-in and newer ones by the name the formula names them with, such as `_xlfn.WEBSERVICE`. The walk stops at a
35
+ * token it does not know the size of.
36
+ */
37
+ export declare function biffCalls(rgce: Uint8Array, nameOf: (ref: BiffNameRef) => string | undefined): Map<string, 'fetch' | 'run'>;
11
38
  /** Whether an OpenDocument formula calls the DDE function. */
12
39
  export declare function callsDde(formula: string): boolean;
13
40
  /** One word of a Word field code. A switch is an unquoted word that starts with a backslash. */