feat: add local OneNote and ONEPKG reader

This commit is contained in:
2026-07-22 17:06:03 +02:00
commit f581cbdced
93 changed files with 14949 additions and 0 deletions

57
docs/FORMAT_SUPPORT.md Normal file
View File

@@ -0,0 +1,57 @@
# Format support
## Version 0.1.0 support matrix
| Input or feature | Status | Tested evidence and boundary |
| ---------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------- |
| Desktop revision-store `.one` | Supported subset | Real 34,904-byte Joplin golden; three pages |
| FSSHTTP-packaged `.one` | Unsupported | Header is detected and rejected with a controlled diagnostic |
| Unknown/legacy `.one` variants | Unsupported | File type and format GUIDs are validated before traversal |
| `.onepkg`, single CAB, LZX | Supported subset | Real Joplin package: LZX `0x1203`, three CFDATA blocks, seven entries, five sections, seven pages |
| 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 | Unsupported | Metadata is recognized; extraction fails with `cab-compression-unsupported` |
| CAB Quantum | Unsupported | Metadata is recognized; extraction fails with `cab-compression-unsupported` |
| 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` |
“Supported subset” is intentionally narrower than “all files with this
extension.” A successful open requires the tested desktop OneStore structures
and content graph nodes. Malformed offsets, counts, references, CAB checksums,
paths, compressed data, and unsupported variants produce bounded diagnostics or
a controlled failure.
The default LZX limit accepts CAB window sizes through 8 MiB, but compatibility
claims are fixture-backed only for window bits 18 (`0x1203`) and 19 (`0x1303`).
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
than guessed.
## Extracted OneNote data
The desktop section path currently returns:
- section display name;
- page ID, title, created/updated timestamp when available, and page level;
- visible Unicode/extended-ASCII rich text as plain text and a text preview;
- diagnostics tied to an offset, structure, or page where available.
It does not preserve visual position, rich-text formatting, list formatting,
tables, images, ink, equations, embedded files, audio/video, active links,
page versions/conflicts, or exports. Unsupported content objects remain visible
as diagnostics instead of being silently presented as parsed.
Within a supported `.onepkg`, each `.one` entry is parsed independently.
Malformed or unsupported sections are marked failed/unsupported while other
sections remain available. Package navigation is temporary normalized-path
order until `.onetoc2` interpretation is implemented.
## Test fixtures
The exact hashes, source revisions, licences, expected page names, and CAB
structures are recorded in
[`tests/fixtures/README.md`](../tests/fixtures/README.md). Tests use no private
notebooks and do not treat a synthetic archive as proof of Microsoft OneNote
export compatibility.

33
docs/PORTING.md Normal file
View File

@@ -0,0 +1,33 @@
# Port provenance
The desktop `.one` reader adapts selected structures from the pinned
`onenote.rs` implementation. It is not a line-for-line or complete port.
| TypeScript module | Upstream path(s) | Revision | Port scope and deviations |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `src/onenote/onestore/file-node.ts` | `onestore/desktop/file_structure/file_node_list_fragment.rs`, `file_node/*`, chunk references | `onenote.rs@5138a39a3f4e72b840932f9872fecde52fa9da60` | Selected; immutable byte input, strict range checks, fragment/child cycle detection, and depth/count limits |
| `src/onenote/onestore/property-set.ts` | `onestore/shared/{object_prop_set,prop_set,property,object_stream_header}.rs` | same | Substantial; bounded vectors/counts/nesting and serializable discriminated unions |
| `src/onenote/onestore/desktop-store.ts` | `onestore/desktop/{file_node,file_structure,objects}/*` | same | Selected desktop section path; explicit missing-reference errors; file-data/attachment payloads skipped with diagnostics |
| `src/onenote/one/{constants,section}.ts` | `one/property*`, `one/property_set/*`, `onenote/{section,page,page_series,outline,rich_text}.rs` | same | Page metadata, titles, and plain text only; bounded graph traversal; visual/embedded content diagnosed |
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 and MSZIP are not ported |
| `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
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.
Licence texts are reproduced in `LICENSES/` and included in the release ZIP.
Focused tests cover successful pinned fixtures plus malformed bounds,
references, checksums, paths, Huffman trees, compressed blocks, limits, and
cancellation. Fixture origins and redistribution terms are recorded separately
in `tests/fixtures/README.md`.

View File

@@ -0,0 +1,46 @@
# Specifications and pinned references
Version 0.1.0 records these immutable implementation revisions:
| Repository | Exact revision | Use |
| ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------- |
| [`toolbox-sdk`](https://git.add-ideas.de/zemion/toolbox-sdk/src/commit/5e206baf42dfae5a00f2743fb2cc501bb12d9b7a) | `5e206baf42dfae5a00f2743fb2cc501bb12d9b7a` | App contract, React shell, manifest schema/testkit, and nested-path behavior |
| [`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, and two unit fixtures |
| [`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
name or package version alone is not used as provenance. Source-level mappings
and behavioural deviations are in [PORTING.md](PORTING.md).
## Normative Microsoft documents
The implementation uses the official specifications as the source of format
truth and the pinned projects as implementation references:
- [[MS-ONESTORE]: OneNote Revision Store File Format](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-onestore/ae670cd2-4b38-4b24-82d1-87cfb2cc3725),
particularly sections 2.2.4 (chunk references), 2.3.1 (header), 2.4
(file-node lists and fragments), 2.5.1-2.5.8 (object-space/revision
manifests), and 2.6.7 (property sets).
- [[MS-ONE]: OneNote File Format](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-one/73d22548-a613-4350-8c23-07d15576be50),
particularly sections 2.1.8-2.1.13 (root objects, page object spaces, and
property sets), 2.2.17-2.2.23 (section/page-series/outline/rich-text nodes),
and 2.2.30, 2.2.34, 2.2.62-2.2.65, and 2.2.91 (page metadata/manifest,
titles, page-space references, and section name).
- [[MS-CAB]: Cabinet File Format](https://download.microsoft.com/download/4/d/a/4da14f27-b4ef-4170-a6e6-5b1ef85b1baa/%5Bms-cab%5D.pdf),
sections 2.1-2.4 (`CFHEADER`, `CFFOLDER`, `CFFILE`, and `CFDATA`) and 3.1
(checksum method).
- [[MS-PATCH]: LZX Delta Compression and Decompression](https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-patch/cc78752a-b4af-4eee-88cb-01f4d8a4c2bf),
sections 2.1-2.7 (bitstream/window concepts, header/E8 translation, blocks,
Huffman trees, pretree encoding, tokens, and decoding). `lzxd` 0.2.5 states
that it was based on revision 9.0; the port removes delta/reference-data use
and applies CAB CFDATA chunk framing.
The Microsoft documents are linked rather than copied. Their published pages
can change independently; the source references and golden bytes remain pinned
for reproducibility.
`one2html` is deliberately not pinned or inspected because visual HTML
rendering is outside this release.

113
docs/SECURITY.md Normal file
View File

@@ -0,0 +1,113 @@
# Security model
## Trust boundary and local processing
Selected `.one` and `.onepkg` files are untrusted. The main thread checks the
source size, reads one file into an `ArrayBuffer`, transfers it to a dedicated
module worker, and receives serializable data-transfer objects. CAB entries are
extracted only into memory; archive paths are never written to the filesystem.
The worker is a fault-containment and responsiveness boundary, not a security
sandbox. A successful open keeps its worker session alive for page requests.
Clearing the file, choosing another file, or unmounting the app terminates that
worker; a failed open also terminates it. JavaScript garbage collection does
not promise immediate release or secure erasure.
The application does not:
- upload or transmit notebook bytes;
- use a backend, proxy, telemetry, analytics, remote fonts, or external content
fetching;
- persist source or derived content in LocalStorage, IndexedDB, Cache Storage,
cookies, or a service worker;
- execute notebook content or inject notebook HTML/SVG;
- use WebAssembly, a Rust runtime, or a native helper.
## Active default limits
The main thread rejects a source larger than 512 MiB before reading it. The
desktop section parser then applies these independent defaults:
| Resource | Default limit |
| ------------------------------------- | -------------------: |
| Section bytes | 512 MiB |
| File-node list fragments / nesting | 16,384 / 64 |
| File nodes | 1,000,000 |
| Objects | 500,000 |
| Properties per set / property nesting | 16,384 / 64 |
| Stream IDs | 1,000,000 |
| One property vector | 64 MiB |
| Object references | 1,000,000 |
| Content-graph depth | 256 |
| Pages per section | 100,000 |
| Diagnostics per section | 10,000 |
| Extracted text per section | 2,000,000 characters |
The CAB/package path applies these defaults:
| Resource | Default limit |
| ------------------------------- | -------------: |
| Cabinet input | 512 MiB |
| Folders / files | 1,024 / 10,000 |
| CFDATA blocks | 131,072 |
| CAB name bytes | 255 |
| Path characters / path segments | 4,096 / 64 |
| One extracted file | 256 MiB |
| One folder output | 512 MiB |
| Total folder output | 512 MiB |
| Total extracted entry bytes | 512 MiB |
| Folder compression ratio | 200:1 |
| LZX window | 8 MiB |
| LZX 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
checksums, rejects overlapping/out-of-range data, rejects absolute/traversal,
empty, control-character, and case-insensitive duplicate paths, and refuses
multi-cabinet/spanning entries.
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 cycles, nesting, object/property/reference counts,
and byte ranges.
## Rendering and links
DTO strings are rendered as React text nodes. Notebook HTML, active SVG,
scripts, attachment payloads, and automatic external navigation are not
rendered. Link activation is not implemented. If links are added, they require
explicit user action, scheme validation, and safe navigation attributes.
## Known gaps and roadmap
- There is no wall-clock deadline. Synchronous desktop `.one` parsing cannot
observe cooperative cancellation until control returns; terminating the
worker is the current hard stop.
- The package flow holds the source, decompressed folder buffers, extracted
entry slices, and parsed DTOs in memory at overlapping times. The byte/count
limits bound these individually but do not guarantee a low peak heap.
- Section text/page/diagnostic limits apply per contained section rather than
once to the complete package.
- CAB checksums detect accidental corruption but do not authenticate a package.
- Format coverage is narrow and has only the documented golden files. New
producer/version claims require licensed regression fixtures and malformed
variants.
- The worker boundary reduces UI impact but is not a substitute for browser and
operating-system isolation. Open especially sensitive files in a dedicated
browser profile if that risk matters.
Planned hardening includes an elapsed-time budget, cancellation checkpoints in
desktop OneStore graph traversal, aggregate package DTO/text budgets, lower
peak-memory extraction, expanded malformed-input/fuzz corpora, and additional
licensed OneNote export fixtures.
## Fixtures and vulnerability reporting
Never commit private notebooks. Fixture provenance and redistribution terms
must be recorded in `tests/fixtures/README.md`.
Report suspected parser vulnerabilities privately to the repository owner
through the Gitea instance before publishing malicious fixtures. Include the
application version, browser, smallest reproducible input, diagnostic, and
observed resource use. Do not attach sensitive notebooks.