feat: parse rich OneNote content resources
This commit is contained in:
177
tests/file-data.test.ts
Normal file
177
tests/file-data.test.ts
Normal 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]!;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user