feat: add bounded Quantum cabinet extraction
This commit is contained in:
168
src/onenote/onepkg/quantum/model.ts
Normal file
168
src/onenote/onepkg/quantum/model.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user