feat: parse rich OneNote content resources

This commit is contained in:
2026-07-22 19:08:49 +02:00
parent 670310ea9e
commit c99615f5e6
13 changed files with 2671 additions and 6 deletions

177
tests/file-data.test.ts Normal file
View File

@@ -0,0 +1,177 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
// SPDX-License-Identifier: MPL-2.0
import { describe, expect, it } from 'vitest';
import {
parseFileDataDeclaration,
parseFileDataStore,
} from '../src/onenote/onestore/file-data.js';
import type { FileNode, FileNodeList } from '../src/onenote/onestore/types.js';
import { fileDataBytes } from '../src/onenote/onestore/types.js';
import { resolveParserLimits } from '../src/onenote/parser/limits.js';
const HEADER = '{BDE316E7-2665-4511-A4C4-8D4D0B7A9EAC}';
const FOOTER = '{71FBA722-0F79-4A0B-BB13-899256426B24}';
const STORAGE_GUID = '{01234567-89AB-CDEF-8123-456789ABCDEF}';
describe('OneStore file-data objects', () => {
it('indexes bounded payload ranges and resolves 0x072 declarations', () => {
const payload = Uint8Array.of(0x89, 0x50, 0x4e, 0x47, 13, 10, 26, 10);
const fixture = fileDataFixture(payload);
const store = parseFileDataStore(
fixture.bytes,
fixture.root,
resolveParserLimits()
);
const blob = store.get(STORAGE_GUID)!;
expect(blob).toMatchObject({ offset: 100, size: payload.length });
expect(blob.source.buffer).toBe(fixture.bytes.buffer);
expect([...fileDataBytes(blob)]).toEqual([...payload]);
const declarationBytes = declarationPayload(
0x072,
`<ifndf>${STORAGE_GUID}`,
'.png'
);
const declaration = parseFileDataDeclaration(
declarationBytes,
node(0x072, 0, declarationBytes.length),
store,
resolveParserLimits()
);
expect(declaration).toMatchObject({
compactId: { guidIndex: 3, value: 7 },
jcid: 0x00080039,
fileData: {
referenceKind: 'embedded',
storageGuid: STORAGE_GUID,
extension: '.png',
blob,
},
});
});
it('keeps external references explicit and enforces payload limits', () => {
const externalBytes = declarationPayload(
0x073,
'<file>media/image.onebin',
'.jpg'
);
expect(
parseFileDataDeclaration(
externalBytes,
node(0x073, 0, externalBytes.length),
new Map(),
resolveParserLimits()
).fileData
).toEqual({
referenceKind: 'external',
dataReference: '<file>media/image.onebin',
extension: '.jpg',
});
const fixture = fileDataFixture(Uint8Array.of(1, 2, 3, 4, 5));
expect(() =>
parseFileDataStore(
fixture.bytes,
fixture.root,
resolveParserLimits({ maxFileDataBytes: 4 })
)
).toThrow(/payload is 5 bytes/);
});
});
function fileDataFixture(payload: Uint8Array): {
bytes: Uint8Array;
root: FileNodeList;
} {
const objectOffset = 64;
const padding = (8 - ((36 + payload.length) % 8)) % 8;
const objectSize = 36 + payload.length + padding + 16;
const bytes = Buffer.alloc(objectOffset + objectSize);
writeGuid(bytes, 0, STORAGE_GUID);
writeGuid(bytes, objectOffset, HEADER);
bytes.writeBigUInt64LE(BigInt(payload.length), objectOffset + 16);
bytes.writeUInt32LE(0xfeedbeef, objectOffset + 24);
bytes.writeBigUInt64LE(0x1234n, objectOffset + 28);
bytes.set(payload, objectOffset + 36);
writeGuid(bytes, objectOffset + 36 + payload.length + padding, FOOTER);
const dataNode: FileNode = {
...node(0x094, 0, 16),
dataReference: {
offset: objectOffset,
size: objectSize,
nil: false,
zero: false,
},
};
return {
bytes,
root: {
id: 0,
nodes: [
{
...node(0x090, 0, 0),
childList: { id: 1, nodes: [dataNode] },
},
],
},
};
}
function declarationPayload(
id: 0x072 | 0x073,
dataReference: string,
extension: string
): Uint8Array {
const data = Buffer.from(dataReference, 'utf16le');
const ext = Buffer.from(extension, 'utf16le');
const countSize = id === 0x072 ? 1 : 4;
const result = Buffer.alloc(
4 + 4 + countSize + 4 + data.length + 4 + ext.length
);
let offset = 0;
result.writeUInt32LE((3 << 8) | 7, offset);
offset += 4;
result.writeUInt32LE(0x00080039, offset);
offset += 4;
if (id === 0x072) result.writeUInt8(1, offset);
else result.writeUInt32LE(1, offset);
offset += countSize;
result.writeUInt32LE(dataReference.length, offset);
offset += 4;
result.set(data, offset);
offset += data.length;
result.writeUInt32LE(extension.length, offset);
offset += 4;
result.set(ext, offset);
return result;
}
function node(
id: number,
payloadOffset: number,
payloadLength: number
): FileNode {
return {
id,
declaredSize: payloadLength,
offset: payloadOffset,
payloadOffset,
payloadLength,
};
}
function writeGuid(target: Buffer, offset: number, guid: string): void {
const hex = guid.replace(/[{}-]/g, '');
const bytes = Buffer.from(hex, 'hex');
const order = [3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15];
order.forEach((sourceIndex, index) => {
target[offset + index] = bytes[sourceIndex]!;
});
}