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