@patrickdk77/archive-defuse 0.0.0-stage → 0.1.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 (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +378 -2
  3. package/dist/aes7z.d.ts +16 -0
  4. package/dist/aes7z.js +131 -0
  5. package/dist/alone.d.ts +21 -0
  6. package/dist/alone.js +65 -0
  7. package/dist/bcj2.d.ts +21 -0
  8. package/dist/bcj2.js +140 -0
  9. package/dist/bzip2.d.ts +21 -0
  10. package/dist/bzip2.js +688 -0
  11. package/dist/cab.d.ts +63 -0
  12. package/dist/cab.js +493 -0
  13. package/dist/checks.d.ts +23 -0
  14. package/dist/checks.js +123 -0
  15. package/dist/cli.d.ts +2 -0
  16. package/dist/cli.js +364 -0
  17. package/dist/codecs.d.ts +39 -0
  18. package/dist/codecs.js +124 -0
  19. package/dist/coders.d.ts +58 -0
  20. package/dist/coders.js +392 -0
  21. package/dist/crc32.d.ts +8 -0
  22. package/dist/crc32.js +59 -0
  23. package/dist/detect.d.ts +10 -0
  24. package/dist/detect.js +95 -0
  25. package/dist/encoder.d.ts +44 -0
  26. package/dist/encoder.js +114 -0
  27. package/dist/engine.d.ts +44 -0
  28. package/dist/engine.js +753 -0
  29. package/dist/errors.d.ts +64 -0
  30. package/dist/errors.js +98 -0
  31. package/dist/filters.d.ts +34 -0
  32. package/dist/filters.js +385 -0
  33. package/dist/findings.d.ts +26 -0
  34. package/dist/findings.js +176 -0
  35. package/dist/formats.d.ts +24 -0
  36. package/dist/formats.js +106 -0
  37. package/dist/index.d.ts +9 -0
  38. package/dist/index.js +53 -0
  39. package/dist/index.mjs +3 -0
  40. package/dist/inflate64.d.ts +6 -0
  41. package/dist/inflate64.js +388 -0
  42. package/dist/io.d.ts +78 -0
  43. package/dist/io.js +379 -0
  44. package/dist/lzma-formats.d.ts +75 -0
  45. package/dist/lzma-formats.js +373 -0
  46. package/dist/lzma-sniff.d.ts +20 -0
  47. package/dist/lzma-sniff.js +73 -0
  48. package/dist/lzma-writers.d.ts +35 -0
  49. package/dist/lzma-writers.js +276 -0
  50. package/dist/lzma1/LICENSE +201 -0
  51. package/dist/lzma1/NOTICE +36 -0
  52. package/dist/lzma1/crc32.d.ts +4 -0
  53. package/dist/lzma1/crc32.js +19 -0
  54. package/dist/lzma1/hash234.d.ts +22 -0
  55. package/dist/lzma1/hash234.js +75 -0
  56. package/dist/lzma1/hc4.d.ts +30 -0
  57. package/dist/lzma1/hc4.js +142 -0
  58. package/dist/lzma1/header.d.ts +32 -0
  59. package/dist/lzma1/header.js +73 -0
  60. package/dist/lzma1/lz-decoder.d.ts +41 -0
  61. package/dist/lzma1/lz-decoder.js +128 -0
  62. package/dist/lzma1/lz-encoder.d.ts +87 -0
  63. package/dist/lzma1/lz-encoder.js +166 -0
  64. package/dist/lzma1/lzma-coder.d.ts +107 -0
  65. package/dist/lzma1/lzma-coder.js +162 -0
  66. package/dist/lzma1/lzma-decoder.d.ts +36 -0
  67. package/dist/lzma1/lzma-decoder.js +484 -0
  68. package/dist/lzma1/lzma-encoder-fast.d.ts +13 -0
  69. package/dist/lzma1/lzma-encoder-fast.js +114 -0
  70. package/dist/lzma1/lzma-encoder.d.ts +105 -0
  71. package/dist/lzma1/lzma-encoder.js +497 -0
  72. package/dist/lzma1/lzma2-decoder.d.ts +85 -0
  73. package/dist/lzma1/lzma2-decoder.js +249 -0
  74. package/dist/lzma1/range-coder.d.ts +18 -0
  75. package/dist/lzma1/range-coder.js +23 -0
  76. package/dist/lzma1/range-decoder.d.ts +41 -0
  77. package/dist/lzma1/range-decoder.js +78 -0
  78. package/dist/lzma1/range-encoder.d.ts +53 -0
  79. package/dist/lzma1/range-encoder.js +269 -0
  80. package/dist/lzma1-stream.d.ts +60 -0
  81. package/dist/lzma1-stream.js +202 -0
  82. package/dist/model.d.ts +141 -0
  83. package/dist/model.js +16 -0
  84. package/dist/names.d.ts +13 -0
  85. package/dist/names.js +66 -0
  86. package/dist/pack.d.ts +79 -0
  87. package/dist/pack.js +590 -0
  88. package/dist/plugins.d.ts +13 -0
  89. package/dist/plugins.js +56 -0
  90. package/dist/rules.d.ts +25 -0
  91. package/dist/rules.js +403 -0
  92. package/dist/score.d.ts +17 -0
  93. package/dist/score.js +58 -0
  94. package/dist/seven.d.ts +75 -0
  95. package/dist/seven.js +492 -0
  96. package/dist/sevenzip.d.ts +133 -0
  97. package/dist/sevenzip.js +529 -0
  98. package/dist/single.d.ts +103 -0
  99. package/dist/single.js +453 -0
  100. package/dist/sniff.d.ts +20 -0
  101. package/dist/sniff.js +224 -0
  102. package/dist/stream-meter.d.ts +39 -0
  103. package/dist/stream-meter.js +54 -0
  104. package/dist/tar.d.ts +102 -0
  105. package/dist/tar.js +765 -0
  106. package/dist/types.d.ts +311 -0
  107. package/dist/types.js +133 -0
  108. package/dist/unpack.d.ts +67 -0
  109. package/dist/unpack.js +194 -0
  110. package/dist/xz.d.ts +25 -0
  111. package/dist/xz.js +333 -0
  112. package/dist/zip-lzma.d.ts +15 -0
  113. package/dist/zip-lzma.js +137 -0
  114. package/dist/zip-writer.d.ts +51 -0
  115. package/dist/zip-writer.js +305 -0
  116. package/dist/zip.d.ts +99 -0
  117. package/dist/zip.js +901 -0
  118. package/dist/zipcrypt.d.ts +47 -0
  119. package/dist/zipcrypt.js +233 -0
  120. package/dist/zstd.d.ts +25 -0
  121. package/dist/zstd.js +160 -0
  122. package/package.json +94 -4
package/dist/pack.js ADDED
@@ -0,0 +1,590 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PackError = void 0;
4
+ exports.createArchive = createArchive;
5
+ exports.createArchiveStream = createArchiveStream;
6
+ exports.createArchiveBytes = createArchiveBytes;
7
+ exports.compressTo = compressTo;
8
+ exports.compressStream = compressStream;
9
+ exports.compress = compress;
10
+ // Making archives and compressed files from scratch, for callers who use this package as an archive library: ZIP,
11
+ // tar on its own or compressed, 7z, and the formats that compress one file. The writers are the ones the rewrite uses.
12
+ const node_stream_1 = require("node:stream");
13
+ const bzip2_1 = require("./bzip2");
14
+ const codecs_1 = require("./codecs");
15
+ const crc32_1 = require("./crc32");
16
+ const io_1 = require("./io");
17
+ const lzma_writers_1 = require("./lzma-writers");
18
+ const rules_1 = require("./rules");
19
+ const tar_1 = require("./tar");
20
+ const types_1 = require("./types");
21
+ const zip_writer_1 = require("./zip-writer");
22
+ const zstd_1 = require("./zstd");
23
+ /** Thrown for an entry this package will not write, before anything of the entry is written. */
24
+ class PackError extends Error {
25
+ }
26
+ exports.PackError = PackError;
27
+ const enc = new TextEncoder();
28
+ /** The reading default of `limits.compressionRatio`. */
29
+ const DEFAULT_RATIO = 1000;
30
+ const toBytes = (c) => (typeof c === 'string' ? enc.encode(c) : c);
31
+ /** Bytes that small chunks of an entry are gathered into before they go on. */
32
+ const BATCH = 64 * 1024;
33
+ /** `chunks`, with `check` before each one, so an aborted signal stops a long read. */
34
+ async function* checked(chunks, check) {
35
+ for await (const c of chunks) {
36
+ check();
37
+ yield c;
38
+ }
39
+ }
40
+ /** The content of an entry as chunks of bytes. */
41
+ async function* chunksOf(data) {
42
+ if (data === undefined)
43
+ return;
44
+ if (typeof data === 'string' || data instanceof Uint8Array) {
45
+ const b = toBytes(data);
46
+ if (b.length)
47
+ yield b;
48
+ return;
49
+ }
50
+ // Small chunks, such as one row of a sheet each, go to the compressor in pieces of BATCH bytes. zlib does the same
51
+ // work either way, and each write costs as much as a large one.
52
+ let pending = [];
53
+ let held = 0;
54
+ for await (const c of data) {
55
+ const b = toBytes(c);
56
+ if (!b.length)
57
+ continue;
58
+ if (held === 0 && b.length >= BATCH) {
59
+ yield b;
60
+ continue;
61
+ }
62
+ pending.push(b);
63
+ held += b.length;
64
+ if (held >= BATCH) {
65
+ yield Buffer.concat(pending, held);
66
+ pending = [];
67
+ held = 0;
68
+ }
69
+ }
70
+ if (held)
71
+ yield Buffer.concat(pending, held);
72
+ }
73
+ /** Why `createArchive` refuses a name, for each name rule of the inspection that changes a name. */
74
+ const REFUSED = {
75
+ [types_1.ArchiveDetail.Empty]: 'is empty',
76
+ [types_1.ArchiveDetail.DriveLetter]: 'starts with a drive letter',
77
+ [types_1.ArchiveDetail.Absolute]: 'is an absolute path',
78
+ [types_1.ArchiveDetail.Traversal]: 'has a ".." part',
79
+ [types_1.ArchiveDetail.EmptySegment]: 'has an empty or "." part',
80
+ [types_1.ArchiveDetail.Backslash]: 'holds a backslash, which Windows reads as a folder separator',
81
+ [types_1.ArchiveDetail.ControlCharacter]: 'holds a control character or a character that reorders text',
82
+ [types_1.ArchiveDetail.WindowsReserved]: 'has a part Windows cannot create as written',
83
+ };
84
+ /**
85
+ * Characters that Windows turns into a slash, a backslash, a colon or a dot when a program that uses the ANSI file
86
+ * calls converts a name with best fit. Taken from Microsoft's best-fit tables at unicode.org for code pages 874, 932,
87
+ * 936, 949, 950 and 1250 to 1258. The yen sign is a backslash in code page 932, the won sign in 949, and the acute
88
+ * accent a slash in 1253.
89
+ */
90
+ const BEST_FIT = {
91
+ '\u00a5': '\\',
92
+ '\u20a9': '\\',
93
+ '\u2216': '\\',
94
+ '\uff3c': '\\',
95
+ '\u00b4': '/',
96
+ '\u2044': '/',
97
+ '\u2215': '/',
98
+ '\uff0f': '/',
99
+ '\u0589': ':',
100
+ '\u2236': ':',
101
+ '\uff1a': ':',
102
+ '\uff0e': '.',
103
+ };
104
+ const BEST_FIT_CHARS = /[\u00a5\u20a9\u2216\uff3c\u00b4\u2044\u2215\uff0f\u0589\u2236\uff1a\uff0e]/g;
105
+ /** The rules whose break lets an extractor write outside its folder. */
106
+ const ESCAPES = [types_1.ArchiveDetail.DriveLetter, types_1.ArchiveDetail.Absolute, types_1.ArchiveDetail.Traversal];
107
+ /**
108
+ * Settles an entry's kind and checks its name. The name must pass the inspection's name rules, so the archive passes
109
+ * this package's own inspection: no ".." part, no absolute path or drive letter, no empty or "." part, no backslash,
110
+ * control character or part Windows cannot create. It must also not leave its folder once NFKC normalization or
111
+ * Windows best fit has changed it, as "\uff0e\uff0e/x" becomes "../x".
112
+ */
113
+ function settle(entry, options, now) {
114
+ const raw = entry.name;
115
+ if (typeof raw !== 'string' || raw === '' || raw === '/')
116
+ throw new PackError('An entry needs a name');
117
+ if (raw.includes('\0'))
118
+ throw new PackError(`Entry name ${JSON.stringify(raw)} holds a NUL`);
119
+ const kind = entry.kind ?? (raw.endsWith('/') ? 'directory' : entry.linkTarget !== undefined ? 'symlink' : 'file');
120
+ if (kind === 'symlink' && entry.linkTarget === undefined)
121
+ throw new PackError(`Symbolic link ${JSON.stringify(raw)} needs a linkTarget`);
122
+ const name = kind === 'directory' ? (raw.endsWith('/') ? raw : `${raw}/`) : raw.replace(/\/+$/, '');
123
+ for (const detail of (0, rules_1.nameDetails)(name, true)) {
124
+ const why = REFUSED[detail];
125
+ if (why)
126
+ throw new PackError(`Entry name ${JSON.stringify(raw)} ${why}`);
127
+ }
128
+ const folds = [
129
+ [name.normalize('NFKC'), 'NFKC normalization'],
130
+ [name.replace(BEST_FIT_CHARS, c => BEST_FIT[c]), 'Windows best fit'],
131
+ ];
132
+ for (const [folded, how] of folds) {
133
+ if (folded !== name && (0, rules_1.nameDetails)(folded, true).some(d => ESCAPES.includes(d)))
134
+ throw new PackError(`Entry name ${JSON.stringify(raw)} leaves its folder after ${how}`);
135
+ }
136
+ return { name, kind, entry, mode: (entry.mode ?? (kind === 'directory' ? 0o755 : kind === 'symlink' ? 0o777 : 0o644)) & 0o7777, mtime: entry.mtime ?? options.mtime ?? now };
137
+ }
138
+ /** The content of a settled entry: a file's data, a link's target, or nothing for a folder. */
139
+ async function contentOf(s) {
140
+ if (s.kind === 'directory')
141
+ return chunksOf(undefined);
142
+ if (s.kind === 'symlink')
143
+ return chunksOf(s.entry.linkTarget);
144
+ const d = s.entry.data;
145
+ return chunksOf(typeof d === 'function' ? await d() : d);
146
+ }
147
+ /** Reads an entry's content into memory or a temporary file, counting its CRC-32. */
148
+ async function spillOf(s, temp, threshold, check) {
149
+ const spill = new io_1.SpillSink(temp, threshold);
150
+ let crc = 0;
151
+ try {
152
+ for await (const c of checked(await contentOf(s), check)) {
153
+ crc = (0, crc32_1.crc32)(c, crc);
154
+ await spill.write(c);
155
+ }
156
+ await spill.close();
157
+ }
158
+ catch (e) {
159
+ await spill.dispose();
160
+ throw e;
161
+ }
162
+ return { spill, crc };
163
+ }
164
+ const isAscii = (s) => /^[\x20-\x7e]*$/.test(s);
165
+ // ---------------------------------------------------------------------------------------------------------------
166
+ // ZIP
167
+ /** A date as the DOS time and date of a ZIP header, local time, clamped to the years DOS can hold. */
168
+ function dosTime(d) {
169
+ const year = Math.min(2107, Math.max(1980, d.getFullYear()));
170
+ return {
171
+ time: (d.getHours() << 11) | (d.getMinutes() << 5) | (d.getSeconds() >> 1),
172
+ date: ((year - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate(),
173
+ };
174
+ }
175
+ const ZIP_TYPE_BITS = { file: 0o100000, directory: 0o040000, symlink: 0o120000 };
176
+ async function writeZipEntries(entries, sink, options, temp, check) {
177
+ const out = new zip_writer_1.ZipOutput(sink);
178
+ const threshold = options.memoryThreshold ?? 8 * 1024 * 1024;
179
+ const level = options.level ?? 6;
180
+ for await (const s of entries) {
181
+ const { time, date } = dosTime(s.mtime);
182
+ const info = {
183
+ name: enc.encode(s.name),
184
+ flags: isAscii(s.name) ? 0 : 0x0800,
185
+ method: 0,
186
+ versionNeeded: s.kind === 'directory' ? 20 : 10,
187
+ versionMadeBy: (3 << 8) | 30,
188
+ externalAttr: (((ZIP_TYPE_BITS[s.kind] | s.mode) << 16) >>> 0) | (s.kind === 'directory' ? 0x10 : 0),
189
+ internalAttr: 0,
190
+ time,
191
+ date,
192
+ crc: 0,
193
+ compressedSize: 0,
194
+ size: 0,
195
+ extras: new Uint8Array(0),
196
+ comment: new Uint8Array(0),
197
+ };
198
+ if (s.kind === 'directory') {
199
+ await out.entry(info, () => chunksOf(undefined), s.name);
200
+ continue;
201
+ }
202
+ if (options.streaming) {
203
+ await streamZipEntry(out, s, info, level, check);
204
+ continue;
205
+ }
206
+ // The content is read once, into memory or a temporary file, and deflated from there, so it can be stored instead
207
+ // when deflate gains nothing or passes the ratio limit.
208
+ const { spill: raw, crc } = await spillOf(s, temp, threshold, check);
209
+ const deflated = new io_1.SpillSink(temp, threshold);
210
+ try {
211
+ let stored = s.kind === 'symlink' || s.entry.store === true || level === 0 || raw.length === 0;
212
+ if (!stored) {
213
+ for await (const c of (0, codecs_1.deflateRaw)(checked(raw.read(), check), level))
214
+ await deflated.write(c);
215
+ await deflated.close();
216
+ stored = deflated.length >= raw.length || (0, lzma_writers_1.passesRatio)(raw.length, deflated.length, options.ratioLimit ?? DEFAULT_RATIO);
217
+ }
218
+ info.method = stored ? 0 : 8;
219
+ info.versionNeeded = stored ? 10 : 20;
220
+ info.crc = crc;
221
+ info.size = raw.length;
222
+ info.compressedSize = stored ? raw.length : deflated.length;
223
+ await out.entry(info, () => checked(stored ? raw.read() : deflated.read(), check), s.name);
224
+ }
225
+ finally {
226
+ await deflated.dispose();
227
+ await raw.dispose();
228
+ }
229
+ }
230
+ await out.end(enc.encode(options.comment ?? ''));
231
+ }
232
+ /**
233
+ * Writes one entry of a streaming ZIP archive. A file is deflated as its data arrives and gets a data descriptor; at
234
+ * level 0, deflate writes stored blocks. An empty file is stored with its sizes. A link's target and a file the caller
235
+ * stores are held in memory, so their headers carry their sizes, since a stored entry with a data descriptor is how a
236
+ * second, hidden entry gets past a reader of the central directory.
237
+ */
238
+ async function streamZipEntry(out, s, info, level, check) {
239
+ const it = checked(await contentOf(s), check)[Symbol.asyncIterator]();
240
+ let first;
241
+ try {
242
+ first = await it.next();
243
+ }
244
+ catch (e) {
245
+ await it.return?.(undefined);
246
+ throw e;
247
+ }
248
+ if (s.kind === 'symlink' || s.entry.store === true || first.done) {
249
+ const parts = [];
250
+ let crc = 0;
251
+ try {
252
+ for (let r = first; !r.done; r = await it.next()) {
253
+ crc = (0, crc32_1.crc32)(r.value, crc);
254
+ parts.push(r.value);
255
+ }
256
+ }
257
+ finally {
258
+ await it.return?.(undefined);
259
+ }
260
+ const data = Buffer.concat(parts);
261
+ Object.assign(info, { method: 0, versionNeeded: 10, crc, size: data.length, compressedSize: data.length });
262
+ await out.entry(info, () => chunksOf(data), s.name);
263
+ return;
264
+ }
265
+ let crc = 0;
266
+ let size = 0;
267
+ const raw = async function* () {
268
+ try {
269
+ for (let r = first; !r.done; r = await it.next()) {
270
+ crc = (0, crc32_1.crc32)(r.value, crc);
271
+ size += r.value.length;
272
+ yield r.value;
273
+ }
274
+ }
275
+ finally {
276
+ await it.return?.(undefined);
277
+ }
278
+ };
279
+ Object.assign(info, { method: 8, versionNeeded: 20 });
280
+ await out.streamEntry(info, (0, codecs_1.deflateRaw)(raw(), level), () => ({ crc, size }));
281
+ }
282
+ // ---------------------------------------------------------------------------------------------------------------
283
+ // tar
284
+ const TAR_TYPES = { file: '0', directory: '5', symlink: '2' };
285
+ async function* tarChunks(entries, options, temp, check) {
286
+ const threshold = options.memoryThreshold ?? 8 * 1024 * 1024;
287
+ let written = 0;
288
+ const pad = (n) => Buffer.alloc((tar_1.BLOCK - (n % tar_1.BLOCK)) % tar_1.BLOCK);
289
+ for await (const s of entries) {
290
+ const { spill } = s.kind === 'file' ? await spillOf(s, temp, threshold, check) : { spill: undefined };
291
+ try {
292
+ const size = spill?.length ?? 0;
293
+ const link = s.kind === 'symlink' ? (s.entry.linkTarget ?? '') : '';
294
+ const mtime = Math.floor(s.mtime.getTime() / 1000);
295
+ // What the ustar header cannot hold goes in a pax header before it.
296
+ const records = [];
297
+ const parts = (0, tar_1.split)(s.name);
298
+ if (!parts)
299
+ records.push((0, tar_1.paxRecord)('path', Buffer.from(s.name, 'utf8')));
300
+ const linkFits = isAscii(link) && link.length <= 100;
301
+ if (!linkFits)
302
+ records.push((0, tar_1.paxRecord)('linkpath', Buffer.from(link, 'utf8')));
303
+ if (size > 0o77777777777)
304
+ records.push((0, tar_1.paxRecord)('size', Buffer.from(String(size))));
305
+ const timeFits = mtime >= 0 && mtime <= 0o77777777777;
306
+ if (!timeFits)
307
+ records.push((0, tar_1.paxRecord)('mtime', Buffer.from(String(mtime))));
308
+ if (records.length) {
309
+ const body = Buffer.concat(records);
310
+ const h = (0, tar_1.header)({
311
+ name: Buffer.from('././@PaxHeader'),
312
+ mode: 0o644,
313
+ uid: 0,
314
+ gid: 0,
315
+ size: body.length,
316
+ mtime: 0,
317
+ typeflag: 'x',
318
+ linkname: Buffer.alloc(0),
319
+ uname: '',
320
+ gname: '',
321
+ prefix: Buffer.alloc(0),
322
+ devmajor: 0,
323
+ devminor: 0,
324
+ });
325
+ yield h;
326
+ yield body;
327
+ yield pad(body.length);
328
+ written += h.length + body.length + pad(body.length).length;
329
+ }
330
+ const h = (0, tar_1.header)({
331
+ name: parts?.name ?? (0, tar_1.asciiFallback)(s.name),
332
+ prefix: parts?.prefix ?? Buffer.alloc(0),
333
+ mode: s.mode,
334
+ uid: 0,
335
+ gid: 0,
336
+ size: size > 0o77777777777 ? 0 : size,
337
+ mtime: timeFits ? mtime : 0,
338
+ typeflag: TAR_TYPES[s.kind],
339
+ linkname: linkFits ? Buffer.from(link, 'latin1') : (0, tar_1.asciiFallback)(link),
340
+ uname: '',
341
+ gname: '',
342
+ devmajor: 0,
343
+ devminor: 0,
344
+ });
345
+ yield h;
346
+ written += h.length;
347
+ if (spill) {
348
+ yield* checked(spill.read(), check);
349
+ const p = pad(size);
350
+ if (p.length)
351
+ yield p;
352
+ written += size + p.length;
353
+ }
354
+ }
355
+ finally {
356
+ await spill?.dispose();
357
+ }
358
+ }
359
+ const end = Buffer.alloc(2 * tar_1.BLOCK);
360
+ yield end;
361
+ written += end.length;
362
+ const tail = (tar_1.RECORD - (written % tar_1.RECORD)) % tar_1.RECORD;
363
+ if (tail)
364
+ yield Buffer.alloc(tail);
365
+ }
366
+ // ---------------------------------------------------------------------------------------------------------------
367
+ // 7z
368
+ /** A date as a Windows FILETIME: 100-nanosecond intervals since 1601, little-endian. */
369
+ function fileTime(d) {
370
+ const t = BigInt(d.getTime() + 11_644_473_600_000) * 10000n;
371
+ const b = Buffer.alloc(8);
372
+ b.writeBigUInt64LE(t < 0n ? 0n : t);
373
+ return b;
374
+ }
375
+ async function writeSevenEntries(entries, sink, options, temp, check) {
376
+ const threshold = options.memoryThreshold ?? 8 * 1024 * 1024;
377
+ const spills = [];
378
+ const out = [];
379
+ try {
380
+ // 7z writes its header after the data, from every entry, so each entry is read before any is written.
381
+ let held = 0;
382
+ for await (const s of entries) {
383
+ const dir = s.kind === 'directory';
384
+ let content;
385
+ if (!dir) {
386
+ const { spill } = await spillOf(s, temp, Math.max(0, threshold - held), check);
387
+ spills.push(spill);
388
+ if (!spill.spilled)
389
+ held += spill.length;
390
+ content = { size: spill.length, chunks: () => spill.read() };
391
+ }
392
+ const typeBits = dir ? 0o040000 : s.kind === 'symlink' ? 0o120000 : 0o100000;
393
+ // The Unix mode sits in the high 16 bits, with 0x8000 to say it is there, as p7zip writes it.
394
+ const attrib = (((typeBits | s.mode) << 16) | 0x8000 | (dir ? 0x10 : 0x20)) >>> 0;
395
+ out.push({ name: dir ? s.name.slice(0, -1) : s.name, dir, attrib, mtime: fileTime(s.mtime), content });
396
+ }
397
+ await (0, lzma_writers_1.writeSevenZip)(out, sink, temp, threshold, check, options.ratioLimit ?? DEFAULT_RATIO);
398
+ }
399
+ finally {
400
+ for (const s of spills)
401
+ await s.dispose();
402
+ }
403
+ }
404
+ // ---------------------------------------------------------------------------------------------------------------
405
+ // One compressed stream
406
+ /** A gzip member around raw deflate, with the CRC-32 and size after it. */
407
+ async function* gzipOf(chunks, level, mtime) {
408
+ const head = Buffer.from([0x1f, 0x8b, 8, 0, 0, 0, 0, 0, 0, 255]);
409
+ head.writeUInt32LE(Math.max(0, Math.min(0xffffffff, Math.floor(mtime.getTime() / 1000))), 4);
410
+ yield head;
411
+ let crc = 0;
412
+ let size = 0;
413
+ async function* tally() {
414
+ for await (const c of chunks) {
415
+ crc = (0, crc32_1.crc32)(c, crc);
416
+ size += c.length;
417
+ yield c;
418
+ }
419
+ }
420
+ yield* (0, codecs_1.deflateRaw)(tally(), level);
421
+ const tail = Buffer.alloc(8);
422
+ tail.writeUInt32LE(crc >>> 0, 0);
423
+ tail.writeUInt32LE(size % 2 ** 32, 4);
424
+ yield tail;
425
+ }
426
+ /** xz and .lzma need the content by offset, or its size first, so it is read into memory or a temporary file. */
427
+ async function* lzmaOf(format, chunks, options, temp, check) {
428
+ const spill = new io_1.SpillSink(temp, options.memoryThreshold ?? 8 * 1024 * 1024);
429
+ try {
430
+ for await (const c of checked(chunks, check))
431
+ await spill.write(c);
432
+ await spill.close();
433
+ const source = spill.length ? spill.source() : undefined;
434
+ // The writers write to a sink, so their output goes to memory or a temporary file and then out.
435
+ const encoded = new io_1.SpillSink(temp, options.memoryThreshold ?? 8 * 1024 * 1024);
436
+ try {
437
+ try {
438
+ if (format === 'xz')
439
+ await (0, lzma_writers_1.writeXz)(source, 4, encoded, check);
440
+ else
441
+ await (0, lzma_writers_1.writeAlone)(source, encoded, check);
442
+ }
443
+ finally {
444
+ await source?.close?.();
445
+ }
446
+ await encoded.close();
447
+ yield* checked(encoded.read(), check);
448
+ }
449
+ finally {
450
+ await encoded.dispose();
451
+ }
452
+ }
453
+ finally {
454
+ await spill.dispose();
455
+ }
456
+ }
457
+ function compressChunks(format, input, options, temp, now, check) {
458
+ const chunks = checked(input, check);
459
+ switch (format) {
460
+ case 'gzip':
461
+ return gzipOf(chunks, options.level ?? 6, options.mtime ?? now);
462
+ case 'bzip2':
463
+ return (0, bzip2_1.bzip2)(chunks, options.level ?? 9);
464
+ case 'zstd':
465
+ return (0, zstd_1.zstdEncode)(chunks);
466
+ case 'brotli':
467
+ return (0, codecs_1.brotliEncode)(chunks);
468
+ case 'xz':
469
+ case 'lzma':
470
+ return lzmaOf(format, chunks, options, temp, check);
471
+ default:
472
+ throw new PackError(`Unknown format ${String(format)}`);
473
+ }
474
+ }
475
+ // ---------------------------------------------------------------------------------------------------------------
476
+ // The calls
477
+ const ARCHIVE_FORMATS = new Set(['zip', 'tar', 'tar.gz', 'tar.bz2', 'tar.zst', 'tar.xz', '7z']);
478
+ const TAR_COMPRESSION = { 'tar.gz': 'gzip', 'tar.bz2': 'bzip2', 'tar.zst': 'zstd', 'tar.xz': 'xz' };
479
+ /** Copies chunks to the sink, then closes it. On failure it aborts the sink, so a partial output never looks whole. */
480
+ async function deliver(produce, sink) {
481
+ try {
482
+ await produce();
483
+ }
484
+ catch (e) {
485
+ try {
486
+ await sink.abort?.(e);
487
+ }
488
+ catch {
489
+ // The write's error is the one to report.
490
+ }
491
+ throw e;
492
+ }
493
+ await sink.close();
494
+ }
495
+ /**
496
+ * Writes an archive of `entries`, in their order, to `sink`, then closes it. Entries can come as a list or as an async
497
+ * iterable, and each file's content as bytes, text or a stream. When anything fails, such as an entry's stream, the
498
+ * sink is aborted instead of closed and the error is thrown.
499
+ */
500
+ async function createArchive(format, entries, sink, options = {}) {
501
+ if (!ARCHIVE_FORMATS.has(format))
502
+ throw new PackError(`Unknown archive format ${String(format)}`);
503
+ // tar needs each entry's size before its data, and 7z writes its header from every entry after the data.
504
+ if (options.streaming && format !== 'zip')
505
+ throw new PackError(`streaming writes ZIP archives only, not ${format}`);
506
+ const now = new Date();
507
+ const temp = new io_1.TempDir(options.tempDir);
508
+ const check = () => options.signal?.throwIfAborted();
509
+ async function* settled() {
510
+ for await (const e of entries) {
511
+ check();
512
+ yield settle(e, options, now);
513
+ }
514
+ }
515
+ try {
516
+ await deliver(async () => {
517
+ check();
518
+ if (format === 'zip')
519
+ return writeZipEntries(settled(), sink, options, temp, check);
520
+ if (format === '7z')
521
+ return writeSevenEntries(settled(), sink, options, temp, check);
522
+ const tar = tarChunks(settled(), options, temp, check);
523
+ const method = TAR_COMPRESSION[format];
524
+ for await (const c of method ? compressChunks(method, tar, options, temp, now, check) : tar)
525
+ await sink.write(c);
526
+ }, sink);
527
+ }
528
+ finally {
529
+ await temp.cleanup();
530
+ }
531
+ }
532
+ /**
533
+ * Runs `write` into a Node Readable. An error destroys the stream, so a consumer never takes a partial output as whole.
534
+ * A consumer that destroys the stream before its end, or the caller's signal, stops the work.
535
+ */
536
+ function readableOf(write, signal) {
537
+ const out = new node_stream_1.PassThrough();
538
+ const controller = new AbortController();
539
+ const stop = () => controller.abort(signal?.reason);
540
+ if (signal?.aborted)
541
+ stop();
542
+ else
543
+ signal?.addEventListener('abort', stop, { once: true });
544
+ out.on('close', () => {
545
+ if (!controller.signal.aborted)
546
+ controller.abort(out.errored ?? new Error('The output stream closed before all data was written'));
547
+ });
548
+ write((0, io_1.writableSink)(out), controller.signal)
549
+ .catch(e => {
550
+ if (!out.destroyed)
551
+ out.destroy(e instanceof Error ? e : new Error(String(e)));
552
+ })
553
+ .finally(() => signal?.removeEventListener('abort', stop));
554
+ return out;
555
+ }
556
+ /** `createArchive` as a Node Readable, to pipe to a file, an HTTP response or an upload. */
557
+ function createArchiveStream(format, entries, options = {}) {
558
+ return readableOf((sink, signal) => createArchive(format, entries, sink, { ...options, signal }), options.signal);
559
+ }
560
+ /** `createArchive` into memory. */
561
+ async function createArchiveBytes(format, entries, options = {}) {
562
+ const sink = (0, io_1.bufferSink)();
563
+ await createArchive(format, entries, sink, options);
564
+ return sink.result();
565
+ }
566
+ /** Compresses `data` as one stream of `format` to `sink`, then closes it. */
567
+ async function compressTo(format, data, sink, options = {}) {
568
+ const temp = new io_1.TempDir(options.tempDir);
569
+ const check = () => options.signal?.throwIfAborted();
570
+ try {
571
+ await deliver(async () => {
572
+ check();
573
+ for await (const c of compressChunks(format, chunksOf(data), options, temp, new Date(), check))
574
+ await sink.write(c);
575
+ }, sink);
576
+ }
577
+ finally {
578
+ await temp.cleanup();
579
+ }
580
+ }
581
+ /** `compressTo` as a Node Readable. */
582
+ function compressStream(format, data, options = {}) {
583
+ return readableOf((sink, signal) => compressTo(format, data, sink, { ...options, signal }), options.signal);
584
+ }
585
+ /** `compressTo` into memory. */
586
+ async function compress(format, data, options = {}) {
587
+ const sink = (0, io_1.bufferSink)();
588
+ await compressTo(format, data, sink, options);
589
+ return sink.result();
590
+ }
@@ -0,0 +1,13 @@
1
+ import type { ArchiveOptions, ContainedFilePlugin } from './types';
2
+ /**
3
+ * Keeps contained files unchanged by type or by name, as pdf-defuse's passThrough does. An entry with a "/" is a MIME
4
+ * type. Any other entry, such as ".csv" or "csv", is a file name extension.
5
+ */
6
+ export declare function passThrough(types: string[]): ContainedFilePlugin;
7
+ /**
8
+ * Defuses archives inside other files, with `options` when given. Without them, it takes the options of the
9
+ * archive-defuse run that found the file, or none when another package found it. It accepts a file by its content,
10
+ * whatever its name says. `limits.nestingDepth` applies to the depth in the context. The archive's findings go into
11
+ * the containing file's report, and its output's findings into the output's report, each with its `weight`.
12
+ */
13
+ export declare function archivePlugin(options?: ArchiveOptions): ContainedFilePlugin;
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.passThrough = passThrough;
4
+ exports.archivePlugin = archivePlugin;
5
+ const detect_1 = require("./detect");
6
+ const engine_1 = require("./engine");
7
+ const score_1 = require("./score");
8
+ const sniff_1 = require("./sniff");
9
+ /**
10
+ * Keeps contained files unchanged by type or by name, as pdf-defuse's passThrough does. An entry with a "/" is a MIME
11
+ * type. Any other entry, such as ".csv" or "csv", is a file name extension.
12
+ */
13
+ function passThrough(types) {
14
+ const set = new Set(types.filter(t => t.includes('/')).map(t => t.toLowerCase()));
15
+ const exts = new Set(types.filter(t => !t.includes('/')).map(t => t.trim().toLowerCase().replace(/^\./, '')));
16
+ return {
17
+ kind: 'file',
18
+ name: `passThrough(${types.join(',')})`,
19
+ // Matches on type alone. Whether the content, the declared type and the name agree is the type check's finding.
20
+ accepts: f => {
21
+ const ext = (0, sniff_1.extensionOf)(f.name);
22
+ if (ext !== undefined && exts.has(ext))
23
+ return true;
24
+ return [f.sniffedType, f.declaredType, (0, sniff_1.typeFromName)(f.name)].some(t => t !== undefined && set.has(t.toLowerCase().split(';')[0].trim()));
25
+ },
26
+ process: async () => 'passed',
27
+ };
28
+ }
29
+ /**
30
+ * Defuses archives inside other files, with `options` when given. Without them, it takes the options of the
31
+ * archive-defuse run that found the file, or none when another package found it. It accepts a file by its content,
32
+ * whatever its name says. `limits.nestingDepth` applies to the depth in the context. The archive's findings go into
33
+ * the containing file's report, and its output's findings into the output's report, each with its `weight`.
34
+ */
35
+ function archivePlugin(options) {
36
+ return {
37
+ kind: 'file',
38
+ name: 'archive',
39
+ // It takes what detectArchive claims, which is what the inspection reads, Brotli included: the content decides.
40
+ accepts: async (f) => (await (0, detect_1.detectArchiveSource)(f.source, { name: f.name, declaredType: f.declaredType })) !== undefined,
41
+ async process(file, sink, context) {
42
+ const run = options ?? context[engine_1.RUN_OPTIONS] ?? {};
43
+ const r = await (0, engine_1.disarmAtDepth)(file.source, sink, run, context.depth, context.deadline);
44
+ // A container in another package scores these by the weights this run used.
45
+ for (const f of [...r.before.findings, ...(r.after?.findings ?? [])])
46
+ f.weight = (0, score_1.weightOf)(f, run);
47
+ const findings = r.before.findings;
48
+ // The sink holds the rewritten archive, also when nothing was removed.
49
+ if (r.status === 'clean')
50
+ return { result: 'passed', findings, outputFindings: r.after?.findings };
51
+ if (r.status === 'defused')
52
+ return { result: 'scrubbed', findings, outputFindings: r.after?.findings };
53
+ return { result: 'removed', findings };
54
+ },
55
+ };
56
+ }
@@ -0,0 +1,25 @@
1
+ import type { FindingFactory } from './findings';
2
+ import type { Entry } from './model';
3
+ import { ArchiveCategory as C, ArchiveDetail as D } from './types';
4
+ /** What the rules decided about one entry. */
5
+ export interface Decision {
6
+ remove: boolean;
7
+ /** The name to write. */
8
+ name: string;
9
+ mode?: number;
10
+ dropCapabilities: boolean;
11
+ /** For a hard link: the index of the entry it links to, when that is an entry before it. */
12
+ linkIndex?: number;
13
+ }
14
+ export type Emit = (category: C, detail: D, e: Entry, data?: Record<string, string | number>) => void;
15
+ /**
16
+ * The name rules a name breaks, in the order they report. `applyRules` reports these, and `createArchive` refuses a
17
+ * name that breaks any but TOO_LONG. A ".." part is TRAVERSAL's alone, and empty parts at the start belong to an
18
+ * absolute or UNC path, which ABSOLUTE reports.
19
+ */
20
+ export declare function nameDetails(name: string, backslashIsSeparator: boolean): D[];
21
+ /** Runs the rules over the entries of an archive. Single-file formats with no name skip the name rules. */
22
+ export declare function applyRules(entries: Entry[], factory: FindingFactory, emit: Emit, options: {
23
+ names: boolean;
24
+ backslashIsSeparator: boolean;
25
+ }): Decision[];