@patrickdk77/archive-defuse 0.0.0-stage → 0.1.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 (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +345 -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/encoder.d.ts +44 -0
  24. package/dist/encoder.js +114 -0
  25. package/dist/engine.d.ts +44 -0
  26. package/dist/engine.js +753 -0
  27. package/dist/errors.d.ts +64 -0
  28. package/dist/errors.js +98 -0
  29. package/dist/filters.d.ts +34 -0
  30. package/dist/filters.js +385 -0
  31. package/dist/findings.d.ts +26 -0
  32. package/dist/findings.js +176 -0
  33. package/dist/formats.d.ts +14 -0
  34. package/dist/formats.js +97 -0
  35. package/dist/index.d.ts +8 -0
  36. package/dist/index.js +50 -0
  37. package/dist/index.mjs +3 -0
  38. package/dist/inflate64.d.ts +6 -0
  39. package/dist/inflate64.js +388 -0
  40. package/dist/io.d.ts +78 -0
  41. package/dist/io.js +379 -0
  42. package/dist/lzma-formats.d.ts +75 -0
  43. package/dist/lzma-formats.js +373 -0
  44. package/dist/lzma-sniff.d.ts +20 -0
  45. package/dist/lzma-sniff.js +73 -0
  46. package/dist/lzma-writers.d.ts +35 -0
  47. package/dist/lzma-writers.js +276 -0
  48. package/dist/lzma1/LICENSE +201 -0
  49. package/dist/lzma1/NOTICE +36 -0
  50. package/dist/lzma1/crc32.d.ts +4 -0
  51. package/dist/lzma1/crc32.js +19 -0
  52. package/dist/lzma1/hash234.d.ts +22 -0
  53. package/dist/lzma1/hash234.js +75 -0
  54. package/dist/lzma1/hc4.d.ts +30 -0
  55. package/dist/lzma1/hc4.js +142 -0
  56. package/dist/lzma1/header.d.ts +32 -0
  57. package/dist/lzma1/header.js +73 -0
  58. package/dist/lzma1/lz-decoder.d.ts +41 -0
  59. package/dist/lzma1/lz-decoder.js +128 -0
  60. package/dist/lzma1/lz-encoder.d.ts +87 -0
  61. package/dist/lzma1/lz-encoder.js +166 -0
  62. package/dist/lzma1/lzma-coder.d.ts +107 -0
  63. package/dist/lzma1/lzma-coder.js +162 -0
  64. package/dist/lzma1/lzma-decoder.d.ts +36 -0
  65. package/dist/lzma1/lzma-decoder.js +484 -0
  66. package/dist/lzma1/lzma-encoder-fast.d.ts +13 -0
  67. package/dist/lzma1/lzma-encoder-fast.js +114 -0
  68. package/dist/lzma1/lzma-encoder.d.ts +105 -0
  69. package/dist/lzma1/lzma-encoder.js +497 -0
  70. package/dist/lzma1/lzma2-decoder.d.ts +85 -0
  71. package/dist/lzma1/lzma2-decoder.js +249 -0
  72. package/dist/lzma1/range-coder.d.ts +18 -0
  73. package/dist/lzma1/range-coder.js +23 -0
  74. package/dist/lzma1/range-decoder.d.ts +41 -0
  75. package/dist/lzma1/range-decoder.js +78 -0
  76. package/dist/lzma1/range-encoder.d.ts +53 -0
  77. package/dist/lzma1/range-encoder.js +269 -0
  78. package/dist/lzma1-stream.d.ts +60 -0
  79. package/dist/lzma1-stream.js +202 -0
  80. package/dist/model.d.ts +141 -0
  81. package/dist/model.js +16 -0
  82. package/dist/names.d.ts +13 -0
  83. package/dist/names.js +66 -0
  84. package/dist/pack.d.ts +72 -0
  85. package/dist/pack.js +514 -0
  86. package/dist/plugins.d.ts +13 -0
  87. package/dist/plugins.js +55 -0
  88. package/dist/rules.d.ts +25 -0
  89. package/dist/rules.js +403 -0
  90. package/dist/score.d.ts +17 -0
  91. package/dist/score.js +58 -0
  92. package/dist/seven.d.ts +75 -0
  93. package/dist/seven.js +492 -0
  94. package/dist/sevenzip.d.ts +133 -0
  95. package/dist/sevenzip.js +529 -0
  96. package/dist/single.d.ts +103 -0
  97. package/dist/single.js +453 -0
  98. package/dist/sniff.d.ts +20 -0
  99. package/dist/sniff.js +224 -0
  100. package/dist/stream-meter.d.ts +39 -0
  101. package/dist/stream-meter.js +54 -0
  102. package/dist/tar.d.ts +102 -0
  103. package/dist/tar.js +765 -0
  104. package/dist/types.d.ts +284 -0
  105. package/dist/types.js +133 -0
  106. package/dist/unpack.d.ts +67 -0
  107. package/dist/unpack.js +194 -0
  108. package/dist/xz.d.ts +25 -0
  109. package/dist/xz.js +333 -0
  110. package/dist/zip-lzma.d.ts +15 -0
  111. package/dist/zip-lzma.js +137 -0
  112. package/dist/zip-writer.d.ts +41 -0
  113. package/dist/zip-writer.js +263 -0
  114. package/dist/zip.d.ts +99 -0
  115. package/dist/zip.js +901 -0
  116. package/dist/zipcrypt.d.ts +47 -0
  117. package/dist/zipcrypt.js +233 -0
  118. package/dist/zstd.d.ts +25 -0
  119. package/dist/zstd.js +160 -0
  120. package/package.json +94 -4
package/dist/checks.js ADDED
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.CHECK_SHA256 = exports.CHECK_CRC64 = exports.CHECK_CRC32 = exports.CHECK_NONE = exports.Crc64 = void 0;
37
+ exports.checkSize = checkSize;
38
+ exports.checkName = checkName;
39
+ exports.makeCheck = makeCheck;
40
+ const crypto = __importStar(require("node:crypto"));
41
+ const crc32_1 = require("./crc32");
42
+ // CRC-64 with the ECMA-182 polynomial, reflected, as xz uses it. Node has no CRC-64, so this table is the only one.
43
+ const POLY_HI = 0xc96c5795;
44
+ const POLY_LO = 0xd7870f42;
45
+ const TABLE_HI = new Uint32Array(256);
46
+ const TABLE_LO = new Uint32Array(256);
47
+ for (let n = 0; n < 256; n++) {
48
+ let lo = n;
49
+ let hi = 0;
50
+ for (let k = 0; k < 8; k++) {
51
+ const carry = lo & 1;
52
+ lo = ((lo >>> 1) | (hi << 31)) >>> 0;
53
+ hi >>>= 1;
54
+ if (carry) {
55
+ lo = (lo ^ POLY_LO) >>> 0;
56
+ hi = (hi ^ POLY_HI) >>> 0;
57
+ }
58
+ }
59
+ TABLE_HI[n] = hi;
60
+ TABLE_LO[n] = lo;
61
+ }
62
+ /** A running CRC-64 as xz computes it. */
63
+ class Crc64 {
64
+ hi = 0xffffffff;
65
+ lo = 0xffffffff;
66
+ update(bytes) {
67
+ let hi = this.hi;
68
+ let lo = this.lo;
69
+ for (const b of bytes) {
70
+ const i = (lo ^ b) & 0xff;
71
+ lo = (((lo >>> 8) | (hi << 24)) ^ TABLE_LO[i]) >>> 0;
72
+ hi = ((hi >>> 8) ^ TABLE_HI[i]) >>> 0;
73
+ }
74
+ this.hi = hi;
75
+ this.lo = lo;
76
+ }
77
+ /** The 8 bytes, little-endian, as xz stores them. */
78
+ digest() {
79
+ const out = Buffer.alloc(8);
80
+ out.writeUInt32LE(~this.lo >>> 0, 0);
81
+ out.writeUInt32LE(~this.hi >>> 0, 4);
82
+ return out;
83
+ }
84
+ }
85
+ exports.Crc64 = Crc64;
86
+ /** xz check IDs this package computes. */
87
+ exports.CHECK_NONE = 0x00;
88
+ exports.CHECK_CRC32 = 0x01;
89
+ exports.CHECK_CRC64 = 0x04;
90
+ exports.CHECK_SHA256 = 0x0a;
91
+ /** Size in bytes of each xz check ID, including the ones reserved for future use. */
92
+ const CHECK_SIZES = [0, 4, 4, 4, 8, 8, 8, 16, 16, 16, 32, 32, 32, 64, 64, 64];
93
+ function checkSize(id) {
94
+ return CHECK_SIZES[id & 0x0f];
95
+ }
96
+ function checkName(id) {
97
+ return id === exports.CHECK_NONE ? 'none' : id === exports.CHECK_CRC32 ? 'CRC32' : id === exports.CHECK_CRC64 ? 'CRC64' : id === exports.CHECK_SHA256 ? 'SHA-256' : `check ID ${id}`;
98
+ }
99
+ /** A check of a known ID, or undefined for an ID this package cannot compute. */
100
+ function makeCheck(id) {
101
+ if (id === exports.CHECK_NONE)
102
+ return { update: () => undefined, digest: () => new Uint8Array(0) };
103
+ if (id === exports.CHECK_CRC32) {
104
+ let c = 0;
105
+ return {
106
+ update: b => {
107
+ c = (0, crc32_1.crc32)(b, c);
108
+ },
109
+ digest: () => {
110
+ const out = Buffer.alloc(4);
111
+ out.writeUInt32LE(c, 0);
112
+ return out;
113
+ },
114
+ };
115
+ }
116
+ if (id === exports.CHECK_CRC64)
117
+ return new Crc64();
118
+ if (id === exports.CHECK_SHA256) {
119
+ const h = crypto.createHash('sha256');
120
+ return { update: b => h.update(b), digest: () => h.digest() };
121
+ }
122
+ return undefined;
123
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare function main(argv: string[]): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,364 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.main = main;
38
+ const fs = __importStar(require("node:fs"));
39
+ const fsp = __importStar(require("node:fs/promises"));
40
+ const path = __importStar(require("node:path"));
41
+ const promises_1 = require("node:stream/promises");
42
+ const node_url_1 = require("node:url");
43
+ const engine_1 = require("./engine");
44
+ const errors_1 = require("./errors");
45
+ const io_1 = require("./io");
46
+ const plugins_1 = require("./plugins");
47
+ const USAGE = `Usage:
48
+ archive-defuse inspect <file> [options]
49
+ archive-defuse defuse <in> <out> [options]
50
+
51
+ Options:
52
+ --password-file <path> read passwords from a file, one per line; ARCHIVE_DEFUSE_PASSWORD holds one
53
+ --plugin <module> load plugins from a file or an installed package, repeatable
54
+ --config <module> load a whole options object, repeatable; the other options apply on top
55
+ --keep-type <type> keep entries of this MIME type or file extension unchanged, repeatable
56
+ --defuse-inner-archives defuse archives inside the archive, such as the tar inside a .tar.gz
57
+ --strip-metadata
58
+ --limit <name>=<value> fileSize, entries, decompressedBytes, totalDecompressedBytes, compressionRatio,
59
+ dictionaryBytes, nestingDepth, timeMs; sizes accept kb, mb, gb
60
+ --json print the result as JSON
61
+
62
+ Use - for stdin or stdout.
63
+ Exit codes: 0 clean, 1 stripped or strippable, 2 rejected, 3 usage or I/O error.`;
64
+ class UsageError extends Error {
65
+ }
66
+ function parseSize(v) {
67
+ const m = /^(\d+(?:\.\d+)?)\s*(b|kb|k|mb|m|gb|g)?$/i.exec(v.trim());
68
+ if (!m)
69
+ throw new UsageError(`Bad size "${v}"`);
70
+ const mult = { b: 1, k: 1024, kb: 1024, m: 1024 ** 2, mb: 1024 ** 2, g: 1024 ** 3, gb: 1024 ** 3 };
71
+ return Math.round(Number(m[1]) * mult[(m[2] ?? 'b').toLowerCase()]);
72
+ }
73
+ // A real dynamic import. TypeScript would compile import() to require() for CommonJS output,
74
+ // and require() cannot load ES modules or file URLs.
75
+ const dynamicImport = new Function('specifier', 'return import(specifier)');
76
+ /** Reads `key` from `v` the way `v?.[key]` does, for a value whose shape is not known. */
77
+ function prop(v, key) {
78
+ return (typeof v === 'object' && v !== null) || typeof v === 'function' ? v[key] : undefined;
79
+ }
80
+ const isObject = (v) => typeof v === 'object' && v !== null;
81
+ async function loadModule(spec) {
82
+ // A bare name ending in .js can be a file in the working directory or a package with that name. The file wins.
83
+ const looksLikePath = spec.startsWith('.') || path.isAbsolute(spec) || (/\.(c|m)?js$/.test(spec) && fs.statSync(spec, { throwIfNoEntry: false })?.isFile() === true);
84
+ let resolved;
85
+ if (looksLikePath)
86
+ resolved = path.resolve(process.cwd(), spec);
87
+ else {
88
+ try {
89
+ resolved = require.resolve(spec, { paths: [process.cwd()] });
90
+ }
91
+ catch (e) {
92
+ // require.resolve applies only the "require" condition, so a package that exports only "import" ends up here.
93
+ if (e.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED')
94
+ throw e;
95
+ const name = spec
96
+ .split('/')
97
+ .slice(0, spec.startsWith('@') ? 2 : 1)
98
+ .join('/');
99
+ const subpath = `.${spec.slice(name.length)}`;
100
+ for (let dir = process.cwd(); resolved === undefined; dir = path.dirname(dir)) {
101
+ const pkg = path.join(dir, 'node_modules', name);
102
+ if (fs.existsSync(path.join(pkg, 'package.json'))) {
103
+ const exp = JSON.parse(fs.readFileSync(path.join(pkg, 'package.json'), 'utf8')).exports;
104
+ const isMap = isObject(exp) && Object.keys(exp).some(k => k.startsWith('.'));
105
+ let target;
106
+ let match;
107
+ if (!isMap)
108
+ target = subpath === '.' ? exp : undefined;
109
+ else if (Object.hasOwn(exp, subpath) && !subpath.includes('*'))
110
+ target = exp[subpath];
111
+ else {
112
+ // A pattern key holds one "*". Node prefers the longest text before the "*", then the longest key, and puts
113
+ // what the "*" matched into the target.
114
+ let best = '';
115
+ for (const k of Object.keys(exp)) {
116
+ const star = k.indexOf('*');
117
+ const trailer = k.slice(star + 1);
118
+ if (star < 0 || trailer.includes('*') || !subpath.startsWith(k.slice(0, star)) || subpath.length === star)
119
+ continue;
120
+ if (trailer && (!subpath.endsWith(trailer) || subpath.length < k.length))
121
+ continue;
122
+ if (!best || star > best.indexOf('*') || (star === best.indexOf('*') && k.length > best.length))
123
+ best = k;
124
+ }
125
+ if (best) {
126
+ target = exp[best];
127
+ match = subpath.slice(best.indexOf('*'), subpath.length - (best.length - best.indexOf('*') - 1));
128
+ // Node refuses a match that would step outside the pattern's directory.
129
+ if (match.split(/[/\\]/).some(s => s === '' || s === '.' || s === '..' || s.toLowerCase() === 'node_modules'))
130
+ throw e;
131
+ }
132
+ }
133
+ // Node takes the first condition it supports, in the order the package lists them.
134
+ while (isObject(target) && !Array.isArray(target))
135
+ target = target[Object.keys(target).find(k => k === 'node' || k === 'import' || k === 'default') ?? ''];
136
+ if (typeof target !== 'string')
137
+ throw e;
138
+ resolved = path.resolve(pkg, match === undefined ? target : target.replaceAll('*', match));
139
+ }
140
+ else if (path.dirname(dir) === dir)
141
+ throw e;
142
+ }
143
+ }
144
+ }
145
+ return dynamicImport((0, node_url_1.pathToFileURL)(resolved).href);
146
+ }
147
+ function pluginsFrom(mod) {
148
+ // import() gives a CommonJS module a default export holding module.exports, so the named exports come first.
149
+ const value = prop(mod, 'plugins') ?? prop(prop(mod, 'default'), 'plugins') ?? prop(mod, 'plugin') ?? prop(prop(mod, 'default'), 'plugin') ?? prop(mod, 'default');
150
+ const list = Array.isArray(value) ? value : [value];
151
+ for (const p of list)
152
+ if (!p || prop(p, 'kind') !== 'file')
153
+ throw new UsageError('A plugin module must export plugins with kind "file"');
154
+ // The loop above checked each entry's kind, which is as far as a plugin's shape can be checked here.
155
+ return list;
156
+ }
157
+ async function parse(argv) {
158
+ const [command, ...rest] = argv;
159
+ if (command !== 'inspect' && command !== 'defuse')
160
+ throw new UsageError(command ? `Unknown command "${command}"` : 'No command');
161
+ const positional = [];
162
+ // Every --config module goes in first and the flags apply on top, wherever they sit on the command line.
163
+ const options = {};
164
+ const ordered = [];
165
+ const limits = {};
166
+ let stripMetadata = false;
167
+ let json = false;
168
+ let password = process.env.ARCHIVE_DEFUSE_PASSWORD;
169
+ for (let i = 0; i < rest.length; i++) {
170
+ const a = rest[i];
171
+ const next = () => {
172
+ const v = rest[++i];
173
+ if (v === undefined)
174
+ throw new UsageError(`${a} needs a value`);
175
+ return v;
176
+ };
177
+ switch (a) {
178
+ case '--password-file':
179
+ // The empty password is always tried, so a blank line adds nothing.
180
+ password = (await fsp.readFile(next(), 'utf8'))
181
+ .split('\n')
182
+ .map(line => line.replace(/\r$/, ''))
183
+ .filter(line => line !== '');
184
+ break;
185
+ case '--plugin':
186
+ ordered.push(...pluginsFrom(await loadModule(next())));
187
+ break;
188
+ case '--config': {
189
+ const mod = await loadModule(next());
190
+ // The module's shape is the caller's to get right, as ArchiveOptions documents it.
191
+ const cfg = (prop(mod, 'default') ?? mod);
192
+ // Modules combine: plugin lists join in order and limits merge. For any other option the later module wins.
193
+ Object.assign(options, cfg, {
194
+ filePlugins: [...(options.filePlugins ?? []), ...(cfg.filePlugins ?? [])],
195
+ limits: { ...options.limits, ...cfg.limits },
196
+ });
197
+ break;
198
+ }
199
+ case '--keep-type':
200
+ ordered.push((0, plugins_1.passThrough)([next()]));
201
+ break;
202
+ case '--defuse-inner-archives':
203
+ ordered.push((0, plugins_1.archivePlugin)());
204
+ break;
205
+ case '--strip-metadata':
206
+ stripMetadata = true;
207
+ break;
208
+ case '--limit': {
209
+ const [name, value] = next().split('=');
210
+ if (value === undefined)
211
+ throw new UsageError('--limit expects name=value');
212
+ if (name === 'fileSize' || name === 'decompressedBytes' || name === 'totalDecompressedBytes' || name === 'dictionaryBytes')
213
+ limits[name] = parseSize(value);
214
+ else if (name === 'entries' || name === 'compressionRatio' || name === 'nestingDepth' || name === 'timeMs') {
215
+ // Number('abc') is NaN, and no comparison with NaN is true, so the limit would never apply.
216
+ if (!/^\d+$/.test(value.trim()))
217
+ throw new UsageError(`Bad value for ${name}: "${value}"`);
218
+ limits[name] = Number(value);
219
+ }
220
+ else
221
+ throw new UsageError(`Unknown limit "${name}"`);
222
+ break;
223
+ }
224
+ case '--json':
225
+ json = true;
226
+ break;
227
+ default:
228
+ if (a === '--password' || a.startsWith('--password='))
229
+ throw new UsageError('Pass the password with --password-file or ARCHIVE_DEFUSE_PASSWORD, not on the command line');
230
+ // What follows "=" can be a secret typed in the wrong place, and stderr often ends up in a log.
231
+ if (a.startsWith('--'))
232
+ throw new UsageError(`Unknown option ${a.replace(/=.*/s, '=...')}`);
233
+ positional.push(a);
234
+ }
235
+ }
236
+ if (stripMetadata)
237
+ options.stripMetadata = true;
238
+ if (ordered.length)
239
+ options.filePlugins = [...(options.filePlugins ?? []), ...ordered];
240
+ if (Object.keys(limits).length)
241
+ options.limits = { ...(options.limits ?? {}), ...limits };
242
+ if (password !== undefined)
243
+ options.password = password;
244
+ return { command, positional, options, json };
245
+ }
246
+ /**
247
+ * The input as a source. stdin is copied to a temporary file first. The copy stops one byte past `fileSize`, so the
248
+ * engine rejects the upload as too large without the rest being read, and at the deadline, where the source then
249
+ * fails the run for time.
250
+ */
251
+ async function inputSource(p, tempDir, fileSize, deadline) {
252
+ if (p !== '-') {
253
+ return {
254
+ source: (0, io_1.fileSource)(p),
255
+ cleanup: async () => {
256
+ // The caller's own file stays.
257
+ },
258
+ };
259
+ }
260
+ const temp = new io_1.TempDir(tempDir);
261
+ try {
262
+ const file = await temp.file('.bin');
263
+ let room = (fileSize ?? Number.POSITIVE_INFINITY) + 1;
264
+ try {
265
+ await (0, promises_1.pipeline)(process.stdin, async function* (chunks) {
266
+ for await (const c of chunks) {
267
+ yield c.subarray(0, room);
268
+ room -= c.length;
269
+ if (room <= 0)
270
+ return;
271
+ }
272
+ }, fs.createWriteStream(file), { signal: deadline === undefined ? undefined : AbortSignal.timeout(Math.max(0, deadline - Date.now())) });
273
+ }
274
+ catch (e) {
275
+ if (e.name !== 'AbortError')
276
+ throw e;
277
+ const late = () => Promise.reject(new errors_1.TimeLimitError('Time limit exceeded'));
278
+ return { source: { size: late, read: late }, cleanup: () => temp.cleanup() };
279
+ }
280
+ return { source: (0, io_1.fileSource)(file), cleanup: () => temp.cleanup() };
281
+ }
282
+ catch (e) {
283
+ await temp.cleanup();
284
+ throw e;
285
+ }
286
+ }
287
+ /** Writes a control character as a JSON escape, since text from the archive could otherwise drive the terminal. */
288
+ const escaped = (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`;
289
+ function report(label, i) {
290
+ const lines = [`${label}: ${i.status}, risk ${i.risk}${i.score === null ? '' : ` (score ${i.score})`}, ${i.format ?? 'unknown format'}, ${i.entries ?? 0} entries`];
291
+ for (const f of i.findings)
292
+ lines.push(` ${f.action.padEnd(6)} ${f.category}/${f.detail}${f.location ? ` [${f.location}]` : ''}: ${f.description}`);
293
+ // A location can hold an entry name from the archive. A line break in one would print a line of its own.
294
+ return lines.map(l => l.replace(/\p{Cc}/gu, escaped)).join('\n');
295
+ }
296
+ async function main(argv) {
297
+ let parsed;
298
+ try {
299
+ parsed = await parse(argv);
300
+ }
301
+ catch (e) {
302
+ process.stderr.write(`${e.message}\n\n${USAGE}\n`);
303
+ return 3;
304
+ }
305
+ const { command, positional, options, json } = parsed;
306
+ try {
307
+ if (command === 'inspect' && positional.length !== 1)
308
+ throw new UsageError('inspect takes one file');
309
+ if (command === 'defuse' && positional.length !== 2)
310
+ throw new UsageError('defuse takes an input and an output');
311
+ const toStdout = command === 'defuse' && positional[1] === '-';
312
+ if (command === 'defuse' && positional[0] !== '-' && !toStdout) {
313
+ // A failed write deletes the partial output, which here would be the only copy of the input.
314
+ const [a, b] = await Promise.all([fsp.stat(positional[0]), fsp.stat(positional[1]).catch(() => undefined)]);
315
+ if (b && a.dev === b.dev && a.ino === b.ino)
316
+ throw new UsageError('The input and the output are the same file');
317
+ }
318
+ // Reading stdin counts toward the time limit, so the run gets what is left of it.
319
+ const deadline = options.limits?.timeMs === undefined ? undefined : Date.now() + options.limits.timeMs;
320
+ const input = await inputSource(positional[0], options.tempDir, options.limits?.fileSize, deadline);
321
+ const runOptions = deadline === undefined ? options : { ...options, limits: { ...options.limits, timeMs: deadline - Date.now() } };
322
+ let text;
323
+ let code;
324
+ try {
325
+ if (command === 'inspect') {
326
+ const r = await (0, engine_1.inspectArchiveSource)(input.source, runOptions);
327
+ text = json ? JSON.stringify(r, null, 2) : report('inspection', r);
328
+ code = r.status === 'clean' ? 0 : r.status === 'strippable' ? 1 : 2;
329
+ }
330
+ else {
331
+ const r = await (0, engine_1.disarmArchiveSource)(input.source, toStdout ? (0, io_1.writableSink)(process.stdout) : (0, io_1.fileSink)(positional[1]), runOptions);
332
+ text = json ? JSON.stringify(r, null, 2) : [`result: ${r.status}`, report('before', r.before), ...(r.after && r.status === 'defused' ? [report('after', r.after)] : [])].join('\n');
333
+ code = r.status === 'clean' ? 0 : r.status === 'defused' ? 1 : 2;
334
+ }
335
+ }
336
+ finally {
337
+ await input.source.close?.();
338
+ await input.cleanup();
339
+ }
340
+ // JSON.stringify escapes C0 controls but not DEL or the C1 range.
341
+ if (json)
342
+ text = text.replace(/[\u007f-\u009f]/g, escaped);
343
+ // The report shares stdout only when the archive goes elsewhere. A report nobody can read is an I/O error.
344
+ const out = toStdout ? process.stderr : process.stdout;
345
+ await new Promise((resolve, reject) => out.write(`${text}\n`, e => (e ? reject(e) : resolve())));
346
+ return code;
347
+ }
348
+ catch (e) {
349
+ process.stderr.write(`${e instanceof UsageError ? `${e.message}\n\n${USAGE}` : `Error: ${e.message}`}\n`);
350
+ return 3;
351
+ }
352
+ }
353
+ if (require.main === module) {
354
+ // main() learns of a failed write from its callback. Node emits the failure as an 'error' event as well, which
355
+ // with no listener would end the process with exit code 1.
356
+ for (const stream of [process.stdout, process.stderr]) {
357
+ stream.on('error', () => {
358
+ process.exitCode = 3;
359
+ });
360
+ }
361
+ main(process.argv.slice(2)).then(code => {
362
+ process.exitCode = code;
363
+ });
364
+ }
@@ -0,0 +1,39 @@
1
+ import * as stream from 'node:stream';
2
+ type ZlibStream = stream.Transform & {
3
+ bytesWritten: number;
4
+ };
5
+ /**
6
+ * Node's zstd streams, where the running Node has them. Node 18, Node 20 and early releases of Node 22 do not. The
7
+ * choice is made once, when the module loads. Without them this package reads and writes no zstd data, and has no
8
+ * code of its own for it.
9
+ */
10
+ type ZstdFactory = (options?: {
11
+ params?: Record<number, number>;
12
+ }) => ZlibStream;
13
+ export declare const zstdDecompressor: ZstdFactory | undefined;
14
+ export declare const zstdCompressor: ZstdFactory | undefined;
15
+ /** How much of its input a transform has used. It grows as the transform works and ends at the total. */
16
+ export interface Consumed {
17
+ bytes: number;
18
+ }
19
+ /**
20
+ * Runs `chunks` through a zlib, Brotli or zstd transform and yields what comes out. A corrupt or cut-short stream
21
+ * throws DataError. `consumed.bytes` follows the input the transform has used, which ends less than it was given when
22
+ * its stream ends before the input does.
23
+ */
24
+ export declare function through(transform: ZlibStream, chunks: AsyncIterable<Uint8Array>, consumed?: Consumed): AsyncGenerator<Uint8Array>;
25
+ /** Raw deflate data, decoded. */
26
+ export declare function inflateRaw(chunks: AsyncIterable<Uint8Array>, consumed?: Consumed): AsyncGenerator<Uint8Array>;
27
+ /** Bytes compressed as raw deflate, at zlib's level 6 or `level`. Level 0 writes stored blocks. */
28
+ export declare function deflateRaw(chunks: AsyncIterable<Uint8Array>, level?: number): AsyncGenerator<Uint8Array>;
29
+ /** Brotli data, decoded. */
30
+ export declare function brotliDecode(chunks: AsyncIterable<Uint8Array>, consumed?: Consumed): AsyncGenerator<Uint8Array>;
31
+ /** Bytes compressed as one Brotli stream. */
32
+ export declare function brotliEncode(chunks: AsyncIterable<Uint8Array>): AsyncGenerator<Uint8Array>;
33
+ /** Yields `chunks` while counting them, and calls `each` with the running total before each one. */
34
+ export declare function counted(chunks: AsyncIterable<Uint8Array>, each: (total: number) => void): AsyncGenerator<Uint8Array>;
35
+ /** One chunk as an async iterable. */
36
+ export declare function once(b: Uint8Array): AsyncGenerator<Uint8Array>;
37
+ /** Every chunk, joined. Only for data the caller has already bounded. */
38
+ export declare function collect(chunks: AsyncIterable<Uint8Array>): Promise<Buffer>;
39
+ export {};
package/dist/codecs.js ADDED
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.zstdCompressor = exports.zstdDecompressor = void 0;
37
+ exports.through = through;
38
+ exports.inflateRaw = inflateRaw;
39
+ exports.deflateRaw = deflateRaw;
40
+ exports.brotliDecode = brotliDecode;
41
+ exports.brotliEncode = brotliEncode;
42
+ exports.counted = counted;
43
+ exports.once = once;
44
+ exports.collect = collect;
45
+ const stream = __importStar(require("node:stream"));
46
+ const zlib = __importStar(require("node:zlib"));
47
+ const errors_1 = require("./errors");
48
+ const zstd = zlib;
49
+ exports.zstdDecompressor = zstd.createZstdDecompress;
50
+ exports.zstdCompressor = zstd.createZstdCompress;
51
+ /**
52
+ * True for an error zlib, Brotli or zstd gave for corrupt or cut-short input. Their errors carry a numeric errno, and
53
+ * an I/O error or a limit thrown on the input side keeps its own code.
54
+ */
55
+ function isCodecError(e) {
56
+ const { code, errno } = (e ?? {});
57
+ if (code === 'ERR_ZLIB_INITIALIZATION_FAILED')
58
+ return true;
59
+ return typeof code === 'string' && typeof errno === 'number' && (code.startsWith('Z_') || code.startsWith('ZSTD_') || code.startsWith('ERR_'));
60
+ }
61
+ /**
62
+ * Runs `chunks` through a zlib, Brotli or zstd transform and yields what comes out. A corrupt or cut-short stream
63
+ * throws DataError. `consumed.bytes` follows the input the transform has used, which ends less than it was given when
64
+ * its stream ends before the input does.
65
+ */
66
+ async function* through(transform, chunks, consumed) {
67
+ const src = stream.Readable.from(chunks);
68
+ src.on('error', err => transform.destroy(err));
69
+ src.pipe(transform);
70
+ try {
71
+ for await (const c of transform) {
72
+ if (consumed)
73
+ consumed.bytes = transform.bytesWritten;
74
+ yield c;
75
+ }
76
+ }
77
+ catch (err) {
78
+ if (isCodecError(err))
79
+ throw new errors_1.DataError(err.message, /unexpected end/i.test(err.message));
80
+ throw err;
81
+ }
82
+ finally {
83
+ if (consumed)
84
+ consumed.bytes = transform.bytesWritten;
85
+ src.destroy();
86
+ transform.destroy();
87
+ }
88
+ }
89
+ /** Raw deflate data, decoded. */
90
+ function inflateRaw(chunks, consumed) {
91
+ return through(zlib.createInflateRaw(), chunks, consumed);
92
+ }
93
+ /** Bytes compressed as raw deflate, at zlib's level 6 or `level`. Level 0 writes stored blocks. */
94
+ function deflateRaw(chunks, level = 6) {
95
+ return through(zlib.createDeflateRaw({ level }), chunks);
96
+ }
97
+ /** Brotli data, decoded. */
98
+ function brotliDecode(chunks, consumed) {
99
+ return through(zlib.createBrotliDecompress(), chunks, consumed);
100
+ }
101
+ /** Bytes compressed as one Brotli stream. */
102
+ function brotliEncode(chunks) {
103
+ return through(zlib.createBrotliCompress(), chunks);
104
+ }
105
+ /** Yields `chunks` while counting them, and calls `each` with the running total before each one. */
106
+ async function* counted(chunks, each) {
107
+ let total = 0;
108
+ for await (const c of chunks) {
109
+ total += c.length;
110
+ each(total);
111
+ yield c;
112
+ }
113
+ }
114
+ /** One chunk as an async iterable. */
115
+ async function* once(b) {
116
+ yield b;
117
+ }
118
+ /** Every chunk, joined. Only for data the caller has already bounded. */
119
+ async function collect(chunks) {
120
+ const parts = [];
121
+ for await (const c of chunks)
122
+ parts.push(c);
123
+ return Buffer.concat(parts);
124
+ }