import { createHash } from 'node:crypto'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; import { parseOneNoteSection } from '../parser/parse-section.js'; import { parseOneNoteTableOfContents } from '../parser/parse-toc.js'; import { enumerateCabinet, extractCabinet } from './cabinet.js'; import { openOneNotePackage } from './package.js'; /* * Joplin CLI test fixture, AGPL-3.0-or-later. See tests/fixtures/README.md. * Pinned upstream revision: 1e73aad7eb08fde5d9e4cb533df40052a5cd32d7 */ const encodedFixture = readFileSync( resolve(process.cwd(), 'tests/fixtures/joplin-test.onepkg.base64'), 'utf8' ); const JOPLIN_ONEPKG = Uint8Array.from( Buffer.from(encodedFixture.replaceAll(/\s/gu, ''), 'base64') ); const ONE_MAGIC = Uint8Array.from([ 0xe4, 0x52, 0x5c, 0x7b, 0x8c, 0xd8, 0xa7, 0x4d, 0xae, 0xb1, 0x53, 0x78, 0xd0, 0x29, 0x96, 0xd3, ]); const EXPECTED_PATHS = [ 'Another section.one', 'Open Notebook.onetoc2', 'Section group/A.one', 'Section group/B.one', 'Section group/Open Notebook.onetoc2', 'Tést!.one', '⅀⸨ Unicode ⸩.one', ]; describe('pinned Joplin .onepkg fixture', () => { it('retains the reviewed upstream bytes', () => { expect(JOPLIN_ONEPKG).toHaveLength(11_066); expect(createHash('sha256').update(JOPLIN_ONEPKG).digest('hex')).toBe( '83cb622d40f0ab127038b629553b6926be3e2a6ce642f169df0e2614c13e8469' ); }); it('enumerates all seven UTF-8/nested entries using LZX 0x1203', () => { const listing = enumerateCabinet(JOPLIN_ONEPKG); expect(listing.entries.map((entry) => entry.normalizedPath)).toEqual( EXPECTED_PATHS ); expect(listing.folders).toHaveLength(1); expect(listing.folders[0]).toMatchObject({ dataBlockCount: 3, compression: { kind: 'lzx', raw: 0x1203, windowBits: 18 }, }); expect( listing.diagnostics.filter( (diagnostic) => diagnostic.code === 'cab-unmarked-utf8-name' ) ).toHaveLength(2); }); it('extracts every entry and preserves native .one headers', async () => { const extraction = await extractCabinet(JOPLIN_ONEPKG); expect( extraction.extractedEntries.map((entry) => entry.normalizedPath) ).toEqual(EXPECTED_PATHS); const sections = extraction.extractedEntries.filter((entry) => entry.normalizedPath.toLowerCase().endsWith('.one') ); expect(sections).toHaveLength(5); for (const section of sections) { expect(section.bytes.subarray(0, ONE_MAGIC.length)).toEqual(ONE_MAGIC); } const opened = await openOneNotePackage(JOPLIN_ONEPKG, { sourceName: 'test.onepkg', }); expect(opened.package.entries).toHaveLength(7); expect(opened.package.sections).toHaveLength(5); expect( opened.package.sections.every( (section) => section.parseStatus === 'not-requested' ) ).toBe(true); expect(opened.package.tree).toMatchObject({ interpreted: true, tocPath: 'Open Notebook.onetoc2', nodes: [ { kind: 'section', displayName: 'Tést!' }, { kind: 'section', displayName: 'Another section' }, { kind: 'section-group', displayName: 'Section group', children: [ { kind: 'section', displayName: 'A' }, { kind: 'section', displayName: 'B' }, ], }, { kind: 'section', displayName: '⅀⸨ Unicode ⸩' }, ], }); }); it('opens and parses all five packaged sections end to end', async () => { const opened = await openOneNotePackage(JOPLIN_ONEPKG, { sourceName: 'test.onepkg', parseSection: ({ bytes, normalizedPath }) => ({ status: 'parsed', value: parseOneNoteSection(bytes, { sourceName: normalizedPath }), }), }); expect(opened.package.sections).toHaveLength(5); expect( opened.package.sections.map((section) => section.parseStatus) ).toEqual(['parsed', 'parsed', 'parsed', 'parsed', 'parsed']); expect( opened.package.entries.some((entry) => entry.parseStatus === 'failed') ).toBe(false); const testSection = opened.package.sections.find( (section) => section.path === 'Tést!.one' ); expect(testSection?.section?.sectionName).toBe('Tést!'); expect(testSection?.section?.pages[0]).toMatchObject({ title: 'Testing…', text: 'Link to page: Page 2', }); }); it('interprets both desktop TOCs with their authored ordering', async () => { const extraction = await extractCabinet(JOPLIN_ONEPKG); const root = extraction.extractedEntries.find( (entry) => entry.normalizedPath === 'Open Notebook.onetoc2' ); const group = extraction.extractedEntries.find( (entry) => entry.normalizedPath === 'Section group/Open Notebook.onetoc2' ); expect(root).toBeDefined(); expect(group).toBeDefined(); expect( parseOneNoteTableOfContents(root!.bytes, { sourceName: root!.normalizedPath, }).entries.map((entry) => [entry.orderingId, entry.filename]) ).toEqual([ [1, 'Tést!.one'], [2, 'OneNote_RecycleBin'], [3, 'Another section.one'], [4, 'Section group'], [5, '⅀⸨ Unicode ⸩.one'], ]); expect( parseOneNoteTableOfContents(group!.bytes, { sourceName: group!.normalizedPath, }).entries.map((entry) => [entry.orderingId, entry.filename]) ).toEqual([ [1, 'A.one'], [2, 'B.one'], ]); }); });