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 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, unsafe, or conflicting internal paths are rejected. 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
+ 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, invalid filename characters (`< > : | ? * "`) and trailing dots/spaces in each destination component are replaced with `_`; selectors and entry metadata remain unchanged. 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
+ 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]:/.test(normalized)) {
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
- .replace(/([. ]+)([\\/]|$)/g, (_match, unsupported, separator) => '_'.repeat(unsupported.length) + separator)
87
- .replace(/[<>:|?*"]/g, '_');
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 (entryPath.includes('\0')
93
- || normalized.startsWith('/')
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).catch((cause) => {
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
@@ -5,6 +5,7 @@ type EntrySelector = EntryId | string;
5
5
  interface ArchiveEntry {
6
6
  readonly id: EntryId;
7
7
  readonly path: string;
8
+ readonly extractedPath: string;
8
9
  readonly size: bigint;
9
10
  readonly compressedSize: bigint;
10
11
  readonly isDirectory: boolean;
package/dist/index.d.mts CHANGED
@@ -5,6 +5,7 @@ type EntrySelector = EntryId | string;
5
5
  interface ArchiveEntry {
6
6
  readonly id: EntryId;
7
7
  readonly path: string;
8
+ readonly extractedPath: string;
8
9
  readonly size: bigint;
9
10
  readonly compressedSize: bigint;
10
11
  readonly isDirectory: boolean;
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ type EntrySelector = EntryId | string;
5
5
  interface ArchiveEntry {
6
6
  readonly id: EntryId;
7
7
  readonly path: string;
8
+ readonly extractedPath: string;
8
9
  readonly size: bigint;
9
10
  readonly compressedSize: bigint;
10
11
  readonly isDirectory: boolean;
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, relative, sep } from 'node:path';
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]:/.test(normalized)) {
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
- .replace(/([. ]+)([\\/]|$)/g, (_match, unsupported, separator) => '_'.repeat(unsupported.length) + separator)
84
- .replace(/[<>:|?*"]/g, '_');
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 (entryPath.includes('\0')
90
- || normalized.startsWith('/')
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).catch((cause) => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "7zip-native",
3
- "version": "0.0.10",
3
+ "version": "0.0.14",
4
4
  "description": "Native 7zip bindings for Node.js",
5
5
  "main": "./dist/index.cjs",
6
6
  "type": "module",
Binary file
Binary file