node-liblzma 2.2.0 → 3.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.
- package/README.md +446 -766
- package/lib/cli/nxz.js +410 -85
- package/lib/cli/nxz.js.map +1 -1
- package/lib/lzma.browser.d.ts +24 -0
- package/lib/lzma.browser.d.ts.map +1 -0
- package/lib/lzma.browser.js +30 -0
- package/lib/lzma.browser.js.map +1 -0
- package/lib/lzma.d.ts.map +1 -1
- package/lib/lzma.inline.d.ts +30 -0
- package/lib/lzma.inline.d.ts.map +1 -0
- package/lib/lzma.inline.js +68 -0
- package/lib/lzma.inline.js.map +1 -0
- package/lib/lzma.js +20 -13
- package/lib/lzma.js.map +1 -1
- package/lib/pool.d.ts.map +1 -1
- package/lib/pool.js +2 -1
- package/lib/pool.js.map +1 -1
- package/lib/wasm/bindings.d.ts +109 -0
- package/lib/wasm/bindings.d.ts.map +1 -0
- package/lib/wasm/bindings.js +320 -0
- package/lib/wasm/bindings.js.map +1 -0
- package/lib/wasm/compress.d.ts +32 -0
- package/lib/wasm/compress.d.ts.map +1 -0
- package/lib/wasm/compress.js +47 -0
- package/lib/wasm/compress.js.map +1 -0
- package/lib/wasm/decompress.d.ts +32 -0
- package/lib/wasm/decompress.d.ts.map +1 -0
- package/lib/wasm/decompress.js +45 -0
- package/lib/wasm/decompress.js.map +1 -0
- package/lib/wasm/index.d.ts +14 -0
- package/lib/wasm/index.d.ts.map +1 -0
- package/lib/wasm/index.js +18 -0
- package/lib/wasm/index.js.map +1 -0
- package/lib/wasm/liblzma.inline.d.ts +10 -0
- package/lib/wasm/liblzma.inline.d.ts.map +1 -0
- package/lib/wasm/liblzma.inline.js +10 -0
- package/lib/wasm/liblzma.inline.js.map +1 -0
- package/lib/wasm/memory.d.ts +57 -0
- package/lib/wasm/memory.d.ts.map +1 -0
- package/lib/wasm/memory.js +110 -0
- package/lib/wasm/memory.js.map +1 -0
- package/lib/wasm/stream.d.ts +35 -0
- package/lib/wasm/stream.d.ts.map +1 -0
- package/lib/wasm/stream.js +168 -0
- package/lib/wasm/stream.js.map +1 -0
- package/lib/wasm/types.d.ts +77 -0
- package/lib/wasm/types.d.ts.map +1 -0
- package/lib/wasm/types.js +55 -0
- package/lib/wasm/types.js.map +1 -0
- package/lib/wasm/utils.d.ts +62 -0
- package/lib/wasm/utils.d.ts.map +1 -0
- package/lib/wasm/utils.js +164 -0
- package/lib/wasm/utils.js.map +1 -0
- package/package.json +31 -8
package/lib/cli/nxz.js
CHANGED
|
@@ -3,10 +3,25 @@
|
|
|
3
3
|
* nxz - Node.js XZ compression CLI
|
|
4
4
|
* A portable xz-like command line tool using node-liblzma
|
|
5
5
|
*/
|
|
6
|
-
import { createReadStream, createWriteStream, existsSync, readFileSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
6
|
+
import { closeSync, createReadStream, createWriteStream, existsSync, fstatSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
7
|
+
import { performance } from 'node:perf_hooks';
|
|
7
8
|
import { pipeline } from 'node:stream/promises';
|
|
8
9
|
import { parseArgs } from 'node:util';
|
|
9
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
|
+
}
|
|
10
25
|
/** Size threshold for using streams vs sync (1 MB) */
|
|
11
26
|
const STREAM_THRESHOLD_BYTES = 1024 * 1024;
|
|
12
27
|
/** Exit codes matching xz conventions */
|
|
@@ -65,10 +80,13 @@ function parseCliArgs(args) {
|
|
|
65
80
|
compress: { type: 'boolean', short: 'z', default: false },
|
|
66
81
|
decompress: { type: 'boolean', short: 'd', default: false },
|
|
67
82
|
list: { type: 'boolean', short: 'l', default: false },
|
|
83
|
+
benchmark: { type: 'boolean', short: 'B', default: false },
|
|
84
|
+
tar: { type: 'boolean', short: 'T', default: false },
|
|
68
85
|
keep: { type: 'boolean', short: 'k', default: false },
|
|
69
86
|
force: { type: 'boolean', short: 'f', default: false },
|
|
70
87
|
stdout: { type: 'boolean', short: 'c', default: false },
|
|
71
88
|
output: { type: 'string', short: 'o' },
|
|
89
|
+
strip: { type: 'string', default: '0' },
|
|
72
90
|
verbose: { type: 'boolean', short: 'v', default: false },
|
|
73
91
|
quiet: { type: 'boolean', short: 'q', default: false },
|
|
74
92
|
help: { type: 'boolean', short: 'h', default: false },
|
|
@@ -82,6 +100,8 @@ function parseCliArgs(args) {
|
|
|
82
100
|
compress: values.compress === true,
|
|
83
101
|
decompress: values.decompress === true,
|
|
84
102
|
list: values.list === true,
|
|
103
|
+
benchmark: values.benchmark === true,
|
|
104
|
+
tar: values.tar === true,
|
|
85
105
|
keep: values.keep === true,
|
|
86
106
|
force: values.force === true,
|
|
87
107
|
stdout: values.stdout === true,
|
|
@@ -92,6 +112,7 @@ function parseCliArgs(args) {
|
|
|
92
112
|
version: values.version === true,
|
|
93
113
|
preset: presetLevel,
|
|
94
114
|
extreme: values.extreme === true,
|
|
115
|
+
strip: Number.parseInt(String(values.strip ?? '0'), 10),
|
|
95
116
|
files: positionals,
|
|
96
117
|
};
|
|
97
118
|
}
|
|
@@ -109,12 +130,18 @@ Operation mode:
|
|
|
109
130
|
-z, --compress force compression
|
|
110
131
|
-d, --decompress force decompression
|
|
111
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)
|
|
112
139
|
|
|
113
140
|
Operation modifiers:
|
|
114
141
|
-k, --keep keep (don't delete) input files
|
|
115
142
|
-f, --force force overwrite of output file
|
|
116
143
|
-c, --stdout write to standard output and don't delete input files
|
|
117
|
-
-o, --output=FILE write output to FILE
|
|
144
|
+
-o, --output=FILE write output to FILE (or directory for tar extract)
|
|
118
145
|
|
|
119
146
|
Compression presets:
|
|
120
147
|
-0 ... -9 compression preset level (default: 6)
|
|
@@ -128,6 +155,14 @@ Other options:
|
|
|
128
155
|
|
|
129
156
|
With no FILE, or when FILE is -, read standard input.
|
|
130
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
|
+
|
|
131
166
|
Report bugs at: https://github.com/oorabona/node-liblzma/issues`);
|
|
132
167
|
}
|
|
133
168
|
/**
|
|
@@ -153,10 +188,33 @@ Thread support: ${threadSupport}
|
|
|
153
188
|
Copyright (C) ${year} Olivier ORABONA
|
|
154
189
|
License: LGPL-3.0`);
|
|
155
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
|
+
}
|
|
156
197
|
/**
|
|
157
198
|
* Determine operation mode based on options and file extension
|
|
158
199
|
*/
|
|
159
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
|
+
}
|
|
160
218
|
if (options.list)
|
|
161
219
|
return 'list';
|
|
162
220
|
if (options.decompress)
|
|
@@ -240,71 +298,96 @@ function listFile(filename, options) {
|
|
|
240
298
|
/**
|
|
241
299
|
* Compress a file
|
|
242
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
|
+
}
|
|
243
357
|
async function compressFile(inputFile, options) {
|
|
244
|
-
const outputFile = options
|
|
245
|
-
? null
|
|
246
|
-
: (options.output ?? getOutputFilename(inputFile, 'compress'));
|
|
247
|
-
// Check if output exists
|
|
358
|
+
const outputFile = resolveOutputFile(inputFile, 'compress', options);
|
|
248
359
|
if (outputFile && existsSync(outputFile) && !options.force) {
|
|
249
360
|
warn(`nxz: ${outputFile}: File already exists; use -f to overwrite`);
|
|
250
361
|
return EXIT_ERROR;
|
|
251
362
|
}
|
|
252
|
-
// Track output file for SIGINT cleanup
|
|
253
363
|
currentOutputFile = outputFile;
|
|
254
364
|
try {
|
|
255
|
-
const
|
|
365
|
+
const { data, size } = readFileSafe(inputFile);
|
|
256
366
|
const presetValue = options.extreme ? options.preset | preset.EXTREME : options.preset;
|
|
257
|
-
if (
|
|
258
|
-
//
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
process.stdout.write(compressed);
|
|
262
|
-
}
|
|
263
|
-
else if (stat.size <= STREAM_THRESHOLD_BYTES) {
|
|
264
|
-
// Small file: use sync
|
|
265
|
-
const input = readFileSync(inputFile);
|
|
266
|
-
const compressed = xzSync(input, { preset: presetValue });
|
|
267
|
-
writeFileSync(outputFile, compressed);
|
|
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);
|
|
268
371
|
}
|
|
269
372
|
else {
|
|
270
|
-
// Large file:
|
|
373
|
+
// Large file: stream compression with optional progress
|
|
271
374
|
const compressor = createXz({ preset: presetValue });
|
|
272
375
|
if (options.verbose) {
|
|
273
|
-
|
|
274
|
-
compressor.on('progress', ({ bytesRead, bytesWritten }) => {
|
|
275
|
-
const percent = Math.floor((bytesRead / stat.size) * 100);
|
|
276
|
-
if (percent !== lastPercent) {
|
|
277
|
-
lastPercent = percent;
|
|
278
|
-
process.stderr.write(`\r${inputFile}: ${percent}% (${formatBytes(bytesRead)} -> ${formatBytes(bytesWritten)})`);
|
|
279
|
-
}
|
|
280
|
-
});
|
|
376
|
+
attachProgress(compressor, inputFile, size);
|
|
281
377
|
}
|
|
282
378
|
await pipeline(createReadStream(inputFile), compressor, createWriteStream(outputFile));
|
|
283
379
|
if (options.verbose) {
|
|
284
380
|
process.stderr.write('\n');
|
|
285
381
|
}
|
|
286
382
|
}
|
|
287
|
-
|
|
288
|
-
if (!options.keep && !options.stdout) {
|
|
289
|
-
unlinkSync(inputFile);
|
|
290
|
-
}
|
|
291
|
-
// Clear tracking after successful completion
|
|
383
|
+
removeOriginalIfNeeded(inputFile, options);
|
|
292
384
|
currentOutputFile = null;
|
|
293
385
|
return EXIT_SUCCESS;
|
|
294
386
|
}
|
|
295
387
|
catch (err) {
|
|
296
388
|
const message = err instanceof Error ? err.message : String(err);
|
|
297
389
|
warn(`nxz: ${inputFile}: ${message}`);
|
|
298
|
-
|
|
299
|
-
if (outputFile && existsSync(outputFile)) {
|
|
300
|
-
try {
|
|
301
|
-
unlinkSync(outputFile);
|
|
302
|
-
}
|
|
303
|
-
catch {
|
|
304
|
-
// Ignore cleanup errors
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
currentOutputFile = null;
|
|
390
|
+
cleanupPartialOutput(outputFile);
|
|
308
391
|
return EXIT_ERROR;
|
|
309
392
|
}
|
|
310
393
|
}
|
|
@@ -312,99 +395,333 @@ async function compressFile(inputFile, options) {
|
|
|
312
395
|
* Decompress a file
|
|
313
396
|
*/
|
|
314
397
|
async function decompressFile(inputFile, options) {
|
|
315
|
-
const outputFile = options
|
|
316
|
-
? null
|
|
317
|
-
: (options.output ?? getOutputFilename(inputFile, 'decompress'));
|
|
318
|
-
// Check if output exists
|
|
398
|
+
const outputFile = resolveOutputFile(inputFile, 'decompress', options);
|
|
319
399
|
if (outputFile && existsSync(outputFile) && !options.force) {
|
|
320
400
|
warn(`nxz: ${outputFile}: File already exists; use -f to overwrite`);
|
|
321
401
|
return EXIT_ERROR;
|
|
322
402
|
}
|
|
323
|
-
// Track output file for SIGINT cleanup
|
|
324
403
|
currentOutputFile = outputFile;
|
|
325
404
|
try {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if (!isXZ(fd)) {
|
|
405
|
+
const { data, size } = readFileSafe(inputFile);
|
|
406
|
+
if (!isXZ(data)) {
|
|
329
407
|
warn(`nxz: ${inputFile}: File format not recognized`);
|
|
330
408
|
currentOutputFile = null;
|
|
331
409
|
return EXIT_ERROR;
|
|
332
410
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
process.stdout.write(decompressed);
|
|
338
|
-
}
|
|
339
|
-
else if (stat.size <= STREAM_THRESHOLD_BYTES) {
|
|
340
|
-
// Small file: use sync
|
|
341
|
-
const decompressed = unxzSync(fd);
|
|
342
|
-
writeFileSync(outputFile, decompressed);
|
|
411
|
+
if (!outputFile || size <= STREAM_THRESHOLD_BYTES) {
|
|
412
|
+
// Stdout or small file: sync decompression
|
|
413
|
+
const decompressed = unxzSync(data);
|
|
414
|
+
writeOutput(decompressed, outputFile);
|
|
343
415
|
}
|
|
344
416
|
else {
|
|
345
|
-
// Large file:
|
|
417
|
+
// Large file: stream decompression with optional progress
|
|
346
418
|
const decompressor = createUnxz();
|
|
347
419
|
if (options.verbose) {
|
|
348
|
-
|
|
349
|
-
decompressor.on('progress', ({ bytesRead, bytesWritten }) => {
|
|
350
|
-
const percent = Math.floor((bytesRead / stat.size) * 100);
|
|
351
|
-
if (percent !== lastPercent) {
|
|
352
|
-
lastPercent = percent;
|
|
353
|
-
process.stderr.write(`\r${inputFile}: ${percent}% (${formatBytes(bytesRead)} -> ${formatBytes(bytesWritten)})`);
|
|
354
|
-
}
|
|
355
|
-
});
|
|
420
|
+
attachProgress(decompressor, inputFile, size);
|
|
356
421
|
}
|
|
357
422
|
await pipeline(createReadStream(inputFile), decompressor, createWriteStream(outputFile));
|
|
358
423
|
if (options.verbose) {
|
|
359
424
|
process.stderr.write('\n');
|
|
360
425
|
}
|
|
361
426
|
}
|
|
362
|
-
|
|
363
|
-
if (!options.keep && !options.stdout) {
|
|
364
|
-
unlinkSync(inputFile);
|
|
365
|
-
}
|
|
366
|
-
// Clear tracking after successful completion
|
|
427
|
+
removeOriginalIfNeeded(inputFile, options);
|
|
367
428
|
currentOutputFile = null;
|
|
368
429
|
return EXIT_SUCCESS;
|
|
369
430
|
}
|
|
370
431
|
catch (err) {
|
|
371
432
|
const message = err instanceof Error ? err.message : String(err);
|
|
372
433
|
warn(`nxz: ${inputFile}: ${message}`);
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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}`);
|
|
377
546
|
}
|
|
378
|
-
|
|
379
|
-
|
|
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));
|
|
380
633
|
}
|
|
381
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
|
+
}
|
|
382
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);
|
|
383
656
|
return EXIT_ERROR;
|
|
384
657
|
}
|
|
385
658
|
}
|
|
386
659
|
/**
|
|
387
|
-
*
|
|
660
|
+
* Extract a tar.xz archive
|
|
388
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
|
+
}
|
|
389
696
|
async function processFile(filename, options) {
|
|
390
697
|
// Check file exists
|
|
391
698
|
if (!existsSync(filename)) {
|
|
392
699
|
warn(`nxz: ${filename}: No such file or directory`);
|
|
393
700
|
return EXIT_ERROR;
|
|
394
701
|
}
|
|
395
|
-
|
|
396
|
-
|
|
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') {
|
|
397
706
|
warn(`nxz: ${filename}: Is a directory, skipping`);
|
|
398
707
|
return EXIT_ERROR;
|
|
399
708
|
}
|
|
400
|
-
const mode = determineMode(options, filename);
|
|
401
709
|
switch (mode) {
|
|
402
710
|
case 'list':
|
|
403
711
|
return listFile(filename, options);
|
|
712
|
+
case 'benchmark':
|
|
713
|
+
return benchmarkFile(filename, options);
|
|
404
714
|
case 'compress':
|
|
405
715
|
return compressFile(filename, options);
|
|
406
716
|
case 'decompress':
|
|
407
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;
|
|
408
725
|
}
|
|
409
726
|
}
|
|
410
727
|
/**
|
|
@@ -462,6 +779,14 @@ async function main() {
|
|
|
462
779
|
const exitCode = await processStdin(options);
|
|
463
780
|
process.exit(exitCode);
|
|
464
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
|
+
}
|
|
465
790
|
// Process each file
|
|
466
791
|
let exitCode = EXIT_SUCCESS;
|
|
467
792
|
for (const file of options.files) {
|