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.
Files changed (47) hide show
  1. package/README.md +50 -4
  2. package/index.d.ts +21 -114
  3. package/lib/errors.d.ts +12 -0
  4. package/lib/errors.d.ts.map +1 -1
  5. package/lib/errors.js +17 -21
  6. package/lib/errors.js.map +1 -1
  7. package/lib/lzma.d.ts +37 -106
  8. package/lib/lzma.d.ts.map +1 -1
  9. package/lib/lzma.js +39 -68
  10. package/lib/lzma.js.map +1 -1
  11. package/lib/pool.d.ts.map +1 -1
  12. package/lib/pool.js +0 -4
  13. package/lib/pool.js.map +1 -1
  14. package/lib/types.d.ts +33 -0
  15. package/lib/types.d.ts.map +1 -1
  16. package/lib/wasm/bindings.d.ts.map +1 -1
  17. package/lib/wasm/bindings.js +4 -2
  18. package/lib/wasm/bindings.js.map +1 -1
  19. package/lib/wasm/compress.d.ts.map +1 -1
  20. package/lib/wasm/compress.js +5 -1
  21. package/lib/wasm/compress.js.map +1 -1
  22. package/lib/wasm/decompress.d.ts.map +1 -1
  23. package/lib/wasm/decompress.js +5 -1
  24. package/lib/wasm/decompress.js.map +1 -1
  25. package/lib/wasm/memory.d.ts.map +1 -1
  26. package/lib/wasm/memory.js +2 -0
  27. package/lib/wasm/memory.js.map +1 -1
  28. package/lib/wasm/stream.d.ts.map +1 -1
  29. package/lib/wasm/stream.js +6 -0
  30. package/lib/wasm/stream.js.map +1 -1
  31. package/lib/wasm/types.d.ts +1 -12
  32. package/lib/wasm/types.d.ts.map +1 -1
  33. package/lib/wasm/types.js +1 -12
  34. package/lib/wasm/types.js.map +1 -1
  35. package/lib/wasm/utils.d.ts +2 -15
  36. package/lib/wasm/utils.d.ts.map +1 -1
  37. package/lib/wasm/utils.js +6 -0
  38. package/lib/wasm/utils.js.map +1 -1
  39. package/package.json +15 -13
  40. package/prebuilds/darwin-x64/node-liblzma.node +0 -0
  41. package/prebuilds/linux-x64/node-liblzma.node +0 -0
  42. package/prebuilds/win32-x64/node-liblzma.node +0 -0
  43. package/src/wasm/liblzma.d.ts +9 -0
  44. package/lib/cli/nxz.d.ts +0 -7
  45. package/lib/cli/nxz.d.ts.map +0 -1
  46. package/lib/cli/nxz.js +0 -811
  47. 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