@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/formulas.js CHANGED
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Quotes = void 0;
3
+ exports.FunctionCalls = exports.Quotes = void 0;
4
+ exports.riskyFunction = riskyFunction;
5
+ exports.formulaCalls = formulaCalls;
6
+ exports.biffCalls = biffCalls;
4
7
  exports.callsDde = callsDde;
5
8
  exports.fieldWords = fieldWords;
6
9
  /**
@@ -31,6 +34,159 @@ class Quotes {
31
34
  }
32
35
  }
33
36
  exports.Quotes = Quotes;
37
+ /**
38
+ * Functions that reach outside the file when a formula is calculated: WEBSERVICE and IMAGE fetch a URL, RTD starts a
39
+ * COM server, CALL and REGISTER call into a DLL, and EXEC runs a program. A prefix such as `_xlfn.` or
40
+ * `COM.MICROSOFT.` names the same function.
41
+ */
42
+ const FETCH = ['WEBSERVICE', 'IMAGE'];
43
+ const RUN = ['RTD', 'CALL', 'REGISTER', 'REGISTER.ID', 'EXEC'];
44
+ /** What a function a formula calls does outside the file, or undefined when it does nothing there. */
45
+ function riskyFunction(name) {
46
+ const n = name.toUpperCase();
47
+ const is = (f) => n === f || n.endsWith(`.${f}`);
48
+ if (FETCH.some(is))
49
+ return 'fetch';
50
+ if (RUN.some(is))
51
+ return 'run';
52
+ return undefined;
53
+ }
54
+ /** The functions, outside quotes, that a formula's text calls and that reach outside the file, as its text arrives. */
55
+ class FunctionCalls {
56
+ quotes = new Quotes();
57
+ word = '';
58
+ spaced = false;
59
+ found = new Map();
60
+ write(t) {
61
+ for (const c of t) {
62
+ if (!this.quotes.outside(c)) {
63
+ this.word = '';
64
+ continue;
65
+ }
66
+ if (/[A-Za-z0-9_.]/.test(c)) {
67
+ if (this.spaced)
68
+ this.word = '';
69
+ this.spaced = false;
70
+ if (this.word.length < 64)
71
+ this.word += c;
72
+ }
73
+ else if (c === ' ')
74
+ this.spaced = this.word !== '';
75
+ else {
76
+ if (c === '(' && this.word) {
77
+ const kind = riskyFunction(this.word);
78
+ if (kind)
79
+ this.found.set(this.word.toUpperCase(), kind);
80
+ }
81
+ this.word = '';
82
+ this.spaced = false;
83
+ }
84
+ }
85
+ }
86
+ }
87
+ exports.FunctionCalls = FunctionCalls;
88
+ /** The functions a formula's whole text calls that reach outside the file. */
89
+ function formulaCalls(formula) {
90
+ const f = new FunctionCalls();
91
+ f.write(formula);
92
+ return f.found;
93
+ }
94
+ /** Built-in functions by their index in Excel's function table (MS-XLS 2.5.198.17): EXEC, REGISTER, CALL, REGISTER.ID and RTD. */
95
+ const FTAB = new Map([
96
+ [110, 'EXEC'],
97
+ [149, 'REGISTER'],
98
+ [150, 'CALL'],
99
+ [267, 'REGISTER.ID'],
100
+ [379, 'RTD'],
101
+ ]);
102
+ /**
103
+ * The functions a BIFF8 formula (MS-XLS 2.5.198) calls that reach outside the file: built-in ones by index, and
104
+ * add-in and newer ones by the name the formula names them with, such as `_xlfn.WEBSERVICE`. The walk stops at a
105
+ * token it does not know the size of.
106
+ */
107
+ function biffCalls(rgce, nameOf) {
108
+ const found = new Map();
109
+ const u16 = (at) => rgce[at] | (rgce[at + 1] << 8);
110
+ const u32 = (at) => (rgce[at] | (rgce[at + 1] << 8) | (rgce[at + 2] << 16) | (rgce[at + 3] << 24)) >>> 0;
111
+ const add = (name) => {
112
+ const kind = name === undefined ? undefined : riskyFunction(name);
113
+ if (name !== undefined && kind)
114
+ found.set(name.toUpperCase(), kind);
115
+ };
116
+ for (let i = 0; i < rgce.length;) {
117
+ const ptg = rgce[i++];
118
+ if (ptg < 0x20) {
119
+ if (ptg === 0x01 || ptg === 0x02)
120
+ i += 4;
121
+ else if (ptg === 0x17)
122
+ i += 2 + (rgce[i] ?? 0) * ((rgce[i + 1] ?? 0) & 1 ? 2 : 1);
123
+ else if (ptg === 0x19) {
124
+ const grbit = rgce[i] ?? 0;
125
+ const w = u16(i + 1);
126
+ i += 3 + (grbit & 0x04 ? (w + 1) * 2 : 0);
127
+ }
128
+ else if (ptg === 0x1c || ptg === 0x1d)
129
+ i += 1;
130
+ else if (ptg === 0x1e)
131
+ i += 2;
132
+ else if (ptg === 0x1f)
133
+ i += 8;
134
+ else if (ptg < 0x03 || ptg > 0x16)
135
+ break;
136
+ continue;
137
+ }
138
+ switch (ptg & 0x1f) {
139
+ case 0x00:
140
+ i += 7;
141
+ break;
142
+ case 0x01:
143
+ add(FTAB.get(u16(i)));
144
+ i += 2;
145
+ break;
146
+ case 0x02:
147
+ add(FTAB.get(u16(i + 1) & 0x7fff));
148
+ i += 3;
149
+ break;
150
+ case 0x03:
151
+ add(nameOf({ kind: 'name', index: u32(i) }));
152
+ i += 4;
153
+ break;
154
+ case 0x04:
155
+ case 0x0a:
156
+ case 0x0c:
157
+ i += 4;
158
+ break;
159
+ case 0x05:
160
+ case 0x0b:
161
+ case 0x0d:
162
+ i += 8;
163
+ break;
164
+ case 0x06:
165
+ case 0x07:
166
+ case 0x08:
167
+ i += 6;
168
+ break;
169
+ case 0x09:
170
+ i += 2;
171
+ break;
172
+ case 0x19:
173
+ add(nameOf({ kind: 'namex', ixti: u16(i), index: u32(i + 2) }));
174
+ i += 6;
175
+ break;
176
+ case 0x1a:
177
+ case 0x1c:
178
+ i += 6;
179
+ break;
180
+ case 0x1b:
181
+ case 0x1d:
182
+ i += 10;
183
+ break;
184
+ default:
185
+ return found;
186
+ }
187
+ }
188
+ return found;
189
+ }
34
190
  /** Whether an OpenDocument formula calls the DDE function. */
35
191
  function callsDde(formula) {
36
192
  const q = new Quotes();
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- export { inspectOffice, inspectOfficeSource } from './engine';
1
+ export { disarmOffice, disarmOfficeSource, inspectOffice, inspectOfficeSource } from './engine';
2
2
  export { allFindingSpecs } from './findings';
3
- export { bufferSource, fileSource } from './io';
3
+ export { bufferSink, bufferSource, fileSink, fileSource, writableSink } from './io';
4
+ export { officePlugin } from './plugins';
4
5
  export { COMBINATIONS, DEFAULT_BANDS, scoreFindings } from './score';
5
6
  export * from './types';
package/dist/index.js CHANGED
@@ -14,15 +14,22 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.scoreFindings = exports.DEFAULT_BANDS = exports.COMBINATIONS = exports.fileSource = exports.bufferSource = exports.allFindingSpecs = exports.inspectOfficeSource = exports.inspectOffice = void 0;
17
+ exports.scoreFindings = exports.DEFAULT_BANDS = exports.COMBINATIONS = exports.officePlugin = exports.writableSink = exports.fileSource = exports.fileSink = exports.bufferSource = exports.bufferSink = exports.allFindingSpecs = exports.inspectOfficeSource = exports.inspectOffice = exports.disarmOfficeSource = exports.disarmOffice = void 0;
18
18
  var engine_1 = require("./engine");
19
+ Object.defineProperty(exports, "disarmOffice", { enumerable: true, get: function () { return engine_1.disarmOffice; } });
20
+ Object.defineProperty(exports, "disarmOfficeSource", { enumerable: true, get: function () { return engine_1.disarmOfficeSource; } });
19
21
  Object.defineProperty(exports, "inspectOffice", { enumerable: true, get: function () { return engine_1.inspectOffice; } });
20
22
  Object.defineProperty(exports, "inspectOfficeSource", { enumerable: true, get: function () { return engine_1.inspectOfficeSource; } });
21
23
  var findings_1 = require("./findings");
22
24
  Object.defineProperty(exports, "allFindingSpecs", { enumerable: true, get: function () { return findings_1.allFindingSpecs; } });
23
25
  var io_1 = require("./io");
26
+ Object.defineProperty(exports, "bufferSink", { enumerable: true, get: function () { return io_1.bufferSink; } });
24
27
  Object.defineProperty(exports, "bufferSource", { enumerable: true, get: function () { return io_1.bufferSource; } });
28
+ Object.defineProperty(exports, "fileSink", { enumerable: true, get: function () { return io_1.fileSink; } });
25
29
  Object.defineProperty(exports, "fileSource", { enumerable: true, get: function () { return io_1.fileSource; } });
30
+ Object.defineProperty(exports, "writableSink", { enumerable: true, get: function () { return io_1.writableSink; } });
31
+ var plugins_1 = require("./plugins");
32
+ Object.defineProperty(exports, "officePlugin", { enumerable: true, get: function () { return plugins_1.officePlugin; } });
26
33
  var score_1 = require("./score");
27
34
  Object.defineProperty(exports, "COMBINATIONS", { enumerable: true, get: function () { return score_1.COMBINATIONS; } });
28
35
  Object.defineProperty(exports, "DEFAULT_BANDS", { enumerable: true, get: function () { return score_1.DEFAULT_BANDS; } });
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  import cjs from './index.js';
2
- export const { inspectOffice, inspectOfficeSource, allFindingSpecs, bufferSource, fileSource, COMBINATIONS, DEFAULT_BANDS, scoreFindings, OfficeCategory, OfficeDetail, OfficeRisk } = cjs;
2
+ export const { disarmOffice, disarmOfficeSource, inspectOffice, inspectOfficeSource, allFindingSpecs, bufferSink, bufferSource, fileSink, fileSource, writableSink, officePlugin, COMBINATIONS, DEFAULT_BANDS, scoreFindings, OfficeCategory, OfficeDetail, OfficeRisk } = cjs;
3
3
  export default cjs;
package/dist/io.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { ByteSource } from './types';
1
+ import type { Writable } from 'node:stream';
2
+ import type { ByteSink, ByteSource } from './types';
2
3
  /** A source over bytes already in memory. Reads return views, not copies. */
3
4
  export declare function bufferSource(bytes: Uint8Array): ByteSource;
4
5
  /** A source over a local file, read at offsets through one file handle. */
@@ -21,3 +22,63 @@ export declare class Reader {
21
22
  /** Yields the range in chunks without caching them. */
22
23
  chunks(offset: number, length: number, chunkSize?: number): AsyncGenerator<Uint8Array>;
23
24
  }
25
+ /** Collects output in memory. */
26
+ export declare function bufferSink(): ByteSink & {
27
+ result(): Uint8Array;
28
+ size(): number;
29
+ };
30
+ /** Writes to a local file. The file is created on the first write. */
31
+ export declare function fileSink(filePath: string): ByteSink;
32
+ /**
33
+ * Wraps any Node Writable, waiting for 'drain' when it applies backpressure. The stream's first error, or its
34
+ * closing before the end, fails the pending write and every later one.
35
+ */
36
+ export declare function writableSink(stream: Writable): ByteSink;
37
+ /** A private temporary directory, removed by cleanup(). It also keeps the run's first I/O error. */
38
+ export declare class TempDir {
39
+ private readonly base;
40
+ private dir?;
41
+ private seq;
42
+ /** The first error from a temporary file, which the engine throws instead of a verdict on the file. */
43
+ ioError?: unknown;
44
+ constructor(base?: string);
45
+ file(suffix?: string): Promise<string>;
46
+ cleanup(): Promise<void>;
47
+ }
48
+ /**
49
+ * A sink that keeps what it is given in memory up to `threshold` bytes and in a temporary file past that, with the MD5
50
+ * of the bytes, which tells the output check that a file inside is the one a plugin returned.
51
+ */
52
+ export declare class SpillSink implements ByteSink {
53
+ private readonly temp;
54
+ private readonly threshold;
55
+ private chunks;
56
+ private size;
57
+ private filePath?;
58
+ private handle?;
59
+ private readonly md5;
60
+ private hex?;
61
+ spilled: boolean;
62
+ constructor(temp: TempDir, threshold: number);
63
+ write(chunk: Uint8Array): Promise<void>;
64
+ close(): Promise<void>;
65
+ /** Closes the sink and deletes its temporary file, once nothing will read what it holds again. */
66
+ dispose(): Promise<void>;
67
+ get length(): number;
68
+ digest(): string;
69
+ /** A source over what was written. Call after close(). */
70
+ source(): ByteSource;
71
+ /** What was written, in chunks. Call after close(). */
72
+ read(chunkSize?: number): AsyncGenerator<Uint8Array>;
73
+ }
74
+ /**
75
+ * Copies a whole source, calling `checkTime` before each chunk. A read that returns more or fewer bytes than it asked
76
+ * for fails the copy, which would otherwise shift every byte after it.
77
+ */
78
+ export declare function copyToSink(source: ByteSource, sink: ByteSink, checkTime?: () => void): Promise<void>;
79
+ /** A source's bytes, from the start, in chunks. */
80
+ export declare function chunksOf(source: ByteSource, size: number, chunkSize?: number): AsyncGenerator<Uint8Array>;
81
+ /** Everything a spill sink holds, in memory. Call after close(). */
82
+ export declare function readAll(sink: SpillSink): Promise<Uint8Array>;
83
+ /** A source's bytes from `start`, `length` of them, in chunks. */
84
+ export declare function rangeChunks(source: ByteSource, start: number, length: number, chunkSize?: number): AsyncGenerator<Uint8Array>;
package/dist/io.js CHANGED
@@ -33,10 +33,20 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.Reader = void 0;
36
+ exports.SpillSink = exports.TempDir = exports.Reader = void 0;
37
37
  exports.bufferSource = bufferSource;
38
38
  exports.fileSource = fileSource;
39
+ exports.bufferSink = bufferSink;
40
+ exports.fileSink = fileSink;
41
+ exports.writableSink = writableSink;
42
+ exports.copyToSink = copyToSink;
43
+ exports.chunksOf = chunksOf;
44
+ exports.readAll = readAll;
45
+ exports.rangeChunks = rangeChunks;
46
+ const crypto = __importStar(require("node:crypto"));
39
47
  const fsp = __importStar(require("node:fs/promises"));
48
+ const os = __importStar(require("node:os"));
49
+ const path = __importStar(require("node:path"));
40
50
  /** A source over bytes already in memory. Reads return views, not copies. */
41
51
  function bufferSource(bytes) {
42
52
  return {
@@ -137,3 +147,237 @@ class Reader {
137
147
  }
138
148
  }
139
149
  exports.Reader = Reader;
150
+ /** Collects output in memory. */
151
+ function bufferSink() {
152
+ const chunks = [];
153
+ let size = 0;
154
+ return {
155
+ async write(chunk) {
156
+ chunks.push(Buffer.from(chunk));
157
+ size += chunk.length;
158
+ },
159
+ async close() {
160
+ // Nothing to release. The chunks stay for result().
161
+ },
162
+ result: () => Buffer.concat(chunks),
163
+ size: () => size,
164
+ };
165
+ }
166
+ /** FileHandle.write may write less than asked, for example at a file size limit; the next write then reports why. */
167
+ async function writeAll(handle, chunk) {
168
+ for (let done = 0; done < chunk.length;)
169
+ done += (await handle.write(chunk, done, chunk.length - done)).bytesWritten;
170
+ }
171
+ /** Writes to a local file. The file is created on the first write. */
172
+ function fileSink(filePath) {
173
+ let handle;
174
+ return {
175
+ async write(chunk) {
176
+ handle ??= await fsp.open(filePath, 'w');
177
+ await writeAll(handle, chunk);
178
+ },
179
+ async close() {
180
+ handle ??= await fsp.open(filePath, 'w');
181
+ await handle.close();
182
+ },
183
+ async abort() {
184
+ const h = handle;
185
+ handle = undefined;
186
+ if (!h)
187
+ return;
188
+ // A partial regular file goes. abort() never unlinks a device or pipe the caller named.
189
+ const partial = (await h.stat().catch(() => undefined))?.isFile();
190
+ await h.close().catch(() => {
191
+ // abort() runs after a failure, which is the error to report.
192
+ });
193
+ if (partial)
194
+ await fsp.rm(filePath, { force: true });
195
+ },
196
+ };
197
+ }
198
+ /**
199
+ * Wraps any Node Writable, waiting for 'drain' when it applies backpressure. The stream's first error, or its
200
+ * closing before the end, fails the pending write and every later one.
201
+ */
202
+ function writableSink(stream) {
203
+ let failure;
204
+ const pending = new Set();
205
+ const fail = (e) => {
206
+ failure ??= e;
207
+ for (const reject of pending)
208
+ reject(failure);
209
+ pending.clear();
210
+ };
211
+ // Node emits some errors after the write callback, and an 'error' with no listener crashes the process, so this
212
+ // listener stays for the stream's whole life.
213
+ stream.on('error', fail);
214
+ stream.on('close', () => {
215
+ if (!stream.writableFinished)
216
+ fail(stream.errored ?? new Error('The output stream closed before all data was written'));
217
+ });
218
+ const settle = (start) => new Promise((resolve, reject) => {
219
+ if (failure)
220
+ return reject(failure);
221
+ if (stream.destroyed)
222
+ return reject(stream.errored ?? new Error('The output stream was destroyed'));
223
+ pending.add(reject);
224
+ start(err => {
225
+ pending.delete(reject);
226
+ if (err)
227
+ reject(err);
228
+ else
229
+ resolve();
230
+ });
231
+ });
232
+ return {
233
+ write: chunk => settle(done => (stream.write(chunk) ? done() : stream.once('drain', () => done()))),
234
+ close: () => settle(done => stream.end((err) => done(err))),
235
+ // The reader sees an error instead of an end, so it cannot mistake a partial output for a whole one.
236
+ abort: async (error) => {
237
+ stream.destroy(error instanceof Error ? error : new Error(String(error)));
238
+ },
239
+ };
240
+ }
241
+ /** A private temporary directory, removed by cleanup(). It also keeps the run's first I/O error. */
242
+ class TempDir {
243
+ base;
244
+ // The pending mkdtemp is shared, so concurrent first calls make one directory and cleanup() removes it.
245
+ dir;
246
+ seq = 0;
247
+ /** The first error from a temporary file, which the engine throws instead of a verdict on the file. */
248
+ ioError;
249
+ constructor(base = os.tmpdir()) {
250
+ this.base = base;
251
+ }
252
+ async file(suffix = '.bin') {
253
+ this.dir ??= fsp.mkdtemp(path.join(this.base, 'office-defuse-'));
254
+ return path.join(await this.dir, `${this.seq++}${suffix}`);
255
+ }
256
+ async cleanup() {
257
+ const dir = await this.dir?.catch(() => undefined);
258
+ this.dir = undefined;
259
+ if (dir)
260
+ await fsp.rm(dir, { recursive: true, force: true });
261
+ }
262
+ }
263
+ exports.TempDir = TempDir;
264
+ /**
265
+ * A sink that keeps what it is given in memory up to `threshold` bytes and in a temporary file past that, with the MD5
266
+ * of the bytes, which tells the output check that a file inside is the one a plugin returned.
267
+ */
268
+ class SpillSink {
269
+ temp;
270
+ threshold;
271
+ chunks = [];
272
+ size = 0;
273
+ filePath;
274
+ handle;
275
+ md5 = crypto.createHash('md5');
276
+ hex;
277
+ spilled = false;
278
+ constructor(temp, threshold) {
279
+ this.temp = temp;
280
+ this.threshold = threshold;
281
+ }
282
+ async write(chunk) {
283
+ this.md5.update(chunk);
284
+ this.size += chunk.length;
285
+ if (!this.handle && this.size <= this.threshold) {
286
+ this.chunks.push(Buffer.from(chunk));
287
+ return;
288
+ }
289
+ try {
290
+ if (!this.handle) {
291
+ this.filePath = await this.temp.file('.spill');
292
+ this.handle = await fsp.open(this.filePath, 'w');
293
+ this.spilled = true;
294
+ for (const c of this.chunks)
295
+ await writeAll(this.handle, c);
296
+ this.chunks = [];
297
+ }
298
+ await writeAll(this.handle, chunk);
299
+ }
300
+ catch (e) {
301
+ this.temp.ioError ??= e;
302
+ throw e;
303
+ }
304
+ }
305
+ async close() {
306
+ await this.handle?.close();
307
+ this.handle = undefined;
308
+ }
309
+ /** Closes the sink and deletes its temporary file, once nothing will read what it holds again. */
310
+ async dispose() {
311
+ await this.close();
312
+ this.chunks = [];
313
+ const file = this.filePath;
314
+ this.filePath = undefined;
315
+ if (file === undefined)
316
+ return;
317
+ try {
318
+ await fsp.rm(file, { force: true });
319
+ }
320
+ catch (e) {
321
+ this.temp.ioError ??= e;
322
+ throw e;
323
+ }
324
+ }
325
+ get length() {
326
+ return this.size;
327
+ }
328
+ digest() {
329
+ this.hex ??= this.md5.digest('hex');
330
+ return this.hex;
331
+ }
332
+ /** A source over what was written. Call after close(). */
333
+ source() {
334
+ return this.filePath ? fileSource(this.filePath) : bufferSource(Buffer.concat(this.chunks));
335
+ }
336
+ /** What was written, in chunks. Call after close(). */
337
+ async *read(chunkSize = 262144) {
338
+ const src = this.source();
339
+ // A consumer that stops early, such as a failed output write, still closes the file.
340
+ try {
341
+ const total = await src.size();
342
+ for (let pos = 0; pos < total; pos += chunkSize)
343
+ yield await src.read(pos, Math.min(chunkSize, total - pos));
344
+ }
345
+ finally {
346
+ await src.close?.();
347
+ }
348
+ }
349
+ }
350
+ exports.SpillSink = SpillSink;
351
+ /**
352
+ * Copies a whole source, calling `checkTime` before each chunk. A read that returns more or fewer bytes than it asked
353
+ * for fails the copy, which would otherwise shift every byte after it.
354
+ */
355
+ async function copyToSink(source, sink, checkTime) {
356
+ const total = await source.size();
357
+ const chunk = 262144;
358
+ for (let pos = 0; pos < total; pos += chunk) {
359
+ checkTime?.();
360
+ const want = Math.min(chunk, total - pos);
361
+ const b = await source.read(pos, want);
362
+ if (b.length !== want)
363
+ throw Object.assign(new Error(`The source returned ${b.length} bytes at offset ${pos} where ${want} were expected`), { code: 'EIO' });
364
+ await sink.write(b);
365
+ }
366
+ }
367
+ /** A source's bytes, from the start, in chunks. */
368
+ async function* chunksOf(source, size, chunkSize = 262144) {
369
+ for (let at = 0; at < size; at += chunkSize)
370
+ yield await source.read(at, Math.min(chunkSize, size - at));
371
+ }
372
+ /** Everything a spill sink holds, in memory. Call after close(). */
373
+ async function readAll(sink) {
374
+ const parts = [];
375
+ for await (const c of sink.read())
376
+ parts.push(c);
377
+ return Buffer.concat(parts);
378
+ }
379
+ /** A source's bytes from `start`, `length` of them, in chunks. */
380
+ async function* rangeChunks(source, start, length, chunkSize = 262144) {
381
+ for (let at = 0; at < length; at += chunkSize)
382
+ yield await source.read(start + at, Math.min(chunkSize, length - at));
383
+ }
package/dist/links.d.ts CHANGED
@@ -24,3 +24,30 @@ export declare const isLocalTarget: (rawTarget: string) => boolean;
24
24
  export declare const isInsidePackage: (rawHref: string) => boolean;
25
25
  /** Whether an OOXML relationship target is not a part name, whatever TargetMode says: a scheme, a drive or a network path. */
26
26
  export declare const isAbsoluteTarget: (rawTarget: string) => boolean;
27
+ /**
28
+ * The hosts a label names, as a lookup: has() answers whether a host is one of them or a subdomain of one, or one of
29
+ * them is a subdomain of the host. No public-suffix list is needed: evil.co.uk is not under mybank.co.uk. The same
30
+ * comparison as pdf-defuse's.
31
+ */
32
+ export declare class LabelSites {
33
+ private readonly hosts;
34
+ /** Every domain some label host is a subdomain of. */
35
+ private readonly parents;
36
+ constructor(names: string[]);
37
+ get size(): number;
38
+ has(host: string): boolean;
39
+ }
40
+ /**
41
+ * The site a label names when the label is itself an address: a web URL, a mail address, or a host with or without a
42
+ * path that starts with www. or ends in a known top-level domain. Text that only mentions an address, an Excel
43
+ * function name and a file name name no site. Excel's function names are all capitals, as typed hosts never are, so
44
+ * a bare host in capitals counts only after WWW.
45
+ */
46
+ export declare function labelSite(raw: string): string | undefined;
47
+ /**
48
+ * The first label of a safe link that is an address, and an address of another site than the one the link opens, or
49
+ * undefined. A label is text the reader sees for the link: its text, its tooltip, a display name. A subdomain or a
50
+ * parent of the label's site counts as the same site, as in pdf-defuse. A target that fails on its own gets its own
51
+ * finding instead.
52
+ */
53
+ export declare function mismatchedLabel(target: string, labels: ReadonlyArray<string | undefined>): string | undefined;