@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/engine.js CHANGED
@@ -2,13 +2,20 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.inspectOfficeSource = inspectOfficeSource;
4
4
  exports.inspectOffice = inspectOffice;
5
+ exports.disarmOfficeSource = disarmOfficeSource;
6
+ exports.disarmAtDepth = disarmAtDepth;
7
+ exports.disarmOffice = disarmOffice;
5
8
  const cfb_1 = require("./cfb");
6
9
  const context_1 = require("./context");
7
10
  const errors_1 = require("./errors");
8
11
  const findings_1 = require("./findings");
12
+ const flat_1 = require("./flat");
9
13
  const io_1 = require("./io");
10
14
  const ole_1 = require("./ole");
11
15
  const package_1 = require("./package");
16
+ const rewrite_cfb_1 = require("./rewrite-cfb");
17
+ const rewrite_rtf_1 = require("./rewrite-rtf");
18
+ const rewrite_zip_1 = require("./rewrite-zip");
12
19
  const rtf_1 = require("./rtf");
13
20
  const score_1 = require("./score");
14
21
  const types_1 = require("./types");
@@ -37,19 +44,31 @@ function resolveLimits(given = {}) {
37
44
  rtfDepth: given.rtfDepth ?? DEFAULT_LIMITS.rtfDepth,
38
45
  fileSize: given.fileSize,
39
46
  timeMs: given.timeMs,
47
+ nestingDepth: given.nestingDepth,
40
48
  };
41
49
  }
50
+ const xmlLimitsOf = (limits) => ({
51
+ depth: limits.xmlDepth,
52
+ tokenSize: limits.xmlTokenSize,
53
+ entityDepth: limits.xmlEntityDepth,
54
+ entityExpansions: limits.xmlEntityExpansions,
55
+ entitySize: limits.xmlEntitySize,
56
+ });
57
+ const PLUGIN_KEPT = new Set([types_1.OfficeDetail.PluginPassed, types_1.OfficeDetail.PluginScrubbed]);
42
58
  /**
43
59
  * `incomplete` marks a run that stopped before it read the whole package. It is rejected without a score, whatever
44
- * the actions say, because an override can make the finding that stopped it look harmless.
60
+ * the actions say, because an override can make the finding that stopped it look harmless. In the check of an output,
61
+ * `isOutput`, the files plugins kept count in the score; in an upload they do not, and a file a plugin cleaned makes
62
+ * the upload strippable, since the output must be written.
45
63
  */
46
- function finish(findings, options, incomplete, format) {
64
+ function finish(findings, options, incomplete, format, isOutput = false, superseded) {
47
65
  const named = { format: format?.format ?? '', mimeType: format?.mimeType ?? '' };
48
66
  if (incomplete)
49
67
  return { status: 'rejected', score: null, risk: types_1.OfficeRisk.Unknown, findings, ...named };
50
68
  const rejected = findings.some(f => f.action === 'reject');
51
- const strippable = findings.some(f => f.action === 'strip');
52
- const { score, risk } = (0, score_1.scoreFindings)(findings, options);
69
+ const strippable = findings.some(f => f.action === 'strip' || (!isOutput && f.detail === types_1.OfficeDetail.PluginScrubbed));
70
+ const scored = findings.filter(f => !superseded?.has(f) && (isOutput || !PLUGIN_KEPT.has(f.detail)));
71
+ const { score, risk } = (0, score_1.scoreFindings)(scored, options);
53
72
  return { status: rejected ? 'rejected' : strippable ? 'strippable' : 'clean', score, risk, findings, ...named };
54
73
  }
55
74
  /**
@@ -92,13 +111,15 @@ const ambiguous = (reason) => new errors_1.StopError(types_1.OfficeCategory.Pack
92
111
  * starts with {\rt is RTF, which is how Word tells them. The ZIP reader runs too, from the end: a file both read is
93
112
  * ambiguous, since Office goes by the first bytes and a ZIP reader by the last. A compound file that does not read is
94
113
  * a ZIP archive with data in front, if the ZIP reader read one. Word reads any file that starts with {\rt as RTF, so
95
- * that does not fail.
114
+ * that does not fail. A file that starts as XML is a flat OpenDocument file or a Flat OPC package, and one that is a
115
+ * ZIP archive as well is ambiguous.
96
116
  */
97
117
  async function openContainer(src, size, limits, settings) {
98
- const head = await src.read(0, Math.min(8, size));
99
- const cfbSignature = head.length === 8 && head.every((b, i) => b === cfb_1.CFB_SIGNATURE[i]);
118
+ const head = await src.read(0, Math.min(1024, size));
119
+ const cfbSignature = head.length >= 8 && cfb_1.CFB_SIGNATURE.every((b, i) => head[i] === b);
100
120
  const rtfSignature = Buffer.from(head.subarray(0, 4)).toString('latin1') === '{\\rt';
101
- const probe = cfbSignature || rtfSignature
121
+ const xml = !cfbSignature && !rtfSignature && (0, flat_1.startsAsXml)(head);
122
+ const probe = cfbSignature || rtfSignature || xml
102
123
  ? await zip_1.ZipArchive.open(src, limits, settings.deadline).catch((e) => {
103
124
  if (e instanceof errors_1.StopError)
104
125
  return undefined;
@@ -110,6 +131,11 @@ async function openContainer(src, size, limits, settings) {
110
131
  throw ambiguous('both RTF and a ZIP archive');
111
132
  return { kind: 'rtf' };
112
133
  }
134
+ if (xml) {
135
+ if (probe)
136
+ throw ambiguous('both XML and a ZIP archive');
137
+ return { kind: 'xml' };
138
+ }
113
139
  if (cfbSignature) {
114
140
  try {
115
141
  const cfb = await cfb_1.CompoundFile.open(src, settings, settings.deadline);
@@ -124,29 +150,63 @@ async function openContainer(src, size, limits, settings) {
124
150
  }
125
151
  return { kind: 'zip', zip: probe ?? (await zip_1.ZipArchive.open(src, limits, settings.deadline)) };
126
152
  }
127
- /** Inspects an Office or OpenDocument file without changing it. The source is closed when the run ends. */
128
- async function inspectOfficeSource(source, options = {}) {
153
+ /** A run of the engine over one file at `depth`, with the deadline the whole upload shares. */
154
+ function newRun(options, depth, deadline, verify) {
155
+ return {
156
+ options,
157
+ depth,
158
+ deadline,
159
+ temp: new io_1.TempDir(options.tempDir),
160
+ threshold: options.memoryThreshold ?? 8 * 1024 * 1024,
161
+ retained: 0,
162
+ verify,
163
+ kept: new Map(),
164
+ held: [],
165
+ superseded: new WeakSet(),
166
+ sources: [],
167
+ };
168
+ }
169
+ /** Releases what a run holds: the copies plugins returned and the temporary directory. */
170
+ async function endRun(run) {
171
+ try {
172
+ for (const s of run.sources.splice(0))
173
+ await s.close?.();
174
+ for (const h of run.held.splice(0))
175
+ await h.dispose();
176
+ }
177
+ finally {
178
+ await run.temp.cleanup();
179
+ }
180
+ }
181
+ /** The finding a limit error from this package or any other gives at depth 0. */
182
+ function limitFinding(factory, e) {
183
+ return factory.make(types_1.OfficeCategory.Limit, e.limit === 'nesting' ? types_1.OfficeDetail.NestingDepth : types_1.OfficeDetail.Time);
184
+ }
185
+ /**
186
+ * Inspects `source` and leaves what the rewrite needs in `run.output`. It throws an I/O error instead of reporting a
187
+ * verdict on the file, and below depth 0 it throws a limit error from the family, which stops the whole upload.
188
+ */
189
+ async function analyze(run, source) {
190
+ const options = run.options;
129
191
  const limits = resolveLimits(options.limits);
130
- const deadline = limits.timeMs !== undefined ? Date.now() + limits.timeMs : undefined;
192
+ const deadline = run.deadline;
131
193
  const factory = new findings_1.FindingFactory(options.actionOverrides);
132
194
  const findings = [];
133
195
  const src = guarded(source);
196
+ const password = options.password;
134
197
  const settings = {
135
198
  entries: limits.entries,
136
199
  decompressedBytes: limits.decompressedBytes,
137
200
  totalDecompressedBytes: limits.totalDecompressedBytes,
138
201
  rtfDepth: limits.rtfDepth,
139
202
  deadline,
140
- password: options.password,
203
+ passwords: typeof password === 'string' ? [password] : (password ?? []),
141
204
  };
142
- const ctx = new context_1.RunContext(factory, {
143
- depth: limits.xmlDepth,
144
- tokenSize: limits.xmlTokenSize,
145
- entityDepth: limits.xmlEntityDepth,
146
- entityExpansions: limits.xmlEntityExpansions,
147
- entitySize: limits.xmlEntitySize,
148
- }, settings);
205
+ const ctx = new context_1.RunContext(factory, xmlLimitsOf(limits), settings, run);
206
+ const isOutput = run.verify !== undefined;
149
207
  try {
208
+ if (limits.nestingDepth !== undefined && run.depth > limits.nestingDepth)
209
+ throw new errors_1.NestingLimitError('Nesting limit exceeded');
150
210
  const size = await src.size();
151
211
  if (limits.fileSize !== undefined && size > limits.fileSize)
152
212
  throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.FileSize, `${size} bytes`, { size, limit: limits.fileSize });
@@ -154,6 +214,11 @@ async function inspectOfficeSource(source, options = {}) {
154
214
  const container = await openContainer(src, size, limits, settings);
155
215
  if (container.kind === 'rtf')
156
216
  await (0, rtf_1.inspectRtf)(ctx, src, size);
217
+ else if (container.kind === 'xml') {
218
+ const output = await (0, flat_1.inspectFlat)(ctx, src, size, limits);
219
+ if (output && ctx.format)
220
+ run.output = output;
221
+ }
157
222
  else if (container.kind === 'cfb') {
158
223
  for (const n of container.cfb.notes)
159
224
  findings.push(factory.make(types_1.OfficeCategory.Container, n.detail, undefined, { bytes: n.bytes }));
@@ -162,39 +227,219 @@ async function inspectOfficeSource(source, options = {}) {
162
227
  else {
163
228
  const zip = container.zip;
164
229
  for (const n of zip.notes)
165
- findings.push(factory.make(types_1.OfficeCategory.Container, n.detail, undefined, { bytes: n.bytes }));
166
- await (0, package_1.inspectPackage)(ctx.withZip(zip));
230
+ findings.push(factory.make(types_1.OfficeCategory.Container, n.detail, undefined, n.offset === undefined ? { bytes: n.bytes } : { bytes: n.bytes, offset: n.offset }));
231
+ const zctx = ctx.withZip(zip);
232
+ await (0, package_1.inspectPackage)(zctx);
233
+ if (ctx.format)
234
+ run.output = { kind: 'zip', zip, plan: zctx.plan, format: ctx.format };
167
235
  }
168
236
  findings.push(...ctx.findings);
169
- return finish(findings, options, false, ctx.format);
237
+ if (run.temp.ioError !== undefined)
238
+ throw run.temp.ioError;
239
+ return finish(findings, options, false, ctx.format, isOutput, run.superseded);
170
240
  }
171
241
  catch (e) {
172
242
  if (src.failure !== undefined)
173
243
  throw src.failure;
244
+ if (run.temp.ioError !== undefined)
245
+ throw run.temp.ioError;
246
+ run.output = undefined;
174
247
  findings.push(...ctx.findings);
175
248
  if (e instanceof errors_1.StopError) {
176
249
  findings.push(factory.make(e.category, e.detail, e.location ?? (typeof e.data?.name === 'string' ? e.data.name : undefined), { ...e.data, reason: e.data?.reason ?? e.message }));
177
250
  }
178
- else if (e?.code === 'DEFUSE_LIMIT') {
179
- // The family's time limit, from this package or any other.
180
- findings.push(factory.make(types_1.OfficeCategory.Limit, types_1.OfficeDetail.Time));
251
+ else if ((0, errors_1.stopsTree)(e) === 'DEFUSE_LIMIT') {
252
+ // The family's time and nesting limits, from this package or any other, stop the whole upload.
253
+ if (run.depth > 0)
254
+ throw e;
255
+ findings.push(limitFinding(factory, e));
256
+ }
257
+ else if ((0, errors_1.isIoError)(e)) {
258
+ throw e;
181
259
  }
182
260
  else {
183
261
  // Anything unexpected rejects the file rather than escaping as an exception.
184
262
  findings.push(factory.make(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unreadable, undefined, { reason: String(e?.message ?? e).slice(0, 200) }));
185
263
  }
186
- return finish(findings, options, true, ctx.format);
264
+ return finish(findings, options, true, ctx.format, isOutput);
265
+ }
266
+ }
267
+ async function closeSource(source) {
268
+ try {
269
+ await source.close?.();
270
+ }
271
+ catch {
272
+ // The verdict stands whether or not the source closes.
273
+ }
274
+ }
275
+ /** Inspects an Office or OpenDocument file without changing it. The source is closed when the run ends. */
276
+ async function inspectOfficeSource(source, options = {}) {
277
+ const deadline = options.limits?.timeMs !== undefined ? Date.now() + options.limits.timeMs : undefined;
278
+ const run = newRun(options, 0, deadline);
279
+ try {
280
+ return await analyze(run, source);
187
281
  }
188
282
  finally {
283
+ await closeSource(source);
284
+ await endRun(run);
285
+ }
286
+ }
287
+ /** Inspects Office or OpenDocument bytes. */
288
+ async function inspectOffice(bytes, options = {}) {
289
+ return inspectOfficeSource((0, io_1.bufferSource)(bytes), options);
290
+ }
291
+ /** Copies to the caller's sink. On failure it aborts the sink instead of closing it, so a partial copy does not look whole. */
292
+ async function deliver(from, sink) {
293
+ try {
294
+ await (0, io_1.copyToSink)(from, sink);
295
+ }
296
+ catch (e) {
189
297
  try {
190
- await source.close?.();
298
+ await sink.abort?.(e);
191
299
  }
192
300
  catch {
193
- // The verdict stands whether or not the source closes.
301
+ /* the copy's error is the one to report */
194
302
  }
303
+ throw e;
195
304
  }
305
+ await sink.close();
196
306
  }
197
- /** Inspects Office or OpenDocument bytes. */
198
- async function inspectOffice(bytes, options = {}) {
199
- return inspectOfficeSource((0, io_1.bufferSource)(bytes), options);
307
+ /** Writes the rewrite of what the run read. */
308
+ async function write(run, output, sink, source) {
309
+ const resolved = resolveLimits(run.options.limits);
310
+ const limits = xmlLimitsOf(resolved);
311
+ if (output.kind === 'zip')
312
+ await (0, rewrite_zip_1.writePackage)(run, output.zip, output.plan, output.format, limits, sink);
313
+ else if (output.kind === 'cfb')
314
+ await (0, rewrite_cfb_1.writeCompound)(run, output.cfb, output.plan, sink);
315
+ else if (output.kind === 'flatodf')
316
+ await (0, flat_1.writeFlatOdf)(run, source, output.plan, limits, sink);
317
+ else if (output.kind === 'flatopc')
318
+ await (0, flat_1.writeFlatOpc)(run, output, limits, resolved, sink);
319
+ else
320
+ await (0, rewrite_rtf_1.writeRtf)(run, source, output.size, output.cuts, sink);
321
+ }
322
+ /** Rejects the upload, keeping its findings and, unless a limit stopped the run, its score. */
323
+ function refuse(before, finding, limited) {
324
+ before.findings.push(finding);
325
+ before.status = 'rejected';
326
+ if (limited) {
327
+ before.score = null;
328
+ before.risk = types_1.OfficeRisk.Unknown;
329
+ }
330
+ return { status: 'rejected', before, removed: [] };
331
+ }
332
+ /**
333
+ * Writes a defused copy of `source` to `sink`. Nothing reaches the sink unless the output passes inspection. The
334
+ * source is closed when the run ends.
335
+ */
336
+ async function disarmOfficeSource(source, sink, options = {}) {
337
+ return disarmAtDepth(source, sink, options, 0);
338
+ }
339
+ /** Internal entry that carries the depth of a file inside another, and the deadline it shares with the upload. */
340
+ async function disarmAtDepth(source, sink, options, depth, deadline = options.limits?.timeMs !== undefined ? Date.now() + options.limits.timeMs : undefined) {
341
+ const run = newRun(options, depth, deadline);
342
+ const factory = new findings_1.FindingFactory(options.actionOverrides);
343
+ let input = source;
344
+ try {
345
+ let before;
346
+ try {
347
+ // A caller's source can change between reads, so the inspection and the rewrite read one snapshot. A file
348
+ // inside another already has a source of the engine's, and one over the size limit is rejected unread.
349
+ const fileSize = options.limits?.fileSize;
350
+ if (depth === 0 && (fileSize === undefined || (await source.size()) <= fileSize)) {
351
+ const snapshot = new io_1.SpillSink(run.temp, run.threshold);
352
+ run.held.push(snapshot);
353
+ try {
354
+ await (0, io_1.copyToSink)(source, snapshot, () => (0, errors_1.checkTime)(deadline));
355
+ }
356
+ finally {
357
+ await snapshot.close();
358
+ }
359
+ input = snapshot.source();
360
+ }
361
+ before = await analyze(run, input);
362
+ }
363
+ catch (e) {
364
+ if ((0, errors_1.stopsTree)(e) !== 'DEFUSE_LIMIT' || depth > 0)
365
+ throw e;
366
+ before = finish([limitFinding(factory, e)], options, true);
367
+ }
368
+ const output = run.output;
369
+ if (before.status === 'rejected' || !output)
370
+ return { status: 'rejected', before, removed: [] };
371
+ const out = new io_1.SpillSink(run.temp, run.threshold);
372
+ run.held.push(out);
373
+ try {
374
+ await write(run, output, out, input);
375
+ await out.close();
376
+ }
377
+ catch (e) {
378
+ await out.close().catch(() => {
379
+ // The write's failure, handled below, is the one that counts.
380
+ });
381
+ if (run.temp.ioError !== undefined)
382
+ throw run.temp.ioError;
383
+ if ((0, errors_1.isIoError)(e))
384
+ throw e;
385
+ if ((0, errors_1.stopsTree)(e) === 'DEFUSE_LIMIT') {
386
+ if (depth > 0)
387
+ throw e;
388
+ return refuse(before, limitFinding(factory, e), true);
389
+ }
390
+ return refuse(before, factory.make(types_1.OfficeCategory.Processing, types_1.OfficeDetail.VerificationFailed, undefined, { reasons: `write failed: ${String(e?.message ?? e).slice(0, 100)}` }), false);
391
+ }
392
+ // The output is checked with the caller's overrides and limits, and the plugins' verdicts in place of the plugins.
393
+ const verifyOptions = {
394
+ actionOverrides: options.actionOverrides,
395
+ scoreWeights: options.scoreWeights,
396
+ scoreBands: options.scoreBands,
397
+ tempDir: options.tempDir,
398
+ memoryThreshold: options.memoryThreshold,
399
+ limits: { ...options.limits, fileSize: undefined, timeMs: undefined, nestingDepth: undefined },
400
+ };
401
+ const outSource = out.source();
402
+ try {
403
+ const check = newRun(verifyOptions, depth, deadline, run.kept);
404
+ let after;
405
+ try {
406
+ after = await analyze(check, outSource);
407
+ }
408
+ finally {
409
+ await endRun(check);
410
+ }
411
+ const reasons = after.findings.filter(f => f.action !== 'info').map(f => `${f.category}/${f.detail}`);
412
+ // A check that stopped early verified nothing, even when an override made the finding that stopped it info.
413
+ if (after.status === 'rejected' && !reasons.length)
414
+ reasons.push('the output check stopped early');
415
+ if (after.format !== before.format)
416
+ reasons.push(`format ${before.format} -> ${after.format}`);
417
+ if (reasons.length)
418
+ return refuse(before, factory.make(types_1.OfficeCategory.Processing, types_1.OfficeDetail.VerificationFailed, undefined, { reasons: [...new Set(reasons)].slice(0, 10).join(', ') }), false);
419
+ await deliver(outSource, sink);
420
+ return { status: before.status === 'clean' ? 'clean' : 'defused', before, after, removed: before.findings.filter(f => f.action === 'strip') };
421
+ }
422
+ finally {
423
+ await closeSource(outSource);
424
+ }
425
+ }
426
+ catch (e) {
427
+ // An I/O error inside a nested file fails the upload's run too, instead of counting as a failed plugin.
428
+ if (depth > 0 && ((0, errors_1.isIoError)(e) || (e !== undefined && e === run.temp.ioError)))
429
+ throw new errors_1.NestedIoError(e);
430
+ throw e;
431
+ }
432
+ finally {
433
+ if (input !== source)
434
+ await closeSource(input);
435
+ if (depth === 0)
436
+ await closeSource(source);
437
+ await endRun(run);
438
+ }
439
+ }
440
+ /** Defuses Office or OpenDocument bytes. `bytes` holds the output unless the file was rejected. */
441
+ async function disarmOffice(bytes, options = {}) {
442
+ const sink = (0, io_1.bufferSink)();
443
+ const result = await disarmOfficeSource((0, io_1.bufferSource)(bytes), sink, options);
444
+ return result.status === 'rejected' ? result : { ...result, bytes: sink.result() };
200
445
  }
package/dist/errors.d.ts CHANGED
@@ -4,6 +4,23 @@ export declare class TimeLimitError extends Error {
4
4
  readonly code = "DEFUSE_LIMIT";
5
5
  readonly limit = "time";
6
6
  }
7
+ /** A file sits deeper in the upload than `limits.nestingDepth` allows. The code makes every package pass it up. */
8
+ export declare class NestingLimitError extends Error {
9
+ readonly code = "DEFUSE_LIMIT";
10
+ readonly limit = "nesting";
11
+ }
12
+ /** An I/O error inside a nested file's own run, carried up so the upload's run fails on it as on its own. */
13
+ export declare class NestedIoError extends Error {
14
+ readonly code = "DEFUSE_IO";
15
+ constructor(cause: unknown);
16
+ }
17
+ /**
18
+ * The code of an error that stops the whole tree of files, from any package: DEFUSE_LIMIT for a time or nesting
19
+ * limit, DEFUSE_IO for an I/O error. Packages cannot share error classes, so the code alone marks one.
20
+ */
21
+ export declare function stopsTree(e: unknown): 'DEFUSE_LIMIT' | 'DEFUSE_IO' | undefined;
22
+ /** An error from the operating system, such as EIO or ENOSPC, which says nothing about the file. */
23
+ export declare const isIoError: (e: unknown) => boolean;
7
24
  /**
8
25
  * A refusal or a limit that stops the run. The engine turns it into the finding it names and rejects the file with
9
26
  * no score, whatever the overrides say.
package/dist/errors.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.StopError = exports.TimeLimitError = void 0;
3
+ exports.StopError = exports.isIoError = exports.NestedIoError = exports.NestingLimitError = exports.TimeLimitError = void 0;
4
+ exports.stopsTree = stopsTree;
4
5
  exports.checkTime = checkTime;
5
6
  /** The run passed its deadline. The code makes every package of the family pass it up instead of treating it as one plugin failing. */
6
7
  class TimeLimitError extends Error {
@@ -8,6 +9,31 @@ class TimeLimitError extends Error {
8
9
  limit = 'time';
9
10
  }
10
11
  exports.TimeLimitError = TimeLimitError;
12
+ /** A file sits deeper in the upload than `limits.nestingDepth` allows. The code makes every package pass it up. */
13
+ class NestingLimitError extends Error {
14
+ code = 'DEFUSE_LIMIT';
15
+ limit = 'nesting';
16
+ }
17
+ exports.NestingLimitError = NestingLimitError;
18
+ /** An I/O error inside a nested file's own run, carried up so the upload's run fails on it as on its own. */
19
+ class NestedIoError extends Error {
20
+ code = 'DEFUSE_IO';
21
+ constructor(cause) {
22
+ super(String(cause?.message ?? cause), { cause });
23
+ }
24
+ }
25
+ exports.NestedIoError = NestedIoError;
26
+ /**
27
+ * The code of an error that stops the whole tree of files, from any package: DEFUSE_LIMIT for a time or nesting
28
+ * limit, DEFUSE_IO for an I/O error. Packages cannot share error classes, so the code alone marks one.
29
+ */
30
+ function stopsTree(e) {
31
+ const code = typeof e === 'object' && e !== null ? e.code : undefined;
32
+ return code === 'DEFUSE_LIMIT' || code === 'DEFUSE_IO' ? code : undefined;
33
+ }
34
+ /** An error from the operating system, such as EIO or ENOSPC, which says nothing about the file. */
35
+ const isIoError = (e) => typeof e?.code === 'string' && /^E[A-Z]+$/.test(e.code);
36
+ exports.isIoError = isIoError;
11
37
  /**
12
38
  * A refusal or a limit that stops the run. The engine turns it into the finding it names and rejects the file with
13
39
  * no score, whatever the overrides say.
package/dist/fields.d.ts CHANGED
@@ -1,28 +1,35 @@
1
1
  import { type RunContext } from './context';
2
+ import type { FieldMarks } from './plan';
2
3
  /**
3
- * Reports a Word field by its code. A field whose name holds the content of an external entity has a type the file
4
- * does not say, as one whose name another field builds does.
5
- */
6
- export declare function evaluateField(ctx: RunContext, location: string, code: string, computed: boolean): void;
7
- /**
8
- * Word field codes, followed through nested fields as the text of a document arrives: a field begins, its code
9
- * comes, a separator starts its result, and it ends. Each format says where those marks are.
4
+ * Reports a Word field by its code, and tells whether any of its findings strips it. A field whose name holds the
5
+ * content of an external entity has a type the file does not say, as one whose name another field builds does. A
6
+ * HYPERLINK field's result is the text the reader sees, and goes with its tooltip into the label check.
10
7
  */
8
+ export declare function evaluateField(ctx: RunContext, location: string, code: string, computed: boolean, result?: string): boolean;
11
9
  export declare class FieldStack {
12
10
  private readonly ctx;
13
11
  private readonly location;
12
+ private readonly onStrip?;
14
13
  private readonly stack;
15
- constructor(ctx: RunContext, location: string);
14
+ /**
15
+ * `onStrip` hears of each field a strip finding names, with the marks the format gave its begin, separator and end,
16
+ * so the rewrite can find the field again.
17
+ */
18
+ constructor(ctx: RunContext, location: string, onStrip?: ((marks: FieldMarks) => void) | undefined);
16
19
  /** A field starts. One that starts before its parent's code has a word in it makes that code at run time. */
17
- begin(): void;
18
- /** A field that is all code, such as OOXML's fldSimple. */
19
- simple(code: string): void;
20
- separate(): void;
21
- end(): void;
20
+ begin(mark?: number): void;
21
+ /** A field whose code is given whole, such as OOXML's fldSimple, whose result follows until simpleEnd(). */
22
+ simpleBegin(code: string, mark?: number): void;
23
+ simpleEnd(): void;
24
+ separate(mark?: number): void;
25
+ end(mark?: number): void;
26
+ private decide;
22
27
  /** Whether a field's code is being read now. */
23
28
  get inCode(): boolean;
24
29
  /** Text of the innermost field's code. White space before the first word counts for nothing, so no amount of it can push the field's name out. */
25
30
  text(t: string): void;
31
+ /** Text of the innermost field's result, the text the reader sees. */
32
+ result(t: string): void;
26
33
  /** A document can end inside a field. Word still reads the code it has. */
27
34
  finish(): void;
28
35
  private nest;