7zip-native 0.0.8 → 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.
Files changed (34) hide show
  1. package/README.md +46 -14
  2. package/bench/compare.mts +25 -11
  3. package/bench/folder-concurrency.mts +5 -12
  4. package/bench/multiprocess-extract.cjs +334 -0
  5. package/dist/index.cjs +215 -110
  6. package/dist/index.d.cts +10 -9
  7. package/dist/index.d.mts +10 -9
  8. package/dist/index.d.ts +10 -9
  9. package/dist/index.mjs +217 -112
  10. package/native/addon.cpp +1399 -71
  11. package/package.json +4 -2
  12. package/patches/deflate-input-buffer-size.patch +47 -0
  13. package/prebuilds/darwin-arm64/7zip_native.node +0 -0
  14. package/prebuilds/darwin-arm64/lib7z.dylib +0 -0
  15. package/prebuilds/darwin-x64/7zip_native.node +0 -0
  16. package/prebuilds/darwin-x64/lib7z.dylib +0 -0
  17. package/prebuilds/linux-arm/7z.so +0 -0
  18. package/prebuilds/linux-arm/7zip_native.node +0 -0
  19. package/prebuilds/linux-arm64/7z.so +0 -0
  20. package/prebuilds/linux-arm64/7zip_native.node +0 -0
  21. package/prebuilds/linux-ia32/7z.so +0 -0
  22. package/prebuilds/linux-ia32/7zip_native.node +0 -0
  23. package/prebuilds/linux-x64/7z.so +0 -0
  24. package/prebuilds/linux-x64/7zip_native.node +0 -0
  25. package/prebuilds/win32-arm64/7z.dll +0 -0
  26. package/prebuilds/win32-arm64/7zip_native.node +0 -0
  27. package/prebuilds/win32-x64/7z.dll +0 -0
  28. package/prebuilds/win32-x64/7zip_native.node +0 -0
  29. package/scripts/build-native.mjs +22 -1
  30. package/vendor/7zip/CPP/7zip/Archive/Archive2.def +1 -0
  31. package/vendor/7zip/CPP/7zip/Bundles/Format7zF/_o/7z.so +0 -0
  32. package/vendor/7zip/CPP/7zip/Bundles/Format7zF/_o/Alloc.o +0 -0
  33. package/vendor/7zip/CPP/7zip/Bundles/Format7zF/_o/DeflateDecoder.o +0 -0
  34. package/vendor/7zip/CPP/7zip/Compress/DeflateDecoder.cpp +15 -1
package/native/addon.cpp CHANGED
@@ -3,11 +3,14 @@
3
3
  #include <algorithm>
4
4
  #include <atomic>
5
5
  #include <cctype>
6
+ #include <chrono>
6
7
  #include <cmath>
7
8
  #include <codecvt>
8
9
  #include <condition_variable>
9
10
  #include <cstdint>
10
11
  #include <cstring>
12
+ #include <cstdio>
13
+ #include <cstdlib>
11
14
  #include <cwctype>
12
15
  #include <deque>
13
16
  #include <filesystem>
@@ -27,6 +30,13 @@
27
30
 
28
31
  #ifndef _WIN32
29
32
  #include <dlfcn.h>
33
+ #include <cerrno>
34
+ #include <fcntl.h>
35
+ #include <unistd.h>
36
+ #else
37
+ #include <fcntl.h>
38
+ #include <io.h>
39
+ #include <sys/stat.h>
30
40
  #endif
31
41
 
32
42
  #include "Common/MyInitGuid.h"
@@ -45,11 +55,14 @@ constexpr std::uint64_t kMaxSingleOutputBytes = 256ULL * 1024 * 1024;
45
55
  constexpr std::uint64_t kMaxConcurrentOutputBytes = 512ULL * 1024 * 1024;
46
56
  constexpr std::uint64_t kMaxReadBytes = 64ULL * 1024 * 1024;
47
57
  constexpr std::size_t kMaxPasswordBytes = 1024 * 1024;
58
+ constexpr std::uint32_t kMinDeflateBufferSize = 4 * 1024;
59
+ constexpr std::uint32_t kMaxDeflateBufferSize = 2 * 1024 * 1024;
48
60
 
49
61
  using CreateObjectFunction = HRESULT (STDAPICALLTYPE *)(const GUID *, const GUID *, void **);
50
62
  using GetHandlerPropertyFunction = HRESULT (STDAPICALLTYPE *)(UInt32, PROPID, PROPVARIANT *);
51
63
  using GetNumberOfFormatsFunction = HRESULT (STDAPICALLTYPE *)(UInt32 *);
52
64
  using GetIsArcFunction = HRESULT (STDAPICALLTYPE *)(UInt32, Func_IsArc *);
65
+ using SetDeflateInputBufferSizeFunction = HRESULT (STDAPICALLTYPE *)(UInt32);
53
66
 
54
67
  std::wstring FromUtf8(const std::string& value);
55
68
 
@@ -63,10 +76,13 @@ std::atomic<std::uint64_t> g_pendingOperations{0};
63
76
  std::atomic<std::uint32_t> g_extractionConcurrency{1};
64
77
  std::atomic<std::uint64_t> g_nextHandle{1};
65
78
  std::atomic<std::uint64_t> g_nextStreamId{1};
79
+ std::atomic<std::uint64_t> g_nextFileTaskId{1};
66
80
  std::mutex g_handlesMutex;
67
81
  std::unordered_map<std::uint64_t, std::shared_ptr<class ArchiveEngine>> g_handles;
68
82
  std::mutex g_streamsMutex;
69
83
  std::unordered_map<std::uint64_t, std::shared_ptr<class NativeStreamState>> g_streams;
84
+ std::mutex g_fileTasksMutex;
85
+ std::unordered_map<std::uint64_t, std::shared_ptr<class NativeFileTaskState>> g_fileTasks;
70
86
  std::mutex g_outputBudgetMutex;
71
87
  std::condition_variable g_outputBudgetAvailable;
72
88
  std::uint64_t g_reservedOutputBytes = 0;
@@ -77,6 +93,7 @@ CreateObjectFunction g_createObject = nullptr;
77
93
  GetHandlerPropertyFunction g_getHandlerProperty = nullptr;
78
94
  GetNumberOfFormatsFunction g_getNumberOfFormats = nullptr;
79
95
  GetIsArcFunction g_getIsArc = nullptr;
96
+ SetDeflateInputBufferSizeFunction g_setDeflateInputBufferSize = nullptr;
80
97
  class ExtractionPool;
81
98
  ExtractionPool *g_extractionPool = nullptr;
82
99
 
@@ -93,6 +110,9 @@ Function LoadEngineFunction(const char *name)
93
110
  std::uint64_t RegisterStream(std::shared_ptr<class NativeStreamState> stream);
94
111
  std::shared_ptr<class NativeStreamState> FindStream(std::uint64_t streamId);
95
112
  void RemoveStream(std::uint64_t streamId);
113
+ std::uint64_t RegisterFileTask(std::shared_ptr<class NativeFileTaskState> task);
114
+ std::shared_ptr<class NativeFileTaskState> FindFileTask(std::uint64_t taskId);
115
+ void RemoveFileTask(std::uint64_t taskId);
96
116
 
97
117
  void EnsureEngineLoaded(const std::string& path)
98
118
  {
@@ -122,8 +142,11 @@ void EnsureEngineLoaded(const std::string& path)
122
142
  g_getHandlerProperty = LoadEngineFunction<GetHandlerPropertyFunction>("GetHandlerProperty2");
123
143
  g_getNumberOfFormats = LoadEngineFunction<GetNumberOfFormatsFunction>("GetNumberOfFormats");
124
144
  g_getIsArc = LoadEngineFunction<GetIsArcFunction>("GetIsArc");
145
+ g_setDeflateInputBufferSize =
146
+ LoadEngineFunction<SetDeflateInputBufferSizeFunction>("SetDeflateInputBufferSize");
125
147
  if (g_createObject == nullptr || g_getHandlerProperty == nullptr
126
- || g_getNumberOfFormats == nullptr || g_getIsArc == nullptr) {
148
+ || g_getNumberOfFormats == nullptr || g_getIsArc == nullptr
149
+ || g_setDeflateInputBufferSize == nullptr) {
127
150
  #ifdef _WIN32
128
151
  ::FreeLibrary(g_engineModule);
129
152
  #else
@@ -134,6 +157,7 @@ void EnsureEngineLoaded(const std::string& path)
134
157
  g_getHandlerProperty = nullptr;
135
158
  g_getNumberOfFormats = nullptr;
136
159
  g_getIsArc = nullptr;
160
+ g_setDeflateInputBufferSize = nullptr;
137
161
  throw std::runtime_error("The bundled 7-Zip engine is missing required exports");
138
162
  }
139
163
  g_enginePath = path;
@@ -198,6 +222,40 @@ struct StreamMessage {
198
222
  };
199
223
 
200
224
  void StreamMessageToJs(Napi::Env env, Napi::Function callback, StreamMessage *message);
225
+ class FileCompletionAcknowledgement {
226
+ public:
227
+ void Acknowledge()
228
+ {
229
+ {
230
+ std::lock_guard<std::mutex> lock(mutex_);
231
+ acknowledged_ = true;
232
+ }
233
+ completed_.notify_one();
234
+ }
235
+
236
+ void Wait()
237
+ {
238
+ std::unique_lock<std::mutex> lock(mutex_);
239
+ completed_.wait(lock, [this] { return acknowledged_; });
240
+ }
241
+
242
+ private:
243
+ std::mutex mutex_;
244
+ std::condition_variable completed_;
245
+ bool acknowledged_ = false;
246
+ };
247
+
248
+ struct FileCompletionMessage {
249
+ enum class Kind {
250
+ File,
251
+ Barrier,
252
+ };
253
+
254
+ Kind kind;
255
+ std::uint32_t entryId;
256
+ std::shared_ptr<FileCompletionAcknowledgement> acknowledgement;
257
+ };
258
+ void FileCompletionToJs(Napi::Env env, Napi::Function callback, FileCompletionMessage *message);
201
259
 
202
260
  class NativeStreamState {
203
261
  public:
@@ -267,6 +325,93 @@ private:
267
325
  std::atomic<bool> callbackReleased_{false};
268
326
  };
269
327
 
328
+ class NativeFileTaskState {
329
+ public:
330
+ void Cancel()
331
+ {
332
+ cancelled_.store(true, std::memory_order_release);
333
+ }
334
+
335
+ bool IsCancelled() const
336
+ {
337
+ return cancelled_.load(std::memory_order_acquire);
338
+ }
339
+
340
+ void Fail(std::string error)
341
+ {
342
+ {
343
+ std::lock_guard<std::mutex> lock(failureMutex_);
344
+ if (failure_.empty()) {
345
+ failure_ = std::move(error);
346
+ }
347
+ }
348
+ Cancel();
349
+ }
350
+
351
+ std::string Failure() const
352
+ {
353
+ std::lock_guard<std::mutex> lock(failureMutex_);
354
+ return failure_;
355
+ }
356
+
357
+ bool ReportFileCompleted(std::uint32_t entryId)
358
+ {
359
+ if (!hasFileCompletionCallback_) {
360
+ return !IsCancelled();
361
+ }
362
+ auto *message = new FileCompletionMessage{
363
+ FileCompletionMessage::Kind::File,
364
+ entryId,
365
+ {},
366
+ };
367
+ const auto status = fileCompletionCallback.BlockingCall(message, FileCompletionToJs);
368
+ if (status != napi_ok) {
369
+ delete message;
370
+ Cancel();
371
+ return false;
372
+ }
373
+ return !IsCancelled();
374
+ }
375
+
376
+ bool WaitForFileCompletions()
377
+ {
378
+ if (!hasFileCompletionCallback_) {
379
+ return true;
380
+ }
381
+ auto acknowledgement = std::make_shared<FileCompletionAcknowledgement>();
382
+ auto *message = new FileCompletionMessage{
383
+ FileCompletionMessage::Kind::Barrier,
384
+ 0,
385
+ acknowledgement,
386
+ };
387
+ const auto status = fileCompletionCallback.BlockingCall(message, FileCompletionToJs);
388
+ if (status != napi_ok) {
389
+ delete message;
390
+ Cancel();
391
+ return false;
392
+ }
393
+ acknowledgement->Wait();
394
+ return true;
395
+ }
396
+
397
+ void ReleaseCallback()
398
+ {
399
+ if (hasFileCompletionCallback_
400
+ && !fileCompletionCallbackReleased_.exchange(true, std::memory_order_acq_rel)) {
401
+ fileCompletionCallback.Release();
402
+ }
403
+ }
404
+ std::uint64_t id = 0;
405
+ Napi::ThreadSafeFunction fileCompletionCallback;
406
+ bool hasFileCompletionCallback_ = false;
407
+
408
+ private:
409
+ std::atomic<bool> cancelled_{false};
410
+ std::atomic<bool> fileCompletionCallbackReleased_{false};
411
+ mutable std::mutex failureMutex_;
412
+ std::string failure_;
413
+ };
414
+
270
415
  std::uint64_t RegisterStream(std::shared_ptr<NativeStreamState> stream)
271
416
  {
272
417
  const auto streamId = g_nextStreamId.fetch_add(1, std::memory_order_relaxed);
@@ -292,6 +437,31 @@ void RemoveStream(std::uint64_t streamId)
292
437
  g_streams.erase(streamId);
293
438
  }
294
439
 
440
+ std::uint64_t RegisterFileTask(std::shared_ptr<NativeFileTaskState> task)
441
+ {
442
+ const auto taskId = g_nextFileTaskId.fetch_add(1, std::memory_order_relaxed);
443
+ if (taskId == 0) {
444
+ throw std::overflow_error("Native file task handle space exhausted");
445
+ }
446
+ std::lock_guard<std::mutex> lock(g_fileTasksMutex);
447
+ task->id = taskId;
448
+ g_fileTasks.emplace(taskId, std::move(task));
449
+ return taskId;
450
+ }
451
+
452
+ std::shared_ptr<NativeFileTaskState> FindFileTask(std::uint64_t taskId)
453
+ {
454
+ std::lock_guard<std::mutex> lock(g_fileTasksMutex);
455
+ const auto found = g_fileTasks.find(taskId);
456
+ return found == g_fileTasks.end() ? nullptr : found->second;
457
+ }
458
+
459
+ void RemoveFileTask(std::uint64_t taskId)
460
+ {
461
+ std::lock_guard<std::mutex> lock(g_fileTasksMutex);
462
+ g_fileTasks.erase(taskId);
463
+ }
464
+
295
465
  FileStamp GetFileStamp(const std::string& path)
296
466
  {
297
467
  std::error_code error;
@@ -489,8 +659,26 @@ struct Entry {
489
659
  struct ArchiveCreateItem {
490
660
  std::string sourcePath;
491
661
  std::string path;
662
+ double mtimeMs = 0;
492
663
  };
493
664
 
665
+ FILETIME UnixMillisecondsToFileTime(double milliseconds)
666
+ {
667
+ constexpr long double windowsEpochOffsetMs = 11644473600000.0L;
668
+ constexpr long double ticksPerMillisecond = 10000.0L;
669
+ const long double ticks =
670
+ (static_cast<long double>(milliseconds) + windowsEpochOffsetMs) * ticksPerMillisecond;
671
+ if (!std::isfinite(milliseconds) || ticks < 0
672
+ || ticks > static_cast<long double>(std::numeric_limits<UInt64>::max())) {
673
+ throw std::out_of_range("Source file modification time is outside the FILETIME range");
674
+ }
675
+ const auto value = static_cast<UInt64>(ticks);
676
+ FILETIME fileTime{};
677
+ fileTime.dwLowDateTime = static_cast<DWORD>(value);
678
+ fileTime.dwHighDateTime = static_cast<DWORD>(value >> 32);
679
+ return fileTime;
680
+ }
681
+
494
682
  std::uint64_t GetPropUInt64(IInArchive *archive, std::uint32_t index, PROPID propId)
495
683
  {
496
684
  NWindows::NCOM::CPropVariant value;
@@ -521,6 +709,14 @@ bool GetPropBool(IInArchive *archive, std::uint32_t index, PROPID propId)
521
709
  return value.boolVal != VARIANT_FALSE;
522
710
  }
523
711
 
712
+ bool IsSolidArchive(IInArchive *archive)
713
+ {
714
+ NWindows::NCOM::CPropVariant value;
715
+ return archive->GetArchiveProperty(kpidSolid, &value) == S_OK
716
+ && value.vt == VT_BOOL
717
+ && value.boolVal != VARIANT_FALSE;
718
+ }
719
+
524
720
  std::string GetPropString(IInArchive *archive, std::uint32_t index, PROPID propId)
525
721
  {
526
722
  NWindows::NCOM::CPropVariant value;
@@ -536,10 +732,32 @@ struct OutputRequest {
536
732
  std::uint64_t length = 0;
537
733
  std::uint64_t entrySize = 0;
538
734
  bool encrypted = false;
735
+ bool abortOnLimit = false;
736
+ bool limitReached = false;
539
737
  std::vector<std::uint8_t> data;
540
738
  std::int32_t operationResult = NArchive::NExtract::NOperationResult::kUnavailable;
541
739
  };
542
740
 
741
+ struct ReadRange {
742
+ std::uint64_t offset;
743
+ std::uint64_t length;
744
+ };
745
+
746
+ struct ExtractionProfile {
747
+ double validationMs = 0;
748
+ double reservationMs = 0;
749
+ double requestSetupMs = 0;
750
+ double contextAcquireMs = 0;
751
+ double solidCheckMs = 0;
752
+ double callbackSetupMs = 0;
753
+ double sevenZipExtractMs = 0;
754
+ double resultProcessingMs = 0;
755
+ double minSevenZipCallMs = 0;
756
+ double maxSevenZipCallMs = 0;
757
+ std::uint32_t sevenZipCalls = 0;
758
+ bool abortPerItem = false;
759
+ };
760
+
543
761
  class FileInStream final : public IInStream, public CMyUnknownImp {
544
762
  Z7_COM_QI_BEGIN2(IInStream)
545
763
  Z7_COM_QI_ENTRY(ISequentialInStream)
@@ -891,6 +1109,8 @@ Z7_COM7F_IMF(ArchiveUpdateCallback::GetProperty(
891
1109
  property = path.c_str();
892
1110
  } else if (propertyId == kpidIsDir) {
893
1111
  property = false;
1112
+ } else if (propertyId == kpidMTime) {
1113
+ property = UnixMillisecondsToFileTime(item.mtimeMs);
894
1114
  } else if (propertyId == kpidSize) {
895
1115
  std::error_code error;
896
1116
  const auto size = std::filesystem::file_size(NativePath(item.sourcePath), error);
@@ -1046,6 +1266,13 @@ Z7_COM7F_IMF(VectorOutStream::Write(const void *data, UInt32 size, UInt32 *proce
1046
1266
  bytes + static_cast<std::size_t>(copyEnd - start));
1047
1267
  }
1048
1268
  seen_ = end;
1269
+ if (request_->abortOnLimit && request_->data.size() == request_->length) {
1270
+ request_->limitReached = true;
1271
+ if (processedSize != nullptr) {
1272
+ *processedSize = 0;
1273
+ }
1274
+ return E_ABORT;
1275
+ }
1049
1276
  if (processedSize != nullptr) {
1050
1277
  *processedSize = size;
1051
1278
  }
@@ -1130,7 +1357,9 @@ Z7_COM7F_IMF(ExtractCallback::PrepareOperation(Int32))
1130
1357
  Z7_COM7F_IMF(ExtractCallback::SetOperationResult(Int32 operationResult))
1131
1358
  {
1132
1359
  if (currentRequest_ != nullptr) {
1133
- currentRequest_->operationResult = operationResult;
1360
+ currentRequest_->operationResult = currentRequest_->limitReached
1361
+ ? NArchive::NExtract::NOperationResult::kOK
1362
+ : operationResult;
1134
1363
  }
1135
1364
  return S_OK;
1136
1365
  }
@@ -1201,67 +1430,657 @@ public:
1201
1430
  std::uint32_t id,
1202
1431
  std::shared_ptr<NativeStreamState> stream,
1203
1432
  const std::optional<std::string>& password)
1204
- : id_(id),
1205
- stream_(std::move(stream)),
1433
+ : id_(id),
1434
+ stream_(std::move(stream)),
1435
+ password_(password),
1436
+ operationResult_(NArchive::NExtract::NOperationResult::kUnavailable),
1437
+ passwordRequested_(false) {}
1438
+
1439
+ std::int32_t OperationResult() const
1440
+ {
1441
+ return operationResult_;
1442
+ }
1443
+
1444
+ bool PasswordRequested() const
1445
+ {
1446
+ return passwordRequested_.load(std::memory_order_acquire);
1447
+ }
1448
+
1449
+ private:
1450
+ std::uint32_t id_;
1451
+ std::shared_ptr<NativeStreamState> stream_;
1452
+ const std::optional<std::string>& password_;
1453
+ std::int32_t operationResult_;
1454
+ std::vector<CMyComPtr<ISequentialOutStream>> streams_;
1455
+ std::atomic<bool> passwordRequested_;
1456
+ };
1457
+
1458
+ Z7_COM7F_IMF(StreamingExtractCallback::SetTotal(UInt64))
1459
+ {
1460
+ return S_OK;
1461
+ }
1462
+
1463
+ Z7_COM7F_IMF(StreamingExtractCallback::SetCompleted(const UInt64 *))
1464
+ {
1465
+ return S_OK;
1466
+ }
1467
+
1468
+ Z7_COM7F_IMF(StreamingExtractCallback::GetStream(
1469
+ UInt32 index, ISequentialOutStream **outStream, Int32 askExtractMode))
1470
+ {
1471
+ *outStream = nullptr;
1472
+ if (askExtractMode != NArchive::NExtract::NAskMode::kExtract || index != id_) {
1473
+ return S_OK;
1474
+ }
1475
+ CMyComPtr<ISequentialOutStream> stream = new StreamingOutStream(stream_);
1476
+ streams_.push_back(stream);
1477
+ *outStream = stream;
1478
+ (*outStream)->AddRef();
1479
+ return S_OK;
1480
+ }
1481
+
1482
+ Z7_COM7F_IMF(StreamingExtractCallback::PrepareOperation(Int32))
1483
+ {
1484
+ return S_OK;
1485
+ }
1486
+
1487
+ Z7_COM7F_IMF(StreamingExtractCallback::SetOperationResult(Int32 operationResult))
1488
+ {
1489
+ operationResult_ = operationResult;
1490
+ return S_OK;
1491
+ }
1492
+
1493
+ Z7_COM7F_IMF(StreamingExtractCallback::CryptoGetTextPassword(BSTR *password))
1494
+ {
1495
+ passwordRequested_.store(true, std::memory_order_release);
1496
+ return GetPasswordBstr(password_, password);
1497
+ }
1498
+
1499
+ class NativeFileOutputState {
1500
+ public:
1501
+ NativeFileOutputState(std::uint32_t id, std::string path, std::uint64_t expectedSize, bool overwrite)
1502
+ : id_(id),
1503
+ path_(std::move(path)),
1504
+ expectedSize_(expectedSize),
1505
+ overwrite_(overwrite) {}
1506
+
1507
+ ~NativeFileOutputState()
1508
+ {
1509
+ Abort();
1510
+ }
1511
+
1512
+ bool Open()
1513
+ {
1514
+ if (overwrite_ && !RemoveOutputFile()) {
1515
+ return false;
1516
+ }
1517
+ #ifdef _WIN32
1518
+ const auto nativePath = NativePath(path_);
1519
+ fileDescriptor_ = ::_wopen(
1520
+ nativePath.c_str(),
1521
+ _O_WRONLY | _O_CREAT | _O_EXCL | _O_BINARY,
1522
+ _S_IREAD | _S_IWRITE);
1523
+ #else
1524
+ const auto nativePath = NativePath(path_);
1525
+ int flags = O_WRONLY | O_CREAT | O_EXCL;
1526
+ #ifdef O_CLOEXEC
1527
+ flags |= O_CLOEXEC;
1528
+ #endif
1529
+ #ifdef O_NOFOLLOW
1530
+ flags |= O_NOFOLLOW;
1531
+ #endif
1532
+ fileDescriptor_ = ::open(nativePath.c_str(), flags, 0600);
1533
+ #endif
1534
+ if (fileDescriptor_ < 0) {
1535
+ SetError("Unable to create extraction output file");
1536
+ return false;
1537
+ }
1538
+ fileCreated_ = true;
1539
+ return true;
1540
+ }
1541
+
1542
+ bool Write(const std::vector<std::uint8_t>& data)
1543
+ {
1544
+ std::size_t offset = 0;
1545
+ while (offset < data.size()) {
1546
+ #ifdef _WIN32
1547
+ const auto written = ::_write(
1548
+ fileDescriptor_,
1549
+ data.data() + offset,
1550
+ static_cast<unsigned int>(data.size() - offset));
1551
+ #else
1552
+ const auto written = ::write(
1553
+ fileDescriptor_,
1554
+ data.data() + offset,
1555
+ data.size() - offset);
1556
+ #endif
1557
+ if (written < 0) {
1558
+ #ifndef _WIN32
1559
+ if (errno == EINTR) {
1560
+ continue;
1561
+ }
1562
+ #endif
1563
+ SetError("Unable to write extraction output file");
1564
+ return false;
1565
+ }
1566
+ if (written == 0) {
1567
+ SetError("Unable to write extraction output file");
1568
+ return false;
1569
+ }
1570
+ const auto count = static_cast<std::size_t>(written);
1571
+ offset += count;
1572
+ bytesWritten_ += count;
1573
+ }
1574
+ return true;
1575
+ }
1576
+
1577
+ bool Close()
1578
+ {
1579
+ if (fileDescriptor_ < 0) {
1580
+ return error_.empty();
1581
+ }
1582
+ #ifdef _WIN32
1583
+ const auto result = ::_close(fileDescriptor_);
1584
+ #else
1585
+ const auto result = ::close(fileDescriptor_);
1586
+ #endif
1587
+ fileDescriptor_ = -1;
1588
+ if (result != 0) {
1589
+ SetError("Unable to close extraction output file");
1590
+ return false;
1591
+ }
1592
+ return true;
1593
+ }
1594
+
1595
+ void Abort()
1596
+ {
1597
+ Close();
1598
+ if (fileCreated_ && !completed_) {
1599
+ RemoveOutputFile();
1600
+ fileCreated_ = false;
1601
+ }
1602
+ }
1603
+
1604
+ void MarkCompleted()
1605
+ {
1606
+ completed_ = true;
1607
+ }
1608
+
1609
+ void AddQueuedBytes(std::uint64_t bytes)
1610
+ {
1611
+ bytesQueued_ += bytes;
1612
+ }
1613
+
1614
+ void MarkSizeLimitExceeded()
1615
+ {
1616
+ sizeLimitExceeded_ = true;
1617
+ }
1618
+
1619
+ std::uint32_t Id() const { return id_; }
1620
+ std::uint64_t ExpectedSize() const { return expectedSize_; }
1621
+ std::uint64_t BytesQueued() const { return bytesQueued_; }
1622
+ std::uint64_t BytesWritten() const { return bytesWritten_; }
1623
+ bool Completed() const { return completed_; }
1624
+ bool SizeLimitExceeded() const { return sizeLimitExceeded_; }
1625
+ const std::string& Error() const { return error_; }
1626
+
1627
+ private:
1628
+ void SetError(const char *message)
1629
+ {
1630
+ if (error_.empty()) {
1631
+ error_ = std::string(message) + ": " + std::strerror(errno);
1632
+ }
1633
+ }
1634
+
1635
+ bool RemoveOutputFile()
1636
+ {
1637
+ const auto nativePath = NativePath(path_);
1638
+ #ifdef _WIN32
1639
+ const auto result = ::_wunlink(nativePath.c_str());
1640
+ #else
1641
+ const auto result = ::unlink(nativePath.c_str());
1642
+ #endif
1643
+ if (result == 0 || errno == ENOENT) {
1644
+ return true;
1645
+ }
1646
+ SetError("Unable to remove extraction output file");
1647
+ return false;
1648
+ }
1649
+
1650
+ std::uint32_t id_;
1651
+ std::string path_;
1652
+ int fileDescriptor_ = -1;
1653
+ std::uint64_t expectedSize_;
1654
+ bool overwrite_;
1655
+ bool fileCreated_ = false;
1656
+ bool completed_ = false;
1657
+ std::uint64_t bytesQueued_ = 0;
1658
+ std::uint64_t bytesWritten_ = 0;
1659
+ bool sizeLimitExceeded_ = false;
1660
+ std::string error_;
1661
+ };
1662
+
1663
+ class AsyncFileWriter {
1664
+ public:
1665
+ explicit AsyncFileWriter(std::shared_ptr<NativeFileTaskState> task)
1666
+ : task_(std::move(task)) {}
1667
+
1668
+ ~AsyncFileWriter()
1669
+ {
1670
+ StopAndJoin();
1671
+ }
1672
+
1673
+ void Start()
1674
+ {
1675
+ worker_ = std::thread([this] { Run(); });
1676
+ }
1677
+
1678
+ bool EnqueueWrite(
1679
+ const std::shared_ptr<NativeFileOutputState>& output,
1680
+ const std::uint8_t *data,
1681
+ std::size_t size)
1682
+ {
1683
+ try {
1684
+ std::vector<std::uint8_t> copy(data, data + size);
1685
+ std::unique_lock<std::mutex> lock(mutex_);
1686
+ spaceAvailable_.wait(lock, [this, size] {
1687
+ return stopping_
1688
+ || task_->IsCancelled()
1689
+ || (HasQueueSpace() && queuedBytes_ + size <= kMaxQueuedBytes);
1690
+ });
1691
+ if (stopping_ || task_->IsCancelled()) {
1692
+ return false;
1693
+ }
1694
+ queue_.push_back({Job::Kind::Write, output, std::move(copy)});
1695
+ queuedBytes_ += size;
1696
+ } catch (const std::exception& error) {
1697
+ task_->Fail(error.what());
1698
+ return false;
1699
+ } catch (...) {
1700
+ task_->Fail("Unable to queue extraction output data");
1701
+ return false;
1702
+ }
1703
+ workAvailable_.notify_one();
1704
+ return true;
1705
+ }
1706
+
1707
+ bool EnqueueFinish(const std::shared_ptr<NativeFileOutputState>& output)
1708
+ {
1709
+ try {
1710
+ {
1711
+ std::unique_lock<std::mutex> lock(mutex_);
1712
+ spaceAvailable_.wait(lock, [this] {
1713
+ return stopping_ || HasQueueSpace();
1714
+ });
1715
+ if (stopping_) {
1716
+ return false;
1717
+ }
1718
+ queue_.push_back({Job::Kind::Finish, output, {}});
1719
+ }
1720
+ workAvailable_.notify_one();
1721
+ return true;
1722
+ } catch (const std::exception& error) {
1723
+ task_->Fail(error.what());
1724
+ return false;
1725
+ } catch (...) {
1726
+ task_->Fail("Unable to queue extraction output completion");
1727
+ return false;
1728
+ }
1729
+ }
1730
+
1731
+ bool EnqueueAbort(const std::shared_ptr<NativeFileOutputState>& output) noexcept
1732
+ {
1733
+ try {
1734
+ {
1735
+ std::unique_lock<std::mutex> lock(mutex_);
1736
+ spaceAvailable_.wait(lock, [this] {
1737
+ return stopping_ || HasQueueSpace();
1738
+ });
1739
+ if (stopping_) {
1740
+ return false;
1741
+ }
1742
+ queue_.push_back({Job::Kind::Abort, output, {}});
1743
+ }
1744
+ workAvailable_.notify_one();
1745
+ return true;
1746
+ } catch (...) {
1747
+ return false;
1748
+ }
1749
+ }
1750
+
1751
+ void WaitUntilIdle()
1752
+ {
1753
+ std::unique_lock<std::mutex> lock(mutex_);
1754
+ idle_.wait(lock, [this] { return queue_.empty() && !active_; });
1755
+ }
1756
+
1757
+ void StopAndJoin()
1758
+ {
1759
+ {
1760
+ std::lock_guard<std::mutex> lock(mutex_);
1761
+ stopping_ = true;
1762
+ }
1763
+ workAvailable_.notify_all();
1764
+ spaceAvailable_.notify_all();
1765
+ if (worker_.joinable()) {
1766
+ worker_.join();
1767
+ }
1768
+ }
1769
+
1770
+ private:
1771
+ struct Job {
1772
+ enum class Kind {
1773
+ Write,
1774
+ Finish,
1775
+ Abort,
1776
+ };
1777
+
1778
+ Kind kind;
1779
+ std::shared_ptr<NativeFileOutputState> output;
1780
+ std::vector<std::uint8_t> data;
1781
+ };
1782
+
1783
+ static constexpr std::size_t kMaxQueuedBytes = 8 * 1024 * 1024;
1784
+ static constexpr std::size_t kMaxQueuedJobs = 1024;
1785
+
1786
+ bool HasQueueSpace() const
1787
+ {
1788
+ return queue_.size() + (active_ ? 1 : 0) < kMaxQueuedJobs;
1789
+ }
1790
+
1791
+ void Run() noexcept
1792
+ {
1793
+ while (true) {
1794
+ Job job;
1795
+ {
1796
+ std::unique_lock<std::mutex> lock(mutex_);
1797
+ workAvailable_.wait(lock, [this] {
1798
+ return stopping_ || !queue_.empty();
1799
+ });
1800
+ if (queue_.empty() && stopping_) {
1801
+ idle_.notify_all();
1802
+ return;
1803
+ }
1804
+ job = std::move(queue_.front());
1805
+ queue_.pop_front();
1806
+ active_ = true;
1807
+ }
1808
+
1809
+ try {
1810
+ Process(job);
1811
+ } catch (const std::exception& error) {
1812
+ task_->Fail(error.what());
1813
+ job.output->Abort();
1814
+ } catch (...) {
1815
+ task_->Fail("Unknown asynchronous extraction output failure");
1816
+ job.output->Abort();
1817
+ }
1818
+
1819
+ {
1820
+ std::lock_guard<std::mutex> lock(mutex_);
1821
+ if (job.kind == Job::Kind::Write) {
1822
+ queuedBytes_ -= job.data.size();
1823
+ }
1824
+ active_ = false;
1825
+ if (queue_.empty()) {
1826
+ idle_.notify_all();
1827
+ }
1828
+ }
1829
+ spaceAvailable_.notify_all();
1830
+ }
1831
+ }
1832
+
1833
+ void Process(const Job& job)
1834
+ {
1835
+ if (job.kind == Job::Kind::Abort) {
1836
+ job.output->Abort();
1837
+ return;
1838
+ }
1839
+ if (job.kind == Job::Kind::Write) {
1840
+ if (task_->IsCancelled()) {
1841
+ return;
1842
+ }
1843
+ if (!job.output->Write(job.data)) {
1844
+ const auto& error = job.output->Error();
1845
+ task_->Fail(error.empty() ? "Unable to write extraction output file" : error);
1846
+ job.output->Abort();
1847
+ }
1848
+ return;
1849
+ }
1850
+ if (task_->IsCancelled()) {
1851
+ job.output->Abort();
1852
+ return;
1853
+ }
1854
+ if (!job.output->Close()) {
1855
+ const auto& error = job.output->Error();
1856
+ task_->Fail(error.empty() ? "Unable to close extraction output file" : error);
1857
+ job.output->Abort();
1858
+ return;
1859
+ }
1860
+ job.output->MarkCompleted();
1861
+ task_->ReportFileCompleted(job.output->Id());
1862
+ }
1863
+
1864
+ std::shared_ptr<NativeFileTaskState> task_;
1865
+ std::mutex mutex_;
1866
+ std::condition_variable workAvailable_;
1867
+ std::condition_variable spaceAvailable_;
1868
+ std::condition_variable idle_;
1869
+ std::deque<Job> queue_;
1870
+ std::size_t queuedBytes_ = 0;
1871
+ bool active_ = false;
1872
+ bool stopping_ = false;
1873
+ std::thread worker_;
1874
+ };
1875
+
1876
+ class NativeFileOutStream final : public ISequentialOutStream, public CMyUnknownImp {
1877
+ Z7_COM_UNKNOWN_IMP_1(ISequentialOutStream)
1878
+ Z7_COM7F_IMP(Write(const void *data, UInt32 size, UInt32 *processedSize))
1879
+
1880
+ public:
1881
+ NativeFileOutStream(
1882
+ std::shared_ptr<NativeFileOutputState> output,
1883
+ AsyncFileWriter& writer,
1884
+ std::shared_ptr<NativeFileTaskState> task)
1885
+ : output_(std::move(output)),
1886
+ writer_(writer),
1887
+ task_(std::move(task)) {}
1888
+
1889
+ ~NativeFileOutStream()
1890
+ {
1891
+ if (!terminalQueued_) {
1892
+ writer_.EnqueueAbort(output_);
1893
+ }
1894
+ }
1895
+
1896
+ bool Finish()
1897
+ {
1898
+ if (terminalQueued_) {
1899
+ return true;
1900
+ }
1901
+ terminalQueued_ = writer_.EnqueueFinish(output_);
1902
+ return terminalQueued_;
1903
+ }
1904
+
1905
+ void Abort()
1906
+ {
1907
+ if (!terminalQueued_) {
1908
+ terminalQueued_ = writer_.EnqueueAbort(output_);
1909
+ }
1910
+ }
1911
+
1912
+ private:
1913
+ std::shared_ptr<NativeFileOutputState> output_;
1914
+ AsyncFileWriter& writer_;
1915
+ std::shared_ptr<NativeFileTaskState> task_;
1916
+ bool terminalQueued_ = false;
1917
+ };
1918
+
1919
+ Z7_COM7F_IMF(NativeFileOutStream::Write(
1920
+ const void *data, UInt32 size, UInt32 *processedSize))
1921
+ {
1922
+ if (processedSize != nullptr) {
1923
+ *processedSize = 0;
1924
+ }
1925
+ if (task_->IsCancelled()) {
1926
+ return E_ABORT;
1927
+ }
1928
+ if (size > output_->ExpectedSize() - std::min(
1929
+ output_->ExpectedSize(), output_->BytesQueued())) {
1930
+ output_->MarkSizeLimitExceeded();
1931
+ return E_ABORT;
1932
+ }
1933
+
1934
+ const auto *bytes = static_cast<const std::uint8_t *>(data);
1935
+ std::uint32_t offset = 0;
1936
+ while (offset < size) {
1937
+ if (task_->IsCancelled()) {
1938
+ return E_ABORT;
1939
+ }
1940
+ const auto chunkSize = std::min<std::uint32_t>(64 * 1024, size - offset);
1941
+ if (!writer_.EnqueueWrite(output_, bytes + offset, chunkSize)) {
1942
+ return task_->IsCancelled() ? E_ABORT : E_FAIL;
1943
+ }
1944
+ output_->AddQueuedBytes(chunkSize);
1945
+ offset += chunkSize;
1946
+ }
1947
+ if (processedSize != nullptr) {
1948
+ *processedSize = size;
1949
+ }
1950
+ return S_OK;
1951
+ }
1952
+
1953
+ struct NativeFileOutputSpec {
1954
+ std::uint32_t id = 0;
1955
+ std::string path;
1956
+ bool overwrite = false;
1957
+ };
1958
+
1959
+ struct FileOutputRequest {
1960
+ std::uint32_t id = 0;
1961
+ std::string path;
1962
+ std::uint64_t expectedSize = 0;
1963
+ bool overwrite = false;
1964
+ std::int32_t operationResult = NArchive::NExtract::NOperationResult::kUnavailable;
1965
+ NativeFileOutStream *stream = nullptr;
1966
+ std::shared_ptr<NativeFileOutputState> output;
1967
+ std::string error;
1968
+ };
1969
+
1970
+ class BatchFileExtractCallback final :
1971
+ public IArchiveExtractCallback,
1972
+ public ICryptoGetTextPassword,
1973
+ public CMyUnknownImp {
1974
+ Z7_COM_QI_BEGIN2(IArchiveExtractCallback)
1975
+ Z7_COM_QI_ENTRY(IProgress)
1976
+ Z7_COM_QI_ENTRY(ICryptoGetTextPassword)
1977
+ Z7_COM_QI_END
1978
+ Z7_COM_ADDREF_RELEASE
1979
+
1980
+ Z7_IFACE_COM7_IMP(IProgress)
1981
+ Z7_IFACE_COM7_IMP(IArchiveExtractCallback)
1982
+ Z7_IFACE_COM7_IMP(ICryptoGetTextPassword)
1983
+
1984
+ public:
1985
+ BatchFileExtractCallback(
1986
+ std::vector<FileOutputRequest>& requests,
1987
+ const std::unordered_map<std::uint32_t, std::size_t>& requestById,
1988
+ std::shared_ptr<NativeFileTaskState> task,
1989
+ AsyncFileWriter& writer,
1990
+ const std::optional<std::string>& password)
1991
+ : requests_(requests),
1992
+ requestById_(requestById),
1993
+ task_(std::move(task)),
1994
+ writer_(writer),
1206
1995
  password_(password),
1207
- operationResult_(NArchive::NExtract::NOperationResult::kUnavailable),
1996
+ currentRequest_(nullptr),
1208
1997
  passwordRequested_(false) {}
1209
1998
 
1210
- std::int32_t OperationResult() const
1211
- {
1212
- return operationResult_;
1213
- }
1214
-
1215
1999
  bool PasswordRequested() const
1216
2000
  {
1217
2001
  return passwordRequested_.load(std::memory_order_acquire);
1218
2002
  }
1219
2003
 
1220
2004
  private:
1221
- std::uint32_t id_;
1222
- std::shared_ptr<NativeStreamState> stream_;
2005
+ std::vector<FileOutputRequest>& requests_;
2006
+ const std::unordered_map<std::uint32_t, std::size_t>& requestById_;
2007
+ std::shared_ptr<NativeFileTaskState> task_;
2008
+ AsyncFileWriter& writer_;
1223
2009
  const std::optional<std::string>& password_;
1224
- std::int32_t operationResult_;
1225
- std::vector<CMyComPtr<ISequentialOutStream>> streams_;
2010
+ FileOutputRequest *currentRequest_;
1226
2011
  std::atomic<bool> passwordRequested_;
2012
+ std::vector<CMyComPtr<ISequentialOutStream>> streams_;
1227
2013
  };
1228
2014
 
1229
- Z7_COM7F_IMF(StreamingExtractCallback::SetTotal(UInt64))
2015
+ Z7_COM7F_IMF(BatchFileExtractCallback::SetTotal(UInt64))
1230
2016
  {
1231
2017
  return S_OK;
1232
2018
  }
1233
2019
 
1234
- Z7_COM7F_IMF(StreamingExtractCallback::SetCompleted(const UInt64 *))
2020
+ Z7_COM7F_IMF(BatchFileExtractCallback::SetCompleted(const UInt64 *))
1235
2021
  {
1236
- return S_OK;
2022
+ return task_->IsCancelled() ? E_ABORT : S_OK;
1237
2023
  }
1238
2024
 
1239
- Z7_COM7F_IMF(StreamingExtractCallback::GetStream(
2025
+ Z7_COM7F_IMF(BatchFileExtractCallback::GetStream(
1240
2026
  UInt32 index, ISequentialOutStream **outStream, Int32 askExtractMode))
1241
2027
  {
1242
2028
  *outStream = nullptr;
1243
- if (askExtractMode != NArchive::NExtract::NAskMode::kExtract || index != id_) {
2029
+ currentRequest_ = nullptr;
2030
+ if (task_->IsCancelled()) {
2031
+ return E_ABORT;
2032
+ }
2033
+ if (askExtractMode != NArchive::NExtract::NAskMode::kExtract) {
1244
2034
  return S_OK;
1245
2035
  }
1246
- CMyComPtr<ISequentialOutStream> stream = new StreamingOutStream(stream_);
1247
- streams_.push_back(stream);
1248
- *outStream = stream;
2036
+ const auto found = requestById_.find(index);
2037
+ if (found == requestById_.end()) {
2038
+ return S_OK;
2039
+ }
2040
+ auto& request = requests_[found->second];
2041
+ currentRequest_ = &request;
2042
+ request.output = std::make_shared<NativeFileOutputState>(
2043
+ request.id, request.path, request.expectedSize, request.overwrite);
2044
+ if (!request.output->Open()) {
2045
+ request.error = request.output->Error();
2046
+ return E_FAIL;
2047
+ }
2048
+ auto *fileStream = new NativeFileOutStream(
2049
+ request.output, writer_, task_);
2050
+ CMyComPtr<ISequentialOutStream> output = fileStream;
2051
+ request.stream = fileStream;
2052
+ streams_.push_back(output);
2053
+ *outStream = output;
1249
2054
  (*outStream)->AddRef();
1250
2055
  return S_OK;
1251
2056
  }
1252
2057
 
1253
- Z7_COM7F_IMF(StreamingExtractCallback::PrepareOperation(Int32))
2058
+ Z7_COM7F_IMF(BatchFileExtractCallback::PrepareOperation(Int32))
1254
2059
  {
1255
- return S_OK;
2060
+ return task_->IsCancelled() ? E_ABORT : S_OK;
1256
2061
  }
1257
2062
 
1258
- Z7_COM7F_IMF(StreamingExtractCallback::SetOperationResult(Int32 operationResult))
2063
+ Z7_COM7F_IMF(BatchFileExtractCallback::SetOperationResult(Int32 operationResult))
1259
2064
  {
1260
- operationResult_ = operationResult;
2065
+ if (currentRequest_ != nullptr) {
2066
+ currentRequest_->operationResult = operationResult;
2067
+ if (operationResult == NArchive::NExtract::NOperationResult::kOK
2068
+ && currentRequest_->stream != nullptr
2069
+ && currentRequest_->error.empty()
2070
+ && currentRequest_->output != nullptr
2071
+ && !currentRequest_->output->SizeLimitExceeded()) {
2072
+ if (!currentRequest_->stream->Finish()) {
2073
+ currentRequest_->error = task_->Failure();
2074
+ return E_ABORT;
2075
+ }
2076
+ } else if (currentRequest_->stream != nullptr) {
2077
+ currentRequest_->stream->Abort();
2078
+ }
2079
+ }
1261
2080
  return S_OK;
1262
2081
  }
1263
2082
 
1264
- Z7_COM7F_IMF(StreamingExtractCallback::CryptoGetTextPassword(BSTR *password))
2083
+ Z7_COM7F_IMF(BatchFileExtractCallback::CryptoGetTextPassword(BSTR *password))
1265
2084
  {
1266
2085
  passwordRequested_.store(true, std::memory_order_release);
1267
2086
  return GetPasswordBstr(password_, password);
@@ -1323,11 +2142,13 @@ public:
1323
2142
  void Open(
1324
2143
  const std::string& path,
1325
2144
  std::optional<std::string> password,
1326
- const std::string& enginePath)
2145
+ const std::string& enginePath,
2146
+ std::uint32_t deflateBufferSize)
1327
2147
  {
1328
2148
  EnsureEngineLoaded(enginePath);
1329
2149
  path_ = path;
1330
2150
  password_ = std::move(password);
2151
+ deflateBufferSize_ = deflateBufferSize;
1331
2152
  const auto extension = Extension(path);
1332
2153
  UInt32 formatCount = 0;
1333
2154
  if (g_getNumberOfFormats(&formatCount) != S_OK || formatCount == 0) {
@@ -1477,17 +2298,27 @@ public:
1477
2298
 
1478
2299
  std::vector<std::vector<std::uint8_t>> Extract(
1479
2300
  const std::vector<std::uint32_t>& ids,
1480
- bool partial,
1481
- std::uint64_t offset,
1482
- std::uint64_t length,
1483
- OutputReservation& outputReservation)
2301
+ const std::vector<ReadRange>& ranges,
2302
+ OutputReservation& outputReservation,
2303
+ ExtractionProfile *profile = nullptr)
1484
2304
  {
2305
+ using Clock = std::chrono::steady_clock;
2306
+ const auto elapsedMs = [](Clock::time_point start) {
2307
+ return std::chrono::duration<double, std::milli>(Clock::now() - start).count();
2308
+ };
2309
+ Clock::time_point phaseStart;
1485
2310
  if (contexts_.empty()) {
1486
2311
  throw std::runtime_error("Archive is closed");
1487
2312
  }
2313
+ if (!ranges.empty() && ranges.size() != ids.size()) {
2314
+ throw std::invalid_argument("Read ranges must match the requested entry count");
2315
+ }
1488
2316
  std::unordered_map<std::uint32_t, std::size_t> requestById;
1489
2317
  requestById.reserve(ids.size());
1490
2318
  std::uint64_t totalRequested = 0;
2319
+ if (profile != nullptr) {
2320
+ phaseStart = Clock::now();
2321
+ }
1491
2322
  for (std::size_t requestIndex = 0; requestIndex < ids.size(); ++requestIndex) {
1492
2323
  const auto id = ids[requestIndex];
1493
2324
  if (id >= entries_.size()) {
@@ -1497,9 +2328,10 @@ public:
1497
2328
  if (entry.directory) {
1498
2329
  throw std::invalid_argument("Cannot read a directory entry");
1499
2330
  }
1500
- const auto start = partial ? std::min(offset, entry.size) : 0;
2331
+ const bool partial = !ranges.empty();
2332
+ const auto start = partial ? std::min(ranges[requestIndex].offset, entry.size) : 0;
1501
2333
  const auto available = entry.size - start;
1502
- const auto requested = partial ? std::min(length, available) : entry.size;
2334
+ const auto requested = partial ? std::min(ranges[requestIndex].length, available) : entry.size;
1503
2335
  if (!requestById.emplace(id, requestIndex).second) {
1504
2336
  throw std::invalid_argument("Duplicate entry IDs are not allowed in a batch");
1505
2337
  }
@@ -1509,14 +2341,24 @@ public:
1509
2341
  }
1510
2342
  totalRequested += requested;
1511
2343
  }
2344
+ if (profile != nullptr) {
2345
+ profile->validationMs = elapsedMs(phaseStart);
2346
+ phaseStart = Clock::now();
2347
+ }
1512
2348
  outputReservation.Acquire(totalRequested);
2349
+ if (profile != nullptr) {
2350
+ profile->reservationMs = elapsedMs(phaseStart);
2351
+ phaseStart = Clock::now();
2352
+ }
1513
2353
  std::vector<OutputRequest> requests;
1514
2354
  requests.reserve(ids.size());
1515
- for (const auto id : ids) {
2355
+ for (std::size_t requestIndex = 0; requestIndex < ids.size(); ++requestIndex) {
2356
+ const auto id = ids[requestIndex];
1516
2357
  const auto& entry = entries_[id];
1517
- const auto start = partial ? std::min(offset, entry.size) : 0;
2358
+ const bool partial = !ranges.empty();
2359
+ const auto start = partial ? std::min(ranges[requestIndex].offset, entry.size) : 0;
1518
2360
  const auto available = entry.size - start;
1519
- const auto requested = partial ? std::min(length, available) : entry.size;
2361
+ const auto requested = partial ? std::min(ranges[requestIndex].length, available) : entry.size;
1520
2362
  OutputRequest request;
1521
2363
  request.id = id;
1522
2364
  request.offset = start;
@@ -1529,6 +2371,11 @@ public:
1529
2371
  std::vector<UInt32> sortedIds;
1530
2372
  sortedIds.reserve(requestById.size());
1531
2373
  for (const auto& item : requestById) {
2374
+ auto& request = requests[item.second];
2375
+ if (!ranges.empty() && request.length == 0) {
2376
+ request.operationResult = NArchive::NExtract::NOperationResult::kOK;
2377
+ continue;
2378
+ }
1532
2379
  sortedIds.push_back(item.first);
1533
2380
  }
1534
2381
  if (std::all_of(requests.begin(), requests.end(), [](const OutputRequest& request) {
@@ -1537,14 +2384,77 @@ public:
1537
2384
  return std::vector<std::vector<std::uint8_t>>(ids.size());
1538
2385
  }
1539
2386
  std::sort(sortedIds.begin(), sortedIds.end());
2387
+ if (profile != nullptr) {
2388
+ profile->requestSetupMs = elapsedMs(phaseStart);
2389
+ phaseStart = Clock::now();
2390
+ }
1540
2391
  auto context = AcquireContext();
2392
+ if (profile != nullptr) {
2393
+ profile->contextAcquireMs = elapsedMs(phaseStart);
2394
+ phaseStart = Clock::now();
2395
+ }
2396
+ const bool abortPerItem = !ranges.empty() && !IsSolidArchive(context->archive);
2397
+ if (profile != nullptr) {
2398
+ profile->solidCheckMs = elapsedMs(phaseStart);
2399
+ profile->abortPerItem = abortPerItem;
2400
+ phaseStart = Clock::now();
2401
+ }
2402
+ if (abortPerItem) {
2403
+ for (auto& request : requests) {
2404
+ request.abortOnLimit = request.offset + request.length < request.entrySize;
2405
+ }
2406
+ }
1541
2407
  auto *extractCallbackObject = new ExtractCallback(requests, requestById, password_);
1542
2408
  CMyComPtr<IArchiveExtractCallback> callback = extractCallbackObject;
1543
- const auto result = context->archive->Extract(
1544
- sortedIds.empty() ? nullptr : sortedIds.data(),
1545
- static_cast<UInt32>(sortedIds.size()),
1546
- 0,
1547
- callback);
2409
+ if (profile != nullptr) {
2410
+ profile->callbackSetupMs = elapsedMs(phaseStart);
2411
+ phaseStart = Clock::now();
2412
+ }
2413
+ HRESULT result = S_OK;
2414
+ if (g_setDeflateInputBufferSize(deflateBufferSize_) != S_OK) {
2415
+ throw std::runtime_error("Unable to configure the Deflate input buffer");
2416
+ }
2417
+ if (abortPerItem) {
2418
+ for (const auto id : sortedIds) {
2419
+ const auto requestIndex = requestById.at(id);
2420
+ const UInt32 itemId = id;
2421
+ const auto callStart = profile != nullptr ? Clock::now() : Clock::time_point{};
2422
+ const auto itemResult = context->archive->Extract(&itemId, 1, 0, callback);
2423
+ if (profile != nullptr) {
2424
+ const double callMs = elapsedMs(callStart);
2425
+ if (profile->sevenZipCalls == 0 || callMs < profile->minSevenZipCallMs) {
2426
+ profile->minSevenZipCallMs = callMs;
2427
+ }
2428
+ profile->maxSevenZipCallMs = std::max(profile->maxSevenZipCallMs, callMs);
2429
+ ++profile->sevenZipCalls;
2430
+ }
2431
+ if (itemResult == E_ABORT && requests[requestIndex].limitReached) {
2432
+ requests[requestIndex].operationResult = NArchive::NExtract::NOperationResult::kOK;
2433
+ continue;
2434
+ }
2435
+ if (itemResult != S_OK) {
2436
+ result = itemResult;
2437
+ break;
2438
+ }
2439
+ }
2440
+ } else {
2441
+ const auto callStart = profile != nullptr ? Clock::now() : Clock::time_point{};
2442
+ result = context->archive->Extract(
2443
+ sortedIds.empty() ? nullptr : sortedIds.data(),
2444
+ static_cast<UInt32>(sortedIds.size()),
2445
+ 0,
2446
+ callback);
2447
+ if (profile != nullptr) {
2448
+ const double callMs = elapsedMs(callStart);
2449
+ profile->sevenZipCalls = 1;
2450
+ profile->minSevenZipCallMs = callMs;
2451
+ profile->maxSevenZipCallMs = callMs;
2452
+ }
2453
+ }
2454
+ if (profile != nullptr) {
2455
+ profile->sevenZipExtractMs = elapsedMs(phaseStart);
2456
+ phaseStart = Clock::now();
2457
+ }
1548
2458
  if (result != S_OK) {
1549
2459
  if (extractCallbackObject->PasswordRequested()) {
1550
2460
  throw std::runtime_error("Archive password is missing or incorrect");
@@ -1567,6 +2477,9 @@ public:
1567
2477
  for (const auto id : ids) {
1568
2478
  resultData.push_back(std::move(requests[requestById.at(id)].data));
1569
2479
  }
2480
+ if (profile != nullptr) {
2481
+ profile->resultProcessingMs = elapsedMs(phaseStart);
2482
+ }
1570
2483
  return resultData;
1571
2484
  }
1572
2485
 
@@ -1585,6 +2498,9 @@ public:
1585
2498
  auto *extractCallbackObject = new StreamingExtractCallback(id, stream, password_);
1586
2499
  CMyComPtr<IArchiveExtractCallback> callback = extractCallbackObject;
1587
2500
  const UInt32 itemIndex = id;
2501
+ if (g_setDeflateInputBufferSize(deflateBufferSize_) != S_OK) {
2502
+ throw std::runtime_error("Unable to configure the Deflate input buffer");
2503
+ }
1588
2504
  const auto result = context->archive->Extract(&itemIndex, 1, 0, callback);
1589
2505
  if (result != S_OK) {
1590
2506
  if (extractCallbackObject->PasswordRequested()) {
@@ -1605,6 +2521,120 @@ public:
1605
2521
  }
1606
2522
  }
1607
2523
 
2524
+ void ExtractToFiles(
2525
+ const std::vector<NativeFileOutputSpec>& outputs,
2526
+ const std::shared_ptr<NativeFileTaskState>& task,
2527
+ AsyncFileWriter& writer)
2528
+ {
2529
+ if (contexts_.empty()) {
2530
+ throw std::runtime_error("Archive is closed");
2531
+ }
2532
+ if (task->IsCancelled()) {
2533
+ const auto failure = task->Failure();
2534
+ throw std::runtime_error(failure.empty()
2535
+ ? "Native folder extraction was cancelled"
2536
+ : failure);
2537
+ }
2538
+ if (outputs.empty()) {
2539
+ return;
2540
+ }
2541
+
2542
+ std::vector<FileOutputRequest> requests;
2543
+ requests.reserve(outputs.size());
2544
+ std::unordered_map<std::uint32_t, std::size_t> requestById;
2545
+ requestById.reserve(outputs.size());
2546
+ std::vector<UInt32> sortedIds;
2547
+ sortedIds.reserve(outputs.size());
2548
+ for (const auto& output : outputs) {
2549
+ const auto id = output.id;
2550
+ const auto& path = output.path;
2551
+ if (id >= entries_.size()) {
2552
+ throw std::out_of_range("Entry ID is out of range");
2553
+ }
2554
+ if (entries_[id].directory) {
2555
+ throw std::invalid_argument("Cannot extract a directory entry to a file");
2556
+ }
2557
+ if (path.empty() || path.find('\0') != std::string::npos) {
2558
+ throw std::invalid_argument("Extraction output paths must be non-empty and contain no null bytes");
2559
+ }
2560
+ if (!requestById.emplace(id, requests.size()).second) {
2561
+ throw std::invalid_argument("Duplicate entry IDs are not allowed in a folder extraction batch");
2562
+ }
2563
+ requests.push_back({
2564
+ id,
2565
+ path,
2566
+ entries_[id].size,
2567
+ output.overwrite,
2568
+ NArchive::NExtract::NOperationResult::kUnavailable,
2569
+ nullptr,
2570
+ nullptr,
2571
+ {},
2572
+ });
2573
+ sortedIds.push_back(id);
2574
+ }
2575
+ std::sort(sortedIds.begin(), sortedIds.end());
2576
+
2577
+ HRESULT result = E_FAIL;
2578
+ bool passwordRequested = false;
2579
+ {
2580
+ auto context = AcquireContext();
2581
+ auto *extractCallbackObject = new BatchFileExtractCallback(
2582
+ requests, requestById, task, writer, password_);
2583
+ CMyComPtr<IArchiveExtractCallback> callback = extractCallbackObject;
2584
+ if (g_setDeflateInputBufferSize(deflateBufferSize_) != S_OK) {
2585
+ throw std::runtime_error("Unable to configure the Deflate input buffer");
2586
+ }
2587
+ result = context->archive->Extract(
2588
+ sortedIds.data(),
2589
+ static_cast<UInt32>(sortedIds.size()),
2590
+ 0,
2591
+ callback);
2592
+ passwordRequested = extractCallbackObject->PasswordRequested();
2593
+ }
2594
+ writer.WaitUntilIdle();
2595
+ const auto writerFailure = task->Failure();
2596
+ if (!writerFailure.empty()) {
2597
+ throw std::runtime_error(writerFailure);
2598
+ }
2599
+ if (task->IsCancelled()) {
2600
+ throw std::runtime_error("Native folder extraction was cancelled");
2601
+ }
2602
+ for (const auto& request : requests) {
2603
+ if (!request.error.empty()) {
2604
+ throw std::runtime_error(request.error);
2605
+ }
2606
+ if (request.output != nullptr && request.output->SizeLimitExceeded()) {
2607
+ throw std::runtime_error(
2608
+ "Extracted entry exceeds its declared size: " + request.path);
2609
+ }
2610
+ }
2611
+ if (result != S_OK) {
2612
+ if (passwordRequested) {
2613
+ throw std::runtime_error("Archive password is missing or incorrect");
2614
+ }
2615
+ throw std::runtime_error("7-Zip failed to extract the requested files");
2616
+ }
2617
+ for (const auto& request : requests) {
2618
+ if (request.operationResult != NArchive::NExtract::NOperationResult::kOK) {
2619
+ if (passwordRequested) {
2620
+ throw std::runtime_error("Archive password is incorrect or encrypted data is invalid");
2621
+ }
2622
+ if (request.operationResult == NArchive::NExtract::NOperationResult::kWrongPassword) {
2623
+ throw std::runtime_error("Archive password is incorrect");
2624
+ }
2625
+ throw std::runtime_error(
2626
+ "Entry extraction failed (unsupported method, integrity error, or password required): "
2627
+ + request.path);
2628
+ }
2629
+ if (request.output == nullptr || !request.output->Completed()) {
2630
+ throw std::runtime_error("Entry output did not finish writing: " + request.path);
2631
+ }
2632
+ if (request.output->BytesWritten() != request.output->BytesQueued()) {
2633
+ throw std::runtime_error("Entry output write count is inconsistent: " + request.path);
2634
+ }
2635
+ }
2636
+ }
2637
+
1608
2638
  private:
1609
2639
  ContextLease AcquireContext()
1610
2640
  {
@@ -1728,6 +2758,7 @@ private:
1728
2758
  FileStamp fileStamp_{};
1729
2759
  std::optional<std::string> password_;
1730
2760
  std::vector<Entry> entries_;
2761
+ std::uint32_t deflateBufferSize_ = 32 * 1024;
1731
2762
  };
1732
2763
 
1733
2764
  Napi::Object EntryToJs(Napi::Env env, const Entry& entry)
@@ -1752,7 +2783,7 @@ Napi::Buffer<std::uint8_t> TakeBuffer(Napi::Env env, std::vector<std::uint8_t> d
1752
2783
  return Napi::Buffer<std::uint8_t>::New(env, 0);
1753
2784
  }
1754
2785
  auto *owned = new std::vector<std::uint8_t>(std::move(data));
1755
- return Napi::Buffer<std::uint8_t>::New(
2786
+ return Napi::Buffer<std::uint8_t>::NewOrCopy(
1756
2787
  env,
1757
2788
  owned->data(),
1758
2789
  owned->size(),
@@ -1789,6 +2820,21 @@ void StreamMessageToJs(Napi::Env env, Napi::Function callback, StreamMessage *me
1789
2820
  }
1790
2821
  }
1791
2822
 
2823
+ void FileCompletionToJs(
2824
+ Napi::Env env,
2825
+ Napi::Function callback,
2826
+ FileCompletionMessage *message)
2827
+ {
2828
+ std::unique_ptr<FileCompletionMessage> owned(message);
2829
+ if (message->kind == FileCompletionMessage::Kind::Barrier) {
2830
+ message->acknowledgement->Acknowledge();
2831
+ return;
2832
+ }
2833
+ if (env != nullptr && callback) {
2834
+ callback.Call({Napi::Number::New(env, message->entryId)});
2835
+ }
2836
+ }
2837
+
1792
2838
  class BoundedAsyncWorker : public Napi::AsyncWorker {
1793
2839
  public:
1794
2840
  BoundedAsyncWorker(Napi::Env env, Napi::Promise::Deferred deferred)
@@ -1815,11 +2861,13 @@ public:
1815
2861
  Napi::Promise::Deferred deferred,
1816
2862
  std::string path,
1817
2863
  std::optional<std::string> password,
1818
- std::string enginePath)
2864
+ std::string enginePath,
2865
+ std::uint32_t deflateBufferSize)
1819
2866
  : BoundedAsyncWorker(env, deferred),
1820
2867
  path_(std::move(path)),
1821
2868
  password_(std::move(password)),
1822
- enginePath_(std::move(enginePath)) {}
2869
+ enginePath_(std::move(enginePath)),
2870
+ deflateBufferSize_(deflateBufferSize) {}
1823
2871
 
1824
2872
  ~OpenWorker() override
1825
2873
  {
@@ -1832,7 +2880,7 @@ public:
1832
2880
  {
1833
2881
  try {
1834
2882
  archive_ = std::make_shared<ArchiveEngine>();
1835
- archive_->Open(path_, std::move(password_), enginePath_);
2883
+ archive_->Open(path_, std::move(password_), enginePath_, deflateBufferSize_);
1836
2884
  } catch (const std::exception& error) {
1837
2885
  SetError(error.what());
1838
2886
  } catch (...) {
@@ -1854,6 +2902,7 @@ private:
1854
2902
  std::string path_;
1855
2903
  std::optional<std::string> password_;
1856
2904
  std::string enginePath_;
2905
+ std::uint32_t deflateBufferSize_;
1857
2906
  std::shared_ptr<ArchiveEngine> archive_;
1858
2907
  };
1859
2908
 
@@ -1897,15 +2946,14 @@ public:
1897
2946
  Napi::Promise::Deferred deferred,
1898
2947
  std::shared_ptr<ArchiveEngine> archive,
1899
2948
  std::vector<std::uint32_t> ids,
1900
- bool partial,
1901
- std::uint64_t offset,
1902
- std::uint64_t length)
2949
+ std::vector<ReadRange> ranges,
2950
+ bool singleOutput)
1903
2951
  : deferred_(deferred),
1904
2952
  archive_(std::move(archive)),
1905
2953
  ids_(std::move(ids)),
1906
- partial_(partial),
1907
- offset_(offset),
1908
- length_(length) {}
2954
+ ranges_(std::move(ranges)),
2955
+ singleOutput_(singleOutput),
2956
+ profileEnabled_(std::getenv("ZIP_NATIVE_PROFILE_READ") != nullptr) {}
1909
2957
 
1910
2958
  ~ExtractWorker() override
1911
2959
  {
@@ -1914,23 +2962,58 @@ public:
1914
2962
 
1915
2963
  void Execute() noexcept override
1916
2964
  {
2965
+ std::chrono::steady_clock::time_point started;
2966
+ if (profileEnabled_) {
2967
+ started = std::chrono::steady_clock::now();
2968
+ }
1917
2969
  try {
1918
- outputs_ = archive_->Extract(ids_, partial_, offset_, length_, outputReservation_);
2970
+ outputs_ = archive_->Extract(
2971
+ ids_,
2972
+ ranges_,
2973
+ outputReservation_,
2974
+ profileEnabled_ ? &profile_ : nullptr);
1919
2975
  } catch (const std::exception& error) {
1920
2976
  error_ = error.what();
1921
2977
  } catch (...) {
1922
2978
  error_ = "Unknown native extraction failure";
1923
2979
  }
2980
+ if (profileEnabled_) {
2981
+ executionMs_ = std::chrono::duration<double, std::milli>(
2982
+ std::chrono::steady_clock::now() - started).count();
2983
+ }
1924
2984
  }
1925
2985
 
1926
2986
  void Complete(Napi::Env env) override
1927
2987
  {
2988
+ if (profileEnabled_) {
2989
+ std::fprintf(
2990
+ stderr,
2991
+ "[7zip-native profile] worker=%.3f ms, validation=%.3f, reservation=%.3f, "
2992
+ "requestSetup=%.3f, contextAcquire=%.3f, solidCheck=%.3f, callbackSetup=%.3f, "
2993
+ "sevenZipExtract=%.3f (%u calls; perItem=%d; callMin=%.3f; callMax=%.3f), "
2994
+ "resultProcessing=%.3f, entries=%zu\n",
2995
+ executionMs_,
2996
+ profile_.validationMs,
2997
+ profile_.reservationMs,
2998
+ profile_.requestSetupMs,
2999
+ profile_.contextAcquireMs,
3000
+ profile_.solidCheckMs,
3001
+ profile_.callbackSetupMs,
3002
+ profile_.sevenZipExtractMs,
3003
+ profile_.sevenZipCalls,
3004
+ profile_.abortPerItem,
3005
+ profile_.minSevenZipCallMs,
3006
+ profile_.maxSevenZipCallMs,
3007
+ profile_.resultProcessingMs,
3008
+ ids_.size());
3009
+ std::fflush(stderr);
3010
+ }
1928
3011
  try {
1929
3012
  if (!error_.empty()) {
1930
3013
  deferred_.Reject(Napi::Error::New(env, error_).Value());
1931
3014
  return;
1932
3015
  }
1933
- if (partial_) {
3016
+ if (singleOutput_) {
1934
3017
  deferred_.Resolve(TakeBuffer(env, std::move(outputs_.front())));
1935
3018
  return;
1936
3019
  }
@@ -1951,9 +3034,11 @@ private:
1951
3034
  OutputReservation outputReservation_;
1952
3035
  std::shared_ptr<ArchiveEngine> archive_;
1953
3036
  std::vector<std::uint32_t> ids_;
1954
- bool partial_;
1955
- std::uint64_t offset_;
1956
- std::uint64_t length_;
3037
+ std::vector<ReadRange> ranges_;
3038
+ bool singleOutput_;
3039
+ bool profileEnabled_;
3040
+ double executionMs_ = 0;
3041
+ ExtractionProfile profile_;
1957
3042
  std::vector<std::vector<std::uint8_t>> outputs_;
1958
3043
  std::string error_;
1959
3044
  };
@@ -2078,6 +3163,65 @@ private:
2078
3163
  bool completed_ = false;
2079
3164
  };
2080
3165
 
3166
+ class ExtractToFilesWorker final : public ExtractionTask {
3167
+ public:
3168
+ ExtractToFilesWorker(
3169
+ Napi::Promise::Deferred deferred,
3170
+ std::shared_ptr<ArchiveEngine> archive,
3171
+ std::vector<NativeFileOutputSpec> outputs,
3172
+ std::shared_ptr<NativeFileTaskState> task)
3173
+ : deferred_(deferred),
3174
+ archive_(std::move(archive)),
3175
+ outputs_(std::move(outputs)),
3176
+ task_(std::move(task)) {}
3177
+
3178
+ ~ExtractToFilesWorker() override
3179
+ {
3180
+ RemoveFileTask(task_->id);
3181
+ task_->ReleaseCallback();
3182
+ g_pendingOperations.fetch_sub(1, std::memory_order_acq_rel);
3183
+ }
3184
+
3185
+ void Execute() noexcept override
3186
+ {
3187
+ AsyncFileWriter writer(task_);
3188
+ try {
3189
+ writer.Start();
3190
+ archive_->ExtractToFiles(outputs_, task_, writer);
3191
+ } catch (const std::exception& error) {
3192
+ error_ = error.what();
3193
+ } catch (...) {
3194
+ error_ = "Unknown native file extraction failure";
3195
+ }
3196
+ writer.StopAndJoin();
3197
+ if (error_.empty()) {
3198
+ error_ = task_->Failure();
3199
+ if (error_.empty() && task_->IsCancelled()) {
3200
+ error_ = "Native folder extraction was cancelled";
3201
+ }
3202
+ }
3203
+ if (!task_->WaitForFileCompletions() && error_.empty()) {
3204
+ error_ = "Unable to deliver native file completion events";
3205
+ }
3206
+ }
3207
+
3208
+ void Complete(Napi::Env env) override
3209
+ {
3210
+ if (!error_.empty()) {
3211
+ deferred_.Reject(Napi::Error::New(env, error_).Value());
3212
+ return;
3213
+ }
3214
+ deferred_.Resolve(env.Undefined());
3215
+ }
3216
+
3217
+ private:
3218
+ Napi::Promise::Deferred deferred_;
3219
+ std::shared_ptr<ArchiveEngine> archive_;
3220
+ std::vector<NativeFileOutputSpec> outputs_;
3221
+ std::shared_ptr<NativeFileTaskState> task_;
3222
+ std::string error_;
3223
+ };
3224
+
2081
3225
  class ExtractionPool {
2082
3226
  public:
2083
3227
  void Initialize(Napi::Env env)
@@ -2244,9 +3388,19 @@ private:
2244
3388
  Napi::Value Open(const Napi::CallbackInfo& info)
2245
3389
  {
2246
3390
  const auto env = info.Env();
2247
- if (info.Length() != 3 || !info[0].IsString()
2248
- || (!info[1].IsUndefined() && !info[1].IsString()) || !info[2].IsString()) {
2249
- throw Napi::TypeError::New(env, "open(path, password?, enginePath) expects path, optional password, and engine path");
3391
+ if (info.Length() != 4 || !info[0].IsString()
3392
+ || (!info[1].IsUndefined() && !info[1].IsString())
3393
+ || !info[2].IsString() || !info[3].IsNumber()) {
3394
+ throw Napi::TypeError::New(
3395
+ env, "open(path, password?, enginePath, deflateBufferSize) expects a path, optional password, engine path, and buffer size");
3396
+ }
3397
+ const double deflateBufferSizeValue = info[3].As<Napi::Number>().DoubleValue();
3398
+ if (!std::isfinite(deflateBufferSizeValue)
3399
+ || std::floor(deflateBufferSizeValue) != deflateBufferSizeValue
3400
+ || deflateBufferSizeValue < kMinDeflateBufferSize
3401
+ || deflateBufferSizeValue > kMaxDeflateBufferSize) {
3402
+ throw Napi::RangeError::New(
3403
+ env, "Deflate buffer size must be an integer between 4096 and 2097152 bytes");
2250
3404
  }
2251
3405
  std::optional<std::string> password;
2252
3406
  if (info[1].IsString()) {
@@ -2264,7 +3418,8 @@ Napi::Value Open(const Napi::CallbackInfo& info)
2264
3418
  deferred,
2265
3419
  info[0].As<Napi::String>().Utf8Value(),
2266
3420
  std::move(password),
2267
- info[2].As<Napi::String>().Utf8Value());
3421
+ info[2].As<Napi::String>().Utf8Value(),
3422
+ static_cast<std::uint32_t>(deflateBufferSizeValue));
2268
3423
  worker->Queue();
2269
3424
  return deferred.Promise();
2270
3425
  }
@@ -2300,13 +3455,19 @@ Napi::Value CreateArchive(const Napi::CallbackInfo& info)
2300
3455
  const auto item = value.As<Napi::Object>();
2301
3456
  const auto sourcePath = item.Get("sourcePath");
2302
3457
  const auto archivePath = item.Get("path");
2303
- if (!sourcePath.IsString() || !archivePath.IsString()) {
3458
+ const auto mtimeMs = item.Get("mtimeMs");
3459
+ if (!sourcePath.IsString() || !archivePath.IsString() || !mtimeMs.IsNumber()) {
2304
3460
  throw Napi::TypeError::New(
2305
- env, "Archive creation entries require string sourcePath and path values");
3461
+ env, "Archive creation entries require string sourcePath, path, and numeric mtimeMs values");
3462
+ }
3463
+ const double sourceMtimeMs = mtimeMs.As<Napi::Number>().DoubleValue();
3464
+ if (!std::isfinite(sourceMtimeMs)) {
3465
+ throw Napi::TypeError::New(env, "Archive creation entry mtimeMs must be finite");
2306
3466
  }
2307
3467
  entries.push_back({
2308
3468
  sourcePath.As<Napi::String>().Utf8Value(),
2309
3469
  archivePath.As<Napi::String>().Utf8Value(),
3470
+ sourceMtimeMs,
2310
3471
  });
2311
3472
  }
2312
3473
  if (!ReserveOperation()) {
@@ -2368,9 +3529,8 @@ Napi::Value QueueExtraction(
2368
3529
  Napi::Promise::Deferred deferred,
2369
3530
  std::shared_ptr<ArchiveEngine> archive,
2370
3531
  std::vector<std::uint32_t> ids,
2371
- bool partial,
2372
- std::uint64_t offset,
2373
- std::uint64_t length)
3532
+ std::vector<ReadRange> ranges,
3533
+ bool singleOutput)
2374
3534
  {
2375
3535
  if (!ReserveOperation()) {
2376
3536
  throw Napi::Error::New(env, "Native operation queue is full");
@@ -2381,7 +3541,7 @@ Napi::Value QueueExtraction(
2381
3541
  throw std::runtime_error("Native extraction pool is not initialized");
2382
3542
  }
2383
3543
  worker = new ExtractWorker(
2384
- deferred, std::move(archive), std::move(ids), partial, offset, length);
3544
+ deferred, std::move(archive), std::move(ids), std::move(ranges), singleOutput);
2385
3545
  g_extractionPool->Queue(env, worker);
2386
3546
  } catch (const std::exception& error) {
2387
3547
  if (worker != nullptr) {
@@ -2424,7 +3584,7 @@ Napi::Value Extract(const Napi::CallbackInfo& info)
2424
3584
  }
2425
3585
  auto deferred = Napi::Promise::Deferred::New(env);
2426
3586
  return QueueExtraction(
2427
- env, deferred, std::move(archive), std::move(ids), false, 0, 0);
3587
+ env, deferred, std::move(archive), std::move(ids), {}, false);
2428
3588
  }
2429
3589
 
2430
3590
  Napi::Value StartStream(const Napi::CallbackInfo& info)
@@ -2491,6 +3651,128 @@ Napi::Value StartStream(const Napi::CallbackInfo& info)
2491
3651
  }
2492
3652
  }
2493
3653
 
3654
+ Napi::Value StartExtractToFiles(const Napi::CallbackInfo& info)
3655
+ {
3656
+ const auto env = info.Env();
3657
+ if (info.Length() < 2
3658
+ || info.Length() > 3
3659
+ || !info[1].IsArray()
3660
+ || (info.Length() == 3 && !info[2].IsUndefined() && !info[2].IsFunction())) {
3661
+ throw Napi::TypeError::New(
3662
+ env, "startExtractToFiles(handle, files, fileCompleteCallback?) expects a handle, file list, and an optional file-completion callback");
3663
+ }
3664
+ const auto handle = GetHandle(info[0]);
3665
+ auto archive = FindArchive(handle);
3666
+ if (!archive) {
3667
+ throw Napi::Error::New(env, "Archive is closed");
3668
+ }
3669
+ const auto inputFiles = info[1].As<Napi::Array>();
3670
+ if (inputFiles.Length() > kMaxEntries) {
3671
+ throw Napi::RangeError::New(env, "Folder extraction batch exceeds the native entry limit");
3672
+ }
3673
+ std::vector<NativeFileOutputSpec> outputs;
3674
+ outputs.reserve(inputFiles.Length());
3675
+ for (std::uint32_t index = 0; index < inputFiles.Length(); ++index) {
3676
+ const auto value = inputFiles.Get(index);
3677
+ if (!value.IsObject() || value.IsNull()) {
3678
+ throw Napi::TypeError::New(env, "Folder extraction files must be objects");
3679
+ }
3680
+ const auto file = value.As<Napi::Object>();
3681
+ std::uint64_t id = 0;
3682
+ const auto path = file.Get("path");
3683
+ const auto overwrite = file.Get("overwrite");
3684
+ if (!ReadUnsigned(file.Get("id"), id)
3685
+ || id > std::numeric_limits<std::uint32_t>::max()
3686
+ || !path.IsString()
3687
+ || !overwrite.IsBoolean()) {
3688
+ throw Napi::TypeError::New(
3689
+ env, "Folder extraction files require a valid entry ID, output path, and overwrite flag");
3690
+ }
3691
+ outputs.push_back({
3692
+ static_cast<std::uint32_t>(id),
3693
+ path.As<Napi::String>().Utf8Value(),
3694
+ overwrite.As<Napi::Boolean>().Value(),
3695
+ });
3696
+ }
3697
+ if (g_extractionPool == nullptr) {
3698
+ throw Napi::Error::New(env, "Native extraction pool is not initialized");
3699
+ }
3700
+ if (!ReserveOperation()) {
3701
+ throw Napi::Error::New(env, "Native operation queue is full");
3702
+ }
3703
+
3704
+ std::shared_ptr<NativeFileTaskState> task;
3705
+ ExtractToFilesWorker *worker = nullptr;
3706
+ std::uint64_t taskId = 0;
3707
+ try {
3708
+ task = std::make_shared<NativeFileTaskState>();
3709
+ if (info.Length() == 3 && info[2].IsFunction()) {
3710
+ task->fileCompletionCallback = Napi::ThreadSafeFunction::New(
3711
+ env,
3712
+ info[2].As<Napi::Function>(),
3713
+ "7zip-native folder file completion",
3714
+ 1,
3715
+ 1);
3716
+ task->hasFileCompletionCallback_ = true;
3717
+ }
3718
+ taskId = RegisterFileTask(task);
3719
+ auto deferred = Napi::Promise::Deferred::New(env);
3720
+ auto result = Napi::Object::New(env);
3721
+ result.Set("taskId", Napi::BigInt::New(env, taskId));
3722
+ result.Set("promise", deferred.Promise());
3723
+ worker = new ExtractToFilesWorker(
3724
+ deferred,
3725
+ std::move(archive),
3726
+ std::move(outputs),
3727
+ task);
3728
+ g_extractionPool->Queue(env, worker);
3729
+ worker = nullptr;
3730
+ return result;
3731
+ } catch (const std::exception& error) {
3732
+ if (worker != nullptr) {
3733
+ delete worker;
3734
+ } else {
3735
+ if (taskId != 0) {
3736
+ RemoveFileTask(taskId);
3737
+ }
3738
+ if (task) {
3739
+ task->Cancel();
3740
+ task->ReleaseCallback();
3741
+ }
3742
+ g_pendingOperations.fetch_sub(1, std::memory_order_acq_rel);
3743
+ }
3744
+ throw Napi::Error::New(env, error.what());
3745
+ } catch (...) {
3746
+ if (worker != nullptr) {
3747
+ delete worker;
3748
+ } else {
3749
+ if (taskId != 0) {
3750
+ RemoveFileTask(taskId);
3751
+ }
3752
+ if (task) {
3753
+ task->Cancel();
3754
+ task->ReleaseCallback();
3755
+ }
3756
+ g_pendingOperations.fetch_sub(1, std::memory_order_acq_rel);
3757
+ }
3758
+ throw Napi::Error::New(env, "Unable to queue native folder extraction");
3759
+ }
3760
+ }
3761
+
3762
+ Napi::Value CancelExtractToFiles(const Napi::CallbackInfo& info)
3763
+ {
3764
+ const auto env = info.Env();
3765
+ std::uint64_t taskId = 0;
3766
+ if (info.Length() != 1 || !ReadUnsigned(info[0], taskId) || taskId == 0) {
3767
+ throw Napi::TypeError::New(
3768
+ env, "cancelExtractToFiles(taskId) expects a positive task ID");
3769
+ }
3770
+ if (const auto task = FindFileTask(taskId)) {
3771
+ task->Cancel();
3772
+ }
3773
+ return env.Undefined();
3774
+ }
3775
+
2494
3776
  Napi::Value ResumeStream(const Napi::CallbackInfo& info)
2495
3777
  {
2496
3778
  const auto env = info.Env();
@@ -2544,9 +3826,52 @@ Napi::Value Read(const Napi::CallbackInfo& info)
2544
3826
  deferred,
2545
3827
  std::move(archive),
2546
3828
  {static_cast<std::uint32_t>(id)},
2547
- true,
2548
- offset,
2549
- length);
3829
+ {{offset, length}},
3830
+ true);
3831
+ }
3832
+
3833
+ Napi::Value ReadMany(const Napi::CallbackInfo& info)
3834
+ {
3835
+ const auto env = info.Env();
3836
+ if (info.Length() != 2 || !info[1].IsArray()) {
3837
+ throw Napi::TypeError::New(env, "readMany(handle, requests) expects an array of read requests");
3838
+ }
3839
+ const auto handle = GetHandle(info[0]);
3840
+ auto archive = FindArchive(handle);
3841
+ if (!archive) {
3842
+ throw Napi::Error::New(env, "Archive is closed");
3843
+ }
3844
+ const auto inputRequests = info[1].As<Napi::Array>();
3845
+ if (inputRequests.Length() > kMaxEntries) {
3846
+ throw Napi::RangeError::New(env, "Batch read exceeds the native entry limit");
3847
+ }
3848
+ std::vector<std::uint32_t> ids;
3849
+ std::vector<ReadRange> ranges;
3850
+ ids.reserve(inputRequests.Length());
3851
+ ranges.reserve(inputRequests.Length());
3852
+ for (std::uint32_t i = 0; i < inputRequests.Length(); ++i) {
3853
+ const auto value = inputRequests.Get(i);
3854
+ if (!value.IsObject() || value.IsNull()) {
3855
+ throw Napi::TypeError::New(env, "Batch read requests must be objects");
3856
+ }
3857
+ const auto request = value.As<Napi::Object>();
3858
+ std::uint64_t id = 0;
3859
+ std::uint64_t offset = 0;
3860
+ std::uint64_t length = 0;
3861
+ if (!ReadUnsigned(request.Get("id"), id) || id > std::numeric_limits<std::uint32_t>::max()
3862
+ || !ReadUnsigned(request.Get("offset"), offset) || !ReadUnsigned(request.Get("length"), length)) {
3863
+ throw Napi::TypeError::New(
3864
+ env, "Batch read requests require a valid entry ID, offset, and length");
3865
+ }
3866
+ if (length > kMaxReadBytes || offset > std::numeric_limits<std::uint64_t>::max() - length) {
3867
+ throw Napi::RangeError::New(env, "Requested range exceeds the native read limit");
3868
+ }
3869
+ ids.push_back(static_cast<std::uint32_t>(id));
3870
+ ranges.push_back({offset, length});
3871
+ }
3872
+ auto deferred = Napi::Promise::Deferred::New(env);
3873
+ return QueueExtraction(
3874
+ env, deferred, std::move(archive), std::move(ids), std::move(ranges), false);
2550
3875
  }
2551
3876
 
2552
3877
  Napi::Value Concurrency(const Napi::CallbackInfo& info)
@@ -2610,7 +3935,10 @@ Napi::Object Init(Napi::Env env, Napi::Object exports)
2610
3935
  exports.Set("create", Napi::Function::New(env, CreateArchive));
2611
3936
  exports.Set("entries", Napi::Function::New(env, Entries));
2612
3937
  exports.Set("extract", Napi::Function::New(env, Extract));
3938
+ exports.Set("readMany", Napi::Function::New(env, ReadMany));
2613
3939
  exports.Set("startStream", Napi::Function::New(env, StartStream));
3940
+ exports.Set("startExtractToFiles", Napi::Function::New(env, StartExtractToFiles));
3941
+ exports.Set("cancelExtractToFiles", Napi::Function::New(env, CancelExtractToFiles));
2614
3942
  exports.Set("resumeStream", Napi::Function::New(env, ResumeStream));
2615
3943
  exports.Set("cancelStream", Napi::Function::New(env, CancelStream));
2616
3944
  exports.Set("read", Napi::Function::New(env, Read));