feat: parse FSSHTTP table of contents stores
This commit is contained in:
136
src/onenote/parser/parse-toc-format.test.ts
Normal file
136
src/onenote/parser/parse-toc-format.test.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import type { ParserDiagnostic } from '../model/diagnostics.js';
|
||||||
|
import type { DesktopOneStore } from '../onestore/desktop-store.js';
|
||||||
|
import type { ExGuid, StoreObject } from '../onestore/types.js';
|
||||||
|
import { exGuidKey } from '../onestore/types.js';
|
||||||
|
import {
|
||||||
|
FSSHTTP_PACKAGING_FORMAT,
|
||||||
|
ONE_SECTION_FILE_TYPE,
|
||||||
|
ONE_TOC_FILE_TYPE,
|
||||||
|
} from './detect-format.js';
|
||||||
|
import { parseOneNoteTableOfContents } from './parse-toc.js';
|
||||||
|
|
||||||
|
const { parseFssHttpOneStoreMock } = vi.hoisted(() => ({
|
||||||
|
parseFssHttpOneStoreMock: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../onestore/fsshttp-store.js', () => ({
|
||||||
|
parseFssHttpOneStore: parseFssHttpOneStoreMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ROOT_ID: ExGuid = {
|
||||||
|
guid: '{6B46CF9E-D34C-401E-90B3-9F4C3DB606C4}',
|
||||||
|
value: 1,
|
||||||
|
};
|
||||||
|
const CONTEXT_ID: ExGuid = {
|
||||||
|
guid: '{00000000-0000-0000-0000-000000000000}',
|
||||||
|
value: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('table-of-contents format dispatch', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
parseFssHttpOneStoreMock.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts an FSSHTTP-shaped .onetoc2 through the shared store adapter', () => {
|
||||||
|
parseFssHttpOneStoreMock.mockImplementation(
|
||||||
|
(
|
||||||
|
_bytes: Uint8Array,
|
||||||
|
_limits: unknown,
|
||||||
|
diagnostics: ParserDiagnostic[]
|
||||||
|
) => {
|
||||||
|
diagnostics.push({
|
||||||
|
severity: 'warning',
|
||||||
|
code: 'FSSHTTP_TEST_DIAGNOSTIC',
|
||||||
|
message: 'Retained adapter diagnostic.',
|
||||||
|
structure: 'Storage Index',
|
||||||
|
recoverable: true,
|
||||||
|
});
|
||||||
|
return tableOfContentsStore();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = parseOneNoteTableOfContents(
|
||||||
|
fssHttpHeader(ONE_TOC_FILE_TYPE),
|
||||||
|
{
|
||||||
|
sourceName: 'Open Notebook.onetoc2',
|
||||||
|
limits: { maxFileBytes: 4_096 },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(parseFssHttpOneStoreMock).toHaveBeenCalledOnce();
|
||||||
|
expect(parseFssHttpOneStoreMock.mock.calls[0]?.[1]).toMatchObject({
|
||||||
|
maxFileBytes: 4_096,
|
||||||
|
});
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
sourceName: 'Open Notebook.onetoc2',
|
||||||
|
fileIdentity: '{921D7914-E817-4615-AEB5-9B1B5EA2E973}',
|
||||||
|
entries: [],
|
||||||
|
diagnostics: [{ code: 'FSSHTTP_TEST_DIAGNOSTIC' }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects an FSSHTTP section even when the adapter returns a valid store', () => {
|
||||||
|
parseFssHttpOneStoreMock.mockReturnValue(sectionStore());
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
parseOneNoteTableOfContents(fssHttpHeader(ONE_SECTION_FILE_TYPE))
|
||||||
|
).toThrowError(
|
||||||
|
expect.objectContaining({
|
||||||
|
code: 'INVALID_FORMAT',
|
||||||
|
structure: 'OneStore table of contents',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function tableOfContentsStore(): DesktopOneStore {
|
||||||
|
const root: StoreObject = {
|
||||||
|
id: ROOT_ID,
|
||||||
|
jcid: 0x0002_0001,
|
||||||
|
props: {
|
||||||
|
objectIds: [],
|
||||||
|
objectSpaceIds: [],
|
||||||
|
contextIds: [],
|
||||||
|
properties: { entries: [] },
|
||||||
|
},
|
||||||
|
mapping: new Map(),
|
||||||
|
contextId: CONTEXT_ID,
|
||||||
|
};
|
||||||
|
const rootObjectSpace = {
|
||||||
|
id: ROOT_ID,
|
||||||
|
roots: new Map([[1, ROOT_ID]]),
|
||||||
|
objects: new Map([[exGuidKey(ROOT_ID), root]]),
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
kind: 'table-of-contents',
|
||||||
|
variant: 'fsshttpb-package-store',
|
||||||
|
fileIdentity: '{921D7914-E817-4615-AEB5-9B1B5EA2E973}',
|
||||||
|
rootObjectSpace,
|
||||||
|
objectSpaces: new Map(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sectionStore(): DesktopOneStore {
|
||||||
|
return { ...tableOfContentsStore(), kind: 'section' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function fssHttpHeader(fileType: string): Uint8Array {
|
||||||
|
const bytes = new Uint8Array(64);
|
||||||
|
writeGuid(bytes, 0, fileType);
|
||||||
|
writeGuid(bytes, 16, '{921D7914-E817-4615-AEB5-9B1B5EA2E973}');
|
||||||
|
writeGuid(bytes, 48, FSSHTTP_PACKAGING_FORMAT);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeGuid(target: Uint8Array, offset: number, guid: string): void {
|
||||||
|
const plain = guid.replace(/[{}-]/gu, '');
|
||||||
|
const canonical = Array.from({ length: 16 }, (_, index) =>
|
||||||
|
Number.parseInt(plain.slice(index * 2, index * 2 + 2), 16)
|
||||||
|
);
|
||||||
|
const order = [3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15];
|
||||||
|
for (let index = 0; index < order.length; index += 1) {
|
||||||
|
target[offset + order[index]!] = canonical[index]!;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ import { BinaryReader } from '../binary/BinaryReader.js';
|
|||||||
import { readGuid } from '../binary/guid.js';
|
import { readGuid } from '../binary/guid.js';
|
||||||
import type { ParserDiagnostic } from '../model/diagnostics.js';
|
import type { ParserDiagnostic } from '../model/diagnostics.js';
|
||||||
import { parseDesktopOneNoteTableOfContentsStore } from '../onestore/desktop-store.js';
|
import { parseDesktopOneNoteTableOfContentsStore } from '../onestore/desktop-store.js';
|
||||||
|
import type { DesktopOneStore } from '../onestore/desktop-store.js';
|
||||||
|
import { parseFssHttpOneStore } from '../onestore/fsshttp-store.js';
|
||||||
import {
|
import {
|
||||||
countObjectReferences,
|
countObjectReferences,
|
||||||
findProperty,
|
findProperty,
|
||||||
@@ -13,7 +15,7 @@ import {
|
|||||||
} from '../onestore/property-set.js';
|
} from '../onestore/property-set.js';
|
||||||
import type { ExGuid, ObjectSpace, StoreObject } from '../onestore/types.js';
|
import type { ExGuid, ObjectSpace, StoreObject } from '../onestore/types.js';
|
||||||
import { exGuidKey } from '../onestore/types.js';
|
import { exGuidKey } from '../onestore/types.js';
|
||||||
import { toBytes } from './detect-format.js';
|
import { detectOneNoteFormat, toBytes } from './detect-format.js';
|
||||||
import { OneNoteParserError } from './error.js';
|
import { OneNoteParserError } from './error.js';
|
||||||
import type { OneNoteParserLimits } from './limits.js';
|
import type { OneNoteParserLimits } from './limits.js';
|
||||||
import { resolveParserLimits } from './limits.js';
|
import { resolveParserLimits } from './limits.js';
|
||||||
@@ -58,7 +60,7 @@ interface TocContext {
|
|||||||
active: Set<string>;
|
active: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Parse a desktop revision-store `.onetoc2` without network access. */
|
/** Parse a supported desktop or FSSHTTPB `.onetoc2` without network access. */
|
||||||
export function parseOneNoteTableOfContents(
|
export function parseOneNoteTableOfContents(
|
||||||
input: ArrayBuffer | Uint8Array,
|
input: ArrayBuffer | Uint8Array,
|
||||||
options: ParseOneNoteTableOfContentsOptions = {}
|
options: ParseOneNoteTableOfContentsOptions = {}
|
||||||
@@ -66,11 +68,7 @@ export function parseOneNoteTableOfContents(
|
|||||||
const bytes = toBytes(input);
|
const bytes = toBytes(input);
|
||||||
const limits = resolveParserLimits(options.limits);
|
const limits = resolveParserLimits(options.limits);
|
||||||
const diagnostics: ParserDiagnostic[] = [];
|
const diagnostics: ParserDiagnostic[] = [];
|
||||||
const store = parseDesktopOneNoteTableOfContentsStore(
|
const store = parseTableOfContentsStore(bytes, limits, diagnostics);
|
||||||
bytes,
|
|
||||||
limits,
|
|
||||||
diagnostics
|
|
||||||
);
|
|
||||||
const space = store.rootObjectSpace;
|
const space = store.rootObjectSpace;
|
||||||
const rootId = space.roots.get(1);
|
const rootId = space.roots.get(1);
|
||||||
if (!rootId) {
|
if (!rootId) {
|
||||||
@@ -110,6 +108,25 @@ export function parseOneNoteTableOfContents(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseTableOfContentsStore(
|
||||||
|
bytes: Uint8Array,
|
||||||
|
limits: OneNoteParserLimits,
|
||||||
|
diagnostics: ParserDiagnostic[]
|
||||||
|
): DesktopOneStore {
|
||||||
|
const store =
|
||||||
|
detectOneNoteFormat(bytes).kind === 'fsshttp-one'
|
||||||
|
? parseFssHttpOneStore(bytes, limits, diagnostics)
|
||||||
|
: parseDesktopOneNoteTableOfContentsStore(bytes, limits, diagnostics);
|
||||||
|
if (store.kind !== 'table-of-contents') {
|
||||||
|
throw new OneNoteParserError(
|
||||||
|
'INVALID_FORMAT',
|
||||||
|
'Expected a OneNote table of contents, found a section',
|
||||||
|
{ structure: 'OneStore table of contents' }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
function parseChildren(
|
function parseChildren(
|
||||||
context: TocContext,
|
context: TocContext,
|
||||||
parent: StoreObject,
|
parent: StoreObject,
|
||||||
|
|||||||
Reference in New Issue
Block a user