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

@@ -0,0 +1,296 @@
/*
* SPDX-License-Identifier: MIT
*
* TypeScript port of compcol `src/quantum/decoder.rs` at commit
* 04a6db2aa7bd487a89c559631d79d1b384139f50.
* Copyright (c) 2026 Karpeles Lab Inc., MIT License.
*
* Modified for synchronous CAB CFDATA chunks, fixed output buffers, explicit
* operation/cancellation budgets, strict frame/output boundaries, and one
* synthetic 0xFF trailer byte per CAB block as required by the reference.
*/
import { fail } from '../errors.js';
import { QuantumBitReader } from './bit-reader.js';
import { QuantumBudget, type QuantumDecoderOptions } from './budget.js';
import { QuantumArithmeticDecoder, QuantumModel } from './model.js';
import {
EXTRA_BITS,
LENGTH_BASE,
LENGTH_EXTRA,
POSITION_BASE,
} from './tables.js';
const FRAME_SIZE = 32 * 1024;
const MIN_WINDOW_BITS = 10;
const MAX_WINDOW_BITS = 21;
const MAX_TRAILER_ZERO_BYTES = 4;
const CAB_BLOCK_SENTINEL = 0xff;
export class QuantumDecoder {
private readonly window: Uint8Array;
private readonly windowMask: number;
private readonly budget: QuantumBudget;
private readonly bitReader: QuantumBitReader;
private readonly arithmetic: QuantumArithmeticDecoder;
private input = new Uint8Array(0);
private windowPosition = 0;
private headerRead = false;
private frameRemaining = FRAME_SIZE;
private finished = false;
private inputFinished = false;
private readonly model0: QuantumModel;
private readonly model1: QuantumModel;
private readonly model2: QuantumModel;
private readonly model3: QuantumModel;
private readonly model4: QuantumModel;
private readonly model5: QuantumModel;
private readonly model6: QuantumModel;
private readonly model6Length: QuantumModel;
private readonly model7: QuantumModel;
constructor(windowBits: number, options: QuantumDecoderOptions) {
if (
!Number.isInteger(windowBits) ||
windowBits < MIN_WINDOW_BITS ||
windowBits > MAX_WINDOW_BITS
) {
fail(
'quantum-invalid-window',
`Quantum window size 2^${windowBits} is outside 2^${MIN_WINDOW_BITS}..2^${MAX_WINDOW_BITS}.`,
{ structure: 'CFFOLDER.typeCompress' }
);
}
const windowSize = 2 ** windowBits;
this.window = new Uint8Array(windowSize);
this.windowMask = windowSize - 1;
this.budget = new QuantumBudget(options);
this.bitReader = new QuantumBitReader(this.budget);
this.arithmetic = new QuantumArithmeticDecoder(this.budget);
const positionModelSize = windowBits * 2;
this.model0 = new QuantumModel(0, 64, this.budget);
this.model1 = new QuantumModel(64, 64, this.budget);
this.model2 = new QuantumModel(128, 64, this.budget);
this.model3 = new QuantumModel(192, 64, this.budget);
this.model4 = new QuantumModel(
0,
Math.min(positionModelSize, 24),
this.budget
);
this.model5 = new QuantumModel(
0,
Math.min(positionModelSize, 36),
this.budget
);
this.model6 = new QuantumModel(0, positionModelSize, this.budget);
this.model6Length = new QuantumModel(0, 27, this.budget);
this.model7 = new QuantumModel(0, 7, this.budget);
}
appendCabBlock(compressed: Uint8Array, isLastBlock: boolean): void {
if (this.inputFinished) {
fail(
'quantum-data-after-final-block',
'Quantum decoder received data after the final CAB block.',
{ structure: 'Quantum stream' }
);
}
this.appendBlockBytes(compressed);
if (isLastBlock) {
this.inputFinished = true;
this.bitReader.setEndOfInput();
}
}
decompressNext(outputLength: number, isLastBlock: boolean): Uint8Array {
if (this.finished) {
fail(
'quantum-data-after-final-block',
'Quantum decoder received data after the final CAB block.',
{ structure: 'Quantum stream' }
);
}
if (
!Number.isInteger(outputLength) ||
outputLength <= 0 ||
outputLength > FRAME_SIZE
) {
fail(
'quantum-invalid-chunk-size',
`Quantum CAB chunk output size ${outputLength} is outside 1..${FRAME_SIZE}.`,
{ structure: 'CFDATA.cbUncomp' }
);
}
if (!isLastBlock && outputLength !== FRAME_SIZE) {
fail(
'quantum-partial-intermediate-frame',
'A non-final Quantum CAB block must produce exactly 32 KiB.',
{ structure: 'CFDATA.cbUncomp' }
);
}
const output = new Uint8Array(outputLength);
let outputOffset = 0;
while (outputOffset < output.length) {
this.budget.consume();
if (this.frameRemaining === 0) this.completeFrameTrailer();
if (!this.headerRead) {
this.arithmetic.initializeFrame(this.bitReader, this.input);
this.headerRead = true;
}
outputOffset = this.decodePacket(output, outputOffset);
}
// CAB blocks before the final partial block are complete Quantum frames.
// Consume their alignment and synthetic sentinel now, matching qtmd.c,
// so a malformed final frame cannot evade trailer validation.
if (this.frameRemaining === 0) this.completeFrameTrailer();
this.compactInput();
if (isLastBlock) this.finished = true;
return output;
}
private decodePacket(output: Uint8Array, outputOffset: number): number {
const selector = this.arithmetic.getSymbol(
this.model7,
this.bitReader,
this.input
);
if (selector < 4) {
const model = [this.model0, this.model1, this.model2, this.model3][
selector
]!;
const symbol = this.arithmetic.getSymbol(
model,
this.bitReader,
this.input
);
this.window[this.windowPosition & this.windowMask] = symbol & 0xff;
output[outputOffset] = symbol & 0xff;
this.windowPosition += 1;
this.frameRemaining -= 1;
this.budget.consume();
return outputOffset + 1;
}
let matchLength: number;
let matchOffset: number;
if (selector === 4 || selector === 5) {
const model = selector === 4 ? this.model4 : this.model5;
const slot = this.arithmetic.getSymbol(model, this.bitReader, this.input);
matchOffset = this.readPosition(slot);
matchLength = selector === 4 ? 3 : 4;
} else if (selector === 6) {
const lengthSlot = this.arithmetic.getSymbol(
this.model6Length,
this.bitReader,
this.input
);
if (lengthSlot >= LENGTH_EXTRA.length) {
fail('quantum-invalid-length-slot', 'Invalid Quantum length slot.', {
structure: 'Quantum match',
});
}
const lengthExtraBits = LENGTH_EXTRA[lengthSlot]!;
const lengthExtra = this.bitReader.readManyBits(
lengthExtraBits,
this.input
);
matchLength = LENGTH_BASE[lengthSlot]! + lengthExtra + 5;
const positionSlot = this.arithmetic.getSymbol(
this.model6,
this.bitReader,
this.input
);
matchOffset = this.readPosition(positionSlot);
} else {
fail('quantum-invalid-selector', 'Invalid Quantum packet selector.', {
structure: 'Quantum packet',
});
}
if (matchOffset <= 0 || matchOffset > this.window.length) {
fail(
'quantum-invalid-match-offset',
`Quantum match offset ${matchOffset} exceeds its ${this.window.length}-byte window.`,
{ structure: 'Quantum match' }
);
}
if (matchLength > this.frameRemaining) {
fail(
'quantum-frame-overrun',
'Quantum match crosses its 32 KiB frame boundary.',
{ structure: 'Quantum match' }
);
}
if (matchLength > output.length - outputOffset) {
fail(
'quantum-output-overrun',
'Quantum match exceeds the CAB block output declaration.',
{ structure: 'CFDATA.cbUncomp' }
);
}
for (let index = 0; index < matchLength; index += 1) {
const source =
(this.windowPosition - matchOffset + this.window.length) &
this.windowMask;
const value = this.window[source]!;
this.window[this.windowPosition & this.windowMask] = value;
output[outputOffset + index] = value;
this.windowPosition += 1;
}
this.frameRemaining -= matchLength;
this.budget.consume(matchLength);
return outputOffset + matchLength;
}
private readPosition(slot: number): number {
if (slot >= EXTRA_BITS.length) {
fail('quantum-invalid-position-slot', 'Invalid Quantum position slot.', {
structure: 'Quantum match',
});
}
const extra = this.bitReader.readManyBits(EXTRA_BITS[slot]!, this.input);
return POSITION_BASE[slot]! + extra + 1;
}
private completeFrameTrailer(): void {
this.bitReader.alignToByte();
let zeroBytes = 0;
while (true) {
const value = this.bitReader.readBits(8, this.input);
if (value === CAB_BLOCK_SENTINEL) break;
if (value !== 0 || zeroBytes >= MAX_TRAILER_ZERO_BYTES) {
fail(
'quantum-invalid-frame-trailer',
'Quantum frame trailer is not zero padding followed by 0xFF.',
{ structure: 'Quantum frame trailer' }
);
}
zeroBytes += 1;
this.budget.consume();
}
this.headerRead = false;
this.frameRemaining = FRAME_SIZE;
}
private appendBlockBytes(compressed: Uint8Array): void {
this.compactInput();
const appended = new Uint8Array(this.input.length + compressed.length + 1);
appended.set(this.input);
appended.set(compressed, this.input.length);
appended[appended.length - 1] = CAB_BLOCK_SENTINEL;
this.input = appended;
}
private compactInput(): void {
const consumed = this.bitReader.offset;
if (consumed === 0) return;
this.input = this.input.slice(consumed);
this.bitReader.rebase(consumed);
}
}