165 lines
5.4 KiB
TypeScript
165 lines
5.4 KiB
TypeScript
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'],
|
|
]);
|
|
});
|
|
});
|