7zip-native 0.0.12 → 0.0.15

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 |
@@ -293,7 +294,7 @@ Windows x86 (ia32) is not supported by the published prebuilt binaries.
293
294
 
294
295
  ## Electron
295
296
 
296
- 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.
297
+ 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. The engine is loaded at runtime rather than linked into the addon, so ensure the matching engine library is included alongside the addon.
297
298
 
298
299
  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:
299
300
 
package/binding.gyp CHANGED
@@ -15,20 +15,20 @@
15
15
  "cflags_cc!": ["-fno-exceptions"],
16
16
  "conditions": [
17
17
  ["OS=='linux'", {
18
+ "sources": [
19
+ "vendor/7zip/CPP/Common/MyWindows.cpp",
20
+ "vendor/7zip/CPP/Windows/PropVariant.cpp"
21
+ ],
18
22
  "cflags_cc": ["-pthread"],
19
23
  "libraries": [
20
24
  "-pthread",
21
- "-ldl",
22
- "-L<(module_root_dir)/vendor/7zip/CPP/7zip/Bundles/Format7zF/_o",
23
- "-l:7z.so",
24
- "-Wl,-rpath,'$$ORIGIN'"
25
+ "-ldl"
25
26
  ]
26
27
  }],
27
28
  ["OS=='mac'", {
28
- "libraries": [
29
- "-L<(module_root_dir)/vendor/7zip/CPP/7zip/Bundles/Format7zF/_o",
30
- "-l7z",
31
- "-Wl,-rpath,@loader_path"
29
+ "sources": [
30
+ "vendor/7zip/CPP/Common/MyWindows.cpp",
31
+ "vendor/7zip/CPP/Windows/PropVariant.cpp"
32
32
  ],
33
33
  "xcode_settings": {
34
34
  "CLANG_CXX_LANGUAGE_STANDARD": "c++17",
package/dist/index.cjs CHANGED
@@ -99,6 +99,11 @@ function sanitizeOutputPath(entryPath) {
99
99
  }
100
100
  return entryPath.replace(/\\/g, '/').split('/').map(sanitizeWindowsSegment).join('/');
101
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);
106
+ }
102
107
  function assertSafeRelativeArchivePath(entryPath) {
103
108
  const normalized = entryPath.replace(/\\/g, '/');
104
109
  const segments = normalized.split('/').filter(segment => segment !== '' && segment !== '.');
@@ -481,7 +486,12 @@ class Archive {
481
486
  }
482
487
  async entries() {
483
488
  this.assertOpen();
484
- 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) => {
485
495
  throw new ArchiveError('ERR_ARCHIVE_OPEN', errorMessage(cause), { cause });
486
496
  });
487
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
 
@@ -96,6 +96,11 @@ function sanitizeOutputPath(entryPath) {
96
96
  }
97
97
  return entryPath.replace(/\\/g, '/').split('/').map(sanitizeWindowsSegment).join('/');
98
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);
103
+ }
99
104
  function assertSafeRelativeArchivePath(entryPath) {
100
105
  const normalized = entryPath.replace(/\\/g, '/');
101
106
  const segments = normalized.split('/').filter(segment => segment !== '' && segment !== '.');
@@ -478,7 +483,12 @@ class Archive {
478
483
  }
479
484
  async entries() {
480
485
  this.assertOpen();
481
- 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) => {
482
492
  throw new ArchiveError('ERR_ARCHIVE_OPEN', errorMessage(cause), { cause });
483
493
  });
484
494
  return this.entriesPromise;
package/native/addon.cpp CHANGED
@@ -2160,12 +2160,12 @@ public:
2160
2160
  GUID classId;
2161
2161
  std::string name;
2162
2162
  bool extensionMatch;
2163
- bool hasSignatureFunction;
2164
- bool signatureMatch;
2163
+ bool contentMatch;
2164
+ unsigned probePriority;
2165
2165
  };
2166
2166
  std::vector<Format> formats;
2167
2167
  formats.reserve(formatCount);
2168
- std::vector<Byte> header(64 * 1024);
2168
+ std::vector<Byte> header(1 << 20);
2169
2169
  auto *probeFile = new FileInStream();
2170
2170
  CMyComPtr<IInStream> probe = probeFile;
2171
2171
  if (!probeFile->Open(path)) {
@@ -2178,6 +2178,8 @@ public:
2178
2178
  if (headerSize == 0) {
2179
2179
  throw std::runtime_error("Archive file is empty");
2180
2180
  }
2181
+ const bool endOfFile = headerSize < header.size()
2182
+ || GetFileStamp(path).size <= headerSize;
2181
2183
  for (UInt32 i = 0; i < formatCount; ++i) {
2182
2184
  NWindows::NCOM::CPropVariant classIdValue;
2183
2185
  if (g_getHandlerProperty(i, NArchive::NHandlerPropID::kClassID, &classIdValue) != S_OK
@@ -2207,38 +2209,95 @@ public:
2207
2209
  }
2208
2210
  Func_IsArc isArchive = nullptr;
2209
2211
  const bool hasSignatureFunction = g_getIsArc(i, &isArchive) == S_OK && isArchive != nullptr;
2210
- const bool signatureMatch = hasSignatureFunction
2211
- && isArchive(header.data(), header.size()) != 0;
2212
- formats.push_back({i, classId, std::move(name), match, hasSignatureFunction, signatureMatch});
2213
- }
2214
- const bool hasSignatureMatch = std::any_of(
2215
- formats.begin(), formats.end(), [](const Format& format) { return format.signatureMatch; });
2216
- const bool hasExtensionMatch = std::any_of(
2217
- formats.begin(), formats.end(), [](const Format& format) { return format.extensionMatch; });
2218
- if (hasSignatureMatch) {
2219
- formats.erase(
2220
- std::remove_if(formats.begin(), formats.end(), [](const Format& format) {
2221
- return !format.signatureMatch
2222
- && !(format.extensionMatch && !format.hasSignatureFunction);
2223
- }),
2224
- formats.end());
2225
- } else if (hasExtensionMatch && std::any_of(
2226
- formats.begin(), formats.end(), [](const Format& format) {
2227
- return format.extensionMatch && !format.hasSignatureFunction;
2228
- })) {
2229
- formats.erase(
2230
- std::remove_if(formats.begin(), formats.end(), [](const Format& format) {
2231
- return !format.extensionMatch || format.hasSignatureFunction;
2232
- }),
2233
- formats.end());
2234
- } else if (hasExtensionMatch) {
2235
- formats.clear();
2212
+ constexpr UInt32 kStartOpenFlags =
2213
+ NArcInfoFlags::kStartOpen
2214
+ | NArcInfoFlags::kPureStartOpen
2215
+ | NArcInfoFlags::kBackwardOpen;
2216
+ NWindows::NCOM::CPropVariant flagsValue;
2217
+ const bool hasFlags = g_getHandlerProperty(
2218
+ i, NArchive::NHandlerPropID::kFlags, &flagsValue) == S_OK
2219
+ && flagsValue.vt == VT_UI4;
2220
+ const UInt32 flags = hasFlags ? flagsValue.ulVal : 0;
2221
+ if (!match && (flags & NArcInfoFlags::kByExtOnlyOpen) != 0) {
2222
+ continue;
2223
+ }
2224
+
2225
+ bool contentMatch = false;
2226
+ unsigned probePriority = match ? 0 : 2;
2227
+ if (hasSignatureFunction) {
2228
+ const auto result = isArchive(header.data(), headerSize);
2229
+ contentMatch = result == k_IsArc_Res_YES
2230
+ || (result == k_IsArc_Res_NEED_MORE && !endOfFile);
2231
+ if (!match && contentMatch) {
2232
+ probePriority = 1;
2233
+ } else if (!match && !contentMatch) {
2234
+ continue;
2235
+ }
2236
+ } else {
2237
+ NWindows::NCOM::CPropVariant signatureOffsetValue;
2238
+ UInt32 signatureOffset = 0;
2239
+ if (g_getHandlerProperty(
2240
+ i, NArchive::NHandlerPropID::kSignatureOffset, &signatureOffsetValue) == S_OK
2241
+ && signatureOffsetValue.vt == VT_UI4) {
2242
+ signatureOffset = signatureOffsetValue.ulVal;
2243
+ }
2244
+ std::vector<std::vector<Byte>> signatures;
2245
+ NWindows::NCOM::CPropVariant signatureValue;
2246
+ if (g_getHandlerProperty(i, NArchive::NHandlerPropID::kSignature, &signatureValue) == S_OK
2247
+ && signatureValue.vt == VT_BSTR
2248
+ && ::SysStringByteLen(signatureValue.bstrVal) != 0) {
2249
+ const auto *data = reinterpret_cast<const Byte *>(signatureValue.bstrVal);
2250
+ const auto size = ::SysStringByteLen(signatureValue.bstrVal);
2251
+ signatures.emplace_back(data, data + size);
2252
+ } else {
2253
+ NWindows::NCOM::CPropVariant multiSignatureValue;
2254
+ if (g_getHandlerProperty(
2255
+ i, NArchive::NHandlerPropID::kMultiSignature, &multiSignatureValue) == S_OK
2256
+ && multiSignatureValue.vt == VT_BSTR) {
2257
+ const auto *data = reinterpret_cast<const Byte *>(multiSignatureValue.bstrVal);
2258
+ const auto size = ::SysStringByteLen(multiSignatureValue.bstrVal);
2259
+ for (UINT offset = 0; offset < size;) {
2260
+ const auto signatureSize = data[offset++];
2261
+ if (signatureSize > size - offset) {
2262
+ signatures.clear();
2263
+ break;
2264
+ }
2265
+ signatures.emplace_back(data + offset, data + offset + signatureSize);
2266
+ offset += signatureSize;
2267
+ }
2268
+ }
2269
+ }
2270
+
2271
+ bool signatureMatch = false;
2272
+ bool needCheck = !hasFlags || signatures.empty()
2273
+ || (flags & kStartOpenFlags) != 0 || signatureOffset != 0;
2274
+ const auto signatureOffsetSize = static_cast<std::size_t>(signatureOffset);
2275
+ for (const auto& signature : signatures) {
2276
+ if (signatureOffsetSize > headerSize
2277
+ || signature.size() > headerSize - signatureOffsetSize) {
2278
+ if (!endOfFile) {
2279
+ needCheck = true;
2280
+ }
2281
+ } else if (std::memcmp(
2282
+ header.data() + signatureOffsetSize, signature.data(), signature.size()) == 0) {
2283
+ signatureMatch = true;
2284
+ break;
2285
+ }
2286
+ }
2287
+ contentMatch = signatureMatch;
2288
+ if (!match && signatureMatch) {
2289
+ probePriority = 1;
2290
+ } else if (!match && !needCheck) {
2291
+ continue;
2292
+ }
2293
+ }
2294
+ formats.push_back({i, classId, std::move(name), match, contentMatch, probePriority});
2236
2295
  }
2237
2296
  if (formats.empty()) {
2238
2297
  throw std::runtime_error("No registered 7-Zip handler recognizes this archive");
2239
2298
  }
2240
2299
  std::stable_sort(formats.begin(), formats.end(), [](const Format& left, const Format& right) {
2241
- return left.extensionMatch && !right.extensionMatch;
2300
+ return left.probePriority < right.probePriority;
2242
2301
  });
2243
2302
 
2244
2303
  bool passwordRequested = false;
@@ -2269,6 +2328,11 @@ public:
2269
2328
  candidate->Close();
2270
2329
  continue;
2271
2330
  }
2331
+ if (!HasValidEntryPaths(candidate) && !format.contentMatch
2332
+ && LowerAscii(format.name) != "gzip") {
2333
+ candidate->Close();
2334
+ continue;
2335
+ }
2272
2336
  auto context = std::make_unique<ArchiveContext>();
2273
2337
  context->archive = candidate;
2274
2338
  context->stream = input;
@@ -2636,6 +2700,20 @@ public:
2636
2700
  }
2637
2701
 
2638
2702
  private:
2703
+ bool HasValidEntryPaths(IInArchive *archive) const
2704
+ {
2705
+ UInt32 count = 0;
2706
+ if (archive->GetNumberOfItems(&count) != S_OK || count > kMaxEntries) {
2707
+ return false;
2708
+ }
2709
+ for (UInt32 i = 0; i < count; ++i) {
2710
+ if (GetPropString(archive, i, kpidPath).empty()) {
2711
+ return false;
2712
+ }
2713
+ }
2714
+ return true;
2715
+ }
2716
+
2639
2717
  ContextLease AcquireContext()
2640
2718
  {
2641
2719
  std::unique_lock<std::mutex> lock(contextMutex_);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "7zip-native",
3
- "version": "0.0.12",
3
+ "version": "0.0.15",
4
4
  "description": "Native 7zip bindings for Node.js",
5
5
  "main": "./dist/index.cjs",
6
6
  "type": "module",
@@ -65,9 +65,20 @@
65
65
  },
66
66
  "keywords": [
67
67
  "7zip",
68
+ "7-zip",
69
+ "archive",
68
70
  "nodejs",
69
71
  "bindings",
70
- "native"
72
+ "native",
73
+ "node-api",
74
+ "napi",
75
+ "zip",
76
+ "rar",
77
+ "tar",
78
+ "wim",
79
+ "compression",
80
+ "decompression",
81
+ "extraction"
71
82
  ],
72
83
  "devDependencies": {
73
84
  "@eslint/js": "^10.0.1",
Binary file
Binary file