@patrickdk77/office-defuse 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +211 -43
  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/detect.d.ts +13 -0
  11. package/dist/detect.js +83 -0
  12. package/dist/engine.d.ts +66 -1
  13. package/dist/engine.js +290 -35
  14. package/dist/errors.d.ts +17 -0
  15. package/dist/errors.js +27 -1
  16. package/dist/fields.d.ts +20 -13
  17. package/dist/fields.js +65 -23
  18. package/dist/findings.d.ts +2 -1
  19. package/dist/findings.js +17 -1
  20. package/dist/flat.d.ts +33 -0
  21. package/dist/flat.js +555 -0
  22. package/dist/formats.d.ts +9 -0
  23. package/dist/formats.js +7 -1
  24. package/dist/formulas.d.ts +27 -0
  25. package/dist/formulas.js +157 -1
  26. package/dist/index.d.ts +4 -2
  27. package/dist/index.js +11 -1
  28. package/dist/index.mjs +1 -1
  29. package/dist/io.d.ts +62 -1
  30. package/dist/io.js +245 -1
  31. package/dist/links.d.ts +27 -0
  32. package/dist/links.js +106 -1
  33. package/dist/msg.d.ts +3 -2
  34. package/dist/msg.js +172 -18
  35. package/dist/objects.d.ts +8 -5
  36. package/dist/objects.js +90 -20
  37. package/dist/odf.d.ts +87 -1
  38. package/dist/odf.js +204 -76
  39. package/dist/offcrypto.d.ts +24 -1
  40. package/dist/offcrypto.js +308 -69
  41. package/dist/ole.d.ts +19 -2
  42. package/dist/ole.js +98 -58
  43. package/dist/oleobj.d.ts +11 -0
  44. package/dist/oleobj.js +20 -3
  45. package/dist/ooxml.d.ts +108 -2
  46. package/dist/ooxml.js +392 -109
  47. package/dist/package.d.ts +14 -0
  48. package/dist/package.js +23 -7
  49. package/dist/plan.d.ts +104 -0
  50. package/dist/plan.js +73 -0
  51. package/dist/plugins.d.ts +8 -0
  52. package/dist/plugins.js +35 -0
  53. package/dist/ppt.js +45 -5
  54. package/dist/rewrite-cfb.d.ts +14 -0
  55. package/dist/rewrite-cfb.js +52 -0
  56. package/dist/rewrite-rtf.d.ts +9 -0
  57. package/dist/rewrite-rtf.js +45 -0
  58. package/dist/rewrite-xml.d.ts +36 -0
  59. package/dist/rewrite-xml.js +341 -0
  60. package/dist/rewrite-zip.d.ts +8 -0
  61. package/dist/rewrite-zip.js +498 -0
  62. package/dist/rtf.js +216 -42
  63. package/dist/rtfcomp.d.ts +7 -0
  64. package/dist/rtfcomp.js +80 -0
  65. package/dist/score.js +1 -1
  66. package/dist/sniff.d.ts +5 -0
  67. package/dist/sniff.js +38 -0
  68. package/dist/types.d.ts +66 -6
  69. package/dist/types.js +14 -0
  70. package/dist/vsd.d.ts +13 -0
  71. package/dist/vsd.js +689 -0
  72. package/dist/word.d.ts +4 -4
  73. package/dist/word.js +70 -23
  74. package/dist/xls.d.ts +10 -4
  75. package/dist/xls.js +58 -10
  76. package/dist/xml.d.ts +30 -0
  77. package/dist/xml.js +45 -23
  78. package/dist/zip-writer.d.ts +4 -0
  79. package/dist/zip-writer.js +20 -7
  80. package/dist/zip.d.ts +11 -2
  81. package/dist/zip.js +67 -6
  82. package/package.json +4 -1
package/dist/flat.js ADDED
@@ -0,0 +1,555 @@
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.inspectFlat = inspectFlat;
6
+ exports.nameFlat = nameFlat;
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
+ /** Decodes base64 that arrives in pieces of any size, white space and line ends included. */
55
+ class Base64 {
56
+ rest = '';
57
+ push(t) {
58
+ const s = this.rest + t.replace(/\s+/g, '');
59
+ const n = s.length - (s.length % 4);
60
+ this.rest = s.slice(n);
61
+ return Buffer.from(s.slice(0, n), 'base64');
62
+ }
63
+ end() {
64
+ const r = this.rest;
65
+ this.rest = '';
66
+ return Buffer.from(r, 'base64');
67
+ }
68
+ }
69
+ /** Decoded bytes the handlers gather while the tokenizer runs, which go to their sinks between chunks. */
70
+ class Pending {
71
+ queue = [];
72
+ closing = [];
73
+ add(sink, b) {
74
+ if (b.length)
75
+ this.queue.push([sink, b]);
76
+ }
77
+ close(sink) {
78
+ this.closing.push(sink);
79
+ }
80
+ async flush() {
81
+ for (const [sink, b] of this.queue.splice(0))
82
+ await sink.write(b);
83
+ for (const sink of this.closing.splice(0))
84
+ await sink.close();
85
+ }
86
+ }
87
+ /**
88
+ * The rules of a flat OpenDocument file: the OpenDocument rules over its one part, and the files it holds in
89
+ * office:binary-data, such as pictures and OLE objects, which go to the plugins.
90
+ */
91
+ class FlatOdf {
92
+ run;
93
+ pending;
94
+ rules;
95
+ binaries = [];
96
+ stack = [];
97
+ current;
98
+ constructor(ctx, run, pending) {
99
+ this.run = run;
100
+ this.pending = pending;
101
+ this.rules = new odf_1.EntryRules(ctx, exports.FLAT_ODF_PART, new Map(), true);
102
+ }
103
+ start(el) {
104
+ this.rules.start(el);
105
+ if (!this.current && (0, odf_1.odfNamespace)(el.ns) === odf_1.NS.office && el.local === 'binary-data') {
106
+ const sink = new io_1.SpillSink(this.run.temp, this.run.threshold);
107
+ this.run.held.push(sink);
108
+ this.current = { binary: { parent: this.stack[this.stack.length - 1], data: el, sink }, b64: new Base64() };
109
+ }
110
+ this.stack.push(el);
111
+ }
112
+ text(t) {
113
+ if (this.current)
114
+ this.pending.add(this.current.binary.sink, this.current.b64.push(t));
115
+ else
116
+ this.rules.text(t);
117
+ }
118
+ end(el) {
119
+ this.stack.pop();
120
+ const c = this.current;
121
+ if (c && c.binary.data.index === el.index) {
122
+ this.pending.add(c.binary.sink, c.b64.end());
123
+ this.pending.close(c.binary.sink);
124
+ this.binaries.push(c.binary);
125
+ this.current = undefined;
126
+ }
127
+ this.rules.end(el);
128
+ }
129
+ }
130
+ /** The parts of a Flat OPC package. XML parts are cut from the text in a second reading, by their offsets. */
131
+ class FlatOpc {
132
+ run;
133
+ pending;
134
+ keep;
135
+ parts = [];
136
+ /** Where the pkg:package start tag begins, which is where the prolog ends. */
137
+ rootStart = 0;
138
+ part;
139
+ xmlDepth = 0;
140
+ b64;
141
+ scopes = [];
142
+ constructor(run, pending,
143
+ /** The parts whose content is kept, by name. The others keep only their name, content type and attributes. */
144
+ keep = () => true) {
145
+ this.run = run;
146
+ this.pending = pending;
147
+ this.keep = keep;
148
+ }
149
+ start(el) {
150
+ this.scopes.push(el.namespaces);
151
+ if (el.depth === 1)
152
+ this.rootStart = el.start;
153
+ if (this.xmlDepth) {
154
+ const xml = this.part?.xml;
155
+ // The part's root inherits the namespaces in scope, and the ones it declares itself it does not need.
156
+ if (xml && el.depth === this.xmlDepth + 1)
157
+ for (const [prefix] of el.namespaces)
158
+ xml.inherited.delete(prefix);
159
+ return;
160
+ }
161
+ if (el.ns !== exports.PKG_NS)
162
+ return;
163
+ if (el.depth === 2 && el.local === 'part') {
164
+ const name = el.attributes.find(a => a.ns === exports.PKG_NS && a.local === 'name')?.value;
165
+ if (name === undefined || name.replace(/^\//, '') === '')
166
+ 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' });
167
+ const contentType = el.attributes.find(a => a.ns === exports.PKG_NS && a.local === 'contentType')?.value ?? '';
168
+ const extra = el.attributes.filter(a => !(a.ns === exports.PKG_NS && (a.local === 'name' || a.local === 'contentType'))).map((a) => [a.qname, a.value]);
169
+ this.part = { name: name.replace(/^\//, ''), contentType, extra };
170
+ }
171
+ else if (el.depth === 3 && this.part && el.local === 'xmlData') {
172
+ const inherited = new Map();
173
+ for (const scope of this.scopes)
174
+ for (const [prefix, uri] of scope)
175
+ inherited.set(prefix, uri);
176
+ this.part.xml = { start: el.end, end: el.end, inherited };
177
+ this.xmlDepth = el.depth;
178
+ }
179
+ else if (el.depth === 3 && this.part && el.local === 'binaryData' && this.keep(this.part.name)) {
180
+ const sink = new io_1.SpillSink(this.run.temp, this.run.threshold);
181
+ this.run.held.push(sink);
182
+ this.part.binary = sink;
183
+ this.b64 = new Base64();
184
+ }
185
+ }
186
+ text(t) {
187
+ if (this.b64 && this.part?.binary)
188
+ this.pending.add(this.part.binary, this.b64.push(t));
189
+ }
190
+ end(el, start) {
191
+ this.scopes.pop();
192
+ if (this.xmlDepth === el.depth && this.part?.xml) {
193
+ this.part.xml.end = start;
194
+ this.xmlDepth = 0;
195
+ }
196
+ else if (this.b64 && el.depth === 3 && this.part?.binary) {
197
+ this.pending.add(this.part.binary, this.b64.end());
198
+ this.pending.close(this.part.binary);
199
+ this.b64 = undefined;
200
+ }
201
+ else if (el.depth === 2 && this.part) {
202
+ this.parts.push(this.part);
203
+ this.part = undefined;
204
+ }
205
+ }
206
+ }
207
+ /**
208
+ * The format a flat file's root element names: a flat OpenDocument format by its office:mimetype, or 'opc' for a Flat
209
+ * OPC package, whose format the package its parts make names. Throws for any other root, or a flat OpenDocument media
210
+ * type this package does not read.
211
+ */
212
+ function flatRoot(el) {
213
+ if ((0, odf_1.odfNamespace)(el.ns) === odf_1.NS.office && el.local === 'document') {
214
+ const type = (0, odf_1.oattr)(el, odf_1.NS.office, 'mimetype') ?? '';
215
+ const format = FLAT_ODF.get(type);
216
+ if (!format)
217
+ 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) });
218
+ return format;
219
+ }
220
+ if (el.ns === exports.PKG_NS && el.local === 'package')
221
+ return 'opc';
222
+ 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) });
223
+ }
224
+ /** Reads the root element and hands everything to the rules of the flat format it names. */
225
+ class FlatReader {
226
+ ctx;
227
+ pending;
228
+ odf;
229
+ opc;
230
+ format;
231
+ constructor(ctx, pending) {
232
+ this.ctx = ctx;
233
+ this.pending = pending;
234
+ }
235
+ start(el) {
236
+ if (el.depth === 1) {
237
+ const format = flatRoot(el);
238
+ if (format === 'opc')
239
+ this.opc = new FlatOpc(this.ctx.run, this.pending);
240
+ else {
241
+ this.format = format;
242
+ this.ctx.format = format;
243
+ this.odf = new FlatOdf(this.ctx, this.ctx.run, this.pending);
244
+ }
245
+ }
246
+ this.odf?.start(el);
247
+ this.opc?.start(el);
248
+ }
249
+ text(t) {
250
+ this.odf?.text(t);
251
+ this.opc?.text(t);
252
+ }
253
+ end(el, start) {
254
+ this.odf?.end(el);
255
+ this.opc?.end(el, start);
256
+ }
257
+ }
258
+ /** Base64 of everything a sink holds. */
259
+ async function base64Of(sink) {
260
+ const chunks = [];
261
+ for await (const c of sink.read())
262
+ chunks.push(Buffer.from(c));
263
+ return Buffer.concat(chunks).toString('base64');
264
+ }
265
+ /**
266
+ * The plugins' verdicts on the files a flat OpenDocument file holds. A file a plugin removed goes with the element
267
+ * that holds it, and one a plugin rewrote goes back in as base64. An OLE object a plugin kept stays, and one no plugin
268
+ * took gets the rule EMBEDDED/OLE_OBJECT gives it. A picture no plugin took stays, since no rule names it.
269
+ */
270
+ async function decideBinaries(ctx, odf) {
271
+ const plan = ctx.plan;
272
+ const decided = new Set();
273
+ let n = 0;
274
+ for (const b of odf.binaries) {
275
+ n++;
276
+ const parent = b.parent;
277
+ if (!parent)
278
+ continue;
279
+ 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;
280
+ const decision = await (0, contained_1.decideContained)(ctx, { location: `${exports.FLAT_ODF_PART} > ${parent.qname} ${n}`, declaredType, chunks: () => b.sink.read() });
281
+ const object = plan.objects.find(o => o.index === parent.index);
282
+ if (object) {
283
+ decided.add(parent.index);
284
+ if (decision?.keep) {
285
+ if (object.finding) {
286
+ object.finding.action = 'info';
287
+ ctx.run.superseded.add(object.finding);
288
+ }
289
+ }
290
+ else if (decision || object.strips)
291
+ plan.element(exports.FLAT_ODF_PART, parent.index, { remove: true });
292
+ }
293
+ else if (decision && !decision.keep)
294
+ plan.element(exports.FLAT_ODF_PART, parent.index, { remove: true });
295
+ if (decision?.keep && decision.replacement)
296
+ plan.element(exports.FLAT_ODF_PART, b.data.index, { content: await base64Of(decision.replacement) });
297
+ }
298
+ // An object with no data of its own gets its rule alone.
299
+ for (const o of plan.objects)
300
+ if (!decided.has(o.index) && o.strips)
301
+ plan.element(exports.FLAT_ODF_PART, o.index, { remove: true });
302
+ }
303
+ /** The ranges of the decoded text that `wanted` names, cut in one reading in order, and the text before `prolog`. */
304
+ async function cutText(src, size, deadline, prolog, wanted) {
305
+ const order = wanted.map((w, i) => ({ ...w, i })).sort((a, b) => a.start - b.start);
306
+ const texts = wanted.map(() => '');
307
+ let head = '';
308
+ let pos = 0;
309
+ let decoder;
310
+ let k = 0;
311
+ const take = (s) => {
312
+ if (pos < prolog)
313
+ head += s.slice(0, Math.max(0, prolog - pos));
314
+ for (let j = k; j < order.length && order[j].start < pos + s.length; j++) {
315
+ const w = order[j];
316
+ const from = Math.max(w.start, pos) - pos;
317
+ const to = Math.min(w.end, pos + s.length) - pos;
318
+ if (to > from)
319
+ texts[w.i] += s.slice(from, to);
320
+ }
321
+ pos += s.length;
322
+ while (k < order.length && order[k].end <= pos)
323
+ k++;
324
+ };
325
+ let first = true;
326
+ for await (const c of chunksOf(src, size, deadline)) {
327
+ if (first) {
328
+ first = false;
329
+ const found = (0, xml_1.detectEncoding)(c) ?? { encoding: 'utf-8', bom: 0 };
330
+ decoder = new node_util_1.TextDecoder(found.encoding, { fatal: true, ignoreBOM: true });
331
+ take(decoder.decode(c.subarray(found.bom), { stream: true }));
332
+ continue;
333
+ }
334
+ take(decoder.decode(c, { stream: true }));
335
+ }
336
+ if (decoder)
337
+ take(decoder.decode());
338
+ return { prolog: head, texts };
339
+ }
340
+ const escapeAttribute = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/"/g, '&quot;');
341
+ /** 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. */
342
+ function partXml(text, inherited) {
343
+ let body = text.trim();
344
+ if (inherited.size) {
345
+ // The root's start tag is the first "<" that does not open a comment or a processing instruction.
346
+ const m = /^(?:\s|<!--[\s\S]*?-->|<\?[\s\S]*?\?>)*<[^\s/>]+/.exec(body);
347
+ if (m) {
348
+ const decls = [...inherited].map(([prefix, uri]) => ` ${prefix ? `xmlns:${prefix}` : 'xmlns'}="${escapeAttribute(uri)}"`).join('');
349
+ body = body.slice(0, m[0].length) + decls + body.slice(m[0].length);
350
+ }
351
+ }
352
+ return Buffer.from(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${body}`, 'utf8');
353
+ }
354
+ /**
355
+ * Inspects a flat XML file: flat OpenDocument, read with the OpenDocument rules over its one part, or Flat OPC,
356
+ * which is unpacked into a package of its parts and read with the OOXML rules. Returns what the rewrite needs.
357
+ */
358
+ async function inspectFlat(ctx, src, size, zipLimits) {
359
+ const pctx = ctx.withPlan();
360
+ const pending = new Pending();
361
+ const reader = new FlatReader(pctx, pending);
362
+ // A finding about the document type comes before the root element, so the first bytes pick the name it gives.
363
+ const head = await src.read(0, Math.min(CHUNK, size));
364
+ const found = (0, xml_1.detectEncoding)(head) ?? { encoding: 'utf-8', bom: 0 };
365
+ const opcHead = new node_util_1.TextDecoder(found.encoding, { fatal: false }).decode(head.subarray(found.bom)).includes(exports.PKG_NS);
366
+ await pctx.parseXml(opcHead ? FLAT_OPC_PART : exports.FLAT_ODF_PART, chunksOf(src, size, ctx.settings.deadline), reader, false, pctx.plan, () => pending.flush());
367
+ if (reader.odf) {
368
+ await decideBinaries(pctx, reader.odf);
369
+ return { kind: 'flatodf', plan: pctx.plan };
370
+ }
371
+ const opc = reader.opc;
372
+ if (!opc)
373
+ return undefined;
374
+ const { zip, prolog } = await packFlatOpc(ctx, src, size, opc, zipLimits);
375
+ const zctx = ctx.withZip(zip);
376
+ const contentTypes = zip.get('[Content_Types].xml');
377
+ if (!contentTypes)
378
+ return undefined;
379
+ const inner = await (0, ooxml_1.inspectOoxml)(zctx, contentTypes, exports.FLAT_OPC_FORMAT);
380
+ const layout = {
381
+ instructions: [...prolog.matchAll(/<\?mso-application\s[^?]*\?>/g)].map(m => m[0]),
382
+ extra: new Map(opc.parts.map(p => [(0, zip_1.nameKey)(p.name), p.extra])),
383
+ };
384
+ return { kind: 'flatopc', zip, plan: zctx.plan, format: inner, layout };
385
+ }
386
+ /**
387
+ * Packs the parts of a Flat OPC package into a ZIP archive, with a [Content_Types].xml that gives each its content
388
+ * type, and returns it with the text before the package element. A part the reader did not keep goes in empty.
389
+ */
390
+ async function packFlatOpc(ctx, src, size, opc, zipLimits) {
391
+ const run = ctx.run;
392
+ const xmlParts = opc.parts.filter(p => p.xml && opc.keep(p.name));
393
+ const cut = await cutText(src, size, ctx.settings.deadline, opc.rootStart, xmlParts.map(p => p.xml ?? { start: 0, end: 0 }));
394
+ // The parts go into a package of their own, with a [Content_Types].xml that gives each its content type.
395
+ const packed = new io_1.SpillSink(run.temp, run.threshold);
396
+ run.held.push(packed);
397
+ const writer = new zip_writer_1.ZipWriter(packed);
398
+ const seen = new Set();
399
+ for (const p of opc.parts) {
400
+ const k = (0, zip_1.nameKey)(p.name);
401
+ if (seen.has(k) || k === (0, zip_1.nameKey)('[Content_Types].xml'))
402
+ 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) });
403
+ seen.add(k);
404
+ }
405
+ 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>`;
406
+ await writer.add('[Content_Types].xml', Buffer.from(types, 'utf8'), { compress: false });
407
+ for (const p of opc.parts) {
408
+ (0, errors_1.checkTime)(ctx.settings.deadline);
409
+ let data = new Uint8Array(0);
410
+ const cutAt = xmlParts.indexOf(p);
411
+ if (p.xml && cutAt >= 0)
412
+ data = partXml(cut.texts[cutAt], p.xml.inherited);
413
+ else if (p.binary) {
414
+ const chunks = [];
415
+ for await (const c of p.binary.read())
416
+ chunks.push(Buffer.from(c));
417
+ data = Buffer.concat(chunks);
418
+ }
419
+ await writer.add(p.name, data, { compress: false });
420
+ }
421
+ await writer.finish();
422
+ await packed.close();
423
+ const packedSource = packed.source();
424
+ run.sources.push(packedSource);
425
+ return { zip: await zip_1.ZipArchive.open(packedSource, zipLimits, ctx.settings.deadline), prolog: cut.prolog };
426
+ }
427
+ /** Stops the reading of a flat file once its root has named a flat OpenDocument format. */
428
+ const ROOT_NAMED = new Error('The root names the format');
429
+ /**
430
+ * Reads a flat file as far as naming its format needs: its root element, and for a Flat OPC package, every part's
431
+ * name, content type and attributes, with the content of the package relationships alone.
432
+ */
433
+ class FlatNamer {
434
+ run;
435
+ pending;
436
+ format;
437
+ opc;
438
+ constructor(run, pending) {
439
+ this.run = run;
440
+ this.pending = pending;
441
+ }
442
+ start(el) {
443
+ if (el.depth === 1) {
444
+ const format = flatRoot(el);
445
+ if (format !== 'opc') {
446
+ this.format = format;
447
+ throw ROOT_NAMED;
448
+ }
449
+ this.opc = new FlatOpc(this.run, this.pending, name => (0, ooxml_1.relsSource)(name) === '');
450
+ }
451
+ this.opc?.start(el);
452
+ }
453
+ text(t) {
454
+ this.opc?.text(t);
455
+ }
456
+ end(el, start) {
457
+ this.opc?.end(el, start);
458
+ }
459
+ }
460
+ /**
461
+ * Names a flat file's format as its inspection does, without the rules: flat OpenDocument by its root element, and a
462
+ * Flat OPC package once the package its parts make names an OOXML format.
463
+ */
464
+ async function nameFlat(ctx, src, size, zipLimits) {
465
+ const pending = new Pending();
466
+ const namer = new FlatNamer(ctx.run, pending);
467
+ try {
468
+ await ctx.parseXml(FLAT_OPC_PART, chunksOf(src, size, ctx.settings.deadline), namer, false, undefined, () => pending.flush());
469
+ }
470
+ catch (e) {
471
+ if (e !== ROOT_NAMED)
472
+ throw e;
473
+ }
474
+ if (namer.format)
475
+ return namer.format;
476
+ const opc = namer.opc;
477
+ if (!opc)
478
+ throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The XML file has no root element', { reason: 'no root element' });
479
+ const { zip } = await packFlatOpc(ctx, src, size, opc, zipLimits);
480
+ const contentTypes = zip.get('[Content_Types].xml');
481
+ if (!contentTypes)
482
+ throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The Flat OPC package has no [Content_Types].xml', { reason: 'no [Content_Types].xml' });
483
+ await (0, ooxml_1.nameOoxml)(ctx.withZip(zip), contentTypes);
484
+ return exports.FLAT_OPC_FORMAT;
485
+ }
486
+ /** Writes a flat OpenDocument file without what the plan removes. */
487
+ async function writeFlatOdf(run, src, plan, limits, sink) {
488
+ const size = await src.size();
489
+ 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 });
490
+ for await (const c of (0, rewrite_xml_1.splice)(chunksOf(src, size, run.deadline), r.edits))
491
+ await sink.write(c);
492
+ }
493
+ /** The content types of a package's parts, by key, from its [Content_Types].xml. */
494
+ async function contentTypesOf(zip, limits) {
495
+ const defaults = new Map();
496
+ const overrides = new Map();
497
+ const entry = zip.get('[Content_Types].xml');
498
+ if (entry) {
499
+ const t = new xml_1.XmlTokenizer({
500
+ start(el) {
501
+ if (el.ns.toLowerCase() !== ooxml_1.CONTENT_TYPES_NS)
502
+ return;
503
+ const type = (0, xml_1.attr)(el, '', 'ContentType') ?? '';
504
+ if (el.local === 'Default')
505
+ defaults.set(((0, xml_1.attr)(el, '', 'Extension') ?? '').toLowerCase(), type);
506
+ else if (el.local === 'Override')
507
+ overrides.set((0, zip_1.nameKey)(((0, xml_1.attr)(el, '', 'PartName') ?? '').replace(/^\//, '')), type);
508
+ },
509
+ }, limits, entry.name);
510
+ for await (const c of zip.read(entry))
511
+ t.write(c);
512
+ t.end();
513
+ }
514
+ return name => overrides.get((0, zip_1.nameKey)(name)) ?? defaults.get(/\.([^./]*)$/.exec(name)?.[1]?.toLowerCase() ?? '') ?? '';
515
+ }
516
+ /** A part's XML as it goes inside pkg:xmlData: decoded, without its byte order mark and XML declaration. */
517
+ function embeddedXml(bytes) {
518
+ const found = (0, xml_1.detectEncoding)(bytes) ?? { encoding: 'utf-8', bom: 0 };
519
+ const text = new node_util_1.TextDecoder(found.encoding, { fatal: true }).decode(bytes.subarray(found.bom));
520
+ return text.replace(/^\s*<\?xml[\s\S]*?\?>\s*/, '');
521
+ }
522
+ /**
523
+ * Writes a Flat OPC package without what the plan removes: the package is rewritten as any OOXML package is, and its
524
+ * parts are written back into one XML file, each with its name, content type and attributes.
525
+ */
526
+ async function writeFlatOpc(run, output, limits, zipLimits, sink) {
527
+ const packed = new io_1.SpillSink(run.temp, run.threshold);
528
+ run.held.push(packed);
529
+ await (0, rewrite_zip_1.writePackage)(run, output.zip, output.plan, output.format, limits, packed);
530
+ await packed.close();
531
+ const packedSource = packed.source();
532
+ run.sources.push(packedSource);
533
+ const zip = await zip_1.ZipArchive.open(packedSource, zipLimits, run.deadline);
534
+ const typeOf = await contentTypesOf(zip, limits);
535
+ const put = (s) => sink.write(Buffer.from(s, 'utf8'));
536
+ 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}">`);
537
+ for (const e of zip.entries) {
538
+ (0, errors_1.checkTime)(run.deadline);
539
+ if (e.directory || e.name === '[Content_Types].xml')
540
+ continue;
541
+ const chunks = [];
542
+ for await (const c of zip.read(e))
543
+ chunks.push(Buffer.from(c));
544
+ const bytes = Buffer.concat(chunks);
545
+ const type = typeOf(e.name);
546
+ const extra = (output.layout.extra.get((0, zip_1.nameKey)(e.name)) ?? []).map(([q, v]) => ` ${q}="${escapeAttribute(v)}"`).join('');
547
+ await put(`<pkg:part pkg:name="/${escapeAttribute(e.name)}" pkg:contentType="${escapeAttribute(type)}"${extra}>`);
548
+ if ((0, ooxml_1.isXmlType)(type.toLowerCase()) || type.toLowerCase().endsWith('xml'))
549
+ await put(`<pkg:xmlData>${embeddedXml(bytes)}</pkg:xmlData>`);
550
+ else
551
+ await put(`<pkg:binaryData>${(bytes.toString('base64').match(/.{1,76}/g) ?? []).join('\n')}</pkg:binaryData>`);
552
+ await put('</pkg:part>');
553
+ }
554
+ await put('</pkg:package>');
555
+ }
package/dist/formats.d.ts CHANGED
@@ -6,5 +6,14 @@ 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>;
18
+ /** RTF, which Word reads in any file that starts with {\rt. */
19
+ export declare const RTF_FORMAT: 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.RTF_FORMAT = 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'],
@@ -44,3 +48,5 @@ exports.ODF_FORMATS = new Map([
44
48
  ['text-master', 'odm'],
45
49
  ['base', 'odb'],
46
50
  ].map(([kind, ext]) => [`application/vnd.oasis.opendocument.${kind}`, f(ext, `application/vnd.oasis.opendocument.${kind}`)]));
51
+ /** RTF, which Word reads in any file that starts with {\rt. */
52
+ exports.RTF_FORMAT = f('rtf', 'application/rtf');
@@ -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. */