7z-iterator 2.0.4 → 2.1.1

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 (46) hide show
  1. package/dist/cjs/FileEntry.js +2 -2
  2. package/dist/cjs/FileEntry.js.map +1 -1
  3. package/dist/cjs/SevenZipIterator.js +18 -13
  4. package/dist/cjs/SevenZipIterator.js.map +1 -1
  5. package/dist/cjs/lib/runDecode.d.cts +5 -0
  6. package/dist/cjs/lib/runDecode.d.ts +5 -0
  7. package/dist/cjs/lib/runDecode.js +55 -0
  8. package/dist/cjs/lib/runDecode.js.map +1 -0
  9. package/dist/cjs/lib/streamToSource.js +5 -5
  10. package/dist/cjs/lib/streamToSource.js.map +1 -1
  11. package/dist/cjs/sevenz/SevenZipParser.d.cts +12 -1
  12. package/dist/cjs/sevenz/SevenZipParser.d.ts +12 -1
  13. package/dist/cjs/sevenz/SevenZipParser.js +325 -217
  14. package/dist/cjs/sevenz/SevenZipParser.js.map +1 -1
  15. package/dist/cjs/sevenz/codecs/createBufferingDecoder.d.cts +2 -1
  16. package/dist/cjs/sevenz/codecs/createBufferingDecoder.d.ts +2 -1
  17. package/dist/cjs/sevenz/codecs/createBufferingDecoder.js +24 -4
  18. package/dist/cjs/sevenz/codecs/createBufferingDecoder.js.map +1 -1
  19. package/dist/cjs/sevenz/codecs/index.d.cts +2 -1
  20. package/dist/cjs/sevenz/codecs/index.d.ts +2 -1
  21. package/dist/cjs/sevenz/codecs/index.js +28 -16
  22. package/dist/cjs/sevenz/codecs/index.js.map +1 -1
  23. package/dist/cjs/sevenz/index.d.cts +1 -1
  24. package/dist/cjs/sevenz/index.d.ts +1 -1
  25. package/dist/cjs/sevenz/index.js.map +1 -1
  26. package/dist/esm/FileEntry.js +1 -1
  27. package/dist/esm/FileEntry.js.map +1 -1
  28. package/dist/esm/SevenZipIterator.js +14 -9
  29. package/dist/esm/SevenZipIterator.js.map +1 -1
  30. package/dist/esm/lib/runDecode.d.ts +5 -0
  31. package/dist/esm/lib/runDecode.js +29 -0
  32. package/dist/esm/lib/runDecode.js.map +1 -0
  33. package/dist/esm/lib/streamToSource.js +1 -1
  34. package/dist/esm/lib/streamToSource.js.map +1 -1
  35. package/dist/esm/sevenz/SevenZipParser.d.ts +12 -1
  36. package/dist/esm/sevenz/SevenZipParser.js +308 -218
  37. package/dist/esm/sevenz/SevenZipParser.js.map +1 -1
  38. package/dist/esm/sevenz/codecs/createBufferingDecoder.d.ts +2 -1
  39. package/dist/esm/sevenz/codecs/createBufferingDecoder.js +19 -4
  40. package/dist/esm/sevenz/codecs/createBufferingDecoder.js.map +1 -1
  41. package/dist/esm/sevenz/codecs/index.d.ts +2 -1
  42. package/dist/esm/sevenz/codecs/index.js +28 -16
  43. package/dist/esm/sevenz/codecs/index.js.map +1 -1
  44. package/dist/esm/sevenz/index.d.ts +1 -1
  45. package/dist/esm/sevenz/index.js.map +1 -1
  46. package/package.json +3 -2
@@ -16,7 +16,8 @@
16
16
  * - Solid archives: multiple files share one folder (decompress once)
17
17
  * - Non-solid: one file per folder
18
18
  * - Supports LZMA, LZMA2, COPY, BCJ2, and other codecs
19
- */ import { crc32, PassThrough } from 'extract-base-iterator';
19
+ */ import once from 'call-once-fn';
20
+ import { crc32, PassThrough } from 'extract-base-iterator';
20
21
  import { defer } from '../lib/defer.js';
21
22
  import { decodeBcj2Multi, getCodec, getCodecName, isBcj2Codec, isCodecSupported } from './codecs/index.js';
22
23
  import { FolderStreamSplitter } from './FolderStreamSplitter.js';
@@ -28,126 +29,187 @@ export { BufferSource, FileSource } from './ArchiveSource.js';
28
29
  /**
29
30
  * SevenZipParser - parses 7z archives and provides entry iteration
30
31
  */ export class SevenZipParser {
32
+ decodeWithCodec(codec, input, properties, unpackSize, callback) {
33
+ const done = once(callback);
34
+ try {
35
+ codec.decode(input, properties, unpackSize, (err, result)=>{
36
+ if (err) return done(err);
37
+ if (!result) return done(createCodedError('Decoder returned no data', ErrorCode.DECOMPRESSION_FAILED));
38
+ done(null, result);
39
+ });
40
+ } catch (err) {
41
+ done(err);
42
+ }
43
+ }
31
44
  /**
32
45
  * Parse the archive structure
33
46
  * Must be called before iterating entries
34
- */ parse() {
35
- if (this.parsed) return;
36
- // Read signature header
37
- const sigBuf = this.source.read(0, SIGNATURE_HEADER_SIZE);
38
- if (sigBuf.length < SIGNATURE_HEADER_SIZE) {
39
- throw createCodedError('Archive too small', ErrorCode.TRUNCATED_ARCHIVE);
40
- }
41
- this.signature = parseSignatureHeader(sigBuf);
42
- // Read encoded header
43
- const headerOffset = SIGNATURE_HEADER_SIZE + this.signature.nextHeaderOffset;
44
- const headerBuf = this.source.read(headerOffset, this.signature.nextHeaderSize);
45
- if (headerBuf.length < this.signature.nextHeaderSize) {
46
- throw createCodedError('Truncated header', ErrorCode.TRUNCATED_ARCHIVE);
47
- }
48
- // Parse encoded header (may need decompression)
47
+ */ parse(callback) {
48
+ if (this.parsed) {
49
+ if (typeof callback === 'function') {
50
+ callback(null);
51
+ return;
52
+ }
53
+ if (typeof Promise === 'undefined') {
54
+ return;
55
+ }
56
+ return Promise.resolve();
57
+ }
58
+ const executor = (done)=>{
59
+ this.parseInternal(done);
60
+ };
61
+ if (typeof callback === 'function') {
62
+ executor(callback);
63
+ return;
64
+ }
65
+ if (typeof Promise === 'undefined') {
66
+ throw new Error('Promises are not available in this runtime. Please provide a callback to parse().');
67
+ }
68
+ return new Promise((resolve, reject)=>{
69
+ executor((err)=>{
70
+ if (err) {
71
+ reject(err);
72
+ return;
73
+ }
74
+ resolve();
75
+ });
76
+ });
77
+ }
78
+ parseInternal(callback) {
79
+ if (this.parsed) {
80
+ callback(null);
81
+ return;
82
+ }
83
+ let signature;
84
+ let headerBuf;
49
85
  try {
50
- const headerResult = parseEncodedHeader(headerBuf, this.signature.nextHeaderCRC);
86
+ const sigBuf = this.source.read(0, SIGNATURE_HEADER_SIZE);
87
+ if (sigBuf.length < SIGNATURE_HEADER_SIZE) {
88
+ callback(createCodedError('Archive too small', ErrorCode.TRUNCATED_ARCHIVE));
89
+ return;
90
+ }
91
+ signature = parseSignatureHeader(sigBuf);
92
+ this.signature = signature;
93
+ const headerOffset = SIGNATURE_HEADER_SIZE + signature.nextHeaderOffset;
94
+ headerBuf = this.source.read(headerOffset, signature.nextHeaderSize);
95
+ if (headerBuf.length < signature.nextHeaderSize) {
96
+ callback(createCodedError('Truncated header', ErrorCode.TRUNCATED_ARCHIVE));
97
+ return;
98
+ }
99
+ } catch (err) {
100
+ callback(err);
101
+ return;
102
+ }
103
+ const finalize = ()=>{
104
+ try {
105
+ this.buildEntries();
106
+ this.parsed = true;
107
+ callback(null);
108
+ } catch (err) {
109
+ callback(err);
110
+ }
111
+ };
112
+ try {
113
+ var _ref;
114
+ var _this_signature;
115
+ const headerResult = parseEncodedHeader(headerBuf, (_ref = (_this_signature = this.signature) === null || _this_signature === void 0 ? void 0 : _this_signature.nextHeaderCRC) !== null && _ref !== void 0 ? _ref : 0);
51
116
  this.streamsInfo = headerResult.streamsInfo || null;
52
117
  this.filesInfo = headerResult.filesInfo;
118
+ finalize();
53
119
  } catch (err) {
54
120
  const codedErr = err;
55
121
  if (codedErr && codedErr.code === ErrorCode.COMPRESSED_HEADER) {
56
- // Header is compressed - need to decompress first
57
- this.handleCompressedHeader(headerBuf);
122
+ this.handleCompressedHeader(headerBuf, (headerErr)=>{
123
+ if (headerErr) {
124
+ callback(headerErr);
125
+ return;
126
+ }
127
+ finalize();
128
+ });
58
129
  } else {
59
- throw err;
130
+ callback(err);
60
131
  }
61
132
  }
62
- // Build entries list
63
- this.buildEntries();
64
- this.parsed = true;
65
133
  }
66
134
  /**
67
135
  * Handle compressed header (kEncodedHeader)
68
- */ handleCompressedHeader(headerBuf) {
136
+ */ handleCompressedHeader(headerBuf, callback) {
69
137
  // Parse the encoded header info to get decompression parameters
70
138
  let offset = 1; // Skip kEncodedHeader byte
71
- // Should have StreamsInfo for the header itself
72
139
  const propertyId = headerBuf[offset++];
73
140
  if (propertyId !== PropertyId.kMainStreamsInfo && propertyId !== PropertyId.kPackInfo) {
74
- throw createCodedError('Expected StreamsInfo in encoded header', ErrorCode.CORRUPT_HEADER);
75
- }
76
- // For now, we parse the streams info from the encoded header block
77
- // This tells us how to decompress the actual header
78
- // Read pack info from the encoded header structure
79
- const packInfoResult = this.parseEncodedHeaderStreams(headerBuf, 1);
80
- // Calculate compressed header position
81
- // For simple archives: header is at SIGNATURE_HEADER_SIZE + packPos
82
- // For BCJ2/complex archives: header may be at the END of pack data area
83
- // The pack data area ends at nextHeaderOffset (where encoded header starts)
84
- const compressedStart = SIGNATURE_HEADER_SIZE + packInfoResult.packPos;
85
- const compressedData = this.source.read(compressedStart, packInfoResult.packSize);
86
- // Decompress using the specified codec
87
- const codec = getCodec(packInfoResult.codecId);
88
- let decompressedHeader = null;
89
- // Try decompressing from the calculated position first
141
+ callback(createCodedError('Expected StreamsInfo in encoded header', ErrorCode.CORRUPT_HEADER));
142
+ return;
143
+ }
144
+ let packInfoResult;
90
145
  try {
91
- decompressedHeader = codec.decode(compressedData, packInfoResult.properties, packInfoResult.unpackSize);
92
- // Verify CRC if present
93
- if (packInfoResult.unpackCRC !== undefined) {
94
- const actualCRC = crc32(decompressedHeader);
95
- if (actualCRC !== packInfoResult.unpackCRC) {
96
- decompressedHeader = null; // CRC mismatch, need to search
97
- }
98
- }
99
- } catch {
100
- decompressedHeader = null; // Decompression failed, need to search
146
+ packInfoResult = this.parseEncodedHeaderStreams(headerBuf, 1);
147
+ } catch (err) {
148
+ callback(err);
149
+ return;
101
150
  }
102
- // If initial decompression failed, search for the correct position as a fallback
103
- // This handles edge cases where packPos doesn't point directly to header pack data
104
- if (decompressedHeader === null && this.signature) {
151
+ const codec = getCodec(packInfoResult.codecId);
152
+ const candidates = [];
153
+ const compressedStart = SIGNATURE_HEADER_SIZE + packInfoResult.packPos;
154
+ candidates.push(this.source.read(compressedStart, packInfoResult.packSize));
155
+ if (this.signature) {
105
156
  const packAreaEnd = SIGNATURE_HEADER_SIZE + this.signature.nextHeaderOffset;
106
157
  const searchStart = packAreaEnd - packInfoResult.packSize;
107
158
  const searchEnd = Math.max(SIGNATURE_HEADER_SIZE, compressedStart - 100000);
108
- // Scan for LZMA data starting with 0x00 (range coder init)
109
- // Try each candidate and validate with CRC
110
159
  const scanChunkSize = 4096;
111
- searchLoop: for(let chunkStart = searchStart; chunkStart >= searchEnd; chunkStart -= scanChunkSize){
160
+ for(let chunkStart = searchStart; chunkStart >= searchEnd; chunkStart -= scanChunkSize){
112
161
  const chunk = this.source.read(chunkStart, scanChunkSize + packInfoResult.packSize);
113
- for(let i = 0; i < Math.min(chunk.length, scanChunkSize); i++){
162
+ const limit = Math.min(chunk.length, scanChunkSize);
163
+ for(let i = 0; i < limit; i++){
114
164
  if (chunk[i] === 0x00) {
115
- const candidateData = chunk.subarray(i, i + packInfoResult.packSize);
116
- if (candidateData.length === packInfoResult.packSize) {
117
- try {
118
- const candidateDecompressed = codec.decode(candidateData, packInfoResult.properties, packInfoResult.unpackSize);
119
- if (packInfoResult.unpackCRC !== undefined) {
120
- const candCRC = crc32(candidateDecompressed);
121
- if (candCRC === packInfoResult.unpackCRC) {
122
- decompressedHeader = candidateDecompressed;
123
- break searchLoop;
124
- }
125
- } else {
126
- decompressedHeader = candidateDecompressed;
127
- break searchLoop;
128
- }
129
- } catch {
130
- // Decompression failed, continue searching
165
+ const end = i + packInfoResult.packSize;
166
+ if (end <= chunk.length) {
167
+ const candidateData = chunk.slice(i, end);
168
+ if (candidateData.length === packInfoResult.packSize) {
169
+ candidates.push(candidateData);
131
170
  }
132
171
  }
133
172
  }
134
173
  }
135
174
  }
136
175
  }
137
- if (decompressedHeader === null) {
138
- throw createCodedError('Failed to decompress header - could not find valid LZMA data', ErrorCode.CORRUPT_HEADER);
139
- }
140
- // Now parse the decompressed header
141
- // It should start with kHeader
176
+ const tryCandidate = (index)=>{
177
+ if (index >= candidates.length) {
178
+ callback(createCodedError('Failed to decompress header - could not find valid LZMA data', ErrorCode.CORRUPT_HEADER));
179
+ return;
180
+ }
181
+ this.decodeWithCodec(codec, candidates[index], packInfoResult.properties, packInfoResult.unpackSize, (err, decompressed)=>{
182
+ if (err || !decompressed) {
183
+ tryCandidate(index + 1);
184
+ return;
185
+ }
186
+ if (packInfoResult.unpackCRC !== undefined) {
187
+ const actualCRC = crc32(decompressed);
188
+ if (actualCRC !== packInfoResult.unpackCRC) {
189
+ tryCandidate(index + 1);
190
+ return;
191
+ }
192
+ }
193
+ this.parseDecompressedHeader(decompressed, callback);
194
+ });
195
+ };
196
+ tryCandidate(0);
197
+ }
198
+ parseDecompressedHeader(decompressedHeader, callback) {
142
199
  let decompOffset = 0;
143
200
  const headerId = decompressedHeader[decompOffset++];
144
201
  if (headerId !== PropertyId.kHeader) {
145
- throw createCodedError('Expected kHeader in decompressed header', ErrorCode.CORRUPT_HEADER);
202
+ callback(createCodedError('Expected kHeader in decompressed header', ErrorCode.CORRUPT_HEADER));
203
+ return;
204
+ }
205
+ try {
206
+ const result = parseHeaderContent(decompressedHeader, decompOffset);
207
+ this.streamsInfo = result.streamsInfo || null;
208
+ this.filesInfo = result.filesInfo;
209
+ callback(null);
210
+ } catch (err) {
211
+ callback(err);
146
212
  }
147
- // Parse the decompressed header using shared function from headers.ts
148
- const result = parseHeaderContent(decompressedHeader, decompOffset);
149
- this.streamsInfo = result.streamsInfo || null;
150
- this.filesInfo = result.filesInfo;
151
213
  }
152
214
  /**
153
215
  * Parse streams info from encoded header block
@@ -349,7 +411,7 @@ export { BufferSource, FileSource } from './ArchiveSource.js';
349
411
  * Get the list of entries
350
412
  */ getEntries() {
351
413
  if (!this.parsed) {
352
- this.parse();
414
+ throw new Error('SevenZipParser has not been parsed yet. Call parse(callback) before accessing entries.');
353
415
  }
354
416
  return this.entries;
355
417
  }
@@ -472,39 +534,43 @@ export { BufferSource, FileSource } from './ArchiveSource.js';
472
534
  started = true;
473
535
  defer(()=>{
474
536
  if (destroyed) return;
475
- try {
476
- const data = this.getDecompressedFolder(folderIdx);
477
- let fileStart = 0;
478
- for(let m = 0; m < entry._streamIndexInFolder; m++){
479
- const prevStreamGlobalIndex = entry._streamIndex - entry._streamIndexInFolder + m;
480
- fileStart += streamsInfo.unpackSizes[prevStreamGlobalIndex];
481
- }
482
- const fileSize = entry.size;
483
- if (fileStart + fileSize > data.length) {
484
- stream.destroy(createCodedError(`File data out of bounds: offset ${fileStart} + size ${fileSize} > decompressed length ${data.length}`, ErrorCode.DECOMPRESSION_FAILED));
537
+ this.getDecompressedFolder(folderIdx, (err, data)=>{
538
+ if (destroyed) return;
539
+ if (err || !data) {
540
+ stream.destroy(err || createCodedError('Unable to decompress folder', ErrorCode.DECOMPRESSION_FAILED));
485
541
  return;
486
542
  }
487
- const fileData = data.slice(fileStart, fileStart + fileSize);
488
- if (entry._crc !== undefined) {
489
- const actualCRC = crc32(fileData);
490
- if (actualCRC !== entry._crc) {
491
- stream.destroy(createCodedError(`CRC mismatch for ${entry.path}: expected ${entry._crc.toString(16)}, got ${actualCRC.toString(16)}`, ErrorCode.CRC_MISMATCH));
543
+ try {
544
+ let fileStart = 0;
545
+ for(let m = 0; m < entry._streamIndexInFolder; m++){
546
+ const prevStreamGlobalIndex = entry._streamIndex - entry._streamIndexInFolder + m;
547
+ fileStart += streamsInfo.unpackSizes[prevStreamGlobalIndex];
548
+ }
549
+ const fileSize = entry.size;
550
+ if (fileStart + fileSize > data.length) {
551
+ stream.destroy(createCodedError(`File data out of bounds: offset ${fileStart} + size ${fileSize} > decompressed length ${data.length}`, ErrorCode.DECOMPRESSION_FAILED));
492
552
  return;
493
553
  }
554
+ const fileData = data.slice(fileStart, fileStart + fileSize);
555
+ if (entry._crc !== undefined) {
556
+ const actualCRC = crc32(fileData);
557
+ if (actualCRC !== entry._crc) {
558
+ stream.destroy(createCodedError(`CRC mismatch for ${entry.path}: expected ${entry._crc.toString(16)}, got ${actualCRC.toString(16)}`, ErrorCode.CRC_MISMATCH));
559
+ return;
560
+ }
561
+ }
562
+ this.extractedPerFolder[folderIdx] = (this.extractedPerFolder[folderIdx] || 0) + 1;
563
+ if (this.extractedPerFolder[folderIdx] >= this.filesPerFolder[folderIdx]) {
564
+ delete this.decompressedCache[folderIdx];
565
+ }
566
+ if (!destroyed) {
567
+ stream.push(fileData);
568
+ stream.push(null);
569
+ }
570
+ } catch (decodeErr) {
571
+ stream.destroy(decodeErr);
494
572
  }
495
- this.extractedPerFolder[folderIdx] = (this.extractedPerFolder[folderIdx] || 0) + 1;
496
- if (this.extractedPerFolder[folderIdx] >= this.filesPerFolder[folderIdx]) {
497
- delete this.decompressedCache[folderIdx];
498
- }
499
- if (!destroyed) {
500
- stream.push(fileData);
501
- stream.push(null);
502
- }
503
- } catch (err) {
504
- if (!destroyed) {
505
- stream.destroy(err);
506
- }
507
- }
573
+ });
508
574
  });
509
575
  }
510
576
  return originalRead(size);
@@ -536,97 +602,137 @@ export { BufferSource, FileSource } from './ArchiveSource.js';
536
602
  /**
537
603
  * Get decompressed data for a folder, with smart caching for solid archives
538
604
  * Only caches when multiple files share a block, releases when last file extracted
539
- */ getDecompressedFolder(folderIndex) {
540
- // Check cache first
605
+ */ getDecompressedFolder(folderIndex, callback) {
541
606
  if (this.decompressedCache[folderIndex]) {
542
- return this.decompressedCache[folderIndex];
607
+ callback(null, this.decompressedCache[folderIndex]);
608
+ return;
609
+ }
610
+ if (this.pendingFolders[folderIndex]) {
611
+ this.pendingFolders[folderIndex].push(callback);
612
+ return;
543
613
  }
544
614
  if (!this.streamsInfo) {
545
- throw createCodedError('No streams info available', ErrorCode.CORRUPT_HEADER);
615
+ callback(createCodedError('No streams info available', ErrorCode.CORRUPT_HEADER));
616
+ return;
546
617
  }
547
- const folder = this.streamsInfo.folders[folderIndex];
548
- // Check how many files remain in this folder
618
+ this.pendingFolders[folderIndex] = [
619
+ callback
620
+ ];
621
+ this.decodeFolderData(folderIndex, (err, data)=>{
622
+ const waiters = this.pendingFolders[folderIndex] || [];
623
+ delete this.pendingFolders[folderIndex];
624
+ if (err || !data) {
625
+ for(let i = 0; i < waiters.length; i++){
626
+ waiters[i](err || createCodedError('Decoder returned no data', ErrorCode.DECOMPRESSION_FAILED));
627
+ }
628
+ return;
629
+ }
630
+ if (this.shouldCacheFolder(folderIndex)) {
631
+ this.decompressedCache[folderIndex] = data;
632
+ }
633
+ for(let i = 0; i < waiters.length; i++){
634
+ waiters[i](null, data);
635
+ }
636
+ });
637
+ }
638
+ shouldCacheFolder(folderIndex) {
549
639
  const filesInFolder = this.filesPerFolder[folderIndex] || 1;
550
640
  const extractedFromFolder = this.extractedPerFolder[folderIndex] || 0;
551
- const remainingFiles = filesInFolder - extractedFromFolder;
552
- // Only cache if more than 1 file remains (including the current one being extracted)
553
- const shouldCache = remainingFiles > 1;
554
- // Check if this folder uses BCJ2 (requires special multi-stream handling)
641
+ return filesInFolder - extractedFromFolder > 1;
642
+ }
643
+ decodeFolderData(folderIndex, callback) {
644
+ if (!this.streamsInfo) {
645
+ callback(createCodedError('No streams info available', ErrorCode.CORRUPT_HEADER));
646
+ return;
647
+ }
648
+ const folder = this.streamsInfo.folders[folderIndex];
649
+ if (!folder) {
650
+ callback(createCodedError('Invalid folder index', ErrorCode.CORRUPT_HEADER));
651
+ return;
652
+ }
555
653
  if (this.folderHasBcj2(folder)) {
556
- const data = this.decompressBcj2Folder(folderIndex);
557
- if (shouldCache) {
558
- this.decompressedCache[folderIndex] = data;
559
- }
560
- return data;
654
+ this.decompressBcj2Folder(folderIndex, callback);
655
+ return;
656
+ }
657
+ const packDataResult = this.readPackedData(folderIndex);
658
+ if (packDataResult instanceof Error) {
659
+ callback(packDataResult);
660
+ return;
661
+ }
662
+ this.decodeFolderCoders(folder, packDataResult, 0, callback);
663
+ }
664
+ readPackedData(folderIndex) {
665
+ if (!this.streamsInfo) {
666
+ return createCodedError('No streams info available', ErrorCode.CORRUPT_HEADER);
667
+ }
668
+ const folder = this.streamsInfo.folders[folderIndex];
669
+ if (!folder) {
670
+ return createCodedError('Invalid folder index', ErrorCode.CORRUPT_HEADER);
561
671
  }
562
- // Calculate packed data position
563
- // Use Math.max to prevent 32-bit signed overflow
564
672
  const signedHeaderSize = SIGNATURE_HEADER_SIZE;
565
673
  const signedPackPos = this.streamsInfo.packPos;
566
674
  let packPos = Math.max(signedHeaderSize, 0) + Math.max(signedPackPos, 0);
567
- // Find which pack stream this folder uses
568
675
  let packStreamIndex = 0;
569
676
  for(let j = 0; j < folderIndex; j++){
570
677
  packStreamIndex += this.streamsInfo.folders[j].packedStreams.length;
571
678
  }
572
- // Calculate position of this pack stream - PREVENT OVERFLOW
573
679
  for(let k = 0; k < packStreamIndex; k++){
574
680
  const size = this.streamsInfo.packSizes[k];
575
681
  if (packPos + size < packPos) {
576
- throw createCodedError(`Pack position overflow at index ${k}`, ErrorCode.CORRUPT_ARCHIVE);
682
+ return createCodedError(`Pack position overflow at index ${k}`, ErrorCode.CORRUPT_ARCHIVE);
577
683
  }
578
684
  packPos += size;
579
685
  }
580
686
  const packSize = this.streamsInfo.packSizes[packStreamIndex];
581
- // Validate pack size to prevent overflow
582
- // Upper bound is Number.MAX_SAFE_INTEGER (2^53-1 = 9PB) - safe for all realistic archives
583
687
  if (packSize < 0 || packSize > Number.MAX_SAFE_INTEGER) {
584
- throw createCodedError(`Invalid pack size: ${packSize}`, ErrorCode.CORRUPT_ARCHIVE);
688
+ return createCodedError(`Invalid pack size: ${packSize}`, ErrorCode.CORRUPT_ARCHIVE);
585
689
  }
586
690
  if (packPos < 0 || packPos > Number.MAX_SAFE_INTEGER) {
587
- throw createCodedError(`Invalid pack position: ${packPos}`, ErrorCode.CORRUPT_ARCHIVE);
588
- }
589
- // Read packed data
590
- const packedData = this.source.read(packPos, packSize);
591
- // Decompress through codec chain
592
- let data2 = packedData;
593
- for(let l = 0; l < folder.coders.length; l++){
594
- const coderInfo = folder.coders[l];
595
- const codec = getCodec(coderInfo.id);
596
- // Get unpack size for this coder (needed by LZMA)
597
- const unpackSize = folder.unpackSizes[l];
598
- // Validate unpack size to prevent overflow
599
- if (unpackSize < 0 || unpackSize > Number.MAX_SAFE_INTEGER) {
600
- throw createCodedError(`Invalid unpack size: ${unpackSize}`, ErrorCode.CORRUPT_ARCHIVE);
601
- }
602
- data2 = codec.decode(data2, coderInfo.properties, unpackSize);
691
+ return createCodedError(`Invalid pack position: ${packPos}`, ErrorCode.CORRUPT_ARCHIVE);
603
692
  }
604
- // Cache only if more files remain in this folder
605
- if (shouldCache) {
606
- this.decompressedCache[folderIndex] = data2;
693
+ return this.source.read(packPos, packSize);
694
+ }
695
+ decodeFolderCoders(folder, input, index, callback) {
696
+ if (index >= folder.coders.length) {
697
+ callback(null, input);
698
+ return;
607
699
  }
608
- return data2;
700
+ const coderInfo = folder.coders[index];
701
+ const codec = getCodec(coderInfo.id);
702
+ const unpackSize = folder.unpackSizes[index];
703
+ if (unpackSize < 0 || unpackSize > Number.MAX_SAFE_INTEGER) {
704
+ callback(createCodedError(`Invalid unpack size: ${unpackSize}`, ErrorCode.CORRUPT_ARCHIVE));
705
+ return;
706
+ }
707
+ this.decodeWithCodec(codec, input, coderInfo.properties, unpackSize, (err, output)=>{
708
+ if (err || !output) {
709
+ callback(err || createCodedError('Decoder returned no data', ErrorCode.DECOMPRESSION_FAILED));
710
+ return;
711
+ }
712
+ this.decodeFolderCoders(folder, output, index + 1, callback);
713
+ });
609
714
  }
610
715
  /**
611
716
  * Decompress a BCJ2 folder with multi-stream handling
612
717
  * BCJ2 uses 4 input streams: main, call, jump, range coder
613
- */ decompressBcj2Folder(folderIndex) {
718
+ */ decompressBcj2Folder(folderIndex, callback) {
614
719
  if (!this.streamsInfo) {
615
- throw createCodedError('No streams info available', ErrorCode.CORRUPT_HEADER);
720
+ callback(createCodedError('No streams info available', ErrorCode.CORRUPT_HEADER));
721
+ return;
616
722
  }
617
723
  const folder = this.streamsInfo.folders[folderIndex];
618
- // Calculate starting pack position
724
+ if (!folder) {
725
+ callback(createCodedError('Invalid folder index', ErrorCode.CORRUPT_HEADER));
726
+ return;
727
+ }
619
728
  let packPos = SIGNATURE_HEADER_SIZE + this.streamsInfo.packPos;
620
- // Find which pack stream index this folder starts at
621
729
  let packStreamIndex = 0;
622
730
  for(let j = 0; j < folderIndex; j++){
623
731
  packStreamIndex += this.streamsInfo.folders[j].packedStreams.length;
624
732
  }
625
- // Calculate position
626
733
  for(let k = 0; k < packStreamIndex; k++){
627
734
  packPos += this.streamsInfo.packSizes[k];
628
735
  }
629
- // Read all pack streams for this folder
630
736
  const numPackStreams = folder.packedStreams.length;
631
737
  const packStreams = [];
632
738
  let currentPos = packPos;
@@ -635,16 +741,7 @@ export { BufferSource, FileSource } from './ArchiveSource.js';
635
741
  packStreams.push(this.source.read(currentPos, size));
636
742
  currentPos += size;
637
743
  }
638
- // Build a map of coder outputs
639
- // For BCJ2, typical structure is:
640
- // Coder 0: LZMA2 (main stream) - 1 in, 1 out
641
- // Coder 1: LZMA (call stream) - 1 in, 1 out
642
- // Coder 2: LZMA (jump stream) - 1 in, 1 out
643
- // Coder 3: BCJ2 - 4 in, 1 out
644
- // Pack streams map to: coder inputs not bound to other coder outputs
645
- // First, decompress each non-BCJ2 coder
646
744
  const coderOutputs = {};
647
- // Find the BCJ2 coder
648
745
  let bcj2CoderIndex = -1;
649
746
  for(let c = 0; c < folder.coders.length; c++){
650
747
  if (isBcj2Codec(folder.coders[c].id)) {
@@ -653,56 +750,50 @@ export { BufferSource, FileSource } from './ArchiveSource.js';
653
750
  }
654
751
  }
655
752
  if (bcj2CoderIndex === -1) {
656
- throw createCodedError('BCJ2 coder not found in folder', ErrorCode.CORRUPT_HEADER);
753
+ callback(createCodedError('BCJ2 coder not found in folder', ErrorCode.CORRUPT_HEADER));
754
+ return;
657
755
  }
658
- // Build input stream index -> pack stream mapping
659
- // folder.packedStreams tells us which input indices are unbound and their order
660
756
  const inputToPackStream = {};
661
757
  for(let pi = 0; pi < folder.packedStreams.length; pi++){
662
758
  inputToPackStream[folder.packedStreams[pi]] = pi;
663
759
  }
664
- // Build output stream index -> coder mapping
665
- const outputToCoder = {};
666
- let totalOutputs = 0;
667
- for(let co = 0; co < folder.coders.length; co++){
668
- const numOut = folder.coders[co].numOutStreams;
669
- for(let outp = 0; outp < numOut; outp++){
670
- outputToCoder[totalOutputs + outp] = co;
671
- }
672
- totalOutputs += numOut;
673
- }
674
- // Decompress non-BCJ2 coders (LZMA, LZMA2)
675
- // We need to process in dependency order
676
- const processed = {};
677
760
  const processOrder = this.getCoderProcessOrder(folder, bcj2CoderIndex);
678
- for(let po = 0; po < processOrder.length; po++){
679
- const coderIdx = processOrder[po];
680
- if (coderIdx === bcj2CoderIndex) continue;
761
+ const processNext = (orderIndex)=>{
762
+ if (orderIndex >= processOrder.length) {
763
+ this.finishBcj2Decode(folder, bcj2CoderIndex, coderOutputs, inputToPackStream, packStreams, callback);
764
+ return;
765
+ }
766
+ const coderIdx = processOrder[orderIndex];
767
+ if (coderIdx === bcj2CoderIndex) {
768
+ processNext(orderIndex + 1);
769
+ return;
770
+ }
681
771
  const coder = folder.coders[coderIdx];
682
772
  const codec = getCodec(coder.id);
683
- // Find input for this coder
684
773
  let coderInputStart = 0;
685
774
  for(let ci2 = 0; ci2 < coderIdx; ci2++){
686
775
  coderInputStart += folder.coders[ci2].numInStreams;
687
776
  }
688
- // Get input data (from pack stream)
689
777
  const inputIdx = coderInputStart;
690
778
  const packStreamIdx = inputToPackStream[inputIdx];
691
779
  const inputData = packStreams[packStreamIdx];
692
- // Decompress
693
780
  const unpackSize = folder.unpackSizes[coderIdx];
694
- const outputData = codec.decode(inputData, coder.properties, unpackSize);
695
- // Store in coder outputs
696
- let coderOutputStart = 0;
697
- for(let co2 = 0; co2 < coderIdx; co2++){
698
- coderOutputStart += folder.coders[co2].numOutStreams;
699
- }
700
- coderOutputs[coderOutputStart] = outputData;
701
- processed[coderIdx] = true;
702
- }
703
- // Now process BCJ2
704
- // BCJ2 has 4 inputs, need to map them correctly
705
- // Standard order: main(LZMA2 output), call(LZMA output), jump(LZMA output), range(raw pack)
781
+ this.decodeWithCodec(codec, inputData, coder.properties, unpackSize, (err, outputData)=>{
782
+ if (err || !outputData) {
783
+ callback(err || createCodedError('Decoder returned no data', ErrorCode.DECOMPRESSION_FAILED));
784
+ return;
785
+ }
786
+ let coderOutputStart = 0;
787
+ for(let co2 = 0; co2 < coderIdx; co2++){
788
+ coderOutputStart += folder.coders[co2].numOutStreams;
789
+ }
790
+ coderOutputs[coderOutputStart] = outputData;
791
+ processNext(orderIndex + 1);
792
+ });
793
+ };
794
+ processNext(0);
795
+ }
796
+ finishBcj2Decode(folder, bcj2CoderIndex, coderOutputs, inputToPackStream, packStreams, callback) {
706
797
  let bcj2InputStart = 0;
707
798
  for(let ci3 = 0; ci3 < bcj2CoderIndex; ci3++){
708
799
  bcj2InputStart += folder.coders[ci3].numInStreams;
@@ -710,7 +801,6 @@ export { BufferSource, FileSource } from './ArchiveSource.js';
710
801
  const bcj2Inputs = [];
711
802
  for(let bi = 0; bi < 4; bi++){
712
803
  const globalIdx = bcj2InputStart + bi;
713
- // Check if this input is bound to a coder output
714
804
  let boundOutput = -1;
715
805
  for(let bp2 = 0; bp2 < folder.bindPairs.length; bp2++){
716
806
  if (folder.bindPairs[bp2].inIndex === globalIdx) {
@@ -719,29 +809,28 @@ export { BufferSource, FileSource } from './ArchiveSource.js';
719
809
  }
720
810
  }
721
811
  if (boundOutput >= 0) {
722
- // Get from coder outputs
723
812
  bcj2Inputs.push(coderOutputs[boundOutput]);
724
813
  } else {
725
- // Get from pack streams
726
814
  const psIdx = inputToPackStream[globalIdx];
727
815
  bcj2Inputs.push(packStreams[psIdx]);
728
816
  }
729
817
  }
730
- // Get BCJ2 unpack size
731
818
  let bcj2OutputStart = 0;
732
819
  for(let co3 = 0; co3 < bcj2CoderIndex; co3++){
733
820
  bcj2OutputStart += folder.coders[co3].numOutStreams;
734
821
  }
735
822
  const bcj2UnpackSize = folder.unpackSizes[bcj2OutputStart];
736
- // Memory optimization: Clear intermediate buffers to help GC
737
- // These are no longer needed after bcj2Inputs is built
738
- for(const key in coderOutputs){
739
- delete coderOutputs[key];
740
- }
741
- // Clear packStreams array (allows GC to free compressed data)
742
- packStreams.length = 0;
743
- // Decode BCJ2
744
- return decodeBcj2Multi(bcj2Inputs, undefined, bcj2UnpackSize);
823
+ try {
824
+ const result = decodeBcj2Multi(bcj2Inputs, undefined, bcj2UnpackSize);
825
+ callback(null, result);
826
+ } catch (err) {
827
+ callback(err);
828
+ } finally{
829
+ for(const key in coderOutputs){
830
+ delete coderOutputs[key];
831
+ }
832
+ packStreams.length = 0;
833
+ }
745
834
  }
746
835
  /**
747
836
  * Get processing order for coders (dependency order)
@@ -1072,6 +1161,7 @@ export { BufferSource, FileSource } from './ArchiveSource.js';
1072
1161
  this.extractedPerFolder = {};
1073
1162
  // Splitter cache for multi-file folder streaming (Phase 2)
1074
1163
  this.folderSplitters = {};
1164
+ this.pendingFolders = {};
1075
1165
  this.source = source;
1076
1166
  }
1077
1167
  }