feat: add bounded Quantum cabinet extraction
This commit is contained in:
@@ -67,6 +67,19 @@ const MULTI_BLOCK_MSZIP_CAB = Uint8Array.from(
|
||||
)
|
||||
);
|
||||
|
||||
/*
|
||||
* Generated one-folder CAB around the pinned compcol qtm.txt CFDATA payload.
|
||||
* Provenance and hashes are recorded in tests/fixtures/README.md.
|
||||
*/
|
||||
const QUANTUM_CAB = Uint8Array.from(
|
||||
Buffer.from(
|
||||
'TVNDRgAAAAB8AAAAAAAAACwAAAAAAAAAAwEBAAEAAAAAAAAARAAAAAEAIhI7AAAAAAAA' +
|
||||
'AAAAAAAAACAAcXRtLnR4dAD9LexLMAA7ANYGaQvLR/AsKjqPLKu7PLkzAYvYWEt7Abpv' +
|
||||
'bVFuOsNnQkvrAjZD1mZWyp5yzDAAAA==',
|
||||
'base64'
|
||||
)
|
||||
);
|
||||
|
||||
const LZX_CAB = binary(
|
||||
'\x4d\x53\x43\x46\x00\x00\x00\x00\x97\x00\x00\x00\x00\x00\x00' +
|
||||
'\x00\x2c\x00\x00\x00\x00\x00\x00\x00\x03\x01\x01\x00\x02\x00' +
|
||||
@@ -226,6 +239,69 @@ describe('CAB enumeration and extraction', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('enumerates and extracts the pinned Quantum CAB payload', async () => {
|
||||
expect(QUANTUM_CAB).toHaveLength(124);
|
||||
expect(createHash('sha256').update(QUANTUM_CAB).digest('hex')).toBe(
|
||||
'f622c5a8f5a000dfaddc5569750344f5a6e35d0de9fbd3c4f0c8720c7d94d291'
|
||||
);
|
||||
const listing = enumerateCabinet(QUANTUM_CAB);
|
||||
expect(listing.folders[0]).toMatchObject({
|
||||
dataBlockCount: 1,
|
||||
compressedSize: 48,
|
||||
uncompressedSize: 59,
|
||||
compression: {
|
||||
kind: 'quantum',
|
||||
raw: 0x1222,
|
||||
level: 2,
|
||||
memoryBits: 18,
|
||||
windowBytes: 262_144,
|
||||
},
|
||||
});
|
||||
expect(listing.diagnostics).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ code: 'cab-compression-unsupported' }),
|
||||
])
|
||||
);
|
||||
|
||||
const extracted = await extractCabinet(QUANTUM_CAB);
|
||||
expect(extracted.extractedEntries[0]!.normalizedPath).toBe('qtm.txt');
|
||||
expect(new TextDecoder().decode(extracted.extractedEntries[0]!.bytes)).toBe(
|
||||
'If you can read this, the Quantum decompressor is working!\n'
|
||||
);
|
||||
});
|
||||
|
||||
it('enforces Quantum type, window, and operation limits', () => {
|
||||
const reservedBits = QUANTUM_CAB.slice();
|
||||
new DataView(
|
||||
reservedBits.buffer,
|
||||
reservedBits.byteOffset,
|
||||
reservedBits.byteLength
|
||||
).setUint16(42, 0x3222, true);
|
||||
expectCode(() => enumerateCabinet(reservedBits), 'cab-invalid-compression');
|
||||
expectCode(
|
||||
() =>
|
||||
enumerateCabinet(QUANTUM_CAB, {
|
||||
limits: { maxQuantumWindowBytes: 128 * 1024 },
|
||||
}),
|
||||
'cab-quantum-window-limit'
|
||||
);
|
||||
expectCode(
|
||||
() =>
|
||||
enumerateCabinet(QUANTUM_CAB, {
|
||||
limits: { maxDecodeOperations: 58 },
|
||||
}),
|
||||
'quantum-operation-limit'
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a corrupt Quantum CFDATA payload before decoding', async () => {
|
||||
const corrupted = QUANTUM_CAB.slice();
|
||||
corrupted[corrupted.length - 1] = corrupted[corrupted.length - 1]! ^ 1;
|
||||
await expect(extractCabinet(corrupted)).rejects.toMatchObject({
|
||||
diagnostic: { code: 'cab-checksum-mismatch' },
|
||||
});
|
||||
});
|
||||
|
||||
it('retains an uncompressed LZX block across CAB chunks', () => {
|
||||
const decoder = new LzxDecoder(18, { maxOperations: 100 });
|
||||
const first = binary(
|
||||
|
||||
@@ -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',
|
||||
|
||||
154
src/onenote/onepkg/quantum/bit-reader.ts
Normal file
154
src/onenote/onepkg/quantum/bit-reader.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* TypeScript port of compcol `src/quantum/bits.rs` at commit
|
||||
* 04a6db2aa7bd487a89c559631d79d1b384139f50.
|
||||
* Copyright (c) 2026 Karpeles Lab Inc., MIT License.
|
||||
* Modified for a synchronous, bounded CAB-block decoder.
|
||||
*/
|
||||
|
||||
import { fail } from '../errors.js';
|
||||
import { QuantumBudget } from './budget.js';
|
||||
|
||||
const BIT_BUFFER_WIDTH = 32;
|
||||
|
||||
export class QuantumBitReader {
|
||||
private bitBuffer = 0;
|
||||
private bitsLeft = 0;
|
||||
private byteOffset = 0;
|
||||
private endOfInput = false;
|
||||
private eofPaddingUsed = false;
|
||||
|
||||
constructor(private readonly budget: QuantumBudget) {}
|
||||
|
||||
get offset(): number {
|
||||
return this.byteOffset;
|
||||
}
|
||||
|
||||
get bufferedBitCount(): number {
|
||||
return this.bitsLeft;
|
||||
}
|
||||
|
||||
setEndOfInput(): void {
|
||||
this.endOfInput = true;
|
||||
}
|
||||
|
||||
ensureBits(bitCount: number, input: Uint8Array): void {
|
||||
if (!Number.isInteger(bitCount) || bitCount < 1 || bitCount > 16) {
|
||||
fail('quantum-invalid-bit-count', 'Invalid Quantum bit-read width.', {
|
||||
structure: 'Quantum bitstream',
|
||||
});
|
||||
}
|
||||
while (this.bitsLeft < bitCount) this.readPair(input);
|
||||
}
|
||||
|
||||
peekBits(bitCount: number): number {
|
||||
if (
|
||||
!Number.isInteger(bitCount) ||
|
||||
bitCount < 1 ||
|
||||
bitCount > BIT_BUFFER_WIDTH ||
|
||||
bitCount > this.bitsLeft
|
||||
) {
|
||||
fail('quantum-invalid-bit-state', 'Invalid Quantum bit-buffer access.', {
|
||||
structure: 'Quantum bitstream',
|
||||
});
|
||||
}
|
||||
return bitCount === BIT_BUFFER_WIDTH
|
||||
? this.bitBuffer
|
||||
: this.bitBuffer >>> (BIT_BUFFER_WIDTH - bitCount);
|
||||
}
|
||||
|
||||
removeBits(bitCount: number): void {
|
||||
if (
|
||||
!Number.isInteger(bitCount) ||
|
||||
bitCount < 0 ||
|
||||
bitCount > this.bitsLeft
|
||||
) {
|
||||
fail('quantum-invalid-bit-state', 'Invalid Quantum bit-buffer advance.', {
|
||||
structure: 'Quantum bitstream',
|
||||
});
|
||||
}
|
||||
this.bitBuffer =
|
||||
bitCount === BIT_BUFFER_WIDTH ? 0 : (this.bitBuffer << bitCount) >>> 0;
|
||||
this.bitsLeft -= bitCount;
|
||||
}
|
||||
|
||||
readBits(bitCount: number, input: Uint8Array): number {
|
||||
this.ensureBits(bitCount, input);
|
||||
const value = this.peekBits(bitCount);
|
||||
this.removeBits(bitCount);
|
||||
return value;
|
||||
}
|
||||
|
||||
readManyBits(bitCount: number, input: Uint8Array): number {
|
||||
if (!Number.isInteger(bitCount) || bitCount < 0 || bitCount > 32) {
|
||||
fail('quantum-invalid-bit-count', 'Invalid Quantum bit-read width.', {
|
||||
structure: 'Quantum bitstream',
|
||||
});
|
||||
}
|
||||
let needed = bitCount;
|
||||
let value = 0;
|
||||
while (needed > 0) {
|
||||
if (this.bitsLeft <= BIT_BUFFER_WIDTH - 16) this.readPair(input);
|
||||
const run = Math.min(this.bitsLeft, needed);
|
||||
value = (value * 2 ** run + this.peekBits(run)) >>> 0;
|
||||
this.removeBits(run);
|
||||
needed -= run;
|
||||
this.budget.consume();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
alignToByte(): void {
|
||||
this.removeBits(this.bitsLeft & 7);
|
||||
}
|
||||
|
||||
rebase(consumedBytes: number): void {
|
||||
if (consumedBytes < 0 || consumedBytes > this.byteOffset) {
|
||||
fail('quantum-invalid-bit-state', 'Invalid Quantum input rebase.', {
|
||||
structure: 'Quantum bitstream',
|
||||
});
|
||||
}
|
||||
this.byteOffset -= consumedBytes;
|
||||
}
|
||||
|
||||
private readPair(input: Uint8Array): void {
|
||||
if (this.bitsLeft > BIT_BUFFER_WIDTH - 16) {
|
||||
fail('quantum-invalid-bit-state', 'Quantum bit buffer overflow.', {
|
||||
offset: this.byteOffset,
|
||||
structure: 'Quantum bitstream',
|
||||
});
|
||||
}
|
||||
if (this.byteOffset + 2 > input.length) {
|
||||
if (this.endOfInput && !this.eofPaddingUsed) {
|
||||
// libmspack supplies two zero bytes once at folder EOF. Preserve a
|
||||
// final odd byte (normally the synthetic CAB sentinel) as the high
|
||||
// byte of that refill instead of discarding it.
|
||||
const remainingByte =
|
||||
this.byteOffset < input.length ? input[this.byteOffset++]! : 0;
|
||||
this.bitBuffer =
|
||||
(this.bitBuffer |
|
||||
(remainingByte << (BIT_BUFFER_WIDTH - 16 - this.bitsLeft))) >>>
|
||||
0;
|
||||
this.eofPaddingUsed = true;
|
||||
this.bitsLeft += 16;
|
||||
this.budget.consume();
|
||||
return;
|
||||
}
|
||||
fail(
|
||||
'quantum-unexpected-eof',
|
||||
'Quantum stream ended while reading compressed bits.',
|
||||
{ offset: this.byteOffset, structure: 'Quantum bitstream' }
|
||||
);
|
||||
}
|
||||
const combined =
|
||||
(input[this.byteOffset]! << 8) | input[this.byteOffset + 1]!;
|
||||
this.byteOffset += 2;
|
||||
this.bitBuffer =
|
||||
(this.bitBuffer |
|
||||
(combined << (BIT_BUFFER_WIDTH - 16 - this.bitsLeft))) >>>
|
||||
0;
|
||||
this.bitsLeft += 16;
|
||||
this.budget.consume();
|
||||
}
|
||||
}
|
||||
44
src/onenote/onepkg/quantum/budget.ts
Normal file
44
src/onenote/onepkg/quantum/budget.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Quantum decoder support adapted from compcol `src/quantum/*` at commit
|
||||
* 04a6db2aa7bd487a89c559631d79d1b384139f50.
|
||||
* Copyright (c) 2026 Karpeles Lab Inc., MIT License.
|
||||
*/
|
||||
|
||||
import { fail } from '../errors.js';
|
||||
|
||||
export interface QuantumDecoderOptions {
|
||||
maxOperations: number;
|
||||
checkCancellation?: () => void;
|
||||
}
|
||||
|
||||
export class QuantumBudget {
|
||||
private operations = 0;
|
||||
private nextCancellationCheck = 4_096;
|
||||
|
||||
constructor(private readonly options: QuantumDecoderOptions) {}
|
||||
|
||||
consume(count = 1): void {
|
||||
if (!Number.isSafeInteger(count) || count < 0) {
|
||||
fail('quantum-operation-overflow', 'Invalid Quantum operation count.', {
|
||||
structure: 'Quantum stream',
|
||||
});
|
||||
}
|
||||
this.operations += count;
|
||||
if (
|
||||
!Number.isSafeInteger(this.operations) ||
|
||||
this.operations > this.options.maxOperations
|
||||
) {
|
||||
fail(
|
||||
'quantum-operation-limit',
|
||||
`Quantum decoding exceeded the configured ${this.options.maxOperations}-operation limit.`,
|
||||
{ structure: 'Quantum stream' }
|
||||
);
|
||||
}
|
||||
if (this.operations >= this.nextCancellationCheck) {
|
||||
this.options.checkCancellation?.();
|
||||
this.nextCancellationCheck = this.operations + 4_096;
|
||||
}
|
||||
}
|
||||
}
|
||||
121
src/onenote/onepkg/quantum/decoder.test.ts
Normal file
121
src/onenote/onepkg/quantum/decoder.test.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { OnePkgError } from '../errors.js';
|
||||
import { QuantumDecoder } from './decoder.js';
|
||||
|
||||
/*
|
||||
* Pinned compcol MIT fixture from tests/quantum.rs at commit
|
||||
* 04a6db2aa7bd487a89c559631d79d1b384139f50. The source bytes are the
|
||||
* Quantum CFDATA payload from libmspack's mszip_lzx_qtm.cab; the compcol test
|
||||
* appends 0xFF, while QuantumDecoder appends that CAB sentinel itself.
|
||||
*/
|
||||
const QTM_TEXT = Uint8Array.from([
|
||||
0xd6, 0x06, 0x69, 0x0b, 0xcb, 0x47, 0xf0, 0x2c, 0x2a, 0x3a, 0x8f, 0x2c, 0xab,
|
||||
0xbb, 0x3c, 0xb9, 0x33, 0x01, 0x8b, 0xd8, 0x58, 0x4b, 0x7b, 0x01, 0xba, 0x6f,
|
||||
0x6d, 0x51, 0x6e, 0x3a, 0xc3, 0x67, 0x42, 0x4b, 0xeb, 0x02, 0x36, 0x43, 0xd6,
|
||||
0x66, 0x56, 0xca, 0x9e, 0x72, 0xcc, 0x30, 0x00, 0x00,
|
||||
]);
|
||||
|
||||
const EXPECTED_TEXT =
|
||||
'If you can read this, the Quantum decompressor is working!\n';
|
||||
|
||||
const ZERO_FRAME = Uint8Array.from([
|
||||
0xff, 0x6d, 0xda, 0x34, 0x62, 0x1a, 0x9b, 0xa9, 0x92, 0x04, 0xd2, 0x80, 0x00,
|
||||
0x20,
|
||||
]);
|
||||
|
||||
const SECOND_ZERO_FRAME = Uint8Array.from([0x69, 0x33, 0x90, 0x00, 0x06, 0x00]);
|
||||
|
||||
function expectCode(action: () => unknown, code: string): void {
|
||||
try {
|
||||
action();
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(OnePkgError);
|
||||
expect((error as OnePkgError).diagnostic.code).toBe(code);
|
||||
return;
|
||||
}
|
||||
throw new Error(`Expected OnePkgError ${code}`);
|
||||
}
|
||||
|
||||
describe('Quantum decoder', () => {
|
||||
it('decodes the pinned compcol qtm.txt CFDATA payload', () => {
|
||||
const decoder = new QuantumDecoder(18, { maxOperations: 1_000_000 });
|
||||
decoder.appendCabBlock(QTM_TEXT, true);
|
||||
const output = decoder.decompressNext(EXPECTED_TEXT.length, true);
|
||||
expect(new TextDecoder().decode(output)).toBe(EXPECTED_TEXT);
|
||||
});
|
||||
|
||||
it('rejects truncated streams and enforces the operation budget', () => {
|
||||
expectCode(() => {
|
||||
const decoder = new QuantumDecoder(18, {
|
||||
maxOperations: 1_000_000,
|
||||
});
|
||||
decoder.appendCabBlock(QTM_TEXT.subarray(0, 4), true);
|
||||
decoder.decompressNext(EXPECTED_TEXT.length, true);
|
||||
}, 'quantum-unexpected-eof');
|
||||
expectCode(() => {
|
||||
const decoder = new QuantumDecoder(18, { maxOperations: 10 });
|
||||
decoder.appendCabBlock(QTM_TEXT, true);
|
||||
decoder.decompressNext(EXPECTED_TEXT.length, true);
|
||||
}, 'quantum-operation-limit');
|
||||
});
|
||||
|
||||
it('checks cancellation during a Quantum frame', () => {
|
||||
const cancelled = new Error('cancelled');
|
||||
const decoder = new QuantumDecoder(18, {
|
||||
maxOperations: 10_000_000,
|
||||
checkCancellation: () => {
|
||||
throw cancelled;
|
||||
},
|
||||
});
|
||||
decoder.appendCabBlock(ZERO_FRAME, true);
|
||||
expect(() => decoder.decompressNext(32_768, true)).toThrow(cancelled);
|
||||
});
|
||||
|
||||
it('appends the CAB sentinel and preserves state across frames', () => {
|
||||
const decoder = new QuantumDecoder(18, { maxOperations: 10_000_000 });
|
||||
decoder.appendCabBlock(ZERO_FRAME, false);
|
||||
decoder.appendCabBlock(SECOND_ZERO_FRAME, true);
|
||||
const first = decoder.decompressNext(32_768, false);
|
||||
const second = decoder.decompressNext(32_768, true);
|
||||
expect(first.every((value) => value === 0)).toBe(true);
|
||||
expect(second.every((value) => value === 0)).toBe(true);
|
||||
});
|
||||
|
||||
it('bounds and validates Quantum frame padding before the sentinel', () => {
|
||||
const invalid = new QuantumDecoder(18, {
|
||||
maxOperations: 10_000_000,
|
||||
});
|
||||
invalid.appendCabBlock(Uint8Array.from([...ZERO_FRAME, 0x42]), true);
|
||||
expectCode(
|
||||
() => invalid.decompressNext(32_768, true),
|
||||
'quantum-invalid-frame-trailer'
|
||||
);
|
||||
|
||||
const tooMuchPadding = new QuantumDecoder(18, {
|
||||
maxOperations: 10_000_000,
|
||||
});
|
||||
tooMuchPadding.appendCabBlock(
|
||||
Uint8Array.from([...ZERO_FRAME, 0, 0, 0, 0, 0]),
|
||||
true
|
||||
);
|
||||
expectCode(
|
||||
() => tooMuchPadding.decompressNext(32_768, true),
|
||||
'quantum-invalid-frame-trailer'
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects partial non-final frames and data after the final block', () => {
|
||||
const decoder = new QuantumDecoder(18, { maxOperations: 1_000_000 });
|
||||
decoder.appendCabBlock(QTM_TEXT, true);
|
||||
expectCode(
|
||||
() => decoder.decompressNext(EXPECTED_TEXT.length, false),
|
||||
'quantum-partial-intermediate-frame'
|
||||
);
|
||||
decoder.decompressNext(EXPECTED_TEXT.length, true);
|
||||
expectCode(
|
||||
() => decoder.decompressNext(EXPECTED_TEXT.length, true),
|
||||
'quantum-data-after-final-block'
|
||||
);
|
||||
});
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
29
src/onenote/onepkg/quantum/tables.ts
Normal file
29
src/onenote/onepkg/quantum/tables.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Direct TypeScript translation of compcol `src/quantum/tables.rs` at commit
|
||||
* 04a6db2aa7bd487a89c559631d79d1b384139f50.
|
||||
* Copyright (c) 2026 Karpeles Lab Inc., MIT License.
|
||||
*/
|
||||
|
||||
export const POSITION_BASE = Uint32Array.from([
|
||||
0, 1, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 384, 512, 768,
|
||||
1_024, 1_536, 2_048, 3_072, 4_096, 6_144, 8_192, 12_288, 16_384, 24_576,
|
||||
32_768, 49_152, 65_536, 98_304, 131_072, 196_608, 262_144, 393_216, 524_288,
|
||||
786_432, 1_048_576, 1_572_864,
|
||||
]);
|
||||
|
||||
export const EXTRA_BITS = Uint8Array.from([
|
||||
0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11,
|
||||
11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19,
|
||||
]);
|
||||
|
||||
export const LENGTH_BASE = Uint8Array.from([
|
||||
0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 14, 18, 22, 26, 30, 38, 46, 54, 62, 78, 94,
|
||||
110, 126, 158, 190, 222, 254,
|
||||
]);
|
||||
|
||||
export const LENGTH_EXTRA = Uint8Array.from([
|
||||
0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5,
|
||||
0,
|
||||
]);
|
||||
@@ -9,6 +9,7 @@ export type CabCompressionMethod =
|
||||
label: 'Quantum';
|
||||
level: number;
|
||||
memoryBits: number;
|
||||
windowBytes: number;
|
||||
}
|
||||
| {
|
||||
kind: 'lzx';
|
||||
@@ -75,6 +76,7 @@ export interface OnePkgLimits {
|
||||
maxExtractedBytes: number;
|
||||
/** Maximum per-folder uncompressed CFDATA bytes / compressed payload bytes. */
|
||||
maxCompressionRatio: number;
|
||||
maxQuantumWindowBytes: number;
|
||||
maxLzxWindowBytes: number;
|
||||
maxDecodeOperations: number;
|
||||
}
|
||||
@@ -92,6 +94,7 @@ export const DEFAULT_ONEPKG_LIMITS: Readonly<OnePkgLimits> = {
|
||||
maxTotalUncompressedBytes: 512 * 1024 * 1024,
|
||||
maxExtractedBytes: 512 * 1024 * 1024,
|
||||
maxCompressionRatio: 200,
|
||||
maxQuantumWindowBytes: 2 * 1024 * 1024,
|
||||
maxLzxWindowBytes: 8 * 1024 * 1024,
|
||||
maxDecodeOperations: 1_000_000_000,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user