feat: add bounded Quantum cabinet extraction
This commit is contained in:
@@ -15,6 +15,7 @@ import { fail, warning } from './errors.js';
|
||||
import { LzxDecoder } from './lzx/decoder.js';
|
||||
import { MsZipDecoder } from './mszip/decoder.js';
|
||||
import { decodeCabFileName, normalizeCabPath } from './path.js';
|
||||
import { QuantumDecoder } from './quantum/decoder.js';
|
||||
import {
|
||||
resolveLimits,
|
||||
type CabCompressionMethod,
|
||||
@@ -60,6 +61,27 @@ interface ParsedCabinet {
|
||||
result: CabEnumerationResult;
|
||||
}
|
||||
|
||||
function verifyDataBlockChecksum(bytes: Uint8Array, block: CabDataBlock): void {
|
||||
if (block.checksum === 0) return;
|
||||
const compressed = bytes.subarray(
|
||||
block.dataOffset,
|
||||
block.dataOffset + block.compressedSize
|
||||
);
|
||||
const actual = calculateDataBlockChecksum(
|
||||
block.reserve,
|
||||
compressed,
|
||||
block.compressedSize,
|
||||
block.uncompressedSize
|
||||
);
|
||||
if (actual !== block.checksum) {
|
||||
fail(
|
||||
'cab-checksum-mismatch',
|
||||
`CAB data block checksum mismatch: expected 0x${block.checksum.toString(16).padStart(8, '0')}, got 0x${actual.toString(16).padStart(8, '0')}.`,
|
||||
{ offset: block.headerOffset, structure: 'CFDATA.csum' }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function checkedAdd(
|
||||
left: number,
|
||||
right: number,
|
||||
@@ -108,7 +130,13 @@ function parseCompression(
|
||||
if (kind === 2) {
|
||||
const level = (raw & 0x00f0) >>> 4;
|
||||
const memoryBits = (raw & 0x1f00) >>> 8;
|
||||
if (level < 1 || level > 7 || memoryBits < 10 || memoryBits > 21) {
|
||||
if (
|
||||
(raw & ~0x1ff2) !== 0 ||
|
||||
level < 1 ||
|
||||
level > 7 ||
|
||||
memoryBits < 10 ||
|
||||
memoryBits > 21
|
||||
) {
|
||||
fail(
|
||||
'cab-invalid-compression',
|
||||
`Invalid Quantum type 0x${raw.toString(16)}.`,
|
||||
@@ -118,7 +146,22 @@ function parseCompression(
|
||||
}
|
||||
);
|
||||
}
|
||||
return { kind: 'quantum', raw, label: 'Quantum', level, memoryBits };
|
||||
const windowBytes = 2 ** memoryBits;
|
||||
if (windowBytes > limits.maxQuantumWindowBytes) {
|
||||
fail(
|
||||
'cab-quantum-window-limit',
|
||||
`Quantum window ${windowBytes} exceeds the configured ${limits.maxQuantumWindowBytes}-byte limit.`,
|
||||
{ offset, structure: 'CFFOLDER.typeCompress' }
|
||||
);
|
||||
}
|
||||
return {
|
||||
kind: 'quantum',
|
||||
raw,
|
||||
label: 'Quantum',
|
||||
level,
|
||||
memoryBits,
|
||||
windowBytes,
|
||||
};
|
||||
}
|
||||
if (kind === 3) {
|
||||
if ((raw & ~0x1f0f) !== 0) {
|
||||
@@ -404,6 +447,7 @@ function parseCabinet(
|
||||
|
||||
let totalFolderOutput = 0;
|
||||
let totalLzxOutput = 0;
|
||||
let totalQuantumOutput = 0;
|
||||
for (const folder of folders) {
|
||||
if (folder.firstDataBlockOffset > declaredSize) {
|
||||
fail(
|
||||
@@ -533,6 +577,14 @@ function parseCabinet(
|
||||
'CFDATA.cbUncomp'
|
||||
);
|
||||
}
|
||||
if (folder.compression.kind === 'quantum') {
|
||||
totalQuantumOutput = checkedAdd(
|
||||
totalQuantumOutput,
|
||||
folder.uncompressedSize,
|
||||
'quantum-operation-limit',
|
||||
'CFDATA.cbUncomp'
|
||||
);
|
||||
}
|
||||
}
|
||||
if (totalFolderOutput > limits.maxTotalUncompressedBytes) {
|
||||
fail(
|
||||
@@ -548,6 +600,13 @@ function parseCabinet(
|
||||
{ structure: 'CFDATA.cbUncomp' }
|
||||
);
|
||||
}
|
||||
if (totalQuantumOutput > limits.maxDecodeOperations) {
|
||||
fail(
|
||||
'quantum-operation-limit',
|
||||
`Quantum output exceeds the configured ${limits.maxDecodeOperations}-operation limit.`,
|
||||
{ structure: 'CFDATA.cbUncomp' }
|
||||
);
|
||||
}
|
||||
|
||||
const dataRanges = folders
|
||||
.filter((folder) => folder.dataBlockCount > 0)
|
||||
@@ -600,18 +659,6 @@ function parseCabinet(
|
||||
)
|
||||
);
|
||||
}
|
||||
for (const folder of folders) {
|
||||
if (folder.compression.kind === 'quantum') {
|
||||
diagnostics.push(
|
||||
warning(
|
||||
'cab-compression-unsupported',
|
||||
`${folder.compression.label} metadata can be listed, but this build cannot extract it.`,
|
||||
{ structure: `CFFOLDER[${folder.index}].typeCompress` }
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
bytes,
|
||||
limits,
|
||||
@@ -655,14 +702,6 @@ export async function extractCabinet(
|
||||
const extractedEntries = [];
|
||||
for (const folder of parsed.folders) {
|
||||
checkCancellation(options.cancellation);
|
||||
if (folder.compression.kind === 'quantum') {
|
||||
fail(
|
||||
'cab-compression-unsupported',
|
||||
`${folder.compression.label} extraction is not implemented.`,
|
||||
{ structure: `CFFOLDER[${folder.index}].typeCompress` }
|
||||
);
|
||||
}
|
||||
|
||||
const folderOutput = new Uint8Array(folder.uncompressedSize);
|
||||
const decoder =
|
||||
folder.compression.kind === 'lzx'
|
||||
@@ -673,34 +712,50 @@ export async function extractCabinet(
|
||||
: undefined;
|
||||
const msZipDecoder =
|
||||
folder.compression.kind === 'mszip' ? new MsZipDecoder() : undefined;
|
||||
const quantumDecoder =
|
||||
folder.compression.kind === 'quantum'
|
||||
? new QuantumDecoder(folder.compression.memoryBits, {
|
||||
maxOperations: parsed.limits.maxDecodeOperations,
|
||||
checkCancellation: () => checkCancellation(options.cancellation),
|
||||
})
|
||||
: undefined;
|
||||
let quantumQueuedBlock = -1;
|
||||
let outputOffset = 0;
|
||||
for (const block of folder.blocks) {
|
||||
for (const [blockIndex, block] of folder.blocks.entries()) {
|
||||
checkCancellation(options.cancellation);
|
||||
if (quantumDecoder !== undefined) {
|
||||
const lookahead = Math.min(blockIndex + 1, folder.blocks.length - 1);
|
||||
while (quantumQueuedBlock < lookahead) {
|
||||
quantumQueuedBlock += 1;
|
||||
const queued = folder.blocks[quantumQueuedBlock]!;
|
||||
verifyDataBlockChecksum(parsed.bytes, queued);
|
||||
quantumDecoder.appendCabBlock(
|
||||
parsed.bytes.subarray(
|
||||
queued.dataOffset,
|
||||
queued.dataOffset + queued.compressedSize
|
||||
),
|
||||
quantumQueuedBlock === folder.blocks.length - 1
|
||||
);
|
||||
}
|
||||
}
|
||||
const compressed = parsed.bytes.subarray(
|
||||
block.dataOffset,
|
||||
block.dataOffset + block.compressedSize
|
||||
);
|
||||
if (block.checksum !== 0) {
|
||||
const actual = calculateDataBlockChecksum(
|
||||
block.reserve,
|
||||
compressed,
|
||||
block.compressedSize,
|
||||
block.uncompressedSize
|
||||
);
|
||||
if (actual !== block.checksum) {
|
||||
fail(
|
||||
'cab-checksum-mismatch',
|
||||
`CAB data block checksum mismatch: expected 0x${block.checksum.toString(16).padStart(8, '0')}, got 0x${actual.toString(16).padStart(8, '0')}.`,
|
||||
{ offset: block.headerOffset, structure: 'CFDATA.csum' }
|
||||
);
|
||||
}
|
||||
if (quantumDecoder === undefined) {
|
||||
verifyDataBlockChecksum(parsed.bytes, block);
|
||||
}
|
||||
const output =
|
||||
decoder !== undefined
|
||||
? decoder.decompressNext(compressed, block.uncompressedSize)
|
||||
: msZipDecoder !== undefined
|
||||
? msZipDecoder.decompressNext(compressed, block.uncompressedSize)
|
||||
: compressed;
|
||||
: quantumDecoder !== undefined
|
||||
? quantumDecoder.decompressNext(
|
||||
block.uncompressedSize,
|
||||
blockIndex === folder.blocks.length - 1
|
||||
)
|
||||
: compressed;
|
||||
if (output.length !== block.uncompressedSize) {
|
||||
fail(
|
||||
'cab-output-size-mismatch',
|
||||
|
||||
Reference in New Issue
Block a user