7zip-native 0.0.10 → 0.0.12

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
@@ -192,7 +192,7 @@ It also accepts these options:
192
192
  - `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
193
  - `overwrite`: Defaults to `false`. When `true`, an existing output file is replaced after the new archive has been written.
194
194
 
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.
195
+ 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
196
 
197
197
  ### Extracting to a folder
198
198
 
@@ -221,7 +221,7 @@ await archive.extractToFolder('./unpacked', {
221
221
 
222
222
  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
223
 
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.
224
+ 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
225
 
226
226
  ### Concurrency
227
227
 
package/dist/index.cjs CHANGED
@@ -66,32 +66,44 @@ 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('/');
88
101
  }
89
102
  function assertSafeRelativeArchivePath(entryPath) {
90
103
  const normalized = entryPath.replace(/\\/g, '/');
91
104
  const segments = normalized.split('/').filter(segment => segment !== '' && segment !== '.');
92
- if (entryPath.includes('\0')
93
- || normalized.startsWith('/')
94
- || /^[a-zA-Z]:/.test(normalized)
105
+ if (normalized.startsWith('/')
106
+ || /^[a-zA-Z]:\//.test(normalized)
95
107
  || segments.length === 0
96
108
  || segments.some(segment => segment === '..')) {
97
109
  throw new ArchiveError('ERR_ARCHIVE_PATH', `Unsafe archive entry path: ${entryPath}`);
package/dist/index.mjs CHANGED
@@ -63,32 +63,44 @@ 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('/');
85
98
  }
86
99
  function assertSafeRelativeArchivePath(entryPath) {
87
100
  const normalized = entryPath.replace(/\\/g, '/');
88
101
  const segments = normalized.split('/').filter(segment => segment !== '' && segment !== '.');
89
- if (entryPath.includes('\0')
90
- || normalized.startsWith('/')
91
- || /^[a-zA-Z]:/.test(normalized)
102
+ if (normalized.startsWith('/')
103
+ || /^[a-zA-Z]:\//.test(normalized)
92
104
  || segments.length === 0
93
105
  || segments.some(segment => segment === '..')) {
94
106
  throw new ArchiveError('ERR_ARCHIVE_PATH', `Unsafe archive entry path: ${entryPath}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "7zip-native",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "Native 7zip bindings for Node.js",
5
5
  "main": "./dist/index.cjs",
6
6
  "type": "module",
Binary file
Binary file