feat: add bounded Quantum cabinet extraction

This commit is contained in:
2026-07-22 19:23:00 +02:00
parent 531612829d
commit a7b1cd7d16
20 changed files with 1099 additions and 66 deletions

View File

@@ -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',