7zip-native 0.0.10 → 0.0.14
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 +3 -2
- package/dist/index.cjs +34 -12
- package/dist/index.d.cts +1 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +35 -13
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/7zip_native.node +0 -0
- package/prebuilds/darwin-x64/7zip_native.node +0 -0
- package/prebuilds/win32-arm64/7z.dll +0 -0
- package/prebuilds/win32-arm64/7zip_native.node +0 -0
- package/prebuilds/win32-x64/7z.dll +0 -0
- package/prebuilds/win32-x64/7zip_native.node +0 -0
package/README.md
CHANGED
|
@@ -90,6 +90,7 @@ Each entry includes:
|
|
|
90
90
|
| --- | --- | --- |
|
|
91
91
|
| `id` | `number` | Stable ID within this opened archive |
|
|
92
92
|
| `path` | `string` | Entry path, using `/` separators |
|
|
93
|
+
| `extractedPath` | `string` | Relative output path after platform-specific filename normalization; Windows-invalid characters are replaced |
|
|
93
94
|
| `size` | `bigint` | Uncompressed size |
|
|
94
95
|
| `compressedSize` | `bigint` | Compressed size |
|
|
95
96
|
| `isDirectory` | `boolean` | Whether the entry is a directory |
|
|
@@ -192,7 +193,7 @@ It also accepts these options:
|
|
|
192
193
|
- `compressionLevel`: Integer from 0 to 9; defaults to 5 and applies only to ZIP and 7z. TAR is uncompressed; WIM uses the upstream handler's default settings.
|
|
193
194
|
- `overwrite`: Defaults to `false`. When `true`, an existing output file is replaced after the new archive has been written.
|
|
194
195
|
|
|
195
|
-
Duplicate,
|
|
196
|
+
Duplicate, absolute, or traversal paths are rejected, but platform-supported filename characters such as trailing spaces are preserved in the archive-internal path. The output is first written to a temporary file beside the destination, then installed atomically, so an existing file is preserved if archive creation fails. Archive creation runs in the bounded native worker pool and does not launch a 7-Zip executable.
|
|
196
197
|
|
|
197
198
|
### Extracting to a folder
|
|
198
199
|
|
|
@@ -221,7 +222,7 @@ await archive.extractToFolder('./unpacked', {
|
|
|
221
222
|
|
|
222
223
|
Each native extraction call receives a batch of entry IDs and output paths. The addon queues disk writes asynchronously while 7-Zip continues extracting later entries; `onExtract` runs after that file's queued writes have completed and the file is closed. File contents are not streamed through Node.js or Electron. The `extractToFolder()` promise resolves after all `onExtract` callbacks have run.
|
|
223
224
|
|
|
224
|
-
On Windows only,
|
|
225
|
+
On Windows only, unsupported filename characters (including control characters), trailing dots/spaces in each destination component, and reserved device names are normalized for the output path; selectors and entry metadata remain unchanged. On Linux and macOS, platform-supported filename characters are preserved. Unsafe archive paths, path collisions, symlinked destination components, and symlink or non-file targets 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.
|
|
225
226
|
|
|
226
227
|
### Concurrency
|
|
227
228
|
|
package/dist/index.cjs
CHANGED
|
@@ -66,32 +66,49 @@ function safeEntrySegments(entryPath) {
|
|
|
66
66
|
throw new ArchiveError('ERR_ARCHIVE_PATH', 'Archive entry path contains a null byte');
|
|
67
67
|
}
|
|
68
68
|
const normalized = entryPath.replace(/\\/g, '/');
|
|
69
|
-
if (normalized.startsWith('/') || /^[a-zA-Z]
|
|
69
|
+
if (normalized.startsWith('/') || /^[a-zA-Z]:\//.test(normalized)) {
|
|
70
70
|
throw new ArchiveError('ERR_ARCHIVE_PATH', `Archive entry path is absolute: ${entryPath}`);
|
|
71
71
|
}
|
|
72
72
|
const segments = normalized.split('/').filter(segment => segment !== '' && segment !== '.');
|
|
73
|
-
if (segments.length === 0 || segments.some(segment => segment === '..'
|
|
74
|
-
|| segment.includes(':')
|
|
75
|
-
|| /[. ]$/.test(segment)
|
|
76
|
-
|| /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(segment))) {
|
|
73
|
+
if (segments.length === 0 || segments.some(segment => segment === '..')) {
|
|
77
74
|
throw new ArchiveError('ERR_ARCHIVE_PATH', `Unsafe archive entry path: ${entryPath}`);
|
|
78
75
|
}
|
|
79
76
|
return segments;
|
|
80
77
|
}
|
|
78
|
+
function sanitizeWindowsSegment(segment) {
|
|
79
|
+
if (segment === '.' || segment === '..') {
|
|
80
|
+
return segment;
|
|
81
|
+
}
|
|
82
|
+
const sanitized = [...segment].map(character => character.charCodeAt(0) < 0x20 || '<>:|?*"'.includes(character) ? '_' : character).join('')
|
|
83
|
+
.replace(/[. ]+$/g, trailing => '_'.repeat(trailing.length));
|
|
84
|
+
const reservedName = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])/i.exec(sanitized);
|
|
85
|
+
if (reservedName) {
|
|
86
|
+
let index = reservedName[0].length;
|
|
87
|
+
while (sanitized[index] === ' ') {
|
|
88
|
+
index++;
|
|
89
|
+
}
|
|
90
|
+
if (index === sanitized.length || sanitized[index] === '.') {
|
|
91
|
+
return `_${sanitized}`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return sanitized;
|
|
95
|
+
}
|
|
81
96
|
function sanitizeOutputPath(entryPath) {
|
|
82
97
|
if (process.platform !== 'win32') {
|
|
83
98
|
return entryPath;
|
|
84
99
|
}
|
|
85
|
-
return entryPath
|
|
86
|
-
|
|
87
|
-
|
|
100
|
+
return entryPath.replace(/\\/g, '/').split('/').map(sanitizeWindowsSegment).join('/');
|
|
101
|
+
}
|
|
102
|
+
function getExtractedPath(entryPath) {
|
|
103
|
+
const normalized = sanitizeOutputPath(entryPath).replace(/\\/g, '/');
|
|
104
|
+
const segments = normalized.split('/').filter(segment => segment !== '' && segment !== '.');
|
|
105
|
+
return segments.join(node_path.sep);
|
|
88
106
|
}
|
|
89
107
|
function assertSafeRelativeArchivePath(entryPath) {
|
|
90
108
|
const normalized = entryPath.replace(/\\/g, '/');
|
|
91
109
|
const segments = normalized.split('/').filter(segment => segment !== '' && segment !== '.');
|
|
92
|
-
if (
|
|
93
|
-
|| normalized
|
|
94
|
-
|| /^[a-zA-Z]:/.test(normalized)
|
|
110
|
+
if (normalized.startsWith('/')
|
|
111
|
+
|| /^[a-zA-Z]:\//.test(normalized)
|
|
95
112
|
|| segments.length === 0
|
|
96
113
|
|| segments.some(segment => segment === '..')) {
|
|
97
114
|
throw new ArchiveError('ERR_ARCHIVE_PATH', `Unsafe archive entry path: ${entryPath}`);
|
|
@@ -469,7 +486,12 @@ class Archive {
|
|
|
469
486
|
}
|
|
470
487
|
async entries() {
|
|
471
488
|
this.assertOpen();
|
|
472
|
-
this.entriesPromise ??= native.entries(this.handle)
|
|
489
|
+
this.entriesPromise ??= native.entries(this.handle)
|
|
490
|
+
.then(entries => entries.map(entry => ({
|
|
491
|
+
...entry,
|
|
492
|
+
extractedPath: getExtractedPath(entry.path),
|
|
493
|
+
})))
|
|
494
|
+
.catch((cause) => {
|
|
473
495
|
throw new ArchiveError('ERR_ARCHIVE_OPEN', errorMessage(cause), { cause });
|
|
474
496
|
});
|
|
475
497
|
return this.entriesPromise;
|
package/dist/index.d.cts
CHANGED
package/dist/index.d.mts
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { rename, link, rm, lstat, mkdir } from 'node:fs/promises';
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import { availableParallelism } from 'node:os';
|
|
6
|
-
import { dirname, resolve, join, extname,
|
|
6
|
+
import { dirname, resolve, join, extname, sep, relative } from 'node:path';
|
|
7
7
|
import { Readable } from 'node:stream';
|
|
8
8
|
import { pipeline } from 'node:stream/promises';
|
|
9
9
|
|
|
@@ -63,32 +63,49 @@ function safeEntrySegments(entryPath) {
|
|
|
63
63
|
throw new ArchiveError('ERR_ARCHIVE_PATH', 'Archive entry path contains a null byte');
|
|
64
64
|
}
|
|
65
65
|
const normalized = entryPath.replace(/\\/g, '/');
|
|
66
|
-
if (normalized.startsWith('/') || /^[a-zA-Z]
|
|
66
|
+
if (normalized.startsWith('/') || /^[a-zA-Z]:\//.test(normalized)) {
|
|
67
67
|
throw new ArchiveError('ERR_ARCHIVE_PATH', `Archive entry path is absolute: ${entryPath}`);
|
|
68
68
|
}
|
|
69
69
|
const segments = normalized.split('/').filter(segment => segment !== '' && segment !== '.');
|
|
70
|
-
if (segments.length === 0 || segments.some(segment => segment === '..'
|
|
71
|
-
|| segment.includes(':')
|
|
72
|
-
|| /[. ]$/.test(segment)
|
|
73
|
-
|| /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(segment))) {
|
|
70
|
+
if (segments.length === 0 || segments.some(segment => segment === '..')) {
|
|
74
71
|
throw new ArchiveError('ERR_ARCHIVE_PATH', `Unsafe archive entry path: ${entryPath}`);
|
|
75
72
|
}
|
|
76
73
|
return segments;
|
|
77
74
|
}
|
|
75
|
+
function sanitizeWindowsSegment(segment) {
|
|
76
|
+
if (segment === '.' || segment === '..') {
|
|
77
|
+
return segment;
|
|
78
|
+
}
|
|
79
|
+
const sanitized = [...segment].map(character => character.charCodeAt(0) < 0x20 || '<>:|?*"'.includes(character) ? '_' : character).join('')
|
|
80
|
+
.replace(/[. ]+$/g, trailing => '_'.repeat(trailing.length));
|
|
81
|
+
const reservedName = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])/i.exec(sanitized);
|
|
82
|
+
if (reservedName) {
|
|
83
|
+
let index = reservedName[0].length;
|
|
84
|
+
while (sanitized[index] === ' ') {
|
|
85
|
+
index++;
|
|
86
|
+
}
|
|
87
|
+
if (index === sanitized.length || sanitized[index] === '.') {
|
|
88
|
+
return `_${sanitized}`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return sanitized;
|
|
92
|
+
}
|
|
78
93
|
function sanitizeOutputPath(entryPath) {
|
|
79
94
|
if (process.platform !== 'win32') {
|
|
80
95
|
return entryPath;
|
|
81
96
|
}
|
|
82
|
-
return entryPath
|
|
83
|
-
|
|
84
|
-
|
|
97
|
+
return entryPath.replace(/\\/g, '/').split('/').map(sanitizeWindowsSegment).join('/');
|
|
98
|
+
}
|
|
99
|
+
function getExtractedPath(entryPath) {
|
|
100
|
+
const normalized = sanitizeOutputPath(entryPath).replace(/\\/g, '/');
|
|
101
|
+
const segments = normalized.split('/').filter(segment => segment !== '' && segment !== '.');
|
|
102
|
+
return segments.join(sep);
|
|
85
103
|
}
|
|
86
104
|
function assertSafeRelativeArchivePath(entryPath) {
|
|
87
105
|
const normalized = entryPath.replace(/\\/g, '/');
|
|
88
106
|
const segments = normalized.split('/').filter(segment => segment !== '' && segment !== '.');
|
|
89
|
-
if (
|
|
90
|
-
|| normalized
|
|
91
|
-
|| /^[a-zA-Z]:/.test(normalized)
|
|
107
|
+
if (normalized.startsWith('/')
|
|
108
|
+
|| /^[a-zA-Z]:\//.test(normalized)
|
|
92
109
|
|| segments.length === 0
|
|
93
110
|
|| segments.some(segment => segment === '..')) {
|
|
94
111
|
throw new ArchiveError('ERR_ARCHIVE_PATH', `Unsafe archive entry path: ${entryPath}`);
|
|
@@ -466,7 +483,12 @@ class Archive {
|
|
|
466
483
|
}
|
|
467
484
|
async entries() {
|
|
468
485
|
this.assertOpen();
|
|
469
|
-
this.entriesPromise ??= native.entries(this.handle)
|
|
486
|
+
this.entriesPromise ??= native.entries(this.handle)
|
|
487
|
+
.then(entries => entries.map(entry => ({
|
|
488
|
+
...entry,
|
|
489
|
+
extractedPath: getExtractedPath(entry.path),
|
|
490
|
+
})))
|
|
491
|
+
.catch((cause) => {
|
|
470
492
|
throw new ArchiveError('ERR_ARCHIVE_OPEN', errorMessage(cause), { cause });
|
|
471
493
|
});
|
|
472
494
|
return this.entriesPromise;
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|