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.
- package/dist/cjs/FileEntry.js +2 -2
- package/dist/cjs/FileEntry.js.map +1 -1
- package/dist/cjs/SevenZipIterator.js +18 -13
- package/dist/cjs/SevenZipIterator.js.map +1 -1
- package/dist/cjs/lib/runDecode.d.cts +5 -0
- package/dist/cjs/lib/runDecode.d.ts +5 -0
- package/dist/cjs/lib/runDecode.js +55 -0
- package/dist/cjs/lib/runDecode.js.map +1 -0
- package/dist/cjs/lib/streamToSource.js +5 -5
- package/dist/cjs/lib/streamToSource.js.map +1 -1
- package/dist/cjs/sevenz/SevenZipParser.d.cts +12 -1
- package/dist/cjs/sevenz/SevenZipParser.d.ts +12 -1
- package/dist/cjs/sevenz/SevenZipParser.js +325 -217
- package/dist/cjs/sevenz/SevenZipParser.js.map +1 -1
- package/dist/cjs/sevenz/codecs/createBufferingDecoder.d.cts +2 -1
- package/dist/cjs/sevenz/codecs/createBufferingDecoder.d.ts +2 -1
- package/dist/cjs/sevenz/codecs/createBufferingDecoder.js +24 -4
- package/dist/cjs/sevenz/codecs/createBufferingDecoder.js.map +1 -1
- package/dist/cjs/sevenz/codecs/index.d.cts +2 -1
- package/dist/cjs/sevenz/codecs/index.d.ts +2 -1
- package/dist/cjs/sevenz/codecs/index.js +28 -16
- package/dist/cjs/sevenz/codecs/index.js.map +1 -1
- package/dist/cjs/sevenz/index.d.cts +1 -1
- package/dist/cjs/sevenz/index.d.ts +1 -1
- package/dist/cjs/sevenz/index.js.map +1 -1
- package/dist/esm/FileEntry.js +1 -1
- package/dist/esm/FileEntry.js.map +1 -1
- package/dist/esm/SevenZipIterator.js +14 -9
- package/dist/esm/SevenZipIterator.js.map +1 -1
- package/dist/esm/lib/runDecode.d.ts +5 -0
- package/dist/esm/lib/runDecode.js +29 -0
- package/dist/esm/lib/runDecode.js.map +1 -0
- package/dist/esm/lib/streamToSource.js +1 -1
- package/dist/esm/lib/streamToSource.js.map +1 -1
- package/dist/esm/sevenz/SevenZipParser.d.ts +12 -1
- package/dist/esm/sevenz/SevenZipParser.js +308 -218
- package/dist/esm/sevenz/SevenZipParser.js.map +1 -1
- package/dist/esm/sevenz/codecs/createBufferingDecoder.d.ts +2 -1
- package/dist/esm/sevenz/codecs/createBufferingDecoder.js +19 -4
- package/dist/esm/sevenz/codecs/createBufferingDecoder.js.map +1 -1
- package/dist/esm/sevenz/codecs/index.d.ts +2 -1
- package/dist/esm/sevenz/codecs/index.js +28 -16
- package/dist/esm/sevenz/codecs/index.js.map +1 -1
- package/dist/esm/sevenz/index.d.ts +1 -1
- package/dist/esm/sevenz/index.js.map +1 -1
- 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
|
|
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)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
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
|
|
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
|
-
|
|
57
|
-
|
|
122
|
+
this.handleCompressedHeader(headerBuf, (headerErr)=>{
|
|
123
|
+
if (headerErr) {
|
|
124
|
+
callback(headerErr);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
finalize();
|
|
128
|
+
});
|
|
58
129
|
} else {
|
|
59
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
160
|
+
for(let chunkStart = searchStart; chunkStart >= searchEnd; chunkStart -= scanChunkSize){
|
|
112
161
|
const chunk = this.source.read(chunkStart, scanChunkSize + packInfoResult.packSize);
|
|
113
|
-
|
|
162
|
+
const limit = Math.min(chunk.length, scanChunkSize);
|
|
163
|
+
for(let i = 0; i < limit; i++){
|
|
114
164
|
if (chunk[i] === 0x00) {
|
|
115
|
-
const
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
615
|
+
callback(createCodedError('No streams info available', ErrorCode.CORRUPT_HEADER));
|
|
616
|
+
return;
|
|
546
617
|
}
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
|
|
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
|
-
|
|
688
|
+
return createCodedError(`Invalid pack size: ${packSize}`, ErrorCode.CORRUPT_ARCHIVE);
|
|
585
689
|
}
|
|
586
690
|
if (packPos < 0 || packPos > Number.MAX_SAFE_INTEGER) {
|
|
587
|
-
|
|
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
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
-
|
|
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
|
-
|
|
720
|
+
callback(createCodedError('No streams info available', ErrorCode.CORRUPT_HEADER));
|
|
721
|
+
return;
|
|
616
722
|
}
|
|
617
723
|
const folder = this.streamsInfo.folders[folderIndex];
|
|
618
|
-
|
|
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
|
-
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
}
|