7zip-native 0.0.1 → 0.0.5

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 CHANGED
@@ -3,102 +3,292 @@
3
3
 
4
4
  # 7zip-native
5
5
 
6
- Native Node-API bindings for the 7-Zip archive engine. Parsing and extraction through the `Archive` API run in native worker tasks and never invoke `7z`, `7zz`, or another archive executable. The optional comparison benchmark invokes `7zip-bin-full` only as a command-line baseline.
6
+ Native Node-API bindings for the 7-Zip archive engine. Use a typed, Promise-based Node.js API to inspect archives, read entry data, stream large files, or extract entries without launching `7z`, `7zz`, or another archive executable.
7
7
 
8
- > **Platform status:** the release workflow builds and tests Linux x64/arm64, macOS x64/arm64, and Windows x64/arm64/x86. Linux x86 (ia32) and ARM32 artifacts are cross-compiled and checked for their target architecture, but are not runtime-tested in CI. This checkout has only been verified locally on Linux x64 with Node.js 22; do not treat other targets as runtime-tested until the corresponding GitHub Actions runs pass. This remains an early end-to-end implementation, not a claim of full parity with every requirement in the project specification.
8
+ The package includes the native addon and the matching 7-Zip engine for each supported target. Compression and decompression are provided by the upstream 7-Zip implementation.
9
9
 
10
- ## Build from source
10
+ ## Installation
11
11
 
12
12
  ```sh
13
- git clone --recurse-submodules https://github.com/ollm/7zip-native.git
14
- cd 7zip-native
15
- npm ci
16
- npm run build
17
- npm test
13
+ npm install 7zip-native
18
14
  ```
19
15
 
20
- Source builds require a C++17 toolchain, Python 3, Node.js headers (downloaded by `node-gyp`), and the upstream submodule. Linux and macOS use GNU make; Windows builds the upstream bundle with `nmake` and Visual C++ build tools. `npm run build:native` compiles the `Format7zF` handler bundle and Node-API addon, then stages both under `prebuilds/<platform>-<arch>/`. Archive operations never shell out.
16
+ The package requires Node.js 18 or newer. The release workflow builds and tests with Node.js 22.
17
+
18
+ ## Quick start
19
+
20
+ ```ts
21
+ import {Archive} from '7zip-native';
21
22
 
22
- The `Build and publish` GitHub Actions workflow builds the nine listed targets for version tags (`v*`) or a manual dispatch. It runs tests on the seven native-runner targets; Linux x86 (ia32) and ARM32 use cross-compilers and receive architecture checks because the runners are x64. For manual runs, publishing is off by default; enable it only when dispatching the matching `v<package-version>` tag. npm publishing uses trusted publishing (OIDC), so configure this repository as a trusted publisher in npm before publishing. The release matrix uses GitHub-hosted arm64 runners and depends on their availability for the repository.
23
+ const archive = await Archive.open('./comics.cbz');
23
24
 
24
- To update upstream deliberately:
25
+ try
26
+ {
27
+ const entries = await archive.entries();
28
+ const cover = await archive.getEntry('chapter/cover.jpg');
25
29
 
26
- ```sh
27
- git submodule update --remote vendor/7zip
28
- git -C vendor/7zip rev-parse HEAD
29
- git add .gitmodules vendor/7zip
30
+ if(cover && !cover.isDirectory)
31
+ {
32
+ const preview = await archive.read(cover.path, {offset: 0, length: 64 * 1024});
33
+ const image = await archive.extract(cover.path);
34
+ console.log(entries.length, preview.length, image.length);
35
+ }
36
+ }
37
+ finally
38
+ {
39
+ await archive.close();
40
+ }
30
41
  ```
31
42
 
32
- Review and commit the new gitlink revision. The current submodule is pinned to 7-Zip 26.05 (`b88c6bc09339de32f7d18bbab900686b244f42ba`).
43
+ The package supports both ES modules and CommonJS:
44
+
45
+ ```js
46
+ // ES module
47
+ import {Archive} from '7zip-native';
48
+
49
+ // CommonJS
50
+ const {Archive} = require('7zip-native');
51
+ ```
33
52
 
34
53
  ## API
35
54
 
55
+ ### `Archive.open(path, options?)`
56
+
57
+ Opens an archive from a filesystem path and indexes its metadata asynchronously.
58
+
36
59
  ```ts
37
- import {Archive} from '7zip-native';
60
+ const archive = await Archive.open('./archive.7z', {password: 'secret'});
61
+ ```
62
+
63
+ `options.password` is passed to the upstream handler when it requests a password. It is held for the lifetime of the archive handle, is not logged, and is limited to 1 MiB of UTF-8 data.
38
64
 
39
- const archive = await Archive.open('/path/to/file.zip');
65
+ ### Listing and looking up entries
66
+
67
+ ```ts
40
68
  const entries = await archive.entries();
41
- const first = entries[0];
42
- if (first && !first.isDirectory) {
43
- const header = await archive.read(first.id, {offset: 0, length: 64 * 1024});
44
- const [cover, page] = await archive.extract([first.id, entries[1]!.id]);
69
+ const byPath = await archive.getEntry('folder/page.jpg');
70
+ const byId = await archive.getEntry(0);
71
+ ```
72
+
73
+ `entries()` returns cached metadata. Entry IDs are zero-based and stable for the lifetime of the opened archive. `getEntry()` returns `undefined` if the ID or full path does not match.
74
+
75
+ All entry-targeted methods accept either the entry's numeric `id` or its exact archive-internal `path`: `extract()`, `read()`, `createReadStream()`, and `extractToStream()`. Batch `extract()` calls can mix IDs and paths. For folder extraction, use either the `ids` or `paths` option; omit both to extract every entry.
76
+
77
+ Each entry includes:
78
+
79
+ | Property | Type | Description |
80
+ | --- | --- | --- |
81
+ | `id` | `number` | Stable ID within this opened archive |
82
+ | `path` | `string` | Entry path, using `/` separators |
83
+ | `size` | `bigint` | Uncompressed size |
84
+ | `compressedSize` | `bigint` | Compressed size |
85
+ | `isDirectory` | `boolean` | Whether the entry is a directory |
86
+ | `encrypted` | `boolean` | Whether the entry is encrypted |
87
+ | `attributes` | `number` | Format-specific attributes |
88
+ | `crc` | `number \| undefined` | CRC when supplied by the format |
89
+
90
+ ### Extracting to memory
91
+
92
+ ```ts
93
+ const image = await archive.extract(entry.id); // Buffer
94
+ const sameImage = await archive.extract(entry.path); // Buffer
95
+ const pages = await archive.extract([page1.id, page2.path]); // Buffer[]
96
+ ```
97
+
98
+ Paths use `/` separators. Batch results are returned in the same order as the requested selectors; duplicate entries are rejected. A missing path rejects with `ERR_INVALID_ENTRY`. A buffered extraction is limited to 256 MiB per entry, with a 512 MiB cap on native results pending transfer to JavaScript. For large entries, use a stream or extract to a folder instead.
99
+
100
+ ### Reading a byte range
101
+
102
+ ```ts
103
+ const header = await archive.read(entry.path, {
104
+ offset: 0,
105
+ length: 64 * 1024,
106
+ });
107
+ ```
108
+
109
+ Offsets and lengths accept safe integers or `bigint`. A range read returns only the requested bytes and is limited to 64 MiB. It uses the handler's normal extraction path, however, so a sequential or solid format may still need to decompress preceding data or the whole entry. It is not guaranteed to be constant-time random access.
110
+
111
+ ### Streams and output destinations
112
+
113
+ Create a standard Node.js `Readable` for a large entry:
114
+
115
+ ```ts
116
+ for await (const chunk of archive.createReadStream(entry.path, {
117
+ highWaterMark: 256 * 1024,
118
+ signal: abortController.signal,
119
+ }))
120
+ {
121
+ // Consume each Buffer chunk before requesting more data.
45
122
  }
123
+ ```
124
+
125
+ The native engine emits bounded 64 KiB chunks and waits for stream demand, preserving backpressure. Destroying the stream or aborting its signal cancels extraction at the next output boundary. Streams default to a 4 GiB output limit; set `maxBytes` to another non-negative `number` or `bigint`, or to `null` to disable the limit.
126
+
127
+ To pipe directly to a Node.js `Writable`:
128
+
129
+ ```ts
130
+ import {createWriteStream} from 'node:fs';
131
+
132
+ await archive.extractToStream(entry.path, createWriteStream('./page.jpg'), {
133
+ signal: abortController.signal,
134
+ });
135
+ ```
136
+
137
+ This respects backpressure and ends the destination when extraction succeeds. A destination error, early close, or abort rejects the Promise.
138
+
139
+ ### Extracting to a folder
140
+
141
+ ```ts
142
+ await archive.extractToFolder('./unpacked', {
143
+ paths: ['chapter/page1.jpg', 'chapter/page2.jpg'], // alternatively, use ids
144
+ maxBytes: 2n * 1024n * 1024n * 1024n,
145
+ signal: abortController.signal,
146
+ });
147
+ ```
148
+
149
+ Folder extraction accepts either `ids` or exact `paths`; omit both to extract every entry, and do not provide both together. Paths use archive-internal `/` separators. Extraction is sequential and uses bounded memory. Its default total output limit is 4 GiB; `maxBytes: null` disables that limit. Existing files are never overwritten. Unsafe paths, path collisions, and symlinked destination components are rejected. Archive links are written as regular file data; the API does not create symbolic or hard links. If extraction fails partway through, files already written (or partially written) may remain in the destination.
150
+
151
+ ### Concurrency
152
+
153
+ ```ts
154
+ const current = Archive.concurrency();
155
+ Archive.concurrency(8);
156
+ Archive.concurrency(0); // reset to the default
157
+ ```
158
+
159
+ The setting controls the process-wide native worker pool used by extraction, streams, and range reads. The default is the lesser of 4 and the parallelism available to Node.js; the allowed range is 1–128 workers, with `0` restoring the default.
160
+
161
+ Concurrent operations on one archive use independent decoder contexts rather than calling one 7-Zip archive object simultaneously. This is safe for handlers that support independent contexts, but it does not guarantee a speedup. Solid archives may decompress shared data more than once; batching related entries may be faster.
162
+
163
+ ### Closing an archive
164
+
165
+ Call `close()` when finished. It rejects future API calls; operations already submitted keep the native state alive until they finish.
166
+
167
+ ```ts
46
168
  await archive.close();
47
169
  ```
48
170
 
49
- - `Archive.open(path, {password})` supplies the archive password to upstream handlers for encrypted headers and entries. The password is kept only for this archive handle and is never logged. Passwords are limited to 1 MiB of UTF-8 data.
50
- - `Archive.open(path)` opens and indexes the archive off the event loop.
51
- - `Archive.concurrency()` returns the process-wide native extraction worker count. `Archive.concurrency(n)` sets it; use `0` to reset to the default. The default is Node's available parallelism, capped at 4 workers to avoid oversubscribing typical archive workloads. The setting applies to extraction, streams, and ranged reads.
52
- - `entries()` returns cached metadata. IDs are zero-based and stable for that archive handle.
53
- - `getEntry(idOrPath)` looks up a cached item by ID or full path.
54
- - `extract(id)` returns one `Buffer`; `extract(ids)` returns buffers in request order. The current in-memory limit is 256 MiB per operation and 512 MiB for native extraction results pending transfer to JavaScript.
55
- - `createReadStream(id, {highWaterMark, maxBytes, signal})` returns a Node.js `Readable`. Native output is delivered in bounded 64 KiB chunks on demand; destroying the stream or aborting its signal cancels extraction. A stream is limited to 4 GiB by default; set `maxBytes` to another non-negative integer or `bigint`, or to `null` for no output-size limit.
56
- - `extractToStream(id, writable, options)` pipes an entry to a Node.js `Writable`, respecting backpressure and ending the destination on success. Destination errors and abort signals stop the source stream.
57
- - `extractToFolder(directory, {ids, maxBytes, signal})` extracts all entries (or the selected IDs) sequentially with bounded memory. Its default total output limit is 4 GiB; `maxBytes: null` disables that limit. Existing files are never overwritten. Unsafe paths, path collisions, and symlinked destination components are rejected. Archive links are written as regular file data; the API does not create symbolic or hard links. A failed extraction can leave already-written or partial files in the destination.
58
- - `read(id, {offset, length})` returns only the selected bytes. Ranges accept safe integers or `bigint`; requested reads are limited to 64 MiB.
59
- - `close()` rejects future calls. Work already submitted retains the native state until that work completes.
60
- - Concurrent `extract()` or `read()` calls on one handle use independent, lazily opened decoder contexts and can run in parallel. Solid archives may need to decompress the same earlier data more than once, so parallelism does not guarantee a speedup; use batched extraction to let 7-Zip process related entries together.
61
- - `ArchiveError` exposes a stable `code`; input validation also uses standard `TypeError` and `RangeError`.
171
+ ## Errors and limits
172
+
173
+ Invalid argument types and values throw standard `TypeError` or `RangeError`. Archive and extraction failures use `ArchiveError`, which has a stable `code` property:
174
+
175
+ | Code | Meaning |
176
+ | --- | --- |
177
+ | `ERR_ARCHIVE_CLOSED` | Operation attempted after `close()` |
178
+ | `ERR_INVALID_ENTRY` | Entry ID is invalid or out of range |
179
+ | `ERR_INVALID_RANGE` | Byte range is invalid or exceeds the native offset limit |
180
+ | `ERR_RESOURCE_LIMIT` | Configured or default output limit was exceeded |
181
+ | `ERR_ARCHIVE_OPEN` | Archive could not be opened or indexed |
182
+ | `ERR_ARCHIVE_PASSWORD` | Password is missing or incorrect when requested by the handler |
183
+ | `ERR_ARCHIVE_PATH` | Unsafe or colliding path during folder extraction |
184
+ | `ERR_ARCHIVE_EXTRACT` | Entry extraction failed |
185
+
186
+ Buffered `extract()` and `read()` do not currently support cancellation. Streams and folder extraction accept an `AbortSignal`.
187
+
188
+ ## Supported formats
189
+
190
+ The package uses the handlers registered in the pinned upstream `Format7zF` bundle, including 7z, ZIP, RAR, TAR, GZIP, BZIP2, XZ, WIM, ISO/UDF, CAB, ARJ, LZH, CPIO, Zstandard, and additional formats enabled by that upstream revision. Whether a particular archive can be extracted depends on its handler and compression method; unsupported methods, corrupt data, and integrity failures reject the operation. Standalone compression codecs are not necessarily archive formats.
191
+
192
+ ### Format-specific notes
62
193
 
63
- Entry metadata includes `id`, `path`, `size`, `compressedSize`, `isDirectory`, `encrypted`, `attributes`, and `crc` when provided by the format. Sizes are `bigint`.
194
+ - **RAR:** extraction uses upstream code subject to its LGPL terms and the unRAR restriction. This package does not create RAR archives or implement the proprietary RAR compression algorithm. Review the upstream notices before redistribution.
195
+ - **ACE:** not supported by this 7-Zip revision.
196
+ - **Encryption:** password callbacks are connected to archive opening and extraction. AES-256 encrypted ZIP extraction and range reads are tested on Linux x64; encrypted-header behavior and password variants in other formats are not yet verified.
197
+ - **Partial reads:** memory use is limited to the requested range, but the decoder may still process more data internally, especially for solid or sequential formats.
64
198
 
65
- ## Formats and limitations
199
+ See [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md) for third-party redistribution terms.
66
200
 
67
- The addon uses the pinned upstream `Format7zF` bundle and its registered handlers rather than a hand-maintained subset. The bundle includes handlers for 7z, ZIP, RAR, TAR, GZIP, BZIP2, XZ, WIM, ISO/UDF, CAB, ARJ, LZH, CPIO, Zstandard, and additional formats enabled by the upstream makefile. An archive's actual extraction still depends on the handler and decoder method being available; unsupported methods, corrupt data, and integrity failures reject extraction.
201
+ ## Platform support
68
202
 
69
- - RAR decompression is upstream code under its LGPL terms and the unRAR restriction. This package does not create RAR archives or implement the proprietary RAR compression algorithm. Read upstream's unRAR notice before redistribution.
70
- - ACE is not supplied by this 7-Zip revision and is not supported.
71
- - The upstream 7z handler is covered by a native open-and-extract integration test on Linux x64. Support for individual compression methods and encrypted variants still depends on the capabilities of the upstream handler.
72
- - Password callbacks are wired for upstream archive opening and extraction. AES-256 encrypted ZIP entry extraction and ranged reads are tested on Linux x64. Wrong or missing passwords requested by a handler are reported with `ArchiveError.code === 'ERR_ARCHIVE_PASSWORD'`. Encrypted-header behavior and password variants in other formats are not yet verified.
73
- - Partial reads bound memory to the selected range but use the normal `IInArchive::Extract` path. Sequential or solid decoders may decompress preceding data or the whole entry; this is not constant-time random access and does not promise early decoder termination.
74
- - Opening and metadata queries use Node's libuv workers. Extraction and ranged reads use a persistent native worker pool, defaulting to the lesser of 4 and the processors available to Node. `Archive.concurrency(n)` sets the process-wide pool size; submitted native operations remain bounded.
75
- - The addon never calls one `IInArchive` instance concurrently. It retains independent archive and input-stream contexts per handle and lazily creates more as concurrent work needs them. Context creation checks the archive's size and modification time; reopen the archive after changing it.
76
- - Concurrent extraction from a solid archive is safe with independent contexts but may repeat decompression of shared solid blocks. There is no decompressed-block cache, so batching related entries can be faster.
77
- - Cancellation of buffered `extract()`/`read()` requests and decompressed-block caching are not implemented. Streaming and folder extraction have bounded defaults that can be adjusted per operation; buffered methods retain their fixed in-memory limits.
78
- - Cross-platform builds and runtime behavior are not claimed until the corresponding matrix jobs succeed. No performance claim is made without measurements on the target platform.
203
+ The release workflow builds eight targets:
79
204
 
80
- ## Benchmark harness
205
+ | Platform | Architecture | CI verification |
206
+ | --- | --- | --- |
207
+ | Linux | x64, ARM64 | Build and runtime tests |
208
+ | Linux | x86 (ia32), ARM32 | Cross-compile and verify binary architecture; not runtime-tested in CI |
209
+ | macOS | x64, ARM64 | Build and runtime tests |
210
+ | Windows | x64, ARM64 | Build and runtime tests |
81
211
 
82
- Run `npm run benchmark -- ./archive.zip` to measure native open/list, first 64 KiB, full extraction, and 1/2/4/8 reads across independent archive handles.
212
+ The published binaries must match the target operating system and architecture. Windows x86 (ia32) prebuilds are not published. The package declares Node.js `>=18`; the workflow currently builds and tests with Node.js 26. This checkout has been verified locally on Linux x64. Do not treat cross-compiled Linux 32-bit artifacts as runtime-tested.
83
213
 
84
- For a direct CLI comparison, install the development dependencies and run:
214
+ ## Electron
215
+
216
+ The addon uses Node-API, but the addon and 7-Zip engine still need to match the Electron application's operating system and architecture. Use a matching package prebuild when available, or rebuild for the Electron version and target architecture using your Electron build tooling.
217
+
218
+ When packaging with ASAR, ensure both `7zip_native.node` and the matching engine library (`7z.dll`, `7z.so`, or `lib7z.dylib`) are physically unpacked and that the runtime paths resolve to those unpacked files. For `electron-builder`, an `asarUnpack` configuration can include:
219
+
220
+ ```json
221
+ {
222
+ "build": {
223
+ "asarUnpack": [
224
+ "node_modules/7zip-native/prebuilds/**/*.node",
225
+ "node_modules/7zip-native/prebuilds/**/*.dll",
226
+ "node_modules/7zip-native/prebuilds/**/*.so",
227
+ "node_modules/7zip-native/prebuilds/**/*.dylib"
228
+ ]
229
+ }
230
+ }
231
+ ```
232
+
233
+ Test the packaged application on each target; ASAR unpacking does not replace checking that the addon and engine can both be loaded from the packaged runtime.
234
+
235
+ ## Build from source
85
236
 
86
237
  ```sh
87
- npm run benchmark:compare -- --archive "./archive.zip" --entry "chapter/page.jpg" --iterations 20 --read-bytes 65536 --concurrency 4
238
+ git clone --recurse-submodules https://github.com/ollm/7zip-native.git
239
+ cd 7zip-native
240
+ npm ci --ignore-scripts
241
+ npm run build
242
+ npm test
88
243
  ```
89
244
 
90
- The comparison uses `7zip-bin-full`'s platform binary to measure open/list, single and batched extraction, prefix reads, and a range read. It validates that both backends list the same number of entries and return identical data before reporting median and p95 latency. It also compares extracting every file sequentially and with the selected concurrency in both backends, using the same bounded batches and validating each batch's output. CLI timings include process startup. The CLI has no ranged-read API, so range measurements extract the whole entry to stdout and slice the requested bytes; this is intentionally reported as a baseline limitation, not as an equivalent random-access implementation. The benchmark verifies the complete entry path/type listing, selected entry, and SHA-256 of every operation's output; result byte counts are included where applicable. By default, the benchmark samples 15 times, reads up to 64 KiB, and uses the native pool's configured default concurrency. Set `--iterations`, `--read-bytes`, and `--concurrency` to change these values. Captured output is capped at 256 MiB; full-archive comparison is skipped if an entry exceeds that limit. Use representative archives; smoke-test timings are not performance claims.
245
+ Source builds require a C++17 toolchain, Python 3, Node.js headers (downloaded by `node-gyp`), and the upstream submodule. Linux and macOS use GNU make; Windows requires Visual C++ build tools and `nmake`. `npm run build:native` compiles the upstream `Format7zF` bundle and Node-API addon, then stages them under `prebuilds/<platform>-<arch>/`.
91
246
 
92
- Compare extracting every file from one archive sequentially versus the configured native concurrency:
247
+ To update the upstream submodule deliberately:
93
248
 
94
249
  ```sh
95
- npm run benchmark:concurrency -- --archive "/path/to/archive.7z" --iterations 20 --concurrencies 1,2,3,4,5,8,16
250
+ git submodule update --remote vendor/7zip
251
+ git -C vendor/7zip rev-parse HEAD
252
+ git add .gitmodules vendor/7zip
96
253
  ```
97
254
 
98
- The report includes median and p95 latency, throughput, and speedup. It defaults to the native pool's configured concurrency. Use `--concurrencies 1,2,4,8,16` to compare several worker counts in one run, or `--concurrency` to compare the serial baseline with one selected count. Each worker count processes the same single-entry jobs and fixed memory-limited batches, and the report shows the maximum number of entries that can overlap. Use `--max-files 20` to select evenly spaced entries from a larger archive; the report shows both the sample and archive entry counts. This can prevent excessive repeated decompression when benchmarking solid archives. It reports measurements only and does not claim a speedup.
255
+ Review the upstream changes and commit the new submodule revision. The current submodule is pinned to 7-Zip 26.05 (`b88c6bc09339de32f7d18bbab900686b244f42ba`).
99
256
 
100
- ## Electron
257
+ ## Benchmarks
258
+
259
+ Run the native benchmark against a representative archive:
260
+
261
+ ```sh
262
+ BENCH_ITERATIONS=20 npm run benchmark -- "./archive.zip"
263
+ ```
264
+
265
+ It measures native open/list, prefix reads, full extraction, and reads across independent archive handles. `BENCH_ITERATIONS` defaults to 15 and must be at least 3.
266
+
267
+ Compare the native API with the `7zip-bin-full` command-line baseline:
268
+
269
+ ```sh
270
+ npm run benchmark:compare -- \
271
+ --archive "./archive.zip" \
272
+ --entry "chapter/page.jpg" \
273
+ --iterations 20 \
274
+ --read-bytes 65536 \
275
+ --concurrency 4
276
+ ```
277
+
278
+ The comparison validates entry listings and output data before reporting median and p95 latency. CLI timings include process startup. Since the CLI has no ranged-read API, its range baseline extracts the entry to stdout and slices the requested bytes; this is not equivalent to native range reading. Captured output is capped at 256 MiB, and full-archive comparison is skipped if an entry exceeds that limit.
279
+
280
+ Compare extraction concurrency:
281
+
282
+ ```sh
283
+ npm run benchmark:concurrency -- \
284
+ --archive "./archive.7z" \
285
+ --iterations 20 \
286
+ --concurrencies 1,2,3,4,5,8,16 \
287
+ --max-files 20
288
+ ```
289
+
290
+ The report includes median and p95 latency, throughput, speedup, and the number of entries that can overlap. `--max-files` selects evenly spaced entries, which is useful when sampling solid archives. Benchmarks report measurements only; results depend on the archive, machine, and workload and do not guarantee a speedup.
101
291
 
102
- The addon uses Node-API, but its native engine and addon still need to match the target OS and architecture. Electron applications must rebuild from source for the Electron target (for example with `npm rebuild` and the appropriate `--runtime=electron --target=<version>` arguments), or package the matching prebuild from `prebuilds/<platform>-<arch>/`. Keep the engine (`7z.so` or `7z.dll`) beside `7zip_native.node`.
292
+ ## License
103
293
 
104
- See [docs/architecture.md](./docs/architecture.md) for verified source and concurrency constraints, and [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md) for redistribution requirements.
294
+ This package is MIT-licensed. The integrated 7-Zip code and other bundled dependencies have their own licensing terms; see [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md) before redistribution.
package/dist/index.cjs CHANGED
@@ -167,6 +167,7 @@ class NativeArchiveReadable extends node_stream.Readable {
167
167
  maxBytes;
168
168
  streamId;
169
169
  outputBytes = 0n;
170
+ starting = false;
170
171
  constructor(start, resumeNative, cancel, options, maxBytes) {
171
172
  super({ highWaterMark: options.highWaterMark, signal: options.signal });
172
173
  this.start = start;
@@ -175,38 +176,63 @@ class NativeArchiveReadable extends node_stream.Readable {
175
176
  this.maxBytes = maxBytes;
176
177
  }
177
178
  _read() {
179
+ if (this.streamId !== undefined) {
180
+ try {
181
+ this.resumeNative(this.streamId);
182
+ }
183
+ catch (cause) {
184
+ this.destroy(extractionError(cause));
185
+ }
186
+ return;
187
+ }
188
+ if (this.starting) {
189
+ return;
190
+ }
191
+ this.starting = true;
178
192
  try {
179
- if (this.streamId === undefined) {
180
- this.streamId = this.start((chunk, error, done) => {
181
- if (this.destroyed) {
193
+ Promise.resolve(this.start((chunk, error, done) => {
194
+ if (this.destroyed) {
195
+ return;
196
+ }
197
+ if (error) {
198
+ this.destroy(extractionError(error));
199
+ }
200
+ else if (done) {
201
+ this.push(null);
202
+ }
203
+ else if (chunk) {
204
+ if (this.maxBytes !== null
205
+ && this.outputBytes + BigInt(chunk.length) > this.maxBytes) {
206
+ this.destroy(new ArchiveError('ERR_RESOURCE_LIMIT', `Stream output exceeds the ${this.maxBytes}-byte limit`));
182
207
  return;
183
208
  }
184
- if (error) {
185
- this.destroy(extractionError(error));
186
- }
187
- else if (done) {
188
- this.push(null);
189
- }
190
- else if (chunk) {
191
- if (this.maxBytes !== null
192
- && this.outputBytes + BigInt(chunk.length) > this.maxBytes) {
193
- this.destroy(new ArchiveError('ERR_RESOURCE_LIMIT', `Stream output exceeds the ${this.maxBytes}-byte limit`));
194
- return;
195
- }
196
- this.outputBytes += BigInt(chunk.length);
197
- this.push(chunk);
209
+ this.outputBytes += BigInt(chunk.length);
210
+ this.push(chunk);
211
+ }
212
+ else {
213
+ this.destroy(new ArchiveError('ERR_ARCHIVE_EXTRACT', 'Native stream returned an empty chunk notification'));
214
+ }
215
+ })).then((streamId) => {
216
+ this.streamId = streamId;
217
+ this.starting = false;
218
+ try {
219
+ if (this.destroyed) {
220
+ this.cancel(streamId);
198
221
  }
199
222
  else {
200
- this.destroy(new ArchiveError('ERR_ARCHIVE_EXTRACT', 'Native stream returned an empty chunk notification'));
223
+ this.resumeNative(streamId);
201
224
  }
202
- });
203
- this.resumeNative(this.streamId);
204
- }
205
- else {
206
- this.resumeNative(this.streamId);
207
- }
225
+ }
226
+ catch (cause) {
227
+ this.destroy(extractionError(cause));
228
+ }
229
+ }, (cause) => {
230
+ this.starting = false;
231
+ this.destroy(extractionError(cause));
232
+ });
208
233
  }
209
234
  catch (cause) {
235
+ this.starting = false;
210
236
  this.destroy(extractionError(cause));
211
237
  }
212
238
  }
@@ -272,21 +298,41 @@ class Archive {
272
298
  }
273
299
  return (await this.entries()).find(entry => typeof idOrPath === 'number' ? entry.id === idOrPath : entry.path === idOrPath);
274
300
  }
275
- async extract(idOrIds) {
301
+ async extract(idOrPathOrPaths) {
276
302
  this.assertOpen();
277
- const ids = Array.isArray(idOrIds) ? [...idOrIds] : [idOrIds];
278
- this.validateIds(ids);
303
+ const isBatch = Array.isArray(idOrPathOrPaths);
304
+ let selectors;
305
+ if (isBatch) {
306
+ selectors = idOrPathOrPaths;
307
+ }
308
+ else {
309
+ selectors = [idOrPathOrPaths];
310
+ }
311
+ const numericIds = selectors.filter((selector) => typeof selector === 'number');
312
+ let ids;
313
+ if (numericIds.length === selectors.length) {
314
+ this.validateIds(numericIds);
315
+ ids = numericIds;
316
+ }
317
+ else {
318
+ ids = await this.resolveEntryIds(selectors);
319
+ }
279
320
  try {
280
321
  const buffers = await native.extract(this.handle, ids);
281
- return Array.isArray(idOrIds) ? buffers : buffers[0];
322
+ return isBatch ? buffers : buffers[0];
282
323
  }
283
324
  catch (cause) {
284
325
  throw extractionError(cause);
285
326
  }
286
327
  }
287
- createReadStream(id, options = {}) {
328
+ createReadStream(idOrPath, options = {}) {
288
329
  this.assertOpen();
289
- this.validateIds([id]);
330
+ if (typeof idOrPath === 'number') {
331
+ this.validateIds([idOrPath]);
332
+ }
333
+ else if (typeof idOrPath !== 'string') {
334
+ throw new TypeError('Entry selector must be an ID or archive path');
335
+ }
290
336
  if (!options || typeof options !== 'object') {
291
337
  throw new TypeError('Stream options must be an object');
292
338
  }
@@ -298,13 +344,18 @@ class Archive {
298
344
  }
299
345
  options.signal?.throwIfAborted();
300
346
  const maxBytes = normalizeMaxBytes(options.maxBytes, 'maxBytes');
301
- return new NativeArchiveReadable(callback => native.startStream(this.handle, id, callback), streamId => native.resumeStream(streamId), streamId => native.cancelStream(streamId), options, maxBytes);
347
+ return new NativeArchiveReadable((callback) => {
348
+ if (typeof idOrPath === 'number') {
349
+ return native.startStream(this.handle, idOrPath, callback);
350
+ }
351
+ return this.resolveEntryId(idOrPath).then(id => native.startStream(this.handle, id, callback));
352
+ }, streamId => native.resumeStream(streamId), streamId => native.cancelStream(streamId), options, maxBytes);
302
353
  }
303
- async extractToStream(id, destination, options = {}) {
354
+ async extractToStream(idOrPath, destination, options = {}) {
304
355
  if (!destination || typeof destination.write !== 'function') {
305
356
  throw new TypeError('Destination must be a writable stream');
306
357
  }
307
- await promises.pipeline(this.createReadStream(id, options), destination, { signal: options.signal });
358
+ await promises.pipeline(this.createReadStream(idOrPath, options), destination, { signal: options.signal });
308
359
  }
309
360
  async extractToFolder(destination, options = {}) {
310
361
  this.assertOpen();
@@ -318,6 +369,9 @@ class Archive {
318
369
  const maxBytes = normalizeMaxBytes(options.maxBytes, 'maxBytes');
319
370
  const entries = await this.entries();
320
371
  let selectedEntries = entries;
372
+ if (options.ids !== undefined && options.paths !== undefined) {
373
+ throw new TypeError('Specify either folder extraction IDs or paths, not both');
374
+ }
321
375
  if (options.ids !== undefined) {
322
376
  if (!Array.isArray(options.ids)) {
323
377
  throw new TypeError('Folder extraction IDs must be an array');
@@ -332,6 +386,21 @@ class Archive {
332
386
  return entry;
333
387
  });
334
388
  }
389
+ else if (options.paths !== undefined) {
390
+ if (!Array.isArray(options.paths)
391
+ || options.paths.some(entryPath => typeof entryPath !== 'string')) {
392
+ throw new TypeError('Folder extraction paths must be an array of strings');
393
+ }
394
+ const ids = await this.resolveEntryIds(options.paths);
395
+ const entriesById = new Map(entries.map(entry => [entry.id, entry]));
396
+ selectedEntries = ids.map((id) => {
397
+ const entry = entriesById.get(id);
398
+ if (!entry) {
399
+ throw new ArchiveError('ERR_INVALID_ENTRY', `Entry ID ${id} is out of range`);
400
+ }
401
+ return entry;
402
+ });
403
+ }
335
404
  const root = node_path.resolve(destination);
336
405
  const plans = makeOutputPlans(root, selectedEntries);
337
406
  const expectedBytes = selectedEntries.reduce((total, entry) => total + (entry.isDirectory ? 0n : entry.size), 0n);
@@ -366,9 +435,16 @@ class Archive {
366
435
  }), limit, output, { signal: options.signal });
367
436
  }
368
437
  }
369
- async read(id, options) {
438
+ async read(idOrPath, options) {
370
439
  this.assertOpen();
371
- this.validateIds([id]);
440
+ let id;
441
+ if (typeof idOrPath === 'number') {
442
+ this.validateIds([idOrPath]);
443
+ id = idOrPath;
444
+ }
445
+ else {
446
+ id = await this.resolveEntryId(idOrPath);
447
+ }
372
448
  const offset = this.toUnsignedBigInt(options.offset ?? 0, 'offset');
373
449
  const length = this.toUnsignedBigInt(options.length, 'length');
374
450
  const maxUInt64 = (1n << 64n) - 1n;
@@ -399,6 +475,25 @@ class Archive {
399
475
  throw new ArchiveError('ERR_INVALID_ENTRY', 'Entry IDs must be unique non-negative safe integers');
400
476
  }
401
477
  }
478
+ async resolveEntryId(selector) {
479
+ if (typeof selector === 'number') {
480
+ this.validateIds([selector]);
481
+ return selector;
482
+ }
483
+ if (typeof selector !== 'string') {
484
+ throw new TypeError('Entry selector must be an ID or archive path');
485
+ }
486
+ const entry = await this.getEntry(selector);
487
+ if (!entry) {
488
+ throw new ArchiveError('ERR_INVALID_ENTRY', `Archive entry "${selector}" was not found`);
489
+ }
490
+ return entry.id;
491
+ }
492
+ async resolveEntryIds(selectors) {
493
+ const ids = await Promise.all(selectors.map(selector => this.resolveEntryId(selector)));
494
+ this.validateIds(ids);
495
+ return ids;
496
+ }
402
497
  toUnsignedBigInt(value, label) {
403
498
  if (typeof value === 'bigint') {
404
499
  if (value < 0n) {
package/dist/index.d.cts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Readable, Writable } from 'node:stream';
2
2
 
3
3
  type EntryId = number;
4
+ type EntrySelector = EntryId | string;
4
5
  interface ArchiveEntry {
5
6
  readonly id: EntryId;
6
7
  readonly path: string;
@@ -25,6 +26,7 @@ interface ReadStreamOptions {
25
26
  }
26
27
  interface ExtractToFolderOptions {
27
28
  readonly ids?: readonly EntryId[];
29
+ readonly paths?: readonly string[];
28
30
  readonly maxBytes?: number | bigint | null;
29
31
  readonly signal?: AbortSignal;
30
32
  }
@@ -42,18 +44,20 @@ declare class Archive {
42
44
  static concurrency(value?: number): number;
43
45
  static open(path: string, options?: OpenOptions): Promise<Archive>;
44
46
  entries(): Promise<readonly ArchiveEntry[]>;
45
- getEntry(idOrPath: EntryId | string): Promise<ArchiveEntry | undefined>;
46
- extract(id: EntryId): Promise<Buffer>;
47
- extract(ids: readonly EntryId[]): Promise<Buffer[]>;
48
- createReadStream(id: EntryId, options?: ReadStreamOptions): Readable;
49
- extractToStream(id: EntryId, destination: Writable, options?: ReadStreamOptions): Promise<void>;
47
+ getEntry(idOrPath: EntrySelector): Promise<ArchiveEntry | undefined>;
48
+ extract(idOrPath: EntrySelector): Promise<Buffer>;
49
+ extract(idsOrPaths: readonly EntrySelector[]): Promise<Buffer[]>;
50
+ createReadStream(idOrPath: EntrySelector, options?: ReadStreamOptions): Readable;
51
+ extractToStream(idOrPath: EntrySelector, destination: Writable, options?: ReadStreamOptions): Promise<void>;
50
52
  extractToFolder(destination: string, options?: ExtractToFolderOptions): Promise<void>;
51
- read(id: EntryId, options: ReadOptions): Promise<Buffer>;
53
+ read(idOrPath: EntrySelector, options: ReadOptions): Promise<Buffer>;
52
54
  close(): Promise<void>;
53
55
  private assertOpen;
54
56
  private validateIds;
57
+ private resolveEntryId;
58
+ private resolveEntryIds;
55
59
  private toUnsignedBigInt;
56
60
  }
57
61
 
58
62
  export { Archive, ArchiveError };
59
- export type { ArchiveEntry, ArchiveErrorCode, EntryId, ExtractToFolderOptions, OpenOptions, ReadOptions, ReadStreamOptions };
63
+ export type { ArchiveEntry, ArchiveErrorCode, EntryId, EntrySelector, ExtractToFolderOptions, OpenOptions, ReadOptions, ReadStreamOptions };
package/dist/index.d.mts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Readable, Writable } from 'node:stream';
2
2
 
3
3
  type EntryId = number;
4
+ type EntrySelector = EntryId | string;
4
5
  interface ArchiveEntry {
5
6
  readonly id: EntryId;
6
7
  readonly path: string;
@@ -25,6 +26,7 @@ interface ReadStreamOptions {
25
26
  }
26
27
  interface ExtractToFolderOptions {
27
28
  readonly ids?: readonly EntryId[];
29
+ readonly paths?: readonly string[];
28
30
  readonly maxBytes?: number | bigint | null;
29
31
  readonly signal?: AbortSignal;
30
32
  }
@@ -42,18 +44,20 @@ declare class Archive {
42
44
  static concurrency(value?: number): number;
43
45
  static open(path: string, options?: OpenOptions): Promise<Archive>;
44
46
  entries(): Promise<readonly ArchiveEntry[]>;
45
- getEntry(idOrPath: EntryId | string): Promise<ArchiveEntry | undefined>;
46
- extract(id: EntryId): Promise<Buffer>;
47
- extract(ids: readonly EntryId[]): Promise<Buffer[]>;
48
- createReadStream(id: EntryId, options?: ReadStreamOptions): Readable;
49
- extractToStream(id: EntryId, destination: Writable, options?: ReadStreamOptions): Promise<void>;
47
+ getEntry(idOrPath: EntrySelector): Promise<ArchiveEntry | undefined>;
48
+ extract(idOrPath: EntrySelector): Promise<Buffer>;
49
+ extract(idsOrPaths: readonly EntrySelector[]): Promise<Buffer[]>;
50
+ createReadStream(idOrPath: EntrySelector, options?: ReadStreamOptions): Readable;
51
+ extractToStream(idOrPath: EntrySelector, destination: Writable, options?: ReadStreamOptions): Promise<void>;
50
52
  extractToFolder(destination: string, options?: ExtractToFolderOptions): Promise<void>;
51
- read(id: EntryId, options: ReadOptions): Promise<Buffer>;
53
+ read(idOrPath: EntrySelector, options: ReadOptions): Promise<Buffer>;
52
54
  close(): Promise<void>;
53
55
  private assertOpen;
54
56
  private validateIds;
57
+ private resolveEntryId;
58
+ private resolveEntryIds;
55
59
  private toUnsignedBigInt;
56
60
  }
57
61
 
58
62
  export { Archive, ArchiveError };
59
- export type { ArchiveEntry, ArchiveErrorCode, EntryId, ExtractToFolderOptions, OpenOptions, ReadOptions, ReadStreamOptions };
63
+ export type { ArchiveEntry, ArchiveErrorCode, EntryId, EntrySelector, ExtractToFolderOptions, OpenOptions, ReadOptions, ReadStreamOptions };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Readable, Writable } from 'node:stream';
2
2
 
3
3
  type EntryId = number;
4
+ type EntrySelector = EntryId | string;
4
5
  interface ArchiveEntry {
5
6
  readonly id: EntryId;
6
7
  readonly path: string;
@@ -25,6 +26,7 @@ interface ReadStreamOptions {
25
26
  }
26
27
  interface ExtractToFolderOptions {
27
28
  readonly ids?: readonly EntryId[];
29
+ readonly paths?: readonly string[];
28
30
  readonly maxBytes?: number | bigint | null;
29
31
  readonly signal?: AbortSignal;
30
32
  }
@@ -42,18 +44,20 @@ declare class Archive {
42
44
  static concurrency(value?: number): number;
43
45
  static open(path: string, options?: OpenOptions): Promise<Archive>;
44
46
  entries(): Promise<readonly ArchiveEntry[]>;
45
- getEntry(idOrPath: EntryId | string): Promise<ArchiveEntry | undefined>;
46
- extract(id: EntryId): Promise<Buffer>;
47
- extract(ids: readonly EntryId[]): Promise<Buffer[]>;
48
- createReadStream(id: EntryId, options?: ReadStreamOptions): Readable;
49
- extractToStream(id: EntryId, destination: Writable, options?: ReadStreamOptions): Promise<void>;
47
+ getEntry(idOrPath: EntrySelector): Promise<ArchiveEntry | undefined>;
48
+ extract(idOrPath: EntrySelector): Promise<Buffer>;
49
+ extract(idsOrPaths: readonly EntrySelector[]): Promise<Buffer[]>;
50
+ createReadStream(idOrPath: EntrySelector, options?: ReadStreamOptions): Readable;
51
+ extractToStream(idOrPath: EntrySelector, destination: Writable, options?: ReadStreamOptions): Promise<void>;
50
52
  extractToFolder(destination: string, options?: ExtractToFolderOptions): Promise<void>;
51
- read(id: EntryId, options: ReadOptions): Promise<Buffer>;
53
+ read(idOrPath: EntrySelector, options: ReadOptions): Promise<Buffer>;
52
54
  close(): Promise<void>;
53
55
  private assertOpen;
54
56
  private validateIds;
57
+ private resolveEntryId;
58
+ private resolveEntryIds;
55
59
  private toUnsignedBigInt;
56
60
  }
57
61
 
58
62
  export { Archive, ArchiveError };
59
- export type { ArchiveEntry, ArchiveErrorCode, EntryId, ExtractToFolderOptions, OpenOptions, ReadOptions, ReadStreamOptions };
63
+ export type { ArchiveEntry, ArchiveErrorCode, EntryId, EntrySelector, ExtractToFolderOptions, OpenOptions, ReadOptions, ReadStreamOptions };
package/dist/index.mjs CHANGED
@@ -164,6 +164,7 @@ class NativeArchiveReadable extends Readable {
164
164
  maxBytes;
165
165
  streamId;
166
166
  outputBytes = 0n;
167
+ starting = false;
167
168
  constructor(start, resumeNative, cancel, options, maxBytes) {
168
169
  super({ highWaterMark: options.highWaterMark, signal: options.signal });
169
170
  this.start = start;
@@ -172,38 +173,63 @@ class NativeArchiveReadable extends Readable {
172
173
  this.maxBytes = maxBytes;
173
174
  }
174
175
  _read() {
176
+ if (this.streamId !== undefined) {
177
+ try {
178
+ this.resumeNative(this.streamId);
179
+ }
180
+ catch (cause) {
181
+ this.destroy(extractionError(cause));
182
+ }
183
+ return;
184
+ }
185
+ if (this.starting) {
186
+ return;
187
+ }
188
+ this.starting = true;
175
189
  try {
176
- if (this.streamId === undefined) {
177
- this.streamId = this.start((chunk, error, done) => {
178
- if (this.destroyed) {
190
+ Promise.resolve(this.start((chunk, error, done) => {
191
+ if (this.destroyed) {
192
+ return;
193
+ }
194
+ if (error) {
195
+ this.destroy(extractionError(error));
196
+ }
197
+ else if (done) {
198
+ this.push(null);
199
+ }
200
+ else if (chunk) {
201
+ if (this.maxBytes !== null
202
+ && this.outputBytes + BigInt(chunk.length) > this.maxBytes) {
203
+ this.destroy(new ArchiveError('ERR_RESOURCE_LIMIT', `Stream output exceeds the ${this.maxBytes}-byte limit`));
179
204
  return;
180
205
  }
181
- if (error) {
182
- this.destroy(extractionError(error));
183
- }
184
- else if (done) {
185
- this.push(null);
186
- }
187
- else if (chunk) {
188
- if (this.maxBytes !== null
189
- && this.outputBytes + BigInt(chunk.length) > this.maxBytes) {
190
- this.destroy(new ArchiveError('ERR_RESOURCE_LIMIT', `Stream output exceeds the ${this.maxBytes}-byte limit`));
191
- return;
192
- }
193
- this.outputBytes += BigInt(chunk.length);
194
- this.push(chunk);
206
+ this.outputBytes += BigInt(chunk.length);
207
+ this.push(chunk);
208
+ }
209
+ else {
210
+ this.destroy(new ArchiveError('ERR_ARCHIVE_EXTRACT', 'Native stream returned an empty chunk notification'));
211
+ }
212
+ })).then((streamId) => {
213
+ this.streamId = streamId;
214
+ this.starting = false;
215
+ try {
216
+ if (this.destroyed) {
217
+ this.cancel(streamId);
195
218
  }
196
219
  else {
197
- this.destroy(new ArchiveError('ERR_ARCHIVE_EXTRACT', 'Native stream returned an empty chunk notification'));
220
+ this.resumeNative(streamId);
198
221
  }
199
- });
200
- this.resumeNative(this.streamId);
201
- }
202
- else {
203
- this.resumeNative(this.streamId);
204
- }
222
+ }
223
+ catch (cause) {
224
+ this.destroy(extractionError(cause));
225
+ }
226
+ }, (cause) => {
227
+ this.starting = false;
228
+ this.destroy(extractionError(cause));
229
+ });
205
230
  }
206
231
  catch (cause) {
232
+ this.starting = false;
207
233
  this.destroy(extractionError(cause));
208
234
  }
209
235
  }
@@ -269,21 +295,41 @@ class Archive {
269
295
  }
270
296
  return (await this.entries()).find(entry => typeof idOrPath === 'number' ? entry.id === idOrPath : entry.path === idOrPath);
271
297
  }
272
- async extract(idOrIds) {
298
+ async extract(idOrPathOrPaths) {
273
299
  this.assertOpen();
274
- const ids = Array.isArray(idOrIds) ? [...idOrIds] : [idOrIds];
275
- this.validateIds(ids);
300
+ const isBatch = Array.isArray(idOrPathOrPaths);
301
+ let selectors;
302
+ if (isBatch) {
303
+ selectors = idOrPathOrPaths;
304
+ }
305
+ else {
306
+ selectors = [idOrPathOrPaths];
307
+ }
308
+ const numericIds = selectors.filter((selector) => typeof selector === 'number');
309
+ let ids;
310
+ if (numericIds.length === selectors.length) {
311
+ this.validateIds(numericIds);
312
+ ids = numericIds;
313
+ }
314
+ else {
315
+ ids = await this.resolveEntryIds(selectors);
316
+ }
276
317
  try {
277
318
  const buffers = await native.extract(this.handle, ids);
278
- return Array.isArray(idOrIds) ? buffers : buffers[0];
319
+ return isBatch ? buffers : buffers[0];
279
320
  }
280
321
  catch (cause) {
281
322
  throw extractionError(cause);
282
323
  }
283
324
  }
284
- createReadStream(id, options = {}) {
325
+ createReadStream(idOrPath, options = {}) {
285
326
  this.assertOpen();
286
- this.validateIds([id]);
327
+ if (typeof idOrPath === 'number') {
328
+ this.validateIds([idOrPath]);
329
+ }
330
+ else if (typeof idOrPath !== 'string') {
331
+ throw new TypeError('Entry selector must be an ID or archive path');
332
+ }
287
333
  if (!options || typeof options !== 'object') {
288
334
  throw new TypeError('Stream options must be an object');
289
335
  }
@@ -295,13 +341,18 @@ class Archive {
295
341
  }
296
342
  options.signal?.throwIfAborted();
297
343
  const maxBytes = normalizeMaxBytes(options.maxBytes, 'maxBytes');
298
- return new NativeArchiveReadable(callback => native.startStream(this.handle, id, callback), streamId => native.resumeStream(streamId), streamId => native.cancelStream(streamId), options, maxBytes);
344
+ return new NativeArchiveReadable((callback) => {
345
+ if (typeof idOrPath === 'number') {
346
+ return native.startStream(this.handle, idOrPath, callback);
347
+ }
348
+ return this.resolveEntryId(idOrPath).then(id => native.startStream(this.handle, id, callback));
349
+ }, streamId => native.resumeStream(streamId), streamId => native.cancelStream(streamId), options, maxBytes);
299
350
  }
300
- async extractToStream(id, destination, options = {}) {
351
+ async extractToStream(idOrPath, destination, options = {}) {
301
352
  if (!destination || typeof destination.write !== 'function') {
302
353
  throw new TypeError('Destination must be a writable stream');
303
354
  }
304
- await pipeline(this.createReadStream(id, options), destination, { signal: options.signal });
355
+ await pipeline(this.createReadStream(idOrPath, options), destination, { signal: options.signal });
305
356
  }
306
357
  async extractToFolder(destination, options = {}) {
307
358
  this.assertOpen();
@@ -315,6 +366,9 @@ class Archive {
315
366
  const maxBytes = normalizeMaxBytes(options.maxBytes, 'maxBytes');
316
367
  const entries = await this.entries();
317
368
  let selectedEntries = entries;
369
+ if (options.ids !== undefined && options.paths !== undefined) {
370
+ throw new TypeError('Specify either folder extraction IDs or paths, not both');
371
+ }
318
372
  if (options.ids !== undefined) {
319
373
  if (!Array.isArray(options.ids)) {
320
374
  throw new TypeError('Folder extraction IDs must be an array');
@@ -329,6 +383,21 @@ class Archive {
329
383
  return entry;
330
384
  });
331
385
  }
386
+ else if (options.paths !== undefined) {
387
+ if (!Array.isArray(options.paths)
388
+ || options.paths.some(entryPath => typeof entryPath !== 'string')) {
389
+ throw new TypeError('Folder extraction paths must be an array of strings');
390
+ }
391
+ const ids = await this.resolveEntryIds(options.paths);
392
+ const entriesById = new Map(entries.map(entry => [entry.id, entry]));
393
+ selectedEntries = ids.map((id) => {
394
+ const entry = entriesById.get(id);
395
+ if (!entry) {
396
+ throw new ArchiveError('ERR_INVALID_ENTRY', `Entry ID ${id} is out of range`);
397
+ }
398
+ return entry;
399
+ });
400
+ }
332
401
  const root = resolve(destination);
333
402
  const plans = makeOutputPlans(root, selectedEntries);
334
403
  const expectedBytes = selectedEntries.reduce((total, entry) => total + (entry.isDirectory ? 0n : entry.size), 0n);
@@ -363,9 +432,16 @@ class Archive {
363
432
  }), limit, output, { signal: options.signal });
364
433
  }
365
434
  }
366
- async read(id, options) {
435
+ async read(idOrPath, options) {
367
436
  this.assertOpen();
368
- this.validateIds([id]);
437
+ let id;
438
+ if (typeof idOrPath === 'number') {
439
+ this.validateIds([idOrPath]);
440
+ id = idOrPath;
441
+ }
442
+ else {
443
+ id = await this.resolveEntryId(idOrPath);
444
+ }
369
445
  const offset = this.toUnsignedBigInt(options.offset ?? 0, 'offset');
370
446
  const length = this.toUnsignedBigInt(options.length, 'length');
371
447
  const maxUInt64 = (1n << 64n) - 1n;
@@ -396,6 +472,25 @@ class Archive {
396
472
  throw new ArchiveError('ERR_INVALID_ENTRY', 'Entry IDs must be unique non-negative safe integers');
397
473
  }
398
474
  }
475
+ async resolveEntryId(selector) {
476
+ if (typeof selector === 'number') {
477
+ this.validateIds([selector]);
478
+ return selector;
479
+ }
480
+ if (typeof selector !== 'string') {
481
+ throw new TypeError('Entry selector must be an ID or archive path');
482
+ }
483
+ const entry = await this.getEntry(selector);
484
+ if (!entry) {
485
+ throw new ArchiveError('ERR_INVALID_ENTRY', `Archive entry "${selector}" was not found`);
486
+ }
487
+ return entry.id;
488
+ }
489
+ async resolveEntryIds(selectors) {
490
+ const ids = await Promise.all(selectors.map(selector => this.resolveEntryId(selector)));
491
+ this.validateIds(ids);
492
+ return ids;
493
+ }
399
494
  toUnsignedBigInt(value, label) {
400
495
  if (typeof value === 'bigint') {
401
496
  if (value < 0n) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "7zip-native",
3
- "version": "0.0.1",
3
+ "version": "0.0.5",
4
4
  "description": "Native 7zip bindings for Node.js",
5
5
  "main": "./dist/index.cjs",
6
6
  "type": "module",
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file