7zip-native 0.0.8 → 0.0.10
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 +44 -12
- package/bench/compare.mts +25 -11
- package/bench/folder-concurrency.mts +5 -12
- package/bench/multiprocess-extract.cjs +334 -0
- package/dist/index.cjs +192 -99
- package/dist/index.d.cts +10 -9
- package/dist/index.d.mts +10 -9
- package/dist/index.d.ts +10 -9
- package/dist/index.mjs +194 -101
- package/native/addon.cpp +1399 -71
- package/package.json +4 -2
- package/patches/deflate-input-buffer-size.patch +47 -0
- package/prebuilds/darwin-arm64/7zip_native.node +0 -0
- package/prebuilds/darwin-arm64/lib7z.dylib +0 -0
- package/prebuilds/darwin-x64/7zip_native.node +0 -0
- package/prebuilds/darwin-x64/lib7z.dylib +0 -0
- package/prebuilds/linux-arm/7z.so +0 -0
- package/prebuilds/linux-arm/7zip_native.node +0 -0
- package/prebuilds/linux-arm64/7z.so +0 -0
- package/prebuilds/linux-arm64/7zip_native.node +0 -0
- package/prebuilds/linux-ia32/7z.so +0 -0
- package/prebuilds/linux-ia32/7zip_native.node +0 -0
- package/prebuilds/linux-x64/7z.so +0 -0
- package/prebuilds/linux-x64/7zip_native.node +0 -0
- package/prebuilds/win32-arm64/7z.dll +0 -0
- package/prebuilds/win32-arm64/7zip_native.node +0 -0
- package/prebuilds/win32-x64/7z.dll +0 -0
- package/prebuilds/win32-x64/7zip_native.node +0 -0
- package/scripts/build-native.mjs +22 -1
- package/vendor/7zip/CPP/7zip/Archive/Archive2.def +1 -0
- package/vendor/7zip/CPP/7zip/Bundles/Format7zF/_o/7z.so +0 -0
- package/vendor/7zip/CPP/7zip/Bundles/Format7zF/_o/Alloc.o +0 -0
- package/vendor/7zip/CPP/7zip/Bundles/Format7zF/_o/DeflateDecoder.o +0 -0
- package/vendor/7zip/CPP/7zip/Compress/DeflateDecoder.cpp +15 -1
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { existsSync
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
2
|
import { rename, link, rm, lstat, mkdir } from 'node:fs/promises';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import { availableParallelism } from 'node:os';
|
|
6
6
|
import { dirname, resolve, join, extname, relative, sep } from 'node:path';
|
|
7
|
-
import { Readable
|
|
7
|
+
import { Readable } from 'node:stream';
|
|
8
8
|
import { pipeline } from 'node:stream/promises';
|
|
9
9
|
|
|
10
10
|
class ArchiveError extends Error {
|
|
@@ -17,6 +17,9 @@ class ArchiveError extends Error {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const defaultMaxOutputBytes = 4n * 1024n * 1024n * 1024n;
|
|
20
|
+
const defaultDeflateBufferSize = 32 * 1024;
|
|
21
|
+
const minDeflateBufferSize = 4 * 1024;
|
|
22
|
+
const maxDeflateBufferSize = 2 * 1024 * 1024;
|
|
20
23
|
function normalizeMaxBytes(value, label) {
|
|
21
24
|
if (value === undefined) {
|
|
22
25
|
return defaultMaxOutputBytes;
|
|
@@ -156,6 +159,19 @@ async function assertReplaceableOutput(filePath) {
|
|
|
156
159
|
}
|
|
157
160
|
}
|
|
158
161
|
}
|
|
162
|
+
async function assertFolderOutputDoesNotExist(filePath) {
|
|
163
|
+
try {
|
|
164
|
+
await lstat(filePath);
|
|
165
|
+
const error = new Error(`Output file already exists: ${filePath}`);
|
|
166
|
+
Object.assign(error, { code: 'EEXIST' });
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (error.code !== 'ENOENT') {
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
159
175
|
async function assertOutputDoesNotExist(filePath) {
|
|
160
176
|
try {
|
|
161
177
|
await lstat(filePath);
|
|
@@ -185,7 +201,7 @@ async function normalizeCreateEntry(entry) {
|
|
|
185
201
|
if (details.isSymbolicLink() || !details.isFile()) {
|
|
186
202
|
throw new ArchiveError('ERR_ARCHIVE_CREATE', `Archive creation source is not a regular file: ${sourcePath}`);
|
|
187
203
|
}
|
|
188
|
-
return { sourcePath, path: internalPath };
|
|
204
|
+
return { sourcePath, path: internalPath, mtimeMs: details.mtimeMs };
|
|
189
205
|
}
|
|
190
206
|
async function ensureExtractionRoot(root) {
|
|
191
207
|
try {
|
|
@@ -315,6 +331,7 @@ class Archive {
|
|
|
315
331
|
handle;
|
|
316
332
|
closed = false;
|
|
317
333
|
entriesPromise;
|
|
334
|
+
entryIndexes;
|
|
318
335
|
constructor(handle) {
|
|
319
336
|
this.handle = handle;
|
|
320
337
|
}
|
|
@@ -339,8 +356,14 @@ class Archive {
|
|
|
339
356
|
throw new RangeError('Password exceeds the 1 MiB native password limit');
|
|
340
357
|
}
|
|
341
358
|
}
|
|
359
|
+
const deflateBufferSize = options.deflateBufferSize ?? defaultDeflateBufferSize;
|
|
360
|
+
if (!Number.isSafeInteger(deflateBufferSize)
|
|
361
|
+
|| deflateBufferSize < minDeflateBufferSize
|
|
362
|
+
|| deflateBufferSize > maxDeflateBufferSize) {
|
|
363
|
+
throw new RangeError(`Deflate buffer size must be an integer between ${minDeflateBufferSize} and ${maxDeflateBufferSize} bytes`);
|
|
364
|
+
}
|
|
342
365
|
try {
|
|
343
|
-
const handle = await native.open(resolve(path), options.password, enginePath);
|
|
366
|
+
const handle = await native.open(resolve(path), options.password, enginePath, deflateBufferSize);
|
|
344
367
|
return new Archive(handle);
|
|
345
368
|
}
|
|
346
369
|
catch (cause) {
|
|
@@ -452,7 +475,23 @@ class Archive {
|
|
|
452
475
|
if (typeof idOrPath === 'number' && (!Number.isSafeInteger(idOrPath) || idOrPath < 0)) {
|
|
453
476
|
throw new RangeError('Entry ID must be a non-negative safe integer');
|
|
454
477
|
}
|
|
455
|
-
|
|
478
|
+
const entries = await this.entries();
|
|
479
|
+
if (!this.entryIndexes) {
|
|
480
|
+
const pathEntries = Object.create(null);
|
|
481
|
+
const idEntries = Object.create(null);
|
|
482
|
+
for (const entry of entries) {
|
|
483
|
+
pathEntries[entry.path] ??= entry;
|
|
484
|
+
idEntries[entry.id] ??= entry;
|
|
485
|
+
}
|
|
486
|
+
this.entryIndexes = { pathEntries, idEntries };
|
|
487
|
+
}
|
|
488
|
+
if (typeof idOrPath === 'number') {
|
|
489
|
+
return this.entryIndexes.idEntries[idOrPath];
|
|
490
|
+
}
|
|
491
|
+
if (typeof idOrPath === 'string') {
|
|
492
|
+
return this.entryIndexes.pathEntries[idOrPath];
|
|
493
|
+
}
|
|
494
|
+
return undefined;
|
|
456
495
|
}
|
|
457
496
|
async extract(idOrPathOrPaths) {
|
|
458
497
|
this.assertOpen();
|
|
@@ -530,9 +569,6 @@ class Archive {
|
|
|
530
569
|
|| options.concurrency > 128)) {
|
|
531
570
|
throw new RangeError('Folder extraction concurrency must be an integer between 1 and 128');
|
|
532
571
|
}
|
|
533
|
-
if (options.onProgress !== undefined && typeof options.onProgress !== 'function') {
|
|
534
|
-
throw new TypeError('Folder extraction onProgress option must be a function');
|
|
535
|
-
}
|
|
536
572
|
if (options.onExtract !== undefined && typeof options.onExtract !== 'function') {
|
|
537
573
|
throw new TypeError('Folder extraction onExtract option must be a function');
|
|
538
574
|
}
|
|
@@ -575,33 +611,13 @@ class Archive {
|
|
|
575
611
|
}
|
|
576
612
|
const root = resolve(destination);
|
|
577
613
|
const plans = makeOutputPlans(root, selectedEntries);
|
|
578
|
-
const
|
|
579
|
-
if (maxBytes !== null &&
|
|
614
|
+
const totalBytes = selectedEntries.reduce((total, entry) => total + (entry.isDirectory ? 0n : entry.size), 0n);
|
|
615
|
+
if (maxBytes !== null && totalBytes > maxBytes) {
|
|
580
616
|
throw new ArchiveError('ERR_RESOURCE_LIMIT', `Folder output exceeds the ${maxBytes}-byte limit`);
|
|
581
617
|
}
|
|
582
618
|
options.signal?.throwIfAborted();
|
|
583
619
|
await ensureExtractionRoot(root);
|
|
584
|
-
let extractedBytes = 0n;
|
|
585
620
|
let filesExtracted = 0;
|
|
586
|
-
const totalFiles = plans.filter(plan => !plan.isDirectory).length;
|
|
587
|
-
const reportProgress = () => {
|
|
588
|
-
let percent = 0;
|
|
589
|
-
if (expectedBytes > 0n) {
|
|
590
|
-
const scaledPercent = extractedBytes * 10000n / expectedBytes;
|
|
591
|
-
percent = Number(scaledPercent > 9900n ? 9900n : scaledPercent) / 100;
|
|
592
|
-
}
|
|
593
|
-
if (filesExtracted === totalFiles) {
|
|
594
|
-
percent = 100;
|
|
595
|
-
}
|
|
596
|
-
options.onProgress?.({
|
|
597
|
-
filesExtracted,
|
|
598
|
-
totalFiles,
|
|
599
|
-
bytesProcessed: extractedBytes,
|
|
600
|
-
totalBytes: expectedBytes,
|
|
601
|
-
percent,
|
|
602
|
-
});
|
|
603
|
-
};
|
|
604
|
-
reportProgress();
|
|
605
621
|
for (const plan of plans) {
|
|
606
622
|
options.signal?.throwIfAborted();
|
|
607
623
|
if (plan.isDirectory) {
|
|
@@ -613,7 +629,6 @@ class Archive {
|
|
|
613
629
|
const extractionSignal = controller.signal;
|
|
614
630
|
let hasFailure = false;
|
|
615
631
|
let failure;
|
|
616
|
-
let nextFileIndex = 0;
|
|
617
632
|
const stop = (error) => {
|
|
618
633
|
if (!hasFailure) {
|
|
619
634
|
hasFailure = true;
|
|
@@ -627,78 +642,77 @@ class Archive {
|
|
|
627
642
|
stop(options.signal?.reason);
|
|
628
643
|
};
|
|
629
644
|
options.signal?.addEventListener('abort', abortFromCaller, { once: true });
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
645
|
+
const groupCount = Math.min(concurrency, filePlans.length);
|
|
646
|
+
const fileGroups = Array.from({ length: groupCount }, () => []);
|
|
647
|
+
filePlans.forEach((plan, index) => {
|
|
648
|
+
fileGroups[index % groupCount].push(plan);
|
|
649
|
+
});
|
|
650
|
+
const extractGroup = async (group) => {
|
|
651
|
+
const outputs = [];
|
|
652
|
+
let completionFailed = false;
|
|
653
|
+
let completionFailure;
|
|
654
|
+
try {
|
|
655
|
+
for (const plan of group) {
|
|
656
|
+
await ensureOutputParents(root, plan.targetPath);
|
|
657
|
+
extractionSignal.throwIfAborted();
|
|
658
|
+
if (options.overwrite) {
|
|
659
|
+
await assertReplaceableOutput(plan.targetPath);
|
|
642
660
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
return;
|
|
661
|
+
else {
|
|
662
|
+
await assertFolderOutputDoesNotExist(plan.targetPath);
|
|
646
663
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
const temporaryPath = join(dirname(plan.targetPath), `.7zip-native-${randomUUID()}.tmp`);
|
|
653
|
-
try {
|
|
654
|
-
await pipeline(this.createReadStream(plan.entry.id, {
|
|
655
|
-
signal: extractionSignal,
|
|
656
|
-
maxBytes: null,
|
|
657
|
-
}), limit, createWriteStream(temporaryPath, { flags: 'wx', mode: 0o600 }), { signal: extractionSignal });
|
|
658
|
-
extractionSignal.throwIfAborted();
|
|
659
|
-
await assertReplaceableOutput(plan.targetPath);
|
|
660
|
-
extractionSignal.throwIfAborted();
|
|
661
|
-
await rename(temporaryPath, plan.targetPath);
|
|
664
|
+
outputs.push({
|
|
665
|
+
id: plan.entry.id,
|
|
666
|
+
path: plan.targetPath,
|
|
667
|
+
overwrite: options.overwrite === true,
|
|
668
|
+
});
|
|
662
669
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
670
|
+
let onFileCompleted;
|
|
671
|
+
const onExtract = options.onExtract;
|
|
672
|
+
if (onExtract) {
|
|
673
|
+
const plansById = new Map(group.map(plan => [plan.entry.id, plan]));
|
|
674
|
+
onFileCompleted = (id) => {
|
|
675
|
+
if (completionFailed || hasFailure) {
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
try {
|
|
679
|
+
const plan = plansById.get(id);
|
|
680
|
+
if (plan === undefined) {
|
|
681
|
+
throw new ArchiveError('ERR_ARCHIVE_EXTRACT', 'Native folder extraction returned an unknown file completion');
|
|
682
|
+
}
|
|
683
|
+
extractionSignal.throwIfAborted();
|
|
684
|
+
filesExtracted++;
|
|
685
|
+
onExtract({
|
|
686
|
+
entry: plan.entry,
|
|
687
|
+
outputPath: plan.targetPath,
|
|
688
|
+
percent: filesExtracted * 100 / filePlans.length,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
catch (error) {
|
|
692
|
+
completionFailed = true;
|
|
693
|
+
completionFailure = error;
|
|
694
|
+
stop(error);
|
|
695
|
+
}
|
|
696
|
+
};
|
|
666
697
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
signal: extractionSignal,
|
|
671
|
-
maxBytes: null,
|
|
672
|
-
}), limit, createWriteStream(plan.targetPath, { flags: 'wx', mode: 0o600 }), { signal: extractionSignal });
|
|
673
|
-
}
|
|
674
|
-
filesExtracted++;
|
|
675
|
-
reportProgress();
|
|
676
|
-
options.onExtract?.({ entry: plan.entry, outputPath: plan.targetPath });
|
|
677
|
-
};
|
|
678
|
-
const runWorker = async () => {
|
|
679
|
-
while (!hasFailure) {
|
|
680
|
-
if (options.signal?.aborted) {
|
|
681
|
-
stop(options.signal.reason);
|
|
682
|
-
return;
|
|
683
|
-
}
|
|
684
|
-
const plan = filePlans[nextFileIndex++];
|
|
685
|
-
if (!plan) {
|
|
686
|
-
return;
|
|
687
|
-
}
|
|
688
|
-
try {
|
|
689
|
-
await extractFile(plan);
|
|
690
|
-
}
|
|
691
|
-
catch (error) {
|
|
692
|
-
stop(error);
|
|
693
|
-
return;
|
|
698
|
+
await this.extractEntriesToFiles(outputs, extractionSignal, onFileCompleted);
|
|
699
|
+
if (completionFailed) {
|
|
700
|
+
throw completionFailure;
|
|
694
701
|
}
|
|
695
702
|
}
|
|
703
|
+
catch (caughtError) {
|
|
704
|
+
throw completionFailed ? completionFailure : caughtError;
|
|
705
|
+
}
|
|
696
706
|
};
|
|
697
707
|
try {
|
|
698
708
|
options.signal?.throwIfAborted();
|
|
699
|
-
const workers =
|
|
700
|
-
|
|
701
|
-
|
|
709
|
+
const workers = fileGroups.map(group => extractGroup(group));
|
|
710
|
+
try {
|
|
711
|
+
await Promise.all(workers);
|
|
712
|
+
}
|
|
713
|
+
catch (error) {
|
|
714
|
+
stop(error);
|
|
715
|
+
await Promise.allSettled(workers);
|
|
702
716
|
throw failure;
|
|
703
717
|
}
|
|
704
718
|
}
|
|
@@ -706,8 +720,67 @@ class Archive {
|
|
|
706
720
|
options.signal?.removeEventListener('abort', abortFromCaller);
|
|
707
721
|
}
|
|
708
722
|
}
|
|
709
|
-
async read(
|
|
723
|
+
async read(idOrPathOrPaths, options) {
|
|
710
724
|
this.assertOpen();
|
|
725
|
+
const maxUInt64 = (1n << 64n) - 1n;
|
|
726
|
+
const normalizeRange = (range) => {
|
|
727
|
+
const offset = this.toUnsignedBigInt(range.offset ?? 0, 'offset');
|
|
728
|
+
const length = this.toUnsignedBigInt(range.length, 'length');
|
|
729
|
+
if (offset > maxUInt64 || length > maxUInt64 - offset) {
|
|
730
|
+
throw new ArchiveError('ERR_INVALID_RANGE', 'Read range exceeds the native 64-bit offset limit');
|
|
731
|
+
}
|
|
732
|
+
return { offset, length };
|
|
733
|
+
};
|
|
734
|
+
if (options === undefined) {
|
|
735
|
+
if (!Array.isArray(idOrPathOrPaths)) {
|
|
736
|
+
throw new TypeError('Read options are required for an entry selector');
|
|
737
|
+
}
|
|
738
|
+
const requests = idOrPathOrPaths;
|
|
739
|
+
const normalized = await Promise.all(requests.map(async (request) => {
|
|
740
|
+
if (!request || typeof request !== 'object' || !('entry' in request)) {
|
|
741
|
+
throw new TypeError('Batch read requests must include an entry selector');
|
|
742
|
+
}
|
|
743
|
+
const range = normalizeRange(request);
|
|
744
|
+
return {
|
|
745
|
+
id: await this.resolveEntryId(request.entry),
|
|
746
|
+
...range,
|
|
747
|
+
};
|
|
748
|
+
}));
|
|
749
|
+
if (!normalized.length) {
|
|
750
|
+
return [];
|
|
751
|
+
}
|
|
752
|
+
try {
|
|
753
|
+
return await native.readMany(this.handle, normalized);
|
|
754
|
+
}
|
|
755
|
+
catch (cause) {
|
|
756
|
+
throw extractionError(cause);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
const isBatch = Array.isArray(idOrPathOrPaths);
|
|
760
|
+
let selectors;
|
|
761
|
+
if (isBatch) {
|
|
762
|
+
selectors = idOrPathOrPaths;
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
selectors = [idOrPathOrPaths];
|
|
766
|
+
}
|
|
767
|
+
const { offset, length } = normalizeRange(options);
|
|
768
|
+
if (isBatch) {
|
|
769
|
+
const ids = await Promise.all(selectors.map(selector => this.resolveEntryId(selector)));
|
|
770
|
+
const uniqueIds = [...new Set(ids)];
|
|
771
|
+
if (!uniqueIds.length) {
|
|
772
|
+
return [];
|
|
773
|
+
}
|
|
774
|
+
try {
|
|
775
|
+
const buffers = await native.readMany(this.handle, uniqueIds.map(id => ({ id, offset, length })));
|
|
776
|
+
const buffersById = new Map(uniqueIds.map((id, index) => [id, buffers[index]]));
|
|
777
|
+
return ids.map(id => buffersById.get(id));
|
|
778
|
+
}
|
|
779
|
+
catch (cause) {
|
|
780
|
+
throw extractionError(cause);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
const idOrPath = selectors[0];
|
|
711
784
|
let id;
|
|
712
785
|
if (typeof idOrPath === 'number') {
|
|
713
786
|
this.validateIds([idOrPath]);
|
|
@@ -716,12 +789,6 @@ class Archive {
|
|
|
716
789
|
else {
|
|
717
790
|
id = await this.resolveEntryId(idOrPath);
|
|
718
791
|
}
|
|
719
|
-
const offset = this.toUnsignedBigInt(options.offset ?? 0, 'offset');
|
|
720
|
-
const length = this.toUnsignedBigInt(options.length, 'length');
|
|
721
|
-
const maxUInt64 = (1n << 64n) - 1n;
|
|
722
|
-
if (offset > maxUInt64 || length > maxUInt64 - offset) {
|
|
723
|
-
throw new ArchiveError('ERR_INVALID_RANGE', 'Read range exceeds the native 64-bit offset limit');
|
|
724
|
-
}
|
|
725
792
|
try {
|
|
726
793
|
return await native.read(this.handle, id, offset, length);
|
|
727
794
|
}
|
|
@@ -729,6 +796,32 @@ class Archive {
|
|
|
729
796
|
throw extractionError(cause);
|
|
730
797
|
}
|
|
731
798
|
}
|
|
799
|
+
async extractEntriesToFiles(outputs, signal, onFileCompleted) {
|
|
800
|
+
let task;
|
|
801
|
+
const cancelTask = () => {
|
|
802
|
+
if (task) {
|
|
803
|
+
native.cancelExtractToFiles(task.taskId);
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
try {
|
|
807
|
+
signal.throwIfAborted();
|
|
808
|
+
task = native.startExtractToFiles(this.handle, outputs, onFileCompleted);
|
|
809
|
+
signal.addEventListener('abort', cancelTask, { once: true });
|
|
810
|
+
if (signal.aborted) {
|
|
811
|
+
cancelTask();
|
|
812
|
+
}
|
|
813
|
+
return await task.promise;
|
|
814
|
+
}
|
|
815
|
+
catch (cause) {
|
|
816
|
+
if (signal.aborted) {
|
|
817
|
+
throw signal.reason ?? cause;
|
|
818
|
+
}
|
|
819
|
+
throw extractionError(cause);
|
|
820
|
+
}
|
|
821
|
+
finally {
|
|
822
|
+
signal.removeEventListener('abort', cancelTask);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
732
825
|
async close() {
|
|
733
826
|
if (this.closed) {
|
|
734
827
|
return;
|