// 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 // // Portions adapted from onenote.rs (MPL-2.0), revision // 5138a39a3f4e72b840932f9872fecde52fa9da60: `onestore/desktop/ // {file_node/shared,objects/file_data_store}.rs`. Deviations: file payloads // remain bounded zero-copy byte ranges, all string/count arithmetic is checked, // and unavailable external/invalid references remain explicit metadata. import { BinaryReader } from '../binary/BinaryReader.js'; import { normalizeGuid, readGuid } from '../binary/guid.js'; import { OneNoteParserError } from '../parser/error.js'; import type { OneNoteParserLimits } from '../parser/limits.js'; import type { CompactId, ExGuid, FileDataBlobReference, FileNode, FileNodeList, ObjectPropSet, StoreFileData, } from './types.js'; const FILE_DATA_STORE_HEADER = '{BDE316E7-2665-4511-A4C4-8D4D0B7A9EAC}'; const FILE_DATA_STORE_FOOTER = '{71FBA722-0F79-4A0B-BB13-899256426B24}'; const FILE_DATA_FIXED_PREFIX_BYTES = 36; export type FileDataStore = ReadonlyMap; export interface ParsedFileDataDeclaration { compactId: CompactId; jcid: number; fileData: StoreFileData; } export function emptyObjectPropSet(): ObjectPropSet { return { objectIds: [], objectSpaceIds: [], contextIds: [], properties: { entries: [] }, }; } /** Parse the optional root-level FileDataStoreListReferenceFND (0x090). */ export function parseFileDataStore( bytes: Uint8Array, rootList: FileNodeList, limits: OneNoteParserLimits ): FileDataStore { const storeNodes = rootList.nodes.filter((node) => node.id === 0x090); if (storeNodes.length > 1) { throw new OneNoteParserError( 'INVALID_STRUCTURE', 'Root FileNodeList contains more than one file-data store', { structure: 'FileDataStoreListReferenceFND' } ); } const storeNode = storeNodes[0]; if (!storeNode) return new Map(); if (!storeNode.childList) { throw new OneNoteParserError( 'MISSING_REFERENCE', 'FileDataStoreListReferenceFND has no child FileNodeList', { offset: storeNode.offset, structure: 'FileDataStoreListReferenceFND' } ); } const result = new Map(); let totalBytes = 0; for (const node of storeNode.childList.nodes) { if (node.id !== 0x094) { throw new OneNoteParserError( 'INVALID_STRUCTURE', `Unexpected FileNode 0x${node.id.toString(16)} in file-data store`, { offset: node.offset, structure: 'FileDataStoreList' } ); } if (result.size >= limits.maxFileDataObjects) { throw new OneNoteParserError( 'LIMIT_EXCEEDED', `File-data object count exceeds ${limits.maxFileDataObjects}`, { offset: node.offset, structure: 'FileDataStoreList' } ); } if ( !node.dataReference || node.dataReference.nil || node.dataReference.zero ) { throw new OneNoteParserError( 'MISSING_REFERENCE', 'FileDataStoreObjectReferenceFND has no data reference', { offset: node.offset, structure: 'FileDataStoreObjectReferenceFND' } ); } const nodeReader = new BinaryReader( bytes, node.payloadOffset, node.payloadLength, 'FileDataStoreObjectReferenceFND' ); const guid = readGuid(nodeReader); if (nodeReader.remaining !== 0) { throw new OneNoteParserError( 'INVALID_STRUCTURE', 'FileDataStoreObjectReferenceFND has trailing payload bytes', { offset: nodeReader.offset, structure: 'FileDataStoreObjectReferenceFND', } ); } if (result.has(guid)) { throw new OneNoteParserError( 'INVALID_STRUCTURE', `Duplicate file-data GUID ${guid}`, { offset: node.offset, structure: 'FileDataStoreList' } ); } const blob = parseFileDataStoreObject(bytes, node, limits); totalBytes = checkedAdd(totalBytes, blob.size, node.offset); if (totalBytes > limits.maxTotalFileDataBytes) { throw new OneNoteParserError( 'LIMIT_EXCEEDED', `File-data payloads exceed ${limits.maxTotalFileDataBytes} bytes`, { offset: node.offset, structure: 'FileDataStoreList' } ); } result.set(guid, blob); } return result; } /** Parse ObjectDeclarationFileData3RefCountFND (0x072/0x073). */ export function parseFileDataDeclaration( bytes: Uint8Array, node: FileNode, store: FileDataStore, limits: OneNoteParserLimits ): ParsedFileDataDeclaration { if (node.id !== 0x072 && node.id !== 0x073) { throw new TypeError('Expected a file-data object declaration'); } const reader = new BinaryReader( bytes, node.payloadOffset, node.payloadLength, 'ObjectDeclarationFileData3RefCountFND' ); const compactId = readCompactId(reader); const jcid = reader.u32(); if (node.id === 0x072) reader.u8(); else reader.u32(); const dataReference = readStorageString(reader, limits); const extension = readStorageString(reader, limits); if (reader.remaining !== 0) { throw new OneNoteParserError( 'INVALID_STRUCTURE', 'File-data declaration has trailing payload bytes', { offset: reader.offset, structure: 'ObjectDeclarationFileData3RefCountFND', } ); } return { compactId, jcid, fileData: resolveFileData(dataReference, extension, store, node), }; } export function resolveCompactFileDataId( compactId: CompactId, mapping: ReadonlyMap ): ExGuid { const guid = mapping.get(compactId.guidIndex); if (!guid) { throw new OneNoteParserError( 'MISSING_REFERENCE', `GlobalIdTable has no GUID at index ${compactId.guidIndex}`, { structure: 'CompactId' } ); } return { guid, value: compactId.value }; } function parseFileDataStoreObject( bytes: Uint8Array, node: FileNode, limits: OneNoteParserLimits ): FileDataBlobReference { const reference = node.dataReference!; const reader = new BinaryReader( bytes, reference.offset, reference.size, 'FileDataStoreObject' ); if (readGuid(reader) !== FILE_DATA_STORE_HEADER) { throw new OneNoteParserError( 'INVALID_STRUCTURE', 'Invalid FileDataStoreObject header GUID', { offset: reference.offset, structure: 'FileDataStoreObject' } ); } const rawLength = reader.u64(); if (rawLength > BigInt(Number.MAX_SAFE_INTEGER)) { throw new OneNoteParserError( 'LIMIT_EXCEEDED', 'File-data length exceeds the safe integer range', { offset: reader.offset - 8, structure: 'FileDataStoreObject' } ); } const size = Number(rawLength); if (size > limits.maxFileDataBytes) { throw new OneNoteParserError( 'LIMIT_EXCEEDED', `File-data payload is ${size} bytes (limit ${limits.maxFileDataBytes})`, { offset: reader.offset - 8, structure: 'FileDataStoreObject' } ); } reader.u32(); // unused; consumers MUST ignore it reader.u64(); // reserved; consumers MUST ignore it const dataOffset = reader.offset; reader.skip(size); const padding = (8 - ((FILE_DATA_FIXED_PREFIX_BYTES + size) % 8)) % 8; reader.skip(padding); if (readGuid(reader) !== FILE_DATA_STORE_FOOTER) { throw new OneNoteParserError( 'INVALID_STRUCTURE', 'Invalid FileDataStoreObject footer GUID', { offset: reader.offset - 16, structure: 'FileDataStoreObject' } ); } if (reader.remaining !== 0) { throw new OneNoteParserError( 'INVALID_STRUCTURE', 'FileDataStoreObject has trailing bytes', { offset: reader.offset, structure: 'FileDataStoreObject' } ); } return { source: bytes, offset: dataOffset, size }; } function readStorageString( reader: BinaryReader, limits: OneNoteParserLimits ): string { const characterCount = reader.u32(); if (characterCount > Math.floor(limits.maxVectorBytes / 2)) { throw new OneNoteParserError( 'LIMIT_EXCEEDED', `Storage string contains ${characterCount} UTF-16 code units`, { offset: reader.offset - 4, structure: 'StringInStorageBuffer' } ); } const byteLength = characterCount * 2; try { return new TextDecoder('utf-16le', { fatal: true }).decode( reader.read(byteLength) ); } catch (error) { if (error instanceof OneNoteParserError) throw error; throw new OneNoteParserError( 'INVALID_STRUCTURE', 'StringInStorageBuffer contains invalid UTF-16', { offset: reader.offset - byteLength, structure: 'StringInStorageBuffer' } ); } } function resolveFileData( dataReference: string, extension: string, store: FileDataStore, node: FileNode ): StoreFileData { if (dataReference.startsWith('')) { const rawGuid = dataReference.slice(''.length); let storageGuid: string; try { storageGuid = normalizeGuid(rawGuid); } catch { throw new OneNoteParserError( 'INVALID_STRUCTURE', `Invalid embedded file-data GUID ${rawGuid}`, { offset: node.offset, structure: 'ObjectDeclarationFileData3RefCountFND', } ); } return { referenceKind: 'embedded', dataReference, extension, storageGuid, blob: store.get(storageGuid), }; } if (dataReference.startsWith('')) { return { referenceKind: 'external', dataReference, extension }; } if (dataReference === '') { return { referenceKind: 'invalid', dataReference, extension }; } throw new OneNoteParserError( 'INVALID_STRUCTURE', `Unknown file-data reference ${dataReference}`, { offset: node.offset, structure: 'ObjectDeclarationFileData3RefCountFND' } ); } function readCompactId(reader: BinaryReader): CompactId { const raw = reader.u32(); return { value: raw & 0xff, guidIndex: raw >>> 8 }; } function checkedAdd(left: number, right: number, offset: number): number { const result = left + right; if (!Number.isSafeInteger(result)) { throw new OneNoteParserError( 'LIMIT_EXCEEDED', 'File-data byte count exceeds the safe integer range', { offset, structure: 'FileDataStoreList' } ); } return result; }