feat: add bounded Quantum cabinet extraction
This commit is contained in:
15
CHANGELOG.md
15
CHANGELOG.md
@@ -8,18 +8,19 @@
|
||||
- Add a worker-hosted native TypeScript parser for a desktop OneNote
|
||||
revision-store subset: bounded file-node and object-space traversal,
|
||||
property sets, page metadata, titles, timestamps, levels, and plain text.
|
||||
- Add in-memory CAB enumeration and extraction for uncompressed, MSZIP, and LZX
|
||||
folders, including checksums, strict paths, explicit resource limits, and
|
||||
cooperative cancellation between data blocks.
|
||||
- Add in-memory CAB enumeration and extraction for uncompressed, MSZIP,
|
||||
Quantum, and LZX folders, including checksums, strict paths, explicit
|
||||
resource limits, and cooperative cancellation between data blocks.
|
||||
- Open contained `.one` sections independently and expose package entries,
|
||||
section/page navigation, partial failures, and structured diagnostics.
|
||||
- Verify a Joplin `.one` golden with three pages and a Joplin LZX `0x1203`
|
||||
`.onepkg` golden with seven entries, five sections, and seven pages. Also
|
||||
verify rust-cab uncompressed, MSZIP, and LZX `0x1303` fixtures plus a
|
||||
generated two-block MSZIP dictionary-chain fixture.
|
||||
generated two-block MSZIP dictionary-chain fixture and pinned Quantum
|
||||
single-/multi-frame payloads.
|
||||
- Add deterministic static release packaging, SHA-256 sidecars, pinned-source
|
||||
provenance, fixture licensing, and security/support documentation.
|
||||
|
||||
Known unsupported areas are FSSHTTP-packaged `.one`, Quantum CAB extraction,
|
||||
multi-cabinet archives, `.onetoc2` hierarchy/order interpretation, visual
|
||||
content, attachments, layout/format fidelity, links, and exports.
|
||||
Known unsupported areas are FSSHTTP-packaged `.one`, multi-cabinet archives,
|
||||
`.onetoc2` hierarchy/order interpretation, visual content, attachments,
|
||||
layout/format fidelity, links, and exports.
|
||||
|
||||
21
LICENSES/compcol-MIT.txt
Normal file
21
LICENSES/compcol-MIT.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Karpeles Lab Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
19
README.md
19
README.md
@@ -14,8 +14,8 @@ and is delivered to Toolbox Portal as an already-built, checksummed static ZIP.
|
||||
- Desktop revision-store `.one` sections from the tested OneNote family. The
|
||||
reader exposes section/page names, stable page IDs, created and updated
|
||||
timestamps when present, page level, and readable plain text.
|
||||
- Single-cabinet `.onepkg` files whose CAB folders are uncompressed, MSZIP, or
|
||||
LZX compressed. Each contained desktop `.one` section is parsed
|
||||
- Single-cabinet `.onepkg` files whose CAB folders are uncompressed, MSZIP,
|
||||
Quantum, or LZX compressed. Each contained desktop `.one` section is parsed
|
||||
independently, so one unsupported section does not hide sections that did
|
||||
parse.
|
||||
- Package entries, safe normalized paths, uncompressed sizes, compression
|
||||
@@ -23,19 +23,20 @@ and is delivered to Toolbox Portal as an already-built, checksummed static ZIP.
|
||||
|
||||
The golden tests parse a real Joplin desktop `.one` file with three pages and a
|
||||
real Joplin `.onepkg` with seven entries, five sections, and seven pages. LZX
|
||||
`0x1203` and `0x1303`, uncompressed CAB, and one- and two-block MSZIP data are
|
||||
covered by fixtures. The MSZIP fixtures are CAB-level evidence, not a claim
|
||||
about a particular OneNote `.onepkg` producer.
|
||||
`0x1203` and `0x1303`, uncompressed CAB, one- and two-block MSZIP data, and
|
||||
single- and multi-frame Quantum data are covered by fixtures. The MSZIP and
|
||||
Quantum fixtures are CAB-level evidence, not a claim about a particular
|
||||
OneNote `.onepkg` producer.
|
||||
|
||||
This is a structural reader, not a full-fidelity OneNote renderer. Images,
|
||||
tables, ink, embedded files, layout, formatting, links, and exports are not
|
||||
rendered. FSSHTTP-packaged `.one`, Quantum CAB extraction, multi-cabinet
|
||||
archives, and `.onetoc2` hierarchy/order interpretation are not supported. See
|
||||
rendered. FSSHTTP-packaged `.one`, multi-cabinet archives, and `.onetoc2`
|
||||
hierarchy/order interpretation are not supported. See
|
||||
[format support](docs/FORMAT_SUPPORT.md) for the precise matrix.
|
||||
|
||||
There is no Rust/Wasm runtime, server, proxy, upload, telemetry, or notebook
|
||||
storage. All parsing and CAB/LZX/MSZIP decompression is ordinary browser
|
||||
JavaScript produced from the TypeScript source.
|
||||
storage. All parsing and CAB/LZX/MSZIP/Quantum decompression is ordinary
|
||||
browser JavaScript produced from the TypeScript source.
|
||||
|
||||
## Use
|
||||
|
||||
|
||||
@@ -23,6 +23,11 @@ adaptation of `lzxd` 0.2.5 (MIT OR Apache-2.0), pinned at
|
||||
option. Adapted modules carry provenance headers and the licence texts are in
|
||||
`LICENSES/rust-cab-MIT.txt` and `LICENSES/lzxd-MIT.txt`.
|
||||
|
||||
The browser-native Quantum decoder adapts compcol's MIT-licensed Quantum
|
||||
modules, pinned at `04a6db2aa7bd487a89c559631d79d1b384139f50`. Its adapted
|
||||
modules carry provenance headers and its licence is in
|
||||
`LICENSES/compcol-MIT.txt`.
|
||||
|
||||
MSZIP raw DEFLATE decoding uses fflate 0.8.3 (MIT). Its complete installed
|
||||
licence text is included in the generated runtime licence bundle.
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
| CAB LZX `0x1303` | Supported extraction | Byte-for-byte rust-cab MIT unit fixture, two text entries |
|
||||
| CAB without compression | Supported extraction | Byte-for-byte rust-cab MIT unit fixture |
|
||||
| CAB MSZIP | Supported extraction | rust-cab single-block fixture plus a generated two-block dictionary-chain fixture |
|
||||
| CAB Quantum | Unsupported | Metadata is recognized; extraction fails with `cab-compression-unsupported` |
|
||||
| CAB Quantum | Supported extraction | Pinned compcol qtm.txt payload, generated one-folder CAB, and two-frame state/sentinel tests |
|
||||
| Multi-cabinet CAB / spanning files | Unsupported | Header flags and spanning folder indexes are rejected |
|
||||
| `.onetoc2` package entries | Listed only | Files are identified, but notebook hierarchy and section order are not interpreted |
|
||||
| Standalone `.onetoc2` | Unsupported in the app | The picker accepts only `.one` and `.onepkg` |
|
||||
@@ -30,6 +30,14 @@ and carries at most 32 KiB of history between blocks in one folder. The MSZIP
|
||||
tests establish CAB extraction behavior; no redistributable MSZIP-compressed
|
||||
OneNote export is currently present, so they do not establish producer-specific
|
||||
`.onepkg` compatibility.
|
||||
Quantum validates level/window metadata, bounds its window to 2 MiB by default,
|
||||
requires full 32 KiB intermediate frames, preserves its arithmetic models and
|
||||
LZ window across frames, and injects exactly one `0xFF` CAB block sentinel.
|
||||
Trailer padding is limited to the documented 0..4 zero bytes; match, frame,
|
||||
output, operation, and cancellation bounds are enforced. Its tests likewise
|
||||
establish CAB extraction behavior only: no redistributable Quantum-compressed
|
||||
OneNote export is currently present.
|
||||
|
||||
Trailing CAB data is ignored with a warning. Reserved CAB areas are bounded and
|
||||
skipped. CAB names are decoded as strict UTF-8; unmarked valid UTF-8 receives a
|
||||
warning, while invalid non-ASCII legacy-code-page names are rejected rather
|
||||
|
||||
@@ -12,11 +12,12 @@ The desktop `.one` reader adapts selected structures from the pinned
|
||||
|
||||
The browser-native package path has separate MIT provenance:
|
||||
|
||||
| TypeScript module | Upstream path(s) | Revision and selected licence | Port scope and deviations |
|
||||
| -------------------------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `src/onenote/onepkg/{cabinet,checksum}.ts` | rust-cab `src/{cabinet,folder,file,ctype,checksum}.rs` | `c5839f5fdfa4c4e7cc9b22f570c79c96af0560e2`, MIT | CAB reading/checksum adapted for immutable browser bytes, strict normalized paths, checked ranges/checksums, whole-folder extraction, explicit limits, and cancellation/yield hooks; writers are not ported |
|
||||
| `src/onenote/onepkg/mszip/decoder.ts` | rust-cab `src/mszip.rs`; fflate raw DEFLATE | rust-cab revision above; fflate 0.8.3, MIT | `CK` framing and persistent 32 KiB folder history follow rust-cab; raw DEFLATE uses fflate with a caller-owned bounded buffer and strict declared-size checks |
|
||||
| `src/onenote/onepkg/lzx/{bit-reader,huffman,decoder}.ts` | lzxd 0.2.5 `src/{bitstream,tree,block,window,lib}.rs` | `4748e43594e3e30cff2ace3a6ad7a376c9816fdd`, MIT option of MIT OR Apache-2.0 | Directly translated core with typed arrays, CAB CFDATA boundaries, persistent folder state, strict malformed-stream errors, and operation/window/cancellation limits; delta reference data is omitted |
|
||||
| TypeScript module | Upstream path(s) | Revision and selected licence | Port scope and deviations |
|
||||
| ------------------------------------------------------------------------ | ------------------------------------------------------ | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `src/onenote/onepkg/{cabinet,checksum}.ts` | rust-cab `src/{cabinet,folder,file,ctype,checksum}.rs` | `c5839f5fdfa4c4e7cc9b22f570c79c96af0560e2`, MIT | CAB reading/checksum adapted for immutable browser bytes, strict normalized paths, checked ranges/checksums, whole-folder extraction, explicit limits, and cancellation/yield hooks; writers are not ported |
|
||||
| `src/onenote/onepkg/mszip/decoder.ts` | rust-cab `src/mszip.rs`; fflate raw DEFLATE | rust-cab revision above; fflate 0.8.3, MIT | `CK` framing and persistent 32 KiB folder history follow rust-cab; raw DEFLATE uses fflate with a caller-owned bounded buffer and strict declared-size checks |
|
||||
| `src/onenote/onepkg/quantum/{bit-reader,budget,decoder,model,tables}.ts` | compcol `src/quantum/{bits,decoder,model,tables}.rs` | `04a6db2aa7bd487a89c559631d79d1b384139f50`, MIT | Native TypeScript arithmetic/model/LZ decoder; fixed CAB output, 2 MiB maximum window, strict match/frame/trailer checks, two-block lookahead, operation budget, and cancellation checkpoints |
|
||||
| `src/onenote/onepkg/lzx/{bit-reader,huffman,decoder}.ts` | lzxd 0.2.5 `src/{bitstream,tree,block,window,lib}.rs` | `4748e43594e3e30cff2ace3a6ad7a376c9816fdd`, MIT option of MIT OR Apache-2.0 | Directly translated core with typed arrays, CAB CFDATA boundaries, persistent folder state, strict malformed-stream errors, and operation/window/cancellation limits; delta reference data is omitted |
|
||||
|
||||
The bounded binary readers, format detection, parser limits/error model, path
|
||||
policy, worker protocol/DTOs, package-to-section boundary, and application UI
|
||||
@@ -24,8 +25,8 @@ are original TypeScript implementations guided by the normative specifications
|
||||
and project requirements.
|
||||
|
||||
Files adapted from `onenote.rs` carry an MPL-2.0 notice, SPDX identifier,
|
||||
upstream path/revision, and deviations. Files adapted from rust-cab or lzxd
|
||||
carry their MIT provenance, copyright notice, exact revision, and deviations.
|
||||
upstream path/revision, and deviations. Files adapted from rust-cab, compcol,
|
||||
or lzxd carry their MIT provenance, copyright notice, exact revision, and deviations.
|
||||
Licence texts are reproduced in `LICENSES/` and included in the release ZIP.
|
||||
|
||||
Focused tests cover successful pinned fixtures plus malformed bounds,
|
||||
|
||||
@@ -8,10 +8,11 @@ Version 0.1.0 records these immutable implementation revisions:
|
||||
| [`toolbox-portal`](https://git.add-ideas.de/zemion/toolbox-portal/src/commit/5d2e466ad61e36a0e2be2a2d8459b1a127f53f11) | `5d2e466ad61e36a0e2be2a2d8459b1a127f53f11` | Reviewed ZIP shape, exact checksum lock, and independent-app assembly model |
|
||||
| [`onenote.rs`](https://github.com/msiemens/onenote.rs/tree/5138a39a3f4e72b840932f9872fecde52fa9da60) | `5138a39a3f4e72b840932f9872fecde52fa9da60` | Selected desktop OneStore and MS-ONE structures adapted into TypeScript |
|
||||
| [`rust-cab`](https://github.com/mdsteele/rust-cab/tree/c5839f5fdfa4c4e7cc9b22f570c79c96af0560e2) | `c5839f5fdfa4c4e7cc9b22f570c79c96af0560e2` | CAB structures, checksum, enumeration/extraction, MSZIP history, and unit fixtures |
|
||||
| [`compcol`](https://github.com/KarpelesLab/compcol/tree/04a6db2aa7bd487a89c559631d79d1b384139f50) | `04a6db2aa7bd487a89c559631d79d1b384139f50` | Quantum bit reader, arithmetic models, LZ window, CAB sentinel behavior, and tests |
|
||||
| [`lzxd` 0.2.5](https://github.com/Lonami/lzxd/tree/4748e43594e3e30cff2ace3a6ad7a376c9816fdd) | `4748e43594e3e30cff2ace3a6ad7a376c9816fdd` | LZX bitstream, Huffman, block/window, match, and E8 logic adapted into TypeScript |
|
||||
|
||||
The SDK and Portal revisions were inspected from the local repositories. The
|
||||
three parser references were cloned and reviewed at the hashes above; a branch
|
||||
four parser references were cloned and reviewed at the hashes above; a branch
|
||||
name or package version alone is not used as provenance. Source-level mappings
|
||||
and behavioural deviations are in [PORTING.md](PORTING.md).
|
||||
|
||||
|
||||
@@ -58,8 +58,9 @@ The CAB/package path applies these defaults:
|
||||
| Total extracted entry bytes | 512 MiB |
|
||||
| Folder compression ratio | 200:1 |
|
||||
| One MSZIP compressed block | 32 KiB + 12 B |
|
||||
| Quantum window | 2 MiB |
|
||||
| LZX window | 8 MiB |
|
||||
| LZX decode operations | 1,000,000,000 |
|
||||
| LZX/Quantum decode operations | 1,000,000,000 |
|
||||
|
||||
A CFDATA block may declare at most 32 KiB of output. Counts and offsets use
|
||||
checked arithmetic and bounded readers. The CAB parser verifies supplied block
|
||||
@@ -71,8 +72,12 @@ MSZIP checks its `CK` framing and raw DEFLATE data, writes into a fixed caller
|
||||
buffer with one overflow-detection byte, requires the exact declared output
|
||||
size, and retains no more than 32 KiB of history. LZX checks its window, Huffman
|
||||
trees, block sizes, match ranges, output size, and operation budget. CAB
|
||||
extraction checks cancellation and yields between data blocks so the worker can
|
||||
process termination. Desktop OneStore traversal checks fragment/reference
|
||||
Quantum limits its window, model scans, arithmetic renormalisation, matches,
|
||||
frame/output boundaries, operation count, and trailer padding. It queues at
|
||||
most the current and next compressed block so a synthetic `0xFF` sentinel can
|
||||
be consumed without buffering a complete folder. CAB extraction checks
|
||||
cancellation during Quantum work and yields between data blocks so the worker
|
||||
can process termination. Desktop OneStore traversal checks fragment/reference
|
||||
cycles, nesting, object/property/reference counts, and byte ranges.
|
||||
|
||||
## Rendering and links
|
||||
|
||||
@@ -26,6 +26,7 @@ export async function collectReleaseMetadataFiles(projectRoot) {
|
||||
|
||||
for (const licenseName of [
|
||||
'AGPL-3.0.txt',
|
||||
'compcol-MIT.txt',
|
||||
'MPL-2.0.txt',
|
||||
'lzxd-MIT.txt',
|
||||
'rust-cab-MIT.txt',
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
37
tests/fixtures/README.md
vendored
37
tests/fixtures/README.md
vendored
@@ -96,6 +96,43 @@ so decoding it without folder history fails.
|
||||
This fixture establishes block-history behavior only. It is not a OneNote
|
||||
export and is not evidence of `.onepkg` producer compatibility.
|
||||
|
||||
## Inline compcol Quantum fixtures
|
||||
|
||||
`src/onenote/onepkg/quantum/decoder.test.ts` copies two small Quantum payloads
|
||||
from compcol
|
||||
[`tests/quantum.rs`](https://github.com/KarpelesLab/compcol/blob/04a6db2aa7bd487a89c559631d79d1b384139f50/tests/quantum.rs)
|
||||
at commit `04a6db2aa7bd487a89c559631d79d1b384139f50`. The 48-byte
|
||||
`qtm.txt` payload decodes with an 18-bit window to the 59-byte ASCII string
|
||||
`If you can read this, the Quantum decompressor is working!` followed by a
|
||||
line feed. The first two compressed blocks of compcol's multi-frame zero-run
|
||||
fixture are 14 and 6 bytes and decode to two 32,768-byte zero frames. Tests
|
||||
exercise arithmetic-model and LZ-window continuity, per-block synthetic
|
||||
sentinels, malformed trailers, truncation, cancellation, and operation limits.
|
||||
|
||||
The immediate reviewed compcol source is MIT-licensed; its exact licence is in
|
||||
`LICENSES/compcol-MIT.txt`. The compcol comments record that the payloads were
|
||||
originally extracted from libmspack CAB test files. This repository copies only
|
||||
the small arrays published in the pinned compcol test source, not those CAB
|
||||
files.
|
||||
|
||||
`src/onenote/onepkg/cabinet.test.ts` wraps the 48-byte payload in a generated,
|
||||
single-folder CAB so the actual CAB enumeration, checksum, and extraction path
|
||||
is covered:
|
||||
|
||||
- CAB compression type: `0x1222` (Quantum level 2, 18-bit/256 KiB window).
|
||||
- CFDATA sizes/checksum: 48 compressed bytes, 59 output bytes, checksum
|
||||
`0x4bec2dfd`.
|
||||
- CAB size: 124 bytes.
|
||||
- CAB SHA-256:
|
||||
`f622c5a8f5a000dfaddc5569750344f5a6e35d0de9fbd3c4f0c8720c7d94d291`.
|
||||
- Wrapper/header/filename licence: original test data under this project's
|
||||
MPL-2.0 licence; embedded payload licence: compcol MIT as above.
|
||||
|
||||
The frozen CAB contains only the compressed payload; the decoder appends the
|
||||
required `0xFF` byte after each CFDATA block in memory. These are CAB-level
|
||||
codec fixtures, not OneNote exports, and do not establish producer-specific
|
||||
`.onepkg` compatibility.
|
||||
|
||||
## Distribution policy
|
||||
|
||||
The decoded fixture bytes are used only by tests. Vite does not copy them into
|
||||
|
||||
@@ -74,6 +74,7 @@ describe('release archive', () => {
|
||||
expect.arrayContaining([
|
||||
'LICENSE',
|
||||
'LICENSES/AGPL-3.0.txt',
|
||||
'LICENSES/compcol-MIT.txt',
|
||||
'LICENSES/MPL-2.0.txt',
|
||||
'LICENSES/THIRD-PARTY-LICENSES.txt',
|
||||
'LICENSES/lzxd-MIT.txt',
|
||||
@@ -94,6 +95,11 @@ describe('release archive', () => {
|
||||
expect(
|
||||
createHash('sha256').update(files['LICENSES/lzxd-MIT.txt']!).digest('hex')
|
||||
).toBe('c7a1dcbf839b1dfb72f24e1de12ab55f789db2c61d894ca9aa8b5f595490d9ca');
|
||||
expect(
|
||||
createHash('sha256')
|
||||
.update(files['LICENSES/compcol-MIT.txt']!)
|
||||
.digest('hex')
|
||||
).toBe('a7009ce74b33a2afcf9ab38007bf3c8f5020d9b816dc5fc7296bf17e6d7dc796');
|
||||
expect(
|
||||
files['LICENSES/THIRD-PARTY-LICENSES.txt']!.toString('utf8')
|
||||
).toContain('fflate@0.8.3');
|
||||
|
||||
Reference in New Issue
Block a user