178 lines
5.0 KiB
TypeScript
178 lines
5.0 KiB
TypeScript
// 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]!;
|
|
});
|
|
}
|