7zip-native 0.0.1 → 0.0.6

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,298 @@
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
+ ### Current version `26.05`
7
+ Upstream 7-Zip archive engine from https://github.com/ip7z/7zip/
7
8
 
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.
9
+ 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.
9
10
 
10
- ## Build from source
11
+ 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.
12
+
13
+ ## Installation
11
14
 
12
15
  ```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
16
+ npm install 7zip-native
18
17
  ```
19
18
 
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.
19
+ The package requires Node.js 18 or newer.
20
+
21
+ ## Quick start
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
+ ```ts
24
+ import {Archive} from '7zip-native';
23
25
 
24
- To update upstream deliberately:
26
+ const archive = await Archive.open('./comics.cbz');
25
27
 
26
- ```sh
27
- git submodule update --remote vendor/7zip
28
- git -C vendor/7zip rev-parse HEAD
29
- git add .gitmodules vendor/7zip
28
+ try
29
+ {
30
+ const entries = await archive.entries();
31
+ const cover = await archive.getEntry('chapter/cover.jpg');
32
+
33
+ if(cover && !cover.isDirectory)
34
+ {
35
+ const preview = await archive.read(cover.path, {offset: 0, length: 64 * 1024});
36
+ const image = await archive.extract(cover.path);
37
+ console.log(entries.length, preview.length, image.length);
38
+ }
39
+ }
40
+ finally
41
+ {
42
+ await archive.close();
43
+ }
30
44
  ```
31
45
 
32
- Review and commit the new gitlink revision. The current submodule is pinned to 7-Zip 26.05 (`b88c6bc09339de32f7d18bbab900686b244f42ba`).
46
+ The package supports both ES modules and CommonJS:
47
+
48
+ ```js
49
+ // ES module
50
+ import {Archive} from '7zip-native';
51
+
52
+ // CommonJS
53
+ const {Archive} = require('7zip-native');
54
+ ```
33
55
 
34
56
  ## API
35
57
 
58
+ ### `Archive.open(path, options?)`
59
+
60
+ Opens an archive from a filesystem path and indexes its metadata asynchronously.
61
+
36
62
  ```ts
37
- import {Archive} from '7zip-native';
63
+ const archive = await Archive.open('./archive.7z', {password: 'secret'});
64
+ ```
38
65
 
39
- const archive = await Archive.open('/path/to/file.zip');
66
+ `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.
67
+
68
+ ### Listing and looking up entries
69
+
70
+ ```ts
40
71
  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]);
72
+ const byPath = await archive.getEntry('folder/page.jpg');
73
+ const byId = await archive.getEntry(0);
74
+ ```
75
+
76
+ `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.
77
+
78
+ 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.
79
+
80
+ Each entry includes:
81
+
82
+ | Property | Type | Description |
83
+ | --- | --- | --- |
84
+ | `id` | `number` | Stable ID within this opened archive |
85
+ | `path` | `string` | Entry path, using `/` separators |
86
+ | `size` | `bigint` | Uncompressed size |
87
+ | `compressedSize` | `bigint` | Compressed size |
88
+ | `isDirectory` | `boolean` | Whether the entry is a directory |
89
+ | `encrypted` | `boolean` | Whether the entry is encrypted |
90
+ | `attributes` | `number` | Format-specific attributes |
91
+ | `crc` | `number \| undefined` | CRC when supplied by the format |
92
+
93
+ ### Extracting to memory
94
+
95
+ ```ts
96
+ const image = await archive.extract(entry.id); // Buffer
97
+ const sameImage = await archive.extract(entry.path); // Buffer
98
+ const pages = await archive.extract([page1.id, page2.path]); // Buffer[]
99
+ ```
100
+
101
+ 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.
102
+
103
+ ### Reading a byte range
104
+
105
+ ```ts
106
+ const header = await archive.read(entry.path, {
107
+ offset: 0,
108
+ length: 64 * 1024,
109
+ });
110
+ ```
111
+
112
+ 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.
113
+
114
+ ### Streams and output destinations
115
+
116
+ Create a standard Node.js `Readable` for a large entry:
117
+
118
+ ```ts
119
+ for await (const chunk of archive.createReadStream(entry.path, {
120
+ highWaterMark: 256 * 1024,
121
+ signal: abortController.signal,
122
+ }))
123
+ {
124
+ // Consume each Buffer chunk before requesting more data.
45
125
  }
126
+ ```
127
+
128
+ 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.
129
+
130
+ To pipe directly to a Node.js `Writable`:
131
+
132
+ ```ts
133
+ import {createWriteStream} from 'node:fs';
134
+
135
+ await archive.extractToStream(entry.path, createWriteStream('./page.jpg'), {
136
+ signal: abortController.signal,
137
+ });
138
+ ```
139
+
140
+ This respects backpressure and ends the destination when extraction succeeds. A destination error, early close, or abort rejects the Promise.
141
+
142
+ ### Extracting to a folder
143
+
144
+ ```ts
145
+ await archive.extractToFolder('./unpacked', {
146
+ paths: ['chapter/page1.jpg', 'chapter/page2.jpg'], // alternatively, use ids
147
+ overwrite: true,
148
+ onProgress: ({filesExtracted, totalFiles, bytesProcessed, totalBytes, percent}) => {
149
+ console.log(`${filesExtracted}/${totalFiles} files, ${percent}% (${bytesProcessed}/${totalBytes} bytes)`);
150
+ },
151
+ maxBytes: 2n * 1024n * 1024n * 1024n,
152
+ signal: abortController.signal,
153
+ });
154
+ ```
155
+
156
+ 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. Set `onProgress` to receive the number of regular files selected and fully extracted, the bytes emitted so far, the total uncompressed bytes, and the calculated percentage. It is called before extraction, as data chunks are processed, and after each file completes. By default, existing files are not overwritten; set `overwrite: true` to replace existing regular files. Replacement is committed only after that entry has been fully extracted, using a temporary file alongside the destination so a failure does not truncate the existing file. This can temporarily require extra disk space up to the size of the entry being replaced. Symlink or non-file targets are rejected. Unsafe archive paths, path collisions, and symlinked destination components are also 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.
157
+
158
+ ### Concurrency
159
+
160
+ ```ts
161
+ const current = Archive.concurrency();
162
+ Archive.concurrency(8);
163
+ Archive.concurrency(0); // reset to the default
164
+ ```
165
+
166
+ 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.
167
+
168
+ 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.
169
+
170
+ ### Closing an archive
171
+
172
+ Call `close()` when finished. It rejects future API calls; operations already submitted keep the native state alive until they finish.
173
+
174
+ ```ts
46
175
  await archive.close();
47
176
  ```
48
177
 
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`.
178
+ ## Errors and limits
179
+
180
+ Invalid argument types and values throw standard `TypeError` or `RangeError`. Archive and extraction failures use `ArchiveError`, which has a stable `code` property:
181
+
182
+ | Code | Meaning |
183
+ | --- | --- |
184
+ | `ERR_ARCHIVE_CLOSED` | Operation attempted after `close()` |
185
+ | `ERR_INVALID_ENTRY` | Entry ID is invalid or out of range |
186
+ | `ERR_INVALID_RANGE` | Byte range is invalid or exceeds the native offset limit |
187
+ | `ERR_RESOURCE_LIMIT` | Configured or default output limit was exceeded |
188
+ | `ERR_ARCHIVE_OPEN` | Archive could not be opened or indexed |
189
+ | `ERR_ARCHIVE_PASSWORD` | Password is missing or incorrect when requested by the handler |
190
+ | `ERR_ARCHIVE_PATH` | Unsafe or colliding path during folder extraction |
191
+ | `ERR_ARCHIVE_EXTRACT` | Entry extraction failed |
192
+
193
+ Buffered `extract()` and `read()` do not currently support cancellation. Streams and folder extraction accept an `AbortSignal`.
194
+
195
+ ## Supported formats
196
+
197
+ 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.
198
+
199
+ ### Format-specific notes
62
200
 
63
- Entry metadata includes `id`, `path`, `size`, `compressedSize`, `isDirectory`, `encrypted`, `attributes`, and `crc` when provided by the format. Sizes are `bigint`.
201
+ - **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.
202
+ - **ACE:** not supported by this 7-Zip revision.
203
+ - **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.
204
+ - **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
205
 
65
- ## Formats and limitations
206
+ See [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md) for third-party redistribution terms.
66
207
 
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.
208
+ ## Platform support
68
209
 
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.
210
+ The package includes prebuilt binaries for:
79
211
 
80
- ## Benchmark harness
212
+ | Platform | Architectures |
213
+ | --- | --- |
214
+ | Linux | x64, x86 (ia32), ARM32, ARM64 |
215
+ | macOS | x64, ARM64 |
216
+ | Windows | x64, ARM64 |
81
217
 
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.
218
+ Windows x86 (ia32) is not supported by the published prebuilt binaries.
83
219
 
84
- For a direct CLI comparison, install the development dependencies and run:
220
+ ## Electron
221
+
222
+ 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.
223
+
224
+ 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:
225
+
226
+ ```json
227
+ {
228
+ "build": {
229
+ "asarUnpack": [
230
+ "node_modules/7zip-native/prebuilds/**/*.node",
231
+ "node_modules/7zip-native/prebuilds/**/*.dll",
232
+ "node_modules/7zip-native/prebuilds/**/*.so",
233
+ "node_modules/7zip-native/prebuilds/**/*.dylib"
234
+ ]
235
+ }
236
+ }
237
+ ```
238
+
239
+ 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.
240
+
241
+ ## Build from source
85
242
 
86
243
  ```sh
87
- npm run benchmark:compare -- --archive "./archive.zip" --entry "chapter/page.jpg" --iterations 20 --read-bytes 65536 --concurrency 4
244
+ git clone --recurse-submodules https://github.com/ollm/7zip-native.git
245
+ cd 7zip-native
246
+ npm ci --ignore-scripts
247
+ npm run build
248
+ npm test
88
249
  ```
89
250
 
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.
251
+ 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
252
 
92
- Compare extracting every file from one archive sequentially versus the configured native concurrency:
253
+ To update the upstream submodule deliberately:
93
254
 
94
255
  ```sh
95
- npm run benchmark:concurrency -- --archive "/path/to/archive.7z" --iterations 20 --concurrencies 1,2,3,4,5,8,16
256
+ git submodule update --remote vendor/7zip
257
+ git -C vendor/7zip rev-parse HEAD
258
+ git add .gitmodules vendor/7zip
96
259
  ```
97
260
 
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.
261
+ Review the upstream changes and commit the new submodule revision. The current submodule is pinned to 7-Zip 26.05 (`b88c6bc09339de32f7d18bbab900686b244f42ba`).
99
262
 
100
- ## Electron
263
+ ## Benchmarks
264
+
265
+ Run the native benchmark against a representative archive:
266
+
267
+ ```sh
268
+ BENCH_ITERATIONS=20 npm run benchmark -- "./archive.zip"
269
+ ```
270
+
271
+ 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.
272
+
273
+ Compare the native API with the `7zip-bin-full` command-line baseline:
274
+
275
+ ```sh
276
+ npm run benchmark:compare -- \
277
+ --archive "./archive.zip" \
278
+ --entry "chapter/page.jpg" \
279
+ --iterations 20 \
280
+ --read-bytes 65536 \
281
+ --concurrency 4
282
+ ```
283
+
284
+ 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.
285
+
286
+ Compare extraction concurrency:
287
+
288
+ ```sh
289
+ npm run benchmark:concurrency -- \
290
+ --archive "./archive.7z" \
291
+ --iterations 20 \
292
+ --concurrencies 1,2,3,4,5,8,16 \
293
+ --max-files 20
294
+ ```
295
+
296
+ 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
297
 
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`.
298
+ ## License
103
299
 
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.
300
+ 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.