node-liblzma 3.1.2 → 4.0.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.
- package/README.md +50 -4
- package/index.d.ts +21 -114
- package/lib/errors.d.ts +12 -0
- package/lib/errors.d.ts.map +1 -1
- package/lib/errors.js +17 -21
- package/lib/errors.js.map +1 -1
- package/lib/lzma.d.ts +37 -106
- package/lib/lzma.d.ts.map +1 -1
- package/lib/lzma.js +39 -68
- package/lib/lzma.js.map +1 -1
- package/lib/pool.d.ts.map +1 -1
- package/lib/pool.js +0 -4
- package/lib/pool.js.map +1 -1
- package/lib/types.d.ts +33 -0
- package/lib/types.d.ts.map +1 -1
- package/lib/wasm/bindings.d.ts.map +1 -1
- package/lib/wasm/bindings.js +4 -2
- package/lib/wasm/bindings.js.map +1 -1
- package/lib/wasm/compress.d.ts.map +1 -1
- package/lib/wasm/compress.js +5 -1
- package/lib/wasm/compress.js.map +1 -1
- package/lib/wasm/decompress.d.ts.map +1 -1
- package/lib/wasm/decompress.js +5 -1
- package/lib/wasm/decompress.js.map +1 -1
- package/lib/wasm/memory.d.ts.map +1 -1
- package/lib/wasm/memory.js +2 -0
- package/lib/wasm/memory.js.map +1 -1
- package/lib/wasm/stream.d.ts.map +1 -1
- package/lib/wasm/stream.js +6 -0
- package/lib/wasm/stream.js.map +1 -1
- package/lib/wasm/types.d.ts +1 -12
- package/lib/wasm/types.d.ts.map +1 -1
- package/lib/wasm/types.js +1 -12
- package/lib/wasm/types.js.map +1 -1
- package/lib/wasm/utils.d.ts +2 -15
- package/lib/wasm/utils.d.ts.map +1 -1
- package/lib/wasm/utils.js +6 -0
- package/lib/wasm/utils.js.map +1 -1
- package/package.json +15 -13
- package/prebuilds/darwin-x64/node-liblzma.node +0 -0
- package/prebuilds/linux-x64/node-liblzma.node +0 -0
- package/prebuilds/win32-x64/node-liblzma.node +0 -0
- package/src/wasm/liblzma.d.ts +9 -0
- package/lib/cli/nxz.d.ts +0 -7
- package/lib/cli/nxz.d.ts.map +0 -1
- package/lib/cli/nxz.js +0 -811
- package/lib/cli/nxz.js.map +0 -1
package/lib/cli/nxz.js
DELETED
|
@@ -1,811 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* nxz - Node.js XZ compression CLI
|
|
4
|
-
* A portable xz-like command line tool using node-liblzma
|
|
5
|
-
*/
|
|
6
|
-
import { closeSync, createReadStream, createWriteStream, existsSync, fstatSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
7
|
-
import { performance } from 'node:perf_hooks';
|
|
8
|
-
import { pipeline } from 'node:stream/promises';
|
|
9
|
-
import { parseArgs } from 'node:util';
|
|
10
|
-
import { check, createUnxz, createXz, hasThreads, isXZ, parseFileIndex, preset, unxzSync, versionString, xzSync, } from '../lzma.js';
|
|
11
|
-
let tarXzModule = null;
|
|
12
|
-
async function loadTarXz() {
|
|
13
|
-
if (!tarXzModule) {
|
|
14
|
-
try {
|
|
15
|
-
// Variable import specifier prevents TypeScript from resolving at compile time
|
|
16
|
-
const modName = 'tar-xz';
|
|
17
|
-
tarXzModule = (await import(modName));
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
throw new Error('tar-xz package not available. Install it with: pnpm add tar-xz');
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return tarXzModule;
|
|
24
|
-
}
|
|
25
|
-
/** Size threshold for using streams vs sync (1 MB) */
|
|
26
|
-
const STREAM_THRESHOLD_BYTES = 1024 * 1024;
|
|
27
|
-
/** Exit codes matching xz conventions */
|
|
28
|
-
const EXIT_SUCCESS = 0;
|
|
29
|
-
const EXIT_ERROR = 1;
|
|
30
|
-
const EXIT_SIGNAL = 128;
|
|
31
|
-
/** Track current output file for SIGINT cleanup */
|
|
32
|
-
let currentOutputFile = null;
|
|
33
|
-
/** Global quiet flag for warn() function */
|
|
34
|
-
let quietMode = false;
|
|
35
|
-
/**
|
|
36
|
-
* Print warning message (respects -q flag)
|
|
37
|
-
*/
|
|
38
|
-
function warn(message) {
|
|
39
|
-
if (!quietMode) {
|
|
40
|
-
console.error(message);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Setup SIGINT handler for graceful cleanup
|
|
45
|
-
*/
|
|
46
|
-
function setupSignalHandlers() {
|
|
47
|
-
process.on('SIGINT', () => {
|
|
48
|
-
// Cleanup partial output file if exists
|
|
49
|
-
if (currentOutputFile && existsSync(currentOutputFile)) {
|
|
50
|
-
try {
|
|
51
|
-
unlinkSync(currentOutputFile);
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
// Ignore cleanup errors
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
// Exit with 128 + signal number (SIGINT = 2)
|
|
58
|
-
process.exit(EXIT_SIGNAL + 2);
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Parse command line arguments
|
|
63
|
-
*/
|
|
64
|
-
function parseCliArgs(args) {
|
|
65
|
-
// Extract preset from args (e.g., -0 through -9)
|
|
66
|
-
let presetLevel = 6; // default
|
|
67
|
-
const filteredArgs = [];
|
|
68
|
-
for (const arg of args) {
|
|
69
|
-
const presetMatch = arg.match(/^-(\d)$/);
|
|
70
|
-
if (presetMatch) {
|
|
71
|
-
presetLevel = Number.parseInt(presetMatch[1], 10);
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
filteredArgs.push(arg);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
const { values, positionals } = parseArgs({
|
|
78
|
-
args: filteredArgs,
|
|
79
|
-
options: {
|
|
80
|
-
compress: { type: 'boolean', short: 'z', default: false },
|
|
81
|
-
decompress: { type: 'boolean', short: 'd', default: false },
|
|
82
|
-
list: { type: 'boolean', short: 'l', default: false },
|
|
83
|
-
benchmark: { type: 'boolean', short: 'B', default: false },
|
|
84
|
-
tar: { type: 'boolean', short: 'T', default: false },
|
|
85
|
-
keep: { type: 'boolean', short: 'k', default: false },
|
|
86
|
-
force: { type: 'boolean', short: 'f', default: false },
|
|
87
|
-
stdout: { type: 'boolean', short: 'c', default: false },
|
|
88
|
-
output: { type: 'string', short: 'o' },
|
|
89
|
-
strip: { type: 'string', default: '0' },
|
|
90
|
-
verbose: { type: 'boolean', short: 'v', default: false },
|
|
91
|
-
quiet: { type: 'boolean', short: 'q', default: false },
|
|
92
|
-
help: { type: 'boolean', short: 'h', default: false },
|
|
93
|
-
version: { type: 'boolean', short: 'V', default: false },
|
|
94
|
-
extreme: { type: 'boolean', short: 'e', default: false },
|
|
95
|
-
},
|
|
96
|
-
allowPositionals: true,
|
|
97
|
-
strict: false,
|
|
98
|
-
});
|
|
99
|
-
return {
|
|
100
|
-
compress: values.compress === true,
|
|
101
|
-
decompress: values.decompress === true,
|
|
102
|
-
list: values.list === true,
|
|
103
|
-
benchmark: values.benchmark === true,
|
|
104
|
-
tar: values.tar === true,
|
|
105
|
-
keep: values.keep === true,
|
|
106
|
-
force: values.force === true,
|
|
107
|
-
stdout: values.stdout === true,
|
|
108
|
-
output: typeof values.output === 'string' ? values.output : null,
|
|
109
|
-
verbose: values.verbose === true,
|
|
110
|
-
quiet: values.quiet === true,
|
|
111
|
-
help: values.help === true,
|
|
112
|
-
version: values.version === true,
|
|
113
|
-
preset: presetLevel,
|
|
114
|
-
extreme: values.extreme === true,
|
|
115
|
-
strip: Number.parseInt(String(values.strip ?? '0'), 10),
|
|
116
|
-
files: positionals,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Print help message
|
|
121
|
-
*/
|
|
122
|
-
function printHelp() {
|
|
123
|
-
console.log(`nxz - Node.js XZ compression CLI (using node-liblzma)
|
|
124
|
-
|
|
125
|
-
Usage: nxz [OPTION]... [FILE]...
|
|
126
|
-
|
|
127
|
-
Compress or decompress FILEs in the .xz format.
|
|
128
|
-
|
|
129
|
-
Operation mode:
|
|
130
|
-
-z, --compress force compression
|
|
131
|
-
-d, --decompress force decompression
|
|
132
|
-
-l, --list list information about .xz files
|
|
133
|
-
-B, --benchmark benchmark native vs WASM compression
|
|
134
|
-
|
|
135
|
-
Archive mode (tar.xz):
|
|
136
|
-
-T, --tar treat file as tar.xz archive
|
|
137
|
-
Auto-detected for .tar.xz and .txz files
|
|
138
|
-
--strip=N strip N leading path components on extract (default: 0)
|
|
139
|
-
|
|
140
|
-
Operation modifiers:
|
|
141
|
-
-k, --keep keep (don't delete) input files
|
|
142
|
-
-f, --force force overwrite of output file
|
|
143
|
-
-c, --stdout write to standard output and don't delete input files
|
|
144
|
-
-o, --output=FILE write output to FILE (or directory for tar extract)
|
|
145
|
-
|
|
146
|
-
Compression presets:
|
|
147
|
-
-0 ... -9 compression preset level (default: 6)
|
|
148
|
-
-e, --extreme use extreme compression (slower)
|
|
149
|
-
|
|
150
|
-
Other options:
|
|
151
|
-
-v, --verbose be verbose (show progress)
|
|
152
|
-
-q, --quiet suppress warnings
|
|
153
|
-
-h, --help display this help and exit
|
|
154
|
-
-V, --version display version information and exit
|
|
155
|
-
|
|
156
|
-
With no FILE, or when FILE is -, read standard input.
|
|
157
|
-
|
|
158
|
-
Examples:
|
|
159
|
-
nxz file.txt compress file.txt to file.txt.xz
|
|
160
|
-
nxz -d file.xz decompress file.xz
|
|
161
|
-
nxz -T -z dir/ create archive.tar.xz from dir/
|
|
162
|
-
nxz -l archive.tar.xz list contents of archive
|
|
163
|
-
nxz -d archive.tar.xz extract archive to current directory
|
|
164
|
-
nxz -d -o dest/ arch.txz extract archive to dest/
|
|
165
|
-
|
|
166
|
-
Report bugs at: https://github.com/oorabona/node-liblzma/issues`);
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* Print version information
|
|
170
|
-
*/
|
|
171
|
-
function printVersion() {
|
|
172
|
-
// Read package.json for nxz version
|
|
173
|
-
const packageJsonPath = new URL('../../package.json', import.meta.url);
|
|
174
|
-
let nxzVersion = 'unknown';
|
|
175
|
-
try {
|
|
176
|
-
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
177
|
-
nxzVersion = pkg.version;
|
|
178
|
-
}
|
|
179
|
-
catch {
|
|
180
|
-
// Ignore error, use 'unknown'
|
|
181
|
-
}
|
|
182
|
-
const threadSupport = hasThreads() ? 'yes' : 'no';
|
|
183
|
-
const year = new Date().getFullYear();
|
|
184
|
-
console.log(`nxz ${nxzVersion}
|
|
185
|
-
node-liblzma using liblzma ${versionString()}
|
|
186
|
-
Thread support: ${threadSupport}
|
|
187
|
-
|
|
188
|
-
Copyright (C) ${year} Olivier ORABONA
|
|
189
|
-
License: LGPL-3.0`);
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Check if file is a tar.xz archive based on extension
|
|
193
|
-
*/
|
|
194
|
-
function isTarXzFile(filename) {
|
|
195
|
-
return filename.endsWith('.tar.xz') || filename.endsWith('.txz');
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Determine operation mode based on options and file extension
|
|
199
|
-
*/
|
|
200
|
-
function determineMode(options, filename) {
|
|
201
|
-
if (options.benchmark)
|
|
202
|
-
return 'benchmark';
|
|
203
|
-
// Tar mode: explicit -T flag or auto-detected from extension
|
|
204
|
-
const isTar = options.tar || isTarXzFile(filename);
|
|
205
|
-
if (isTar) {
|
|
206
|
-
if (options.list)
|
|
207
|
-
return 'tar-list';
|
|
208
|
-
if (options.decompress)
|
|
209
|
-
return 'tar-extract';
|
|
210
|
-
if (options.compress)
|
|
211
|
-
return 'tar-create';
|
|
212
|
-
// Auto-detect: if file exists and is .tar.xz/.txz, extract; otherwise create
|
|
213
|
-
if (isTarXzFile(filename)) {
|
|
214
|
-
return options.list ? 'tar-list' : 'tar-extract';
|
|
215
|
-
}
|
|
216
|
-
return 'tar-create';
|
|
217
|
-
}
|
|
218
|
-
if (options.list)
|
|
219
|
-
return 'list';
|
|
220
|
-
if (options.decompress)
|
|
221
|
-
return 'decompress';
|
|
222
|
-
if (options.compress)
|
|
223
|
-
return 'compress';
|
|
224
|
-
// Auto-detect from extension
|
|
225
|
-
if (filename.endsWith('.xz') || filename.endsWith('.lzma')) {
|
|
226
|
-
return 'decompress';
|
|
227
|
-
}
|
|
228
|
-
return 'compress';
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Get output filename based on operation mode
|
|
232
|
-
*/
|
|
233
|
-
function getOutputFilename(inputFile, mode) {
|
|
234
|
-
if (mode === 'compress') {
|
|
235
|
-
return `${inputFile}.xz`;
|
|
236
|
-
}
|
|
237
|
-
// Decompress: remove .xz or .lzma extension
|
|
238
|
-
if (inputFile.endsWith('.xz')) {
|
|
239
|
-
return inputFile.slice(0, -3);
|
|
240
|
-
}
|
|
241
|
-
if (inputFile.endsWith('.lzma')) {
|
|
242
|
-
return inputFile.slice(0, -5);
|
|
243
|
-
}
|
|
244
|
-
return `${inputFile}.out`;
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* Format bytes to human-readable string
|
|
248
|
-
*/
|
|
249
|
-
function formatBytes(bytes) {
|
|
250
|
-
if (bytes < 1024)
|
|
251
|
-
return `${bytes} B`;
|
|
252
|
-
if (bytes < 1024 * 1024)
|
|
253
|
-
return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
254
|
-
if (bytes < 1024 * 1024 * 1024)
|
|
255
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
|
|
256
|
-
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GiB`;
|
|
257
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* List information about an XZ file
|
|
260
|
-
*/
|
|
261
|
-
function listFile(filename, options) {
|
|
262
|
-
try {
|
|
263
|
-
const data = readFileSync(filename);
|
|
264
|
-
const info = parseFileIndex(data);
|
|
265
|
-
const ratio = info.compressedSize > 0
|
|
266
|
-
? ((1 - info.uncompressedSize / info.compressedSize) * -100).toFixed(1)
|
|
267
|
-
: '0.0';
|
|
268
|
-
const checkNames = {
|
|
269
|
-
[check.NONE]: 'None',
|
|
270
|
-
[check.CRC32]: 'CRC32',
|
|
271
|
-
[check.CRC64]: 'CRC64',
|
|
272
|
-
[check.SHA256]: 'SHA-256',
|
|
273
|
-
};
|
|
274
|
-
const checkName = checkNames[info.check] ?? `Unknown(${info.check})`;
|
|
275
|
-
if (options.verbose) {
|
|
276
|
-
console.log(`File: ${filename}`);
|
|
277
|
-
console.log(` Streams: ${info.streamCount}`);
|
|
278
|
-
console.log(` Blocks: ${info.blockCount}`);
|
|
279
|
-
console.log(` Compressed: ${formatBytes(info.compressedSize)}`);
|
|
280
|
-
console.log(` Uncompressed: ${formatBytes(info.uncompressedSize)}`);
|
|
281
|
-
console.log(` Ratio: ${ratio}%`);
|
|
282
|
-
console.log(` Check: ${checkName}`);
|
|
283
|
-
console.log(` Memory needed: ${formatBytes(info.memoryUsage)}`);
|
|
284
|
-
}
|
|
285
|
-
else {
|
|
286
|
-
// Compact format similar to xz -l
|
|
287
|
-
console.log('Strms Blocks Compressed Uncompressed Ratio Check Filename');
|
|
288
|
-
console.log(` ${info.streamCount} ${info.blockCount} ${formatBytes(info.compressedSize).padStart(12)} ${formatBytes(info.uncompressedSize).padStart(12)} ${ratio.padStart(5)}% ${checkName.padEnd(6)} ${filename}`);
|
|
289
|
-
}
|
|
290
|
-
return EXIT_SUCCESS;
|
|
291
|
-
}
|
|
292
|
-
catch (err) {
|
|
293
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
294
|
-
warn(`nxz: ${filename}: ${message}`);
|
|
295
|
-
return EXIT_ERROR;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
/**
|
|
299
|
-
* Compress a file
|
|
300
|
-
*/
|
|
301
|
-
/** Resolve output file and check for overwrites. Returns null for stdout mode. */
|
|
302
|
-
function resolveOutputFile(inputFile, mode, options) {
|
|
303
|
-
if (options.stdout)
|
|
304
|
-
return null;
|
|
305
|
-
return options.output ?? getOutputFilename(inputFile, mode);
|
|
306
|
-
}
|
|
307
|
-
/** Attach verbose progress tracking to a stream. */
|
|
308
|
-
function attachProgress(stream, inputFile, totalSize) {
|
|
309
|
-
let lastPercent = -1;
|
|
310
|
-
stream.on('progress', ({ bytesRead, bytesWritten }) => {
|
|
311
|
-
const percent = Math.floor((bytesRead / totalSize) * 100);
|
|
312
|
-
if (percent !== lastPercent) {
|
|
313
|
-
lastPercent = percent;
|
|
314
|
-
process.stderr.write(`\r${inputFile}: ${percent}% (${formatBytes(bytesRead)} -> ${formatBytes(bytesWritten)})`);
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
/** Clean up a partial output file and reset tracking state. */
|
|
319
|
-
function cleanupPartialOutput(outputFile) {
|
|
320
|
-
if (outputFile && existsSync(outputFile)) {
|
|
321
|
-
try {
|
|
322
|
-
unlinkSync(outputFile);
|
|
323
|
-
}
|
|
324
|
-
catch {
|
|
325
|
-
// Ignore cleanup errors
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
currentOutputFile = null;
|
|
329
|
-
}
|
|
330
|
-
/** Read file content using fd-based operations (avoids TOCTOU race). Returns [data, fileSize]. */
|
|
331
|
-
function readFileSafe(inputFile) {
|
|
332
|
-
const fd = openSync(inputFile, 'r');
|
|
333
|
-
try {
|
|
334
|
-
const size = fstatSync(fd).size;
|
|
335
|
-
const data = readFileSync(fd);
|
|
336
|
-
return { data, size };
|
|
337
|
-
}
|
|
338
|
-
finally {
|
|
339
|
-
closeSync(fd);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
/** Write compressed/decompressed output to stdout or file. */
|
|
343
|
-
function writeOutput(output, outputFile) {
|
|
344
|
-
if (outputFile) {
|
|
345
|
-
writeFileSync(outputFile, output);
|
|
346
|
-
}
|
|
347
|
-
else {
|
|
348
|
-
process.stdout.write(output);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
/** Delete the original file after successful compression/decompression. */
|
|
352
|
-
function removeOriginalIfNeeded(inputFile, options) {
|
|
353
|
-
if (!options.keep && !options.stdout) {
|
|
354
|
-
unlinkSync(inputFile);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
async function compressFile(inputFile, options) {
|
|
358
|
-
const outputFile = resolveOutputFile(inputFile, 'compress', options);
|
|
359
|
-
if (outputFile && existsSync(outputFile) && !options.force) {
|
|
360
|
-
warn(`nxz: ${outputFile}: File already exists; use -f to overwrite`);
|
|
361
|
-
return EXIT_ERROR;
|
|
362
|
-
}
|
|
363
|
-
currentOutputFile = outputFile;
|
|
364
|
-
try {
|
|
365
|
-
const { data, size } = readFileSafe(inputFile);
|
|
366
|
-
const presetValue = options.extreme ? options.preset | preset.EXTREME : options.preset;
|
|
367
|
-
if (!outputFile || size <= STREAM_THRESHOLD_BYTES) {
|
|
368
|
-
// Stdout or small file: sync compression
|
|
369
|
-
const compressed = xzSync(data, { preset: presetValue });
|
|
370
|
-
writeOutput(compressed, outputFile);
|
|
371
|
-
}
|
|
372
|
-
else {
|
|
373
|
-
// Large file: stream compression with optional progress
|
|
374
|
-
const compressor = createXz({ preset: presetValue });
|
|
375
|
-
if (options.verbose) {
|
|
376
|
-
attachProgress(compressor, inputFile, size);
|
|
377
|
-
}
|
|
378
|
-
await pipeline(createReadStream(inputFile), compressor, createWriteStream(outputFile));
|
|
379
|
-
if (options.verbose) {
|
|
380
|
-
process.stderr.write('\n');
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
removeOriginalIfNeeded(inputFile, options);
|
|
384
|
-
currentOutputFile = null;
|
|
385
|
-
return EXIT_SUCCESS;
|
|
386
|
-
}
|
|
387
|
-
catch (err) {
|
|
388
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
389
|
-
warn(`nxz: ${inputFile}: ${message}`);
|
|
390
|
-
cleanupPartialOutput(outputFile);
|
|
391
|
-
return EXIT_ERROR;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
/**
|
|
395
|
-
* Decompress a file
|
|
396
|
-
*/
|
|
397
|
-
async function decompressFile(inputFile, options) {
|
|
398
|
-
const outputFile = resolveOutputFile(inputFile, 'decompress', options);
|
|
399
|
-
if (outputFile && existsSync(outputFile) && !options.force) {
|
|
400
|
-
warn(`nxz: ${outputFile}: File already exists; use -f to overwrite`);
|
|
401
|
-
return EXIT_ERROR;
|
|
402
|
-
}
|
|
403
|
-
currentOutputFile = outputFile;
|
|
404
|
-
try {
|
|
405
|
-
const { data, size } = readFileSafe(inputFile);
|
|
406
|
-
if (!isXZ(data)) {
|
|
407
|
-
warn(`nxz: ${inputFile}: File format not recognized`);
|
|
408
|
-
currentOutputFile = null;
|
|
409
|
-
return EXIT_ERROR;
|
|
410
|
-
}
|
|
411
|
-
if (!outputFile || size <= STREAM_THRESHOLD_BYTES) {
|
|
412
|
-
// Stdout or small file: sync decompression
|
|
413
|
-
const decompressed = unxzSync(data);
|
|
414
|
-
writeOutput(decompressed, outputFile);
|
|
415
|
-
}
|
|
416
|
-
else {
|
|
417
|
-
// Large file: stream decompression with optional progress
|
|
418
|
-
const decompressor = createUnxz();
|
|
419
|
-
if (options.verbose) {
|
|
420
|
-
attachProgress(decompressor, inputFile, size);
|
|
421
|
-
}
|
|
422
|
-
await pipeline(createReadStream(inputFile), decompressor, createWriteStream(outputFile));
|
|
423
|
-
if (options.verbose) {
|
|
424
|
-
process.stderr.write('\n');
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
removeOriginalIfNeeded(inputFile, options);
|
|
428
|
-
currentOutputFile = null;
|
|
429
|
-
return EXIT_SUCCESS;
|
|
430
|
-
}
|
|
431
|
-
catch (err) {
|
|
432
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
433
|
-
warn(`nxz: ${inputFile}: ${message}`);
|
|
434
|
-
cleanupPartialOutput(outputFile);
|
|
435
|
-
return EXIT_ERROR;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* Process a single file
|
|
440
|
-
*/
|
|
441
|
-
/** Measure execution time of an async function in milliseconds. */
|
|
442
|
-
async function measureAsync(fn) {
|
|
443
|
-
const start = performance.now();
|
|
444
|
-
const result = await fn();
|
|
445
|
-
return { result, ms: performance.now() - start };
|
|
446
|
-
}
|
|
447
|
-
/** Measure execution time of a sync function in milliseconds. */
|
|
448
|
-
function measureSync(fn) {
|
|
449
|
-
const start = performance.now();
|
|
450
|
-
const result = fn();
|
|
451
|
-
return { result, ms: performance.now() - start };
|
|
452
|
-
}
|
|
453
|
-
/** Format milliseconds for display. */
|
|
454
|
-
function formatMs(ms) {
|
|
455
|
-
if (ms < 1)
|
|
456
|
-
return `${(ms * 1000).toFixed(0)} µs`;
|
|
457
|
-
if (ms < 1000)
|
|
458
|
-
return `${ms.toFixed(1)} ms`;
|
|
459
|
-
return `${(ms / 1000).toFixed(2)} s`;
|
|
460
|
-
}
|
|
461
|
-
/** Format a ratio as "Nx faster" or "Nx slower". */
|
|
462
|
-
function formatSpeedup(baseline, candidate) {
|
|
463
|
-
if (candidate === 0 || baseline === 0)
|
|
464
|
-
return 'N/A';
|
|
465
|
-
const ratio = baseline / candidate;
|
|
466
|
-
if (ratio >= 1)
|
|
467
|
-
return `${ratio.toFixed(1)}x faster`;
|
|
468
|
-
return `${(1 / ratio).toFixed(1)}x slower`;
|
|
469
|
-
}
|
|
470
|
-
/**
|
|
471
|
-
* Benchmark native vs WASM compression/decompression on a file.
|
|
472
|
-
*/
|
|
473
|
-
async function benchmarkFile(inputFile, options) {
|
|
474
|
-
// Initialize WASM module with Node.js file-based loader
|
|
475
|
-
const { initModule, resetModule } = await import('../wasm/bindings.js');
|
|
476
|
-
const { xzAsync: wasmXzAsync, unxzAsync: wasmUnxzAsync } = await import('../wasm/index.js');
|
|
477
|
-
const { readFileSync: fsReadFileSync } = await import('node:fs');
|
|
478
|
-
const { fileURLToPath } = await import('node:url');
|
|
479
|
-
const { dirname, join } = await import('node:path');
|
|
480
|
-
const wasmDir = dirname(fileURLToPath(import.meta.url));
|
|
481
|
-
const wasmPath = join(wasmDir, '..', 'wasm', 'liblzma.wasm');
|
|
482
|
-
resetModule();
|
|
483
|
-
await initModule(async () => {
|
|
484
|
-
const { default: createLZMA } = await import('../wasm/liblzma.js');
|
|
485
|
-
const wasmBinary = fsReadFileSync(wasmPath);
|
|
486
|
-
return (await createLZMA({ wasmBinary }));
|
|
487
|
-
});
|
|
488
|
-
const { data, size } = readFileSafe(inputFile);
|
|
489
|
-
const presetValue = options.extreme ? options.preset | preset.EXTREME : options.preset;
|
|
490
|
-
console.error(`\nBenchmark: ${inputFile} (${formatBytes(size)}, preset ${options.preset}${options.extreme ? 'e' : ''})\n`);
|
|
491
|
-
// --- Compression ---
|
|
492
|
-
const nativeCompress = measureSync(() => xzSync(data, { preset: presetValue }));
|
|
493
|
-
const wasmCompress = await measureAsync(() => wasmXzAsync(data, { preset: presetValue }));
|
|
494
|
-
// --- Decompression ---
|
|
495
|
-
const nativeDecompress = measureSync(() => unxzSync(nativeCompress.result));
|
|
496
|
-
const wasmDecompress = await measureAsync(() => wasmUnxzAsync(wasmCompress.result));
|
|
497
|
-
// --- Verify correctness ---
|
|
498
|
-
const nativeOk = Buffer.compare(nativeDecompress.result, data) === 0;
|
|
499
|
-
const wasmOk = Buffer.compare(Buffer.from(wasmDecompress.result), data) === 0;
|
|
500
|
-
// --- Cross-decompression ---
|
|
501
|
-
const crossNativeToWasm = await measureAsync(() => wasmUnxzAsync(nativeCompress.result));
|
|
502
|
-
const crossWasmToNative = measureSync(() => unxzSync(Buffer.from(wasmCompress.result)));
|
|
503
|
-
const crossOk1 = Buffer.compare(Buffer.from(crossNativeToWasm.result), data) === 0;
|
|
504
|
-
const crossOk2 = Buffer.compare(crossWasmToNative.result, data) === 0;
|
|
505
|
-
// --- Output table ---
|
|
506
|
-
const col1 = 22;
|
|
507
|
-
const col2 = 16;
|
|
508
|
-
const col3 = 16;
|
|
509
|
-
const col4 = 18;
|
|
510
|
-
const sep = '-'.repeat(col1 + col2 + col3 + col4 + 7);
|
|
511
|
-
const row = (label, native, wasm, diff) => ` ${label.padEnd(col1)} ${native.padStart(col2)} ${wasm.padStart(col3)} ${diff.padStart(col4)}`;
|
|
512
|
-
console.error(sep);
|
|
513
|
-
console.error(row('', 'Native', 'WASM', 'Comparison'));
|
|
514
|
-
console.error(sep);
|
|
515
|
-
console.error(row('Compress time', formatMs(nativeCompress.ms), formatMs(wasmCompress.ms), formatSpeedup(wasmCompress.ms, nativeCompress.ms)));
|
|
516
|
-
console.error(row('Compressed size', formatBytes(nativeCompress.result.length), formatBytes(wasmCompress.result.length), nativeCompress.result.length === wasmCompress.result.length
|
|
517
|
-
? 'identical'
|
|
518
|
-
: `${((wasmCompress.result.length / nativeCompress.result.length - 1) * 100).toFixed(1)}%`));
|
|
519
|
-
console.error(row('Decompress time', formatMs(nativeDecompress.ms), formatMs(wasmDecompress.ms), formatSpeedup(wasmDecompress.ms, nativeDecompress.ms)));
|
|
520
|
-
console.error(row('Roundtrip OK', nativeOk ? 'YES' : 'FAIL', wasmOk ? 'YES' : 'FAIL', ''));
|
|
521
|
-
console.error(sep);
|
|
522
|
-
console.error(row('Cross: Native→WASM', '', formatMs(crossNativeToWasm.ms), crossOk1 ? 'OK' : 'FAIL'));
|
|
523
|
-
console.error(row('Cross: WASM→Native', formatMs(crossWasmToNative.ms), '', crossOk2 ? 'OK' : 'FAIL'));
|
|
524
|
-
console.error(sep);
|
|
525
|
-
const allOk = nativeOk && wasmOk && crossOk1 && crossOk2;
|
|
526
|
-
console.error(`\n Verdict: ${allOk ? 'ALL PASS — Both backends produce valid output' : 'FAIL — Data mismatch detected'}\n`);
|
|
527
|
-
// Reset WASM module to not interfere with other operations
|
|
528
|
-
resetModule();
|
|
529
|
-
return allOk ? EXIT_SUCCESS : EXIT_ERROR;
|
|
530
|
-
}
|
|
531
|
-
/**
|
|
532
|
-
* List contents of a tar.xz archive
|
|
533
|
-
*/
|
|
534
|
-
async function listTarFile(filename, options) {
|
|
535
|
-
try {
|
|
536
|
-
const tarXz = await loadTarXz();
|
|
537
|
-
const entries = await tarXz.list({ file: filename });
|
|
538
|
-
if (options.verbose) {
|
|
539
|
-
// Verbose format with permissions, size, date
|
|
540
|
-
for (const entry of entries) {
|
|
541
|
-
const typeChar = entry.type === '5' ? 'd' : '-';
|
|
542
|
-
const modeStr = entry.mode?.toString(8).padStart(4, '0') ?? '0644';
|
|
543
|
-
const size = formatBytes(entry.size).padStart(10);
|
|
544
|
-
const date = entry.mtime ? new Date(entry.mtime * 1000).toISOString().slice(0, 16) : '';
|
|
545
|
-
console.log(`${typeChar}${modeStr} ${size} ${date} ${entry.name}`);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
else {
|
|
549
|
-
// Simple format: just names
|
|
550
|
-
for (const entry of entries) {
|
|
551
|
-
console.log(entry.name);
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
if (!options.quiet) {
|
|
555
|
-
console.error(`\nTotal: ${entries.length} entries`);
|
|
556
|
-
}
|
|
557
|
-
return EXIT_SUCCESS;
|
|
558
|
-
}
|
|
559
|
-
catch (err) {
|
|
560
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
561
|
-
warn(`nxz: ${filename}: ${message}`);
|
|
562
|
-
return EXIT_ERROR;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
/**
|
|
566
|
-
* Create a tar.xz archive from files/directories
|
|
567
|
-
*/
|
|
568
|
-
function findCommonParent(paths) {
|
|
569
|
-
if (paths.length === 0)
|
|
570
|
-
return process.cwd();
|
|
571
|
-
if (paths.length === 1)
|
|
572
|
-
return paths[0];
|
|
573
|
-
const parts = paths.map((p) => p.split('/'));
|
|
574
|
-
const common = [];
|
|
575
|
-
for (let i = 0; i < parts[0].length; i++) {
|
|
576
|
-
const segment = parts[0][i];
|
|
577
|
-
if (parts.every((p) => p[i] === segment)) {
|
|
578
|
-
common.push(segment);
|
|
579
|
-
}
|
|
580
|
-
else {
|
|
581
|
-
break;
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
return common.join('/') || '/';
|
|
585
|
-
}
|
|
586
|
-
async function createTarFile(files, options) {
|
|
587
|
-
try {
|
|
588
|
-
const tarXz = await loadTarXz();
|
|
589
|
-
const path = await import('node:path');
|
|
590
|
-
// Determine output filename
|
|
591
|
-
let outputFile = options.output;
|
|
592
|
-
if (!outputFile) {
|
|
593
|
-
// Use first file/dir name as base
|
|
594
|
-
const base = path.basename(files[0]).replace(/\/$/, '');
|
|
595
|
-
outputFile = `${base}.tar.xz`;
|
|
596
|
-
}
|
|
597
|
-
if (existsSync(outputFile) && !options.force) {
|
|
598
|
-
warn(`nxz: ${outputFile}: File already exists; use -f to overwrite`);
|
|
599
|
-
return EXIT_ERROR;
|
|
600
|
-
}
|
|
601
|
-
currentOutputFile = outputFile;
|
|
602
|
-
// Resolve all files to absolute paths for consistent handling
|
|
603
|
-
const resolvedFiles = files.map((f) => path.resolve(f));
|
|
604
|
-
// Determine cwd (common parent directory)
|
|
605
|
-
let cwd;
|
|
606
|
-
if (resolvedFiles.length === 1 && statSync(resolvedFiles[0]).isDirectory()) {
|
|
607
|
-
cwd = resolvedFiles[0];
|
|
608
|
-
}
|
|
609
|
-
else {
|
|
610
|
-
// Find common parent of all files
|
|
611
|
-
const parents = resolvedFiles.map((f) => (statSync(f).isDirectory() ? f : path.dirname(f)));
|
|
612
|
-
// Use the first file's parent as cwd for simple cases
|
|
613
|
-
cwd = parents.length === 1 ? parents[0] : findCommonParent(parents);
|
|
614
|
-
}
|
|
615
|
-
// Collect files to archive (relative to cwd)
|
|
616
|
-
const filesToArchive = [];
|
|
617
|
-
for (const file of resolvedFiles) {
|
|
618
|
-
if (statSync(file).isDirectory()) {
|
|
619
|
-
const { readdirSync } = await import('node:fs');
|
|
620
|
-
const entries = readdirSync(file, { recursive: true, withFileTypes: true });
|
|
621
|
-
const dirRelative = path.relative(cwd, file);
|
|
622
|
-
for (const entry of entries) {
|
|
623
|
-
if (entry.isFile()) {
|
|
624
|
-
const entryPath = entry.parentPath === file
|
|
625
|
-
? entry.name
|
|
626
|
-
: `${entry.parentPath.slice(file.length + 1)}/${entry.name}`;
|
|
627
|
-
filesToArchive.push(dirRelative ? `${dirRelative}/${entryPath}` : entryPath);
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
else {
|
|
632
|
-
filesToArchive.push(path.relative(cwd, file));
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
const presetValue = options.extreme ? options.preset | preset.EXTREME : options.preset;
|
|
636
|
-
if (options.verbose) {
|
|
637
|
-
console.error(`Creating ${outputFile} from ${filesToArchive.length} files...`);
|
|
638
|
-
}
|
|
639
|
-
await tarXz.create({
|
|
640
|
-
file: outputFile,
|
|
641
|
-
cwd,
|
|
642
|
-
files: filesToArchive,
|
|
643
|
-
preset: presetValue,
|
|
644
|
-
});
|
|
645
|
-
if (options.verbose) {
|
|
646
|
-
const stats = statSync(outputFile);
|
|
647
|
-
console.error(`Created ${outputFile} (${formatBytes(stats.size)})`);
|
|
648
|
-
}
|
|
649
|
-
currentOutputFile = null;
|
|
650
|
-
return EXIT_SUCCESS;
|
|
651
|
-
}
|
|
652
|
-
catch (err) {
|
|
653
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
654
|
-
warn(`nxz: ${message}`);
|
|
655
|
-
cleanupPartialOutput(currentOutputFile);
|
|
656
|
-
return EXIT_ERROR;
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
/**
|
|
660
|
-
* Extract a tar.xz archive
|
|
661
|
-
*/
|
|
662
|
-
async function extractTarFile(filename, options) {
|
|
663
|
-
try {
|
|
664
|
-
const tarXz = await loadTarXz();
|
|
665
|
-
// Determine output directory
|
|
666
|
-
const outputDir = options.output ?? process.cwd();
|
|
667
|
-
// Create output directory if it doesn't exist
|
|
668
|
-
if (!existsSync(outputDir)) {
|
|
669
|
-
const { mkdirSync } = await import('node:fs');
|
|
670
|
-
mkdirSync(outputDir, { recursive: true });
|
|
671
|
-
}
|
|
672
|
-
if (options.verbose) {
|
|
673
|
-
console.error(`Extracting ${filename} to ${outputDir}...`);
|
|
674
|
-
}
|
|
675
|
-
const entries = await tarXz.extract({
|
|
676
|
-
file: filename,
|
|
677
|
-
cwd: outputDir,
|
|
678
|
-
strip: options.strip,
|
|
679
|
-
});
|
|
680
|
-
if (options.verbose) {
|
|
681
|
-
for (const entry of entries) {
|
|
682
|
-
console.error(` ${entry.name}`);
|
|
683
|
-
}
|
|
684
|
-
console.error(`\nExtracted ${entries.length} entries`);
|
|
685
|
-
}
|
|
686
|
-
// Delete original if not keeping
|
|
687
|
-
removeOriginalIfNeeded(filename, options);
|
|
688
|
-
return EXIT_SUCCESS;
|
|
689
|
-
}
|
|
690
|
-
catch (err) {
|
|
691
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
692
|
-
warn(`nxz: ${filename}: ${message}`);
|
|
693
|
-
return EXIT_ERROR;
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
async function processFile(filename, options) {
|
|
697
|
-
// Check file exists
|
|
698
|
-
if (!existsSync(filename)) {
|
|
699
|
-
warn(`nxz: ${filename}: No such file or directory`);
|
|
700
|
-
return EXIT_ERROR;
|
|
701
|
-
}
|
|
702
|
-
const isDir = statSync(filename).isDirectory();
|
|
703
|
-
const mode = determineMode(options, filename);
|
|
704
|
-
// Directories are only allowed for tar-create mode
|
|
705
|
-
if (isDir && mode !== 'tar-create') {
|
|
706
|
-
warn(`nxz: ${filename}: Is a directory, skipping`);
|
|
707
|
-
return EXIT_ERROR;
|
|
708
|
-
}
|
|
709
|
-
switch (mode) {
|
|
710
|
-
case 'list':
|
|
711
|
-
return listFile(filename, options);
|
|
712
|
-
case 'benchmark':
|
|
713
|
-
return benchmarkFile(filename, options);
|
|
714
|
-
case 'compress':
|
|
715
|
-
return compressFile(filename, options);
|
|
716
|
-
case 'decompress':
|
|
717
|
-
return decompressFile(filename, options);
|
|
718
|
-
case 'tar-list':
|
|
719
|
-
return listTarFile(filename, options);
|
|
720
|
-
case 'tar-extract':
|
|
721
|
-
return extractTarFile(filename, options);
|
|
722
|
-
case 'tar-create':
|
|
723
|
-
// tar-create is handled separately since it takes multiple files
|
|
724
|
-
return EXIT_SUCCESS;
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
/**
|
|
728
|
-
* Read from stdin and process
|
|
729
|
-
*/
|
|
730
|
-
async function processStdin(options) {
|
|
731
|
-
const chunks = [];
|
|
732
|
-
for await (const chunk of process.stdin) {
|
|
733
|
-
chunks.push(chunk);
|
|
734
|
-
}
|
|
735
|
-
const input = Buffer.concat(chunks);
|
|
736
|
-
const mode = options.decompress ? 'decompress' : 'compress';
|
|
737
|
-
try {
|
|
738
|
-
if (mode === 'compress') {
|
|
739
|
-
const presetValue = options.extreme ? options.preset | preset.EXTREME : options.preset;
|
|
740
|
-
const compressed = xzSync(input, { preset: presetValue });
|
|
741
|
-
process.stdout.write(compressed);
|
|
742
|
-
}
|
|
743
|
-
else {
|
|
744
|
-
if (!isXZ(input)) {
|
|
745
|
-
warn('nxz: (stdin): File format not recognized');
|
|
746
|
-
return EXIT_ERROR;
|
|
747
|
-
}
|
|
748
|
-
const decompressed = unxzSync(input);
|
|
749
|
-
process.stdout.write(decompressed);
|
|
750
|
-
}
|
|
751
|
-
return EXIT_SUCCESS;
|
|
752
|
-
}
|
|
753
|
-
catch (err) {
|
|
754
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
755
|
-
warn(`nxz: (stdin): ${message}`);
|
|
756
|
-
return EXIT_ERROR;
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
/**
|
|
760
|
-
* Main entry point
|
|
761
|
-
*/
|
|
762
|
-
async function main() {
|
|
763
|
-
// Setup signal handlers for graceful cleanup
|
|
764
|
-
setupSignalHandlers();
|
|
765
|
-
const options = parseCliArgs(process.argv.slice(2));
|
|
766
|
-
// Set global quiet mode
|
|
767
|
-
quietMode = options.quiet;
|
|
768
|
-
// Handle help and version first
|
|
769
|
-
if (options.help) {
|
|
770
|
-
printHelp();
|
|
771
|
-
process.exit(EXIT_SUCCESS);
|
|
772
|
-
}
|
|
773
|
-
if (options.version) {
|
|
774
|
-
printVersion();
|
|
775
|
-
process.exit(EXIT_SUCCESS);
|
|
776
|
-
}
|
|
777
|
-
// No files: read from stdin
|
|
778
|
-
if (options.files.length === 0 || (options.files.length === 1 && options.files[0] === '-')) {
|
|
779
|
-
const exitCode = await processStdin(options);
|
|
780
|
-
process.exit(exitCode);
|
|
781
|
-
}
|
|
782
|
-
// Check for tar-create mode: -T with files that aren't .tar.xz archives
|
|
783
|
-
if (options.files.length > 0) {
|
|
784
|
-
const mode = determineMode(options, options.files[0]);
|
|
785
|
-
if (mode === 'tar-create') {
|
|
786
|
-
const exitCode = await createTarFile(options.files, options);
|
|
787
|
-
process.exit(exitCode);
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
// Process each file
|
|
791
|
-
let exitCode = EXIT_SUCCESS;
|
|
792
|
-
for (const file of options.files) {
|
|
793
|
-
if (file === '-') {
|
|
794
|
-
const code = await processStdin(options);
|
|
795
|
-
if (code !== EXIT_SUCCESS)
|
|
796
|
-
exitCode = code;
|
|
797
|
-
}
|
|
798
|
-
else {
|
|
799
|
-
const code = await processFile(file, options);
|
|
800
|
-
if (code !== EXIT_SUCCESS)
|
|
801
|
-
exitCode = code;
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
process.exit(exitCode);
|
|
805
|
-
}
|
|
806
|
-
// Run main
|
|
807
|
-
main().catch((err) => {
|
|
808
|
-
console.error(`nxz: ${err.message}`);
|
|
809
|
-
process.exit(EXIT_ERROR);
|
|
810
|
-
});
|
|
811
|
-
//# sourceMappingURL=nxz.js.map
|