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/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { existsSync, createWriteStream } from 'node:fs';
2
- import { lstat, mkdir } from 'node:fs/promises';
2
+ import { rename, rm, lstat, mkdir } from 'node:fs/promises';
3
+ import { randomUUID } from 'node:crypto';
3
4
  import { createRequire } from 'node:module';
4
5
  import { availableParallelism } from 'node:os';
5
6
  import { dirname, resolve, join, relative, sep } from 'node:path';
@@ -121,6 +122,19 @@ async function ensureDirectory(directoryPath) {
121
122
  throw new ArchiveError('ERR_ARCHIVE_PATH', `Destination path is not a real directory: ${directoryPath}`);
122
123
  }
123
124
  }
125
+ async function assertReplaceableOutput(filePath) {
126
+ try {
127
+ const details = await lstat(filePath);
128
+ if (!details.isFile()) {
129
+ throw new ArchiveError('ERR_ARCHIVE_PATH', `Cannot replace a non-file extraction target: ${filePath}`);
130
+ }
131
+ }
132
+ catch (error) {
133
+ if (error instanceof ArchiveError || error.code !== 'ENOENT') {
134
+ throw error;
135
+ }
136
+ }
137
+ }
124
138
  async function ensureExtractionRoot(root) {
125
139
  try {
126
140
  const details = await lstat(root);
@@ -164,6 +178,7 @@ class NativeArchiveReadable extends Readable {
164
178
  maxBytes;
165
179
  streamId;
166
180
  outputBytes = 0n;
181
+ starting = false;
167
182
  constructor(start, resumeNative, cancel, options, maxBytes) {
168
183
  super({ highWaterMark: options.highWaterMark, signal: options.signal });
169
184
  this.start = start;
@@ -172,38 +187,63 @@ class NativeArchiveReadable extends Readable {
172
187
  this.maxBytes = maxBytes;
173
188
  }
174
189
  _read() {
190
+ if (this.streamId !== undefined) {
191
+ try {
192
+ this.resumeNative(this.streamId);
193
+ }
194
+ catch (cause) {
195
+ this.destroy(extractionError(cause));
196
+ }
197
+ return;
198
+ }
199
+ if (this.starting) {
200
+ return;
201
+ }
202
+ this.starting = true;
175
203
  try {
176
- if (this.streamId === undefined) {
177
- this.streamId = this.start((chunk, error, done) => {
178
- if (this.destroyed) {
204
+ Promise.resolve(this.start((chunk, error, done) => {
205
+ if (this.destroyed) {
206
+ return;
207
+ }
208
+ if (error) {
209
+ this.destroy(extractionError(error));
210
+ }
211
+ else if (done) {
212
+ this.push(null);
213
+ }
214
+ else if (chunk) {
215
+ if (this.maxBytes !== null
216
+ && this.outputBytes + BigInt(chunk.length) > this.maxBytes) {
217
+ this.destroy(new ArchiveError('ERR_RESOURCE_LIMIT', `Stream output exceeds the ${this.maxBytes}-byte limit`));
179
218
  return;
180
219
  }
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);
220
+ this.outputBytes += BigInt(chunk.length);
221
+ this.push(chunk);
222
+ }
223
+ else {
224
+ this.destroy(new ArchiveError('ERR_ARCHIVE_EXTRACT', 'Native stream returned an empty chunk notification'));
225
+ }
226
+ })).then((streamId) => {
227
+ this.streamId = streamId;
228
+ this.starting = false;
229
+ try {
230
+ if (this.destroyed) {
231
+ this.cancel(streamId);
195
232
  }
196
233
  else {
197
- this.destroy(new ArchiveError('ERR_ARCHIVE_EXTRACT', 'Native stream returned an empty chunk notification'));
234
+ this.resumeNative(streamId);
198
235
  }
199
- });
200
- this.resumeNative(this.streamId);
201
- }
202
- else {
203
- this.resumeNative(this.streamId);
204
- }
236
+ }
237
+ catch (cause) {
238
+ this.destroy(extractionError(cause));
239
+ }
240
+ }, (cause) => {
241
+ this.starting = false;
242
+ this.destroy(extractionError(cause));
243
+ });
205
244
  }
206
245
  catch (cause) {
246
+ this.starting = false;
207
247
  this.destroy(extractionError(cause));
208
248
  }
209
249
  }
@@ -269,21 +309,41 @@ class Archive {
269
309
  }
270
310
  return (await this.entries()).find(entry => typeof idOrPath === 'number' ? entry.id === idOrPath : entry.path === idOrPath);
271
311
  }
272
- async extract(idOrIds) {
312
+ async extract(idOrPathOrPaths) {
273
313
  this.assertOpen();
274
- const ids = Array.isArray(idOrIds) ? [...idOrIds] : [idOrIds];
275
- this.validateIds(ids);
314
+ const isBatch = Array.isArray(idOrPathOrPaths);
315
+ let selectors;
316
+ if (isBatch) {
317
+ selectors = idOrPathOrPaths;
318
+ }
319
+ else {
320
+ selectors = [idOrPathOrPaths];
321
+ }
322
+ const numericIds = selectors.filter((selector) => typeof selector === 'number');
323
+ let ids;
324
+ if (numericIds.length === selectors.length) {
325
+ this.validateIds(numericIds);
326
+ ids = numericIds;
327
+ }
328
+ else {
329
+ ids = await this.resolveEntryIds(selectors);
330
+ }
276
331
  try {
277
332
  const buffers = await native.extract(this.handle, ids);
278
- return Array.isArray(idOrIds) ? buffers : buffers[0];
333
+ return isBatch ? buffers : buffers[0];
279
334
  }
280
335
  catch (cause) {
281
336
  throw extractionError(cause);
282
337
  }
283
338
  }
284
- createReadStream(id, options = {}) {
339
+ createReadStream(idOrPath, options = {}) {
285
340
  this.assertOpen();
286
- this.validateIds([id]);
341
+ if (typeof idOrPath === 'number') {
342
+ this.validateIds([idOrPath]);
343
+ }
344
+ else if (typeof idOrPath !== 'string') {
345
+ throw new TypeError('Entry selector must be an ID or archive path');
346
+ }
287
347
  if (!options || typeof options !== 'object') {
288
348
  throw new TypeError('Stream options must be an object');
289
349
  }
@@ -295,13 +355,18 @@ class Archive {
295
355
  }
296
356
  options.signal?.throwIfAborted();
297
357
  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);
358
+ return new NativeArchiveReadable((callback) => {
359
+ if (typeof idOrPath === 'number') {
360
+ return native.startStream(this.handle, idOrPath, callback);
361
+ }
362
+ return this.resolveEntryId(idOrPath).then(id => native.startStream(this.handle, id, callback));
363
+ }, streamId => native.resumeStream(streamId), streamId => native.cancelStream(streamId), options, maxBytes);
299
364
  }
300
- async extractToStream(id, destination, options = {}) {
365
+ async extractToStream(idOrPath, destination, options = {}) {
301
366
  if (!destination || typeof destination.write !== 'function') {
302
367
  throw new TypeError('Destination must be a writable stream');
303
368
  }
304
- await pipeline(this.createReadStream(id, options), destination, { signal: options.signal });
369
+ await pipeline(this.createReadStream(idOrPath, options), destination, { signal: options.signal });
305
370
  }
306
371
  async extractToFolder(destination, options = {}) {
307
372
  this.assertOpen();
@@ -311,10 +376,19 @@ class Archive {
311
376
  if (!options || typeof options !== 'object') {
312
377
  throw new TypeError('Folder extraction options must be an object');
313
378
  }
379
+ if (options.overwrite !== undefined && typeof options.overwrite !== 'boolean') {
380
+ throw new TypeError('Folder extraction overwrite option must be a boolean');
381
+ }
382
+ if (options.onProgress !== undefined && typeof options.onProgress !== 'function') {
383
+ throw new TypeError('Folder extraction onProgress option must be a function');
384
+ }
314
385
  options.signal?.throwIfAborted();
315
386
  const maxBytes = normalizeMaxBytes(options.maxBytes, 'maxBytes');
316
387
  const entries = await this.entries();
317
388
  let selectedEntries = entries;
389
+ if (options.ids !== undefined && options.paths !== undefined) {
390
+ throw new TypeError('Specify either folder extraction IDs or paths, not both');
391
+ }
318
392
  if (options.ids !== undefined) {
319
393
  if (!Array.isArray(options.ids)) {
320
394
  throw new TypeError('Folder extraction IDs must be an array');
@@ -329,6 +403,21 @@ class Archive {
329
403
  return entry;
330
404
  });
331
405
  }
406
+ else if (options.paths !== undefined) {
407
+ if (!Array.isArray(options.paths)
408
+ || options.paths.some(entryPath => typeof entryPath !== 'string')) {
409
+ throw new TypeError('Folder extraction paths must be an array of strings');
410
+ }
411
+ const ids = await this.resolveEntryIds(options.paths);
412
+ const entriesById = new Map(entries.map(entry => [entry.id, entry]));
413
+ selectedEntries = ids.map((id) => {
414
+ const entry = entriesById.get(id);
415
+ if (!entry) {
416
+ throw new ArchiveError('ERR_INVALID_ENTRY', `Entry ID ${id} is out of range`);
417
+ }
418
+ return entry;
419
+ });
420
+ }
332
421
  const root = resolve(destination);
333
422
  const plans = makeOutputPlans(root, selectedEntries);
334
423
  const expectedBytes = selectedEntries.reduce((total, entry) => total + (entry.isDirectory ? 0n : entry.size), 0n);
@@ -338,6 +427,26 @@ class Archive {
338
427
  options.signal?.throwIfAborted();
339
428
  await ensureExtractionRoot(root);
340
429
  let extractedBytes = 0n;
430
+ let filesExtracted = 0;
431
+ const totalFiles = plans.filter(plan => !plan.isDirectory).length;
432
+ const reportProgress = () => {
433
+ let percent = 0;
434
+ if (expectedBytes > 0n) {
435
+ const scaledPercent = extractedBytes * 10000n / expectedBytes;
436
+ percent = Number(scaledPercent > 9900n ? 9900n : scaledPercent) / 100;
437
+ }
438
+ if (filesExtracted === totalFiles) {
439
+ percent = 100;
440
+ }
441
+ options.onProgress?.({
442
+ filesExtracted,
443
+ totalFiles,
444
+ bytesProcessed: extractedBytes,
445
+ totalBytes: expectedBytes,
446
+ percent,
447
+ });
448
+ };
449
+ reportProgress();
341
450
  for (const plan of plans) {
342
451
  options.signal?.throwIfAborted();
343
452
  if (plan.isDirectory) {
@@ -345,7 +454,6 @@ class Archive {
345
454
  continue;
346
455
  }
347
456
  await ensureOutputParents(root, plan.targetPath);
348
- const output = createWriteStream(plan.targetPath, { flags: 'wx', mode: 0o600 });
349
457
  const limit = new Transform({
350
458
  transform(chunk, _encoding, callback) {
351
459
  const size = BigInt(chunk.length);
@@ -354,18 +462,52 @@ class Archive {
354
462
  return;
355
463
  }
356
464
  extractedBytes += size;
465
+ try {
466
+ reportProgress();
467
+ }
468
+ catch (error) {
469
+ callback(error instanceof Error ? error : new Error(String(error)));
470
+ return;
471
+ }
357
472
  callback(null, chunk);
358
473
  },
359
474
  });
360
- await pipeline(this.createReadStream(plan.entry.id, {
361
- signal: options.signal,
362
- maxBytes: null,
363
- }), limit, output, { signal: options.signal });
475
+ if (options.overwrite) {
476
+ await assertReplaceableOutput(plan.targetPath);
477
+ const temporaryPath = join(dirname(plan.targetPath), `.7zip-native-${randomUUID()}.tmp`);
478
+ try {
479
+ await pipeline(this.createReadStream(plan.entry.id, {
480
+ signal: options.signal,
481
+ maxBytes: null,
482
+ }), limit, createWriteStream(temporaryPath, { flags: 'wx', mode: 0o600 }), { signal: options.signal });
483
+ await assertReplaceableOutput(plan.targetPath);
484
+ await rename(temporaryPath, plan.targetPath);
485
+ }
486
+ catch (error) {
487
+ await rm(temporaryPath, { force: true });
488
+ throw error;
489
+ }
490
+ }
491
+ else {
492
+ await pipeline(this.createReadStream(plan.entry.id, {
493
+ signal: options.signal,
494
+ maxBytes: null,
495
+ }), limit, createWriteStream(plan.targetPath, { flags: 'wx', mode: 0o600 }), { signal: options.signal });
496
+ }
497
+ filesExtracted++;
498
+ reportProgress();
364
499
  }
365
500
  }
366
- async read(id, options) {
501
+ async read(idOrPath, options) {
367
502
  this.assertOpen();
368
- this.validateIds([id]);
503
+ let id;
504
+ if (typeof idOrPath === 'number') {
505
+ this.validateIds([idOrPath]);
506
+ id = idOrPath;
507
+ }
508
+ else {
509
+ id = await this.resolveEntryId(idOrPath);
510
+ }
369
511
  const offset = this.toUnsignedBigInt(options.offset ?? 0, 'offset');
370
512
  const length = this.toUnsignedBigInt(options.length, 'length');
371
513
  const maxUInt64 = (1n << 64n) - 1n;
@@ -396,6 +538,25 @@ class Archive {
396
538
  throw new ArchiveError('ERR_INVALID_ENTRY', 'Entry IDs must be unique non-negative safe integers');
397
539
  }
398
540
  }
541
+ async resolveEntryId(selector) {
542
+ if (typeof selector === 'number') {
543
+ this.validateIds([selector]);
544
+ return selector;
545
+ }
546
+ if (typeof selector !== 'string') {
547
+ throw new TypeError('Entry selector must be an ID or archive path');
548
+ }
549
+ const entry = await this.getEntry(selector);
550
+ if (!entry) {
551
+ throw new ArchiveError('ERR_INVALID_ENTRY', `Archive entry "${selector}" was not found`);
552
+ }
553
+ return entry.id;
554
+ }
555
+ async resolveEntryIds(selectors) {
556
+ const ids = await Promise.all(selectors.map(selector => this.resolveEntryId(selector)));
557
+ this.validateIds(ids);
558
+ return ids;
559
+ }
399
560
  toUnsignedBigInt(value, label) {
400
561
  if (typeof value === 'bigint') {
401
562
  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.6",
4
4
  "description": "Native 7zip bindings for Node.js",
5
5
  "main": "./dist/index.cjs",
6
6
  "type": "module",
@@ -30,6 +30,7 @@
30
30
  "dist/",
31
31
  "prebuilds/",
32
32
  "native/",
33
+ "scripts/",
33
34
  "vendor/7zip/",
34
35
  "binding.gyp",
35
36
  "README.md",
@@ -48,6 +49,7 @@
48
49
  "url": "https://github.com/sponsors/ollm"
49
50
  },
50
51
  "scripts": {
52
+ "install": "node scripts/verify-prebuild.mjs",
51
53
  "prepare": "npm run build && node scripts/clean-upstream-build.mjs",
52
54
  "build": "npm run build:native && npm run eslint && npx rollup -c && npm run typecheck",
53
55
  "build:native": "node scripts/build-native.mjs",
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,119 @@
1
+ import {spawn} from 'node:child_process';
2
+ import { copyFile, mkdir, rm } from 'node:fs/promises';
3
+ import {createRequire} from 'node:module';
4
+ import {availableParallelism} from 'node:os';
5
+ import { join, resolve } from 'node:path';
6
+ import {fileURLToPath} from 'node:url';
7
+
8
+ const root = resolve(fileURLToPath(new URL('..', import.meta.url)));
9
+ const bundleDirectory = join(root, 'vendor', '7zip', 'CPP', '7zip', 'Bundles', 'Format7zF');
10
+ const releaseDirectory = join(root, 'build', 'Release');
11
+ const targetArch = process.env.BUILD_ARCH || process.arch;
12
+ const target = `${process.platform}-${targetArch}`;
13
+ const engineName = process.platform === 'win32'
14
+ ? '7z.dll'
15
+ : process.platform === 'darwin' ? 'lib7z.dylib' : '7z.so';
16
+ const require = createRequire(import.meta.url);
17
+
18
+ function run(command, args, cwd, env = process.env)
19
+ {
20
+ return new Promise((resolvePromise, rejectPromise) => {
21
+ const child = spawn(command, args, {cwd, env, stdio: 'inherit'});
22
+ child.once('error', rejectPromise);
23
+ child.once('close', (code, signal) => {
24
+ if(code === 0)
25
+ {
26
+ resolvePromise();
27
+ }
28
+ else
29
+ {
30
+ rejectPromise(new Error(`${command} exited with ${signal ?? code}`));
31
+ }
32
+ });
33
+ });
34
+ }
35
+
36
+ if(process.platform === 'win32')
37
+ {
38
+ const upstreamArch = targetArch === 'ia32' ? 'x86' : targetArch;
39
+ await run('nmake', ['/f', 'makefile', `PLATFORM=${upstreamArch}`], bundleDirectory);
40
+ }
41
+ else
42
+ {
43
+ const args = [
44
+ '-C',
45
+ bundleDirectory,
46
+ '-f',
47
+ 'makefile.gcc',
48
+ `-j${process.env.BUILD_JOBS ?? Math.min(availableParallelism(), 2)}`,
49
+ ];
50
+ const buildEnv = {...process.env};
51
+ if(targetArch === 'ia32')
52
+ {
53
+ args.push('MY_ARCH=-m32', 'CC=gcc', 'CXX=g++');
54
+ buildEnv.CXXFLAGS = `${buildEnv.CXXFLAGS ?? ''} -m32`.trim();
55
+ buildEnv.LDFLAGS = `${buildEnv.LDFLAGS ?? ''} -m32`.trim();
56
+ }
57
+ else if(targetArch === 'arm')
58
+ {
59
+ const compilerPrefix = 'arm-linux-gnueabihf-';
60
+ args.push(
61
+ `CC=${compilerPrefix}gcc`,
62
+ `CXX=${compilerPrefix}g++`,
63
+ 'MY_ARCH=-march=armv7-a -mfpu=vfpv3-d16 -mfloat-abi=hard',
64
+ );
65
+ buildEnv.CC = `${compilerPrefix}gcc`;
66
+ buildEnv.CXX = `${compilerPrefix}g++`;
67
+ buildEnv.CXXFLAGS = `${buildEnv.CXXFLAGS ?? ''} -march=armv7-a -mfpu=vfpv3-d16 -mfloat-abi=hard`.trim();
68
+ buildEnv.LDFLAGS = `${buildEnv.LDFLAGS ?? ''} -march=armv7-a -mfpu=vfpv3-d16 -mfloat-abi=hard`.trim();
69
+ }
70
+ if(process.platform === 'darwin')
71
+ {
72
+ args.push(
73
+ 'LIB2=-lpthread',
74
+ 'PROG=lib7z',
75
+ 'SHARED_EXT=.dylib',
76
+ 'LDFLAGS=-shared -fPIC -Wl,-install_name,@rpath/lib7z.dylib',
77
+ );
78
+ }
79
+ await run('make', args, root, buildEnv);
80
+ }
81
+
82
+ const upstreamEnginePath = process.platform === 'win32'
83
+ ? join(bundleDirectory, targetArch === 'ia32' ? 'x86' : targetArch, engineName)
84
+ : join(bundleDirectory, '_o', engineName);
85
+ const nodeGypArgs = [
86
+ require.resolve('node-gyp/bin/node-gyp.js'),
87
+ 'rebuild',
88
+ ];
89
+ const nodeGypEnv = {...process.env};
90
+ if(targetArch !== process.arch)
91
+ {
92
+ nodeGypArgs.push(`--arch=${targetArch}`);
93
+ if(targetArch === 'ia32')
94
+ {
95
+ nodeGypEnv.CC = 'gcc';
96
+ nodeGypEnv.CXX = 'g++';
97
+ nodeGypEnv.CXXFLAGS = `${nodeGypEnv.CXXFLAGS ?? ''} -m32`.trim();
98
+ nodeGypEnv.LDFLAGS = `${nodeGypEnv.LDFLAGS ?? ''} -m32`.trim();
99
+ }
100
+ else if(targetArch === 'arm')
101
+ {
102
+ nodeGypEnv.CC = 'arm-linux-gnueabihf-gcc';
103
+ nodeGypEnv.CXX = 'arm-linux-gnueabihf-g++';
104
+ nodeGypEnv.CXXFLAGS = `${nodeGypEnv.CXXFLAGS ?? ''} -march=armv7-a -mfpu=vfpv3-d16 -mfloat-abi=hard`.trim();
105
+ nodeGypEnv.LDFLAGS = `${nodeGypEnv.LDFLAGS ?? ''} -march=armv7-a -mfpu=vfpv3-d16 -mfloat-abi=hard`.trim();
106
+ }
107
+ }
108
+ await run(process.execPath, nodeGypArgs, root, nodeGypEnv);
109
+
110
+ await mkdir(releaseDirectory, {recursive: true});
111
+ const releaseEnginePath = join(releaseDirectory, engineName);
112
+ await copyFile(upstreamEnginePath, releaseEnginePath);
113
+ const prebuildDirectory = join(root, 'prebuilds', target);
114
+ await rm(prebuildDirectory, {recursive: true, force: true});
115
+ await mkdir(prebuildDirectory, {recursive: true});
116
+ await Promise.all([
117
+ copyFile(join(releaseDirectory, '7zip_native.node'), join(prebuildDirectory, '7zip_native.node')),
118
+ copyFile(releaseEnginePath, join(prebuildDirectory, engineName)),
119
+ ]);
@@ -0,0 +1,9 @@
1
+ import {rm} from 'node:fs/promises';
2
+ import {fileURLToPath} from 'node:url';
3
+
4
+ const upstreamBuildDirectory = new URL(
5
+ '../vendor/7zip/CPP/7zip/Bundles/Format7zF/_o/',
6
+ import.meta.url,
7
+ );
8
+
9
+ await rm(fileURLToPath(upstreamBuildDirectory), {recursive: true, force: true});
@@ -0,0 +1,29 @@
1
+ import {existsSync} from 'node:fs';
2
+ import {dirname, join} from 'node:path';
3
+ import {fileURLToPath} from 'node:url';
4
+
5
+ const packageDirectory = dirname(fileURLToPath(import.meta.url));
6
+ const target = `${process.platform}-${process.arch}`;
7
+ const engineName = process.platform === 'win32'
8
+ ? '7z.dll'
9
+ : process.platform === 'darwin' ? 'lib7z.dylib' : '7z.so';
10
+ const runtimeDirectories = [
11
+ join(packageDirectory, `../prebuilds/${target}`),
12
+ join(packageDirectory, '../build/Release'),
13
+ ];
14
+
15
+ if(runtimeDirectories.some(directory =>
16
+ existsSync(join(directory, '7zip_native.node')) && existsSync(join(directory, engineName))))
17
+ {
18
+ process.exit(0);
19
+ }
20
+
21
+ if(existsSync(join(packageDirectory, '../.git')))
22
+ {
23
+ process.exit(0);
24
+ }
25
+
26
+ throw new Error(
27
+ `No prebuilt 7zip-native binaries are available for ${target}. `
28
+ + 'Check the supported platforms in the package README.',
29
+ );