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,168 @@
/*
* SPDX-License-Identifier: MIT
*
* TypeScript port of compcol `src/quantum/model.rs` at commit
* 04a6db2aa7bd487a89c559631d79d1b384139f50.
* Copyright (c) 2026 Karpeles Lab Inc., MIT License.
* Modified to account for decoder work and report structured CAB errors.
*/
import { fail } from '../errors.js';
import { QuantumBitReader } from './bit-reader.js';
import { QuantumBudget } from './budget.js';
export class QuantumModel {
shiftsLeft = 4;
readonly symbols = new Uint16Array(65);
readonly cumulativeFrequencies = new Uint16Array(65);
constructor(
start: number,
readonly entries: number,
private readonly budget: QuantumBudget
) {
if (!Number.isInteger(entries) || entries < 1 || entries > 64) {
fail('quantum-invalid-model', 'Quantum model size is out of range.', {
structure: 'Quantum probability model',
});
}
for (let index = 0; index <= entries; index += 1) {
this.symbols[index] = start + index;
this.cumulativeFrequencies[index] = entries - index;
}
}
update(): void {
this.shiftsLeft -= 1;
if (this.shiftsLeft !== 0) {
for (let index = this.entries - 1; index >= 0; index -= 1) {
let frequency = this.cumulativeFrequencies[index]! >>> 1;
if (frequency <= this.cumulativeFrequencies[index + 1]!) {
frequency = this.cumulativeFrequencies[index + 1]! + 1;
}
this.cumulativeFrequencies[index] = frequency;
this.budget.consume();
}
return;
}
this.shiftsLeft = 50;
for (let index = 0; index < this.entries; index += 1) {
this.cumulativeFrequencies[index] =
(this.cumulativeFrequencies[index]! -
this.cumulativeFrequencies[index + 1]! +
1) >>>
1;
this.budget.consume();
}
// Preserve compcol/libmspack's selection-sort tie behavior exactly.
for (let left = 0; left < this.entries - 1; left += 1) {
for (let right = left + 1; right < this.entries; right += 1) {
if (
this.cumulativeFrequencies[left]! < this.cumulativeFrequencies[right]!
) {
const frequency = this.cumulativeFrequencies[left]!;
this.cumulativeFrequencies[left] = this.cumulativeFrequencies[right]!;
this.cumulativeFrequencies[right] = frequency;
const symbol = this.symbols[left]!;
this.symbols[left] = this.symbols[right]!;
this.symbols[right] = symbol;
}
this.budget.consume();
}
}
for (let index = this.entries - 1; index >= 0; index -= 1) {
this.cumulativeFrequencies[index] =
this.cumulativeFrequencies[index]! +
this.cumulativeFrequencies[index + 1]!;
this.budget.consume();
}
}
}
export class QuantumArithmeticDecoder {
private high = 0;
private low = 0;
private current = 0;
constructor(private readonly budget: QuantumBudget) {}
initializeFrame(reader: QuantumBitReader, input: Uint8Array): void {
this.high = 0xffff;
this.low = 0;
this.current = reader.readBits(16, input);
}
getSymbol(
model: QuantumModel,
reader: QuantumBitReader,
input: Uint8Array
): number {
const high = this.high >>> 0;
const low = this.low >>> 0;
const current = this.current >>> 0;
const range = (((high - low) >>> 0) & 0xffff) + 1;
const total = model.cumulativeFrequencies[0]!;
if (total === 0) {
fail('quantum-invalid-model', 'Quantum probability model is exhausted.', {
structure: 'Quantum probability model',
});
}
const scaledCurrent = (current - low + 1) >>> 0;
const numerator = (Math.imul(scaledCurrent, total) - 1) >>> 0;
const symbolFrequency = Math.floor(numerator / range) & 0xffff;
let index = 1;
while (
index < model.entries &&
model.cumulativeFrequencies[index]! > symbolFrequency
) {
index += 1;
this.budget.consume();
}
const symbol = model.symbols[index - 1]!;
const range2 = (high - low + 1) >>> 0;
const frequencyLow = model.cumulativeFrequencies[index - 1]!;
const frequencyHigh = model.cumulativeFrequencies[index]!;
let nextHigh =
(low + Math.floor((Math.imul(frequencyLow, range2) >>> 0) / total) - 1) &
0xffff;
let nextLow =
(low + Math.floor((Math.imul(frequencyHigh, range2) >>> 0) / total)) &
0xffff;
for (let update = index - 1; update >= 0; update -= 1) {
model.cumulativeFrequencies[update] =
(model.cumulativeFrequencies[update]! + 8) & 0xffff;
this.budget.consume();
}
if (model.cumulativeFrequencies[0]! > 3_800) model.update();
let nextCurrent = current & 0xffff;
while (true) {
if ((nextLow & 0x8000) !== (nextHigh & 0x8000)) {
if ((nextLow & 0x4000) !== 0 && (nextHigh & 0x4000) === 0) {
nextCurrent ^= 0x4000;
nextLow &= 0x3fff;
nextHigh |= 0x4000;
} else {
break;
}
}
nextLow = (nextLow << 1) & 0xffff;
nextHigh = ((nextHigh << 1) | 1) & 0xffff;
reader.ensureBits(1, input);
const bit = reader.peekBits(1);
reader.removeBits(1);
nextCurrent = ((nextCurrent << 1) | bit) & 0xffff;
this.budget.consume();
}
this.high = nextHigh;
this.low = nextLow;
this.current = nextCurrent;
this.budget.consume();
return symbol;
}
}