Skip to content

Spike: offline OneNote .one-file importer (PoC) - #10649

Draft
eliandoran wants to merge 2 commits into
mainfrom
feature/onenote_file_import_poc
Draft

Spike: offline OneNote .one-file importer (PoC)#10649
eliandoran wants to merge 2 commits into
mainfrom
feature/onenote_file_import_poc

Conversation

@eliandoran

Copy link
Copy Markdown
Contributor

⚠️ Proof of concept / spike. Text + structure + media only. Opening as a draft to get eyes on the approach, not for merge as-is.

What this is

A from-scratch TypeScript parser for the OneNote desktop .one / .onetoc2 binary format (MS-ONESTORE revision store + MS-ONE object model), plus an importer that turns a .one file into a Trilium note tree — fully offline, no Microsoft Graph, no account. This reaches the .one-file case the Graph API fundamentally cannot (local/unsynced notebooks, and the content Graph drops).

The parser is pure DataView/Uint8Array with no external dependencies or runtime, so it lives in trilium-core and runs in the Node server and the standalone/WASM build alike.

Technology evaluation

I tried three approaches before landing on pure-TS:

Tech Verdict
Rust onenote_parser → WASM/native Ruled out — no Rust toolchain available to build unattended (best fidelity, but not reachable in this environment)
Python pyOneNote sidecar Rejected — crashed (infinite recursion) on the sample files, forensic/partial (no ink, incomplete rich text), and the wrong runtime for a Node/Electron/WASM app
Pure TypeScript Chosen — architectural fit, zero deps; ported the format from the msiemens/onenote.rs reference

How it's wired

  • services/import/onenote-file/one_parser.ts — decodes the file-node lists, object-space/revision/object-group graph and property sets; extracts page hierarchy, titles, body text (reading order) and embedded images/files.
  • services/import/onenote-file/importer.ts — parse → section root note → one note per page (subpage levels nested), text as HTML, images inline, other files as attachments.
  • dispatch.ts routes the .one/.onetoc2 extension to it (read from the upload buffer; no format tag needed).
  • A new "OneNote file" import-dialog provider (offline), distinct from the existing Graph-based "OneNote" provider.

To try it: pnpm server:start → Import → OneNote file → drop a .one exported from OneNote desktop.

Proven against real files

Validated against the onenote.rs desktop test corpus:

  • Page hierarchy, titles, body text in reading order
  • Unicode (accents, CJK), math (as OOXML text), handwriting-recognition text
  • Embedded images and files (extracted a .docx, PNGs; parsed a 112 MB file with embedded PDFs)
  • Correctly rejects the OneDrive/FSSHTTPB variant with a clear error
  • Filters ink U+FFFC placeholders; strips hyperlink markers

Tests

  • one_parser.spec.ts — parser: pages/titles/text, embedded-image bytes, format rejection
  • importer.spec.ts — end-to-end: .one → 3-page note tree with HTML body and an inline image attachment

5 tests pass; pnpm typecheck is clean. The fixture is a small desktop .one from onenote.rs (MPL-2.0), attributed in the spec.

Scope / not done yet (why it's a draft)

  • Not handled: formatting (bold/italic/colors/sizes), ink strokes, tables, note tags/to-dos, math rendering, cross-page links, page timestamps. Text + structure + media only.
  • Desktop format only (.one desktop export / .onetoc2); OneDrive-downloaded FSSHTTPB files are rejected (a separate code path in the reference parser).
  • The Graph-based importer remains the higher-fidelity path for cloud-synced notebooks; this complements it for the offline case.

Open questions for reviewers

  • Is an offline .one path worth productionizing, or should effort stay on the Graph importer?
  • If yes: incrementally grow this parser (formatting/tables/ink), or revisit Rust onenote_parser → WASM for full fidelity once a build toolchain is in place?
  • OK to ship a small third-party .one test fixture (MPL-2.0), or should we generate/host our own?

🤖 Generated with Claude Code

Adds a from-scratch TypeScript parser for the OneNote desktop .one /
.onetoc2 binary format (MS-ONESTORE revision store + MS-ONE object
model), ported from the msiemens/onenote.rs reference. It decodes the
file node lists, object-space/revision/object-group graph and property
sets to extract the page hierarchy, page titles, body text (reading
order) and embedded images/files — all in pure TS (DataView/Uint8Array),
so it runs in the Node server and the standalone/WASM build alike, with
no external runtime.

Technology evaluation (see the report): Rust onenote_parser -> WASM was
ruled out (no toolchain here, can't build unattended); Python pyOneNote
crashed on the samples and is the wrong runtime for Trilium; a pure-TS
parser is the architectural fit and proved out against the onenote.rs
desktop test corpus (text, unicode, math, embedded files, a 112 MB
file), correctly rejecting the OneDrive/FSSHTTPB variant.

Wiring:
- services/import/onenote-file/one_parser.ts — the binary decoder.
- services/import/onenote-file/importer.ts — parse -> section root note,
  a note per page (subpage levels nested), text as HTML, images inline
  and other files as attachments.
- dispatch.ts routes the .one/.onetoc2 extension to it (read from the
  upload buffer, no format tag).
- a new "OneNote file" import-dialog provider (offline, no account),
  distinct from the Graph-based "OneNote" provider.

Scope (PoC): formatting, ink, tables, note tags, math and cross-page
links are not handled yet; the Graph importer remains the higher-fidelity
path for cloud notebooks, while this reaches the offline .one-file case
Graph cannot. Tests cover the parser and the end-to-end import against a
small desktop .one fixture (from onenote.rs, MPL-2.0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@github-actions

Copy link
Copy Markdown
Contributor

🖥️ App preview is ready!

🔗 Preview URL: https://pr-10649.trilium-app.pages.dev
📖 Production URL: https://app.triliumnotes.org

✅ All checks passed

This preview will be updated automatically with new commits.

@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 19.46kB (0.02%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
standalone-esm 53.3MB 19.21kB (0.04%) ⬆️
client-esm 49.56MB 254 bytes (0.0%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: client-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
src/src-*.css -4.27kB 248.92kB -1.69%
src/desktop_layout-*.js -9 bytes 80.36kB -0.01%
src/import_dialog-*.js 2.45kB 27.97kB 9.6% ⚠️
src/desktop_layout-*.css 237 bytes 12.7kB 1.9%
src/print-*.css 712 bytes 7.91kB 9.89% ⚠️
src/MediaPreview-*.css -101 bytes 4.3kB -2.29%
src/import_dialog-*.css 852 bytes 4.2kB 25.48% ⚠️
src/SiblingNavigator-*.css 384 bytes 1.21kB 46.43% ⚠️
src/ImageViewer-*.css -85 bytes 1.07kB -7.38%
src/FormCheckbox-*.js 88 bytes 1.04kB 9.24% ⚠️

Files in src/import_dialog-*.js:

  • ./src/widgets/dialogs/import/useProviderImport.ts → Total Size: 2.14kB

  • ./src/widgets/dialogs/import/index.ts → Total Size: 187 bytes

  • ./src/widgets/dialogs/import/onenote_file.tsx → Total Size: 1.98kB

view changes for bundle: standalone-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/abstract_provider-BcA4PHmk.js (New) 2.08MB 2.08MB 100.0% 🚀
src/src.css -4.27kB 248.57kB -1.69%
assets/src-D0j-jPDI.js (New) 213.43kB 213.43kB 100.0% 🚀
assets/crypto_provider-CI3EocSN.js (New) 97.15kB 97.15kB 100.0% 🚀
src/desktop_layout.js -9 bytes 80.03kB -0.01%
assets/in_app_help_provider-B-8V8e4h.js (New) 78.74kB 78.74kB 100.0% 🚀
assets/zip-BreSJQcr.js (New) 56.14kB 56.14kB 100.0% 🚀
src/import_dialog.js 2.44kB 27.79kB 9.61% ⚠️
src/desktop_layout.css 237 bytes 12.7kB 1.9%
assets/browser_routes-q1L2KdQX.js (New) 8.21kB 8.21kB 100.0% 🚀
src/print.css 712 bytes 7.91kB 9.89% ⚠️
assets/becca_loader-DJZhpKIQ.js (New) 5.71kB 5.71kB 100.0% 🚀
assets/local-server-worker-CPA14aaT.js (New) 4.65kB 4.65kB 100.0% 🚀
src/MediaPreview.css -101 bytes 4.3kB -2.29%
src/import_dialog.css 852 bytes 4.2kB 25.48% ⚠️
assets/html-9RJ7V3OX.js (New) 2.93kB 2.93kB 100.0% 🚀
assets/ru-D67I8-6a.js (New) 2.39kB 2.39kB 100.0% 🚀
assets/backup_provider-C5VLYdcn.js (New) 2.32kB 2.32kB 100.0% 🚀
assets/uk-DWVmBwUK.js (New) 2.31kB 2.31kB 100.0% 🚀
assets/log_provider-B3Vfk5gW.js (New) 1.96kB 1.96kB 100.0% 🚀
assets/0216__move_content_into_blobs-Cd6vR-gw.js (New) 1.9kB 1.9kB 100.0% 🚀
assets/zip_export_provider_factory-DqLiz_Fh.js (New) 1.83kB 1.83kB 100.0% 🚀
assets/ar-DgrP9z7D.js (New) 1.81kB 1.81kB 100.0% 🚀
assets/cs-B4xdADcX.js (New) 1.78kB 1.78kB 100.0% 🚀
assets/pl-D9Dbrz0u.js (New) 1.74kB 1.74kB 100.0% 🚀
assets/hi-C5TMGluA.js (New) 1.74kB 1.74kB 100.0% 🚀
assets/zh-cn-C9KwAw-L.js (New) 1.54kB 1.54kB 100.0% 🚀
assets/de-fu6q4ltd.js (New) 1.52kB 1.52kB 100.0% 🚀
assets/zh-tw-C8sZsDoI.js (New) 1.51kB 1.51kB 100.0% 🚀
assets/ja-BKRJYyHy.js (New) 1.35kB 1.35kB 100.0% 🚀
assets/pt-M0ZDuQAb.js (New) 1.3kB 1.3kB 100.0% 🚀
assets/ga-ClUwFz6x.js (New) 1.29kB 1.29kB 100.0% 🚀
assets/en-gb-Bovfg2q8.js (New) 1.29kB 1.29kB 100.0% 🚀
assets/pt-br-DZLN7ZK6.js (New) 1.28kB 1.28kB 100.0% 🚀
assets/fr-BHjm7Na6.js (New) 1.27kB 1.27kB 100.0% 🚀
assets/es-Drk7zLyM.js (New) 1.25kB 1.25kB 100.0% 🚀
assets/it-ALNeFXnG.js (New) 1.23kB 1.23kB 100.0% 🚀
assets/ro-B4hrZjcr.js (New) 1.22kB 1.22kB 100.0% 🚀
assets/id-wH9xrn13.js (New) 1.22kB 1.22kB 100.0% 🚀
src/SiblingNavigator.css 384 bytes 1.21kB 46.43% ⚠️
src/ImageViewer.css -85 bytes 1.07kB -7.38%
src/FormCheckbox.js 88 bytes 990 bytes 9.76% ⚠️
assets/0233__migrate_geo_map_to_collection-DdTkDpZK.js (New) 777 bytes 777 bytes 100.0% 🚀
assets/0220__migrate_images_to_attachments-BGXQrwzl.js (New) 672 bytes 672 bytes 100.0% 🚀
assets/0239__disable_totp_when_mfa_was_turned_off-DkGgdb8U.js (New) 623 bytes 623 bytes 100.0% 🚀
assets/0234__migrate_ai_chat_to_code-BkgYUG9F.js (New) 443 bytes 443 bytes 100.0% 🚀
assets/markdown-0sHDXF2S.js (New) 323 bytes 323 bytes 100.0% 🚀
assets/abstract_provider-MGWSJhxe.js (Deleted) -2.08MB 0 bytes -100.0% 🗑️
assets/src-B3-4mSky.js (Deleted) -194.86kB 0 bytes -100.0% 🗑️
assets/crypto_provider-Bz0bCRYi.js (Deleted) -97.15kB 0 bytes -100.0% 🗑️
assets/in_app_help_provider-CzPHweHv.js (Deleted) -78.74kB 0 bytes -100.0% 🗑️
assets/zip-BOJ-_ooW.js (Deleted) -56.09kB 0 bytes -100.0% 🗑️
assets/browser_routes-BjJwP6UP.js (Deleted) -8.21kB 0 bytes -100.0% 🗑️
assets/becca_loader-Cyy1ieH8.js (Deleted) -5.71kB 0 bytes -100.0% 🗑️
assets/local-server-worker-CaxveOSU.js (Deleted) -4.65kB 0 bytes -100.0% 🗑️
assets/html-D_Jm_bXj.js (Deleted) -2.93kB 0 bytes -100.0% 🗑️
assets/ru-BsXDtgro.js (Deleted) -2.39kB 0 bytes -100.0% 🗑️
assets/backup_provider-GSbPHuyh.js (Deleted) -2.32kB 0 bytes -100.0% 🗑️
assets/uk-*.js (Deleted) -2.31kB 0 bytes -100.0% 🗑️
assets/log_provider-D2jiKerc.js (Deleted) -1.96kB 0 bytes -100.0% 🗑️
assets/0216__move_content_into_blobs-BOkqEBA9.js (Deleted) -1.9kB 0 bytes -100.0% 🗑️
assets/zip_export_provider_factory-0ioPcbi3.js (Deleted) -1.83kB 0 bytes -100.0% 🗑️
assets/ar-D5uJFJ2s.js (Deleted) -1.81kB 0 bytes -100.0% 🗑️
assets/cs-BXWdlwK_.js (Deleted) -1.78kB 0 bytes -100.0% 🗑️
assets/pl-FQqBwsAr.js (Deleted) -1.74kB 0 bytes -100.0% 🗑️
assets/hi-ms5s6NN-.js (Deleted) -1.74kB 0 bytes -100.0% 🗑️
assets/zh-cn-NaMBDRri.js (Deleted) -1.54kB 0 bytes -100.0% 🗑️
assets/de-Bv0i-HMx.js (Deleted) -1.52kB 0 bytes -100.0% 🗑️
assets/zh-tw-Bt1pUMRW.js (Deleted) -1.51kB 0 bytes -100.0% 🗑️
assets/ja-CJIz_QQN.js (Deleted) -1.35kB 0 bytes -100.0% 🗑️
assets/pt-DEDI2NXR.js (Deleted) -1.3kB 0 bytes -100.0% 🗑️
assets/ga-DLrUpkZF.js (Deleted) -1.29kB 0 bytes -100.0% 🗑️
assets/en-gb-kF20oExB.js (Deleted) -1.29kB 0 bytes -100.0% 🗑️
assets/pt-br-PA0OgaXg.js (Deleted) -1.28kB 0 bytes -100.0% 🗑️
assets/fr-Bri9jtMa.js (Deleted) -1.27kB 0 bytes -100.0% 🗑️
assets/es-DdBsOSmj.js (Deleted) -1.25kB 0 bytes -100.0% 🗑️
assets/it-BC3ZFtsl.js (Deleted) -1.23kB 0 bytes -100.0% 🗑️
assets/ro-DbI5aaE7.js (Deleted) -1.22kB 0 bytes -100.0% 🗑️
assets/id-CiSQ0OkJ.js (Deleted) -1.22kB 0 bytes -100.0% 🗑️
assets/0233__migrate_geo_map_to_collection-B1NyPlQD.js (Deleted) -777 bytes 0 bytes -100.0% 🗑️
assets/0220__migrate_images_to_attachments-DDyA0VSV.js (Deleted) -672 bytes 0 bytes -100.0% 🗑️
assets/0239__disable_totp_when_mfa_was_turned_off-CdNlhQbz.js (Deleted) -623 bytes 0 bytes -100.0% 🗑️
assets/0234__migrate_ai_chat_to_code-Li92OWZC.js (Deleted) -443 bytes 0 bytes -100.0% 🗑️
assets/markdown-9W3Ano5N.js (Deleted) -323 bytes 0 bytes -100.0% 🗑️

@greptile-apps

greptile-apps Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a pure-TypeScript, offline parser for the OneNote desktop .one / .onetoc2 binary format (MS-ONESTORE revision store), plus an importer that converts a section file into a Trilium note tree — page hierarchy, titles, body text, and embedded images/files — with no Microsoft account or Graph API required. It is explicitly drafted as a proof-of-concept spike to gather feedback on the approach.

  • one_parser.ts: ~1 200-line zero-dependency binary parser ported from the msiemens/onenote.rs reference, decoding file-node lists, object-space/revision/object-group graphs and property sets; five parser and integration tests pass against a real .one fixture.
  • importer.ts: Converts the parsed OneSection into a Trilium note tree, nesting sub-pages by level, inlining images, and attaching other embedded files; adds an optional debug mode that serialises each page's raw object space as a JSON attachment.
  • Client/API wiring: Adds a new "OneNote file" import dialog provider, threads the debug flag through the upload and Electron native-import paths, and routes .one/.onetoc2 extensions in the dispatcher.

Confidence Score: 4/5

Safe to merge as a draft/PoC; one logic error in the image-vs-file dispatch should be fixed before promotion to production.

The importer dispatches content blocks to inline-image or file-attachment rendering using only the file extension, ignoring the block.image flag set by the parser. An ImageNode with an unrecognized extension is silently demoted to a file attachment link, and an EmbeddedFileNode with a .png/.jpg extension is inlined as an image instead of being attached. The parser has additional known open issues from prior review rounds (surrogate pairs, compact-id variants, u64 precision) that the author acknowledged as PoC scope. No data loss or security issues were found.

importer.ts (image/file dispatch logic) and one_parser.ts (open parser issues from prior review threads, acknowledged as PoC scope).

Important Files Changed

Filename Overview
packages/trilium-core/src/services/import/onenote-file/one_parser.ts 1172-line pure-TS binary parser for the MS-ONESTORE format; known open issues (surrogate pairs in decodeUtf16, GlobalIdTableEntry2/3FNDX not decoded, u64 precision, bytesN truncation) flagged in prior review threads; no new production-code violations found beyond those.
packages/trilium-core/src/services/import/onenote-file/importer.ts Converts parsed OneSection to a Trilium note tree; image/file dispatch uses extension only, ignoring block.image, which misroutes ImageNodes with unrecognized extensions and EmbeddedFileNodes with image extensions.
packages/trilium-core/src/services/import/onenote-file/one_debug.ts Serializes a parsed object space to JSON for the debug import mode; correctly uses encodeBase64 from the binary utilities module; no issues found.
packages/trilium-core/src/services/import/dispatch.ts Routes .one/.onetoc2 extensions to the new offline importer and adds the optional debug flag; changes are minimal and consistent with existing dispatch patterns.
apps/client/src/widgets/dialogs/import/onenote_file.tsx New Preact panel for the offline OneNote file provider; correctly reuses existing Card/FileDropZone/OptionsRow components and follows the same pattern as other import provider panels.
apps/client/src/widgets/dialogs/import/useProviderImport.ts Adds optional debug flag threaded through both the upload and native-import code paths; always serializes debug as "true"/"false" string so the server-side !== "false" pattern works correctly.
packages/trilium-core/src/services/import/onenote-file/importer.spec.ts Integration tests against the fixture file covering note tree shape, inline image extraction, and debug-mode source attachment; coverage is appropriate for a PoC.
packages/trilium-core/src/services/import/onenote-file/one_debug.spec.ts Thorough unit tests for the debug dumper covering reference resolution order, binary payload base64, text preview heuristics, and a live round-trip against the fixture; no issues.
packages/trilium-core/src/services/import/onenote-file/one_parser.spec.ts Parser unit tests cover page hierarchy, text extraction, image byte verification, and format-rejection paths; well-structured and sufficient for a spike.
apps/client/src/widgets/dialogs/import/useProviderImport.spec.tsx Adds a test verifying the debug flag flows to both the upload and native-import code paths; the updated Obsidian test expectation correctly reflects the new always-present debug field.

Sequence Diagram

sequenceDiagram
    participant Client as Browser / Electron
    participant Route as routes/api/import.ts
    participant Dispatch as import/dispatch.ts
    participant Parser as one_parser.ts
    participant Importer as onenote-file/importer.ts
    participant NoteService as noteService
    participant ImageService as imageService

    Client->>Route: "POST /api/notes/{id}/import (.one file, debug?)"
    Route->>Dispatch: importFile(taskContext, file, parentNote, options)
    Note over Dispatch: extension === ".one" or ".onetoc2"
    Dispatch->>Importer: importOneFile(taskContext, buffer, rootNote, fileName)
    Importer->>Parser: "parseOneSection(buffer, {retainObjectSpaces})"
    Parser-->>Importer: "OneSection {pages[], diagnostics[]}"
    Importer->>NoteService: createNewNote (section root)
    loop Each page
        Importer->>NoteService: createNewNote (page note, nested by level)
        loop Each content block
            alt "block.kind === text"
                Importer->>Importer: escapeHtml + wrap in p
            else IMAGE_EXTENSIONS.has(ext)
                Importer->>ImageService: saveImageToAttachment
                ImageService-->>Importer: "{attachmentId, title}"
            else file attachment
                Importer->>NoteService: note.saveAttachment
            end
        end
        Importer->>NoteService: note.setContent(sanitizeHtml(html))
        opt debug mode
            Importer->>Importer: dumpObjectSpace(page.space)
            Importer->>NoteService: note.saveAttachment (source.json)
        end
    end
    Importer-->>Dispatch: rootNote (BNote)
    Dispatch-->>Route: rootNote
    Route-->>Client: 200 OK
Loading

Reviews (2): Last reviewed commit: "feat(onenote): attach the parsed object ..." | Re-trigger Greptile

Comment on lines +985 to +995
const container = containerId ? space.objects.get(containerId) : undefined;
if (container?.fileData) {
const name = stringProp(obj, PROP.ImageFilename) ?? "image";
out.push({ kind: "file", name, ext: mediaExtension(name, container.fileExt), bytes: container.fileData, image: true });
}
break;
}
case JCID.EmbeddedFileNode: {
const containerId = objectRefs(obj, PROP.EmbeddedFileContainer)[0];
const container = containerId ? space.objects.get(containerId) : undefined;
if (container?.fileData) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 UTF-16 surrogate pairs not decoded

String.fromCharCode(code) treats each 16-bit code unit as a standalone character. UTF-16 encodes any code point above U+FFFF (emoji, many math symbols, CJK Extension B) as two consecutive code units — a high surrogate (0xD800–0xDBFF) followed by a low surrogate (0xDC00–0xDFFF). Calling fromCharCode on each independently produces two garbled replacement characters instead of the intended glyph.

Concretely: a page containing a single emoji like 😀 (U+1F600, encoded as 0xD83D 0xDE00 in UTF-16LE) will appear as two broken characters when imported.

Fix in Claude Code

Comment on lines +1078 to +1113
if (!rootSpace) {
diagnostics.push("root object space not found");
return { pages: [], diagnostics };
}

const sectionId = rootSpace.roots.get(ROLE_DEFAULT_CONTENT);
const section = sectionId ? rootSpace.objects.get(sectionId) : undefined;
if (!section) {
diagnostics.push("section node not found");
return { pages: [], diagnostics };
}
if (section.jcid === JCID.TocContainer) {
diagnostics.push("this is a .onetoc2 table-of-contents, not a section");
}

const pages: OnePage[] = [];
// SectionNode -> ElementChildNodes -> PageSeriesNodes
for (const seriesId of objectRefs(section, PROP.ElementChildNodes)) {
const series = rootSpace.objects.get(seriesId);
if (!series || series.jcid !== JCID.PageSeriesNode) {
continue;
}
const pageSpaceIds = objectSpaceRefs(series, PROP.ChildGraphSpaceElementNodes);
// Page metadata objects (for level), XORed with the seed.
const metaIds = objectRefs(series, PROP.MetaDataObjectsAboveGraphSpace).map((id) => xorExGuid(id, XOR_SEED_GUID));

pageSpaceIds.forEach((spaceId, index) => {
const pageSpace = spaces.get(spaceId);
if (!pageSpace) {
return;
}
let level = 0;
const metaId = metaIds[index];
const meta = metaId ? rootSpace.objects.get(metaId) : undefined;
if (meta) {
level = u32Prop(meta, PROP.PageLevel) ?? 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 GlobalIdTableEntry2FNDX / Entry3FNDX nodes silently dropped

Both parseRevisionList and parseObjectGroup switch on GlobalIdTableEntryFNDX (0x024) only. The two compact-form variants — GlobalIdTableEntry2FNDX (0x025) and GlobalIdTableEntry3FNDX (0x026) — are defined in FN but fall through to default: break, so they never populate the GUID table. Any file that uses these variants will have incomplete ID tables, causing later resolveCompact calls to return <unresolved:N>:n strings. Objects keyed under those IDs are then silently absent from the extracted pages, resulting in missing text or images with no error reported.

Fix in Claude Code

Comment on lines +38 to +41
u64(): number {
const lo = this.u32();
const hi = this.u32();
return hi * 0x1_0000_0000 + lo;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 u64() loses precision for offsets above 2⁵³

hi * 0x1_0000_0000 + lo is plain IEEE-754 double arithmetic. JavaScript integers are exact only up to Number.MAX_SAFE_INTEGER (2⁵³ − 1). The cbLength field in file-data objects is also read as u64 and could theoretically hold a large value that miscomputes the byte count, causing bytesN to return incorrect data.

Fix in Claude Code

Comment on lines +43 to +46
bytesN(n: number): Uint8Array {
const out = this.bytes.subarray(this.pos, this.pos + n);
this.pos += n;
return out;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 bytesN silently under-reads on truncated/malformed input

bytes.subarray(this.pos, this.pos + n) clamps at the actual buffer length without signalling an error, while this.pos += n always advances by the full requested amount. If a crafted or truncated file declares a length that exceeds the remaining buffer, subarray returns a shorter slice silently, but subsequent reads start from the advanced (now out-of-range) position. A bounds check before the read would prevent silent mis-parses.

Fix in Claude Code

@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

The Graph importer can attach each page's original HTML for diagnosing a
conversion; the offline .one importer had no equivalent, because there is no
source markup — the closest thing is the object graph the page was decoded
from, so debug mode now dumps that as JSON per page note.

The dump is id-driven rather than name-driven: every object and every property
is emitted keyed by its raw jcid / property id, with the parser's tables used
only as annotation. The parser already retains whole property sets, so
formatting, tables and note tags — everything the importer walks past today —
survive into the dump without it having to understand them first. Blobs are
base64'd in full for the same reason, and rendered in both text encodings the
format uses: a latin-1 run decodes as convincing CJK when read as UTF-16, so
picking one would hide half the text behind plausible gibberish.

The serializer lives beside the parser rather than inside it, to keep the
parser free of the debug format and avoid an import cycle; the parser only
gains an option to retain the object spaces it would otherwise discard.

Also carries the in-progress FSSHTTPB (OneDrive-downloaded section) detection
from the working tree, which touches the same parser file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant