// 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: `one/property/{simple, // object_reference}.rs`. Deviations: values stay in the TypeScript OneStore // model and malformed property types are rejected rather than coerced. import { BinaryReader } from '../binary/BinaryReader.js'; import { readGuid } from '../binary/guid.js'; import { countObjectReferences, countObjectSpaceReferences, findProperty, referenceOffset, } from '../onestore/property-set.js'; import type { ExGuid, ObjectSpace, PropertyEntry, StoreObject, StoreObjectSpaceReference, } from '../onestore/types.js'; import { exGuidKey } from '../onestore/types.js'; export function objectReferences( object: StoreObject, propertyId: number ): ExGuid[] { const property = findProperty(object.props, propertyId); if (!property) return []; let count: number; if (property.value.kind === 'objectId') count = 1; else if (property.value.kind === 'objectIds') count = property.value.count; else throw propertyTypeError(propertyId, 'an object reference'); const offset = referenceOffset( object.props, propertyId, countObjectReferences ); if (object.resolvedObjectIds) { return resolvedReferenceSlice(object.resolvedObjectIds, offset, count); } return resolveReferenceSlice(object, object.props.objectIds, offset, count); } export function objectSpaceReferences( object: StoreObject, propertyId: number ): StoreObjectSpaceReference[] { const property = findProperty(object.props, propertyId); if (!property) return []; let count: number; if (property.value.kind === 'objectSpaceId') count = 1; else if (property.value.kind === 'objectSpaceIds') count = property.value.count; else throw propertyTypeError(propertyId, 'an object-space reference'); const offset = referenceOffset( object.props, propertyId, countObjectSpaceReferences ); if (object.resolvedObjectSpaceIds) { return resolvedReferenceSlice(object.resolvedObjectSpaceIds, offset, count); } return resolveReferenceSlice( object, object.props.objectSpaceIds, offset, count ).map((id) => ({ id, key: exGuidKey(id) })); } export function rootObject( space: ObjectSpace, role: number, structure: string ): StoreObject { const id = space.roots.get(role); if (!id) throw new Error(`Object space has no ${structure}`); return getObject(space, id, structure); } export function getObject( space: ObjectSpace, id: ExGuid, structure: string ): StoreObject { const object = space.objects.get(exGuidKey(id)); if (!object) throw new Error(`${structure} object ${exGuidKey(id)} is missing`); return object; } export function assertJcid( object: StoreObject, expected: number, structure: string ): void { if (object.jcid !== expected) { throw new Error( `${structure} has JCID 0x${object.jcid.toString(16)}, expected 0x${expected.toString(16)}` ); } } export function optionalEntry( object: StoreObject, propertyId: number ): PropertyEntry | undefined { return findProperty(object.props, propertyId); } export function optionalBytes( object: StoreObject, propertyId: number ): Uint8Array | undefined { const value = optionalEntry(object, propertyId)?.value; if (!value) return undefined; if (value.kind !== 'bytes') throw propertyTypeError(propertyId, 'bytes'); return value.value; } export function optionalString( object: StoreObject, propertyId: number ): string | undefined { const bytes = optionalBytes(object, propertyId); if (!bytes) return undefined; if (bytes.byteLength % 2 !== 0) { throw new Error( `UTF-16 property 0x${propertyId.toString(16)} has odd length` ); } return new TextDecoder('utf-16le', { fatal: true }).decode(bytes); } export function optionalLatin1( object: StoreObject, propertyId: number ): string | undefined { const bytes = optionalBytes(object, propertyId); if (!bytes) return undefined; let result = ''; for (const value of bytes) result += String.fromCharCode(value); return result; } export function requiredGuid( object: StoreObject, propertyId: number, structure: string ): string { const bytes = optionalBytes(object, propertyId); if (!bytes || bytes.length !== 16) { throw new Error(`${structure} is absent or not 16 bytes`); } return readGuid(new BinaryReader(bytes, 0, 16, structure)); } export function optionalBool( object: StoreObject, propertyId: number ): boolean | undefined { const value = optionalEntry(object, propertyId)?.value; if (!value) return undefined; if (value.kind !== 'bool') throw propertyTypeError(propertyId, 'a Boolean'); return value.value; } export function optionalU8( object: StoreObject, propertyId: number ): number | undefined { const value = optionalEntry(object, propertyId)?.value; if (!value) return undefined; if (value.kind !== 'u8') throw propertyTypeError(propertyId, 'a u8'); return value.value; } export function optionalU16( object: StoreObject, propertyId: number ): number | undefined { const value = optionalEntry(object, propertyId)?.value; if (!value) return undefined; if (value.kind !== 'u16') throw propertyTypeError(propertyId, 'a u16'); return value.value; } export function optionalU32( object: StoreObject, propertyId: number ): number | undefined { const value = optionalEntry(object, propertyId)?.value; if (!value) return undefined; if (value.kind !== 'u32') throw propertyTypeError(propertyId, 'a u32'); return value.value; } export function optionalU64( object: StoreObject, propertyId: number ): bigint | undefined { const value = optionalEntry(object, propertyId)?.value; if (!value) return undefined; if (value.kind !== 'u64') throw propertyTypeError(propertyId, 'a u64'); return value.value; } export function optionalF32( object: StoreObject, propertyId: number ): number | undefined { const raw = optionalU32(object, propertyId); if (raw === undefined) return undefined; const data = new DataView(new ArrayBuffer(4)); data.setUint32(0, raw, true); const value = data.getFloat32(0, true); if (!Number.isFinite(value)) { throw new Error( `Float property 0x${propertyId.toString(16)} is not finite` ); } return value; } export function optionalU32Vector( object: StoreObject, propertyId: number ): number[] | undefined { const bytes = optionalBytes(object, propertyId); if (!bytes) return undefined; if (bytes.byteLength % 4 !== 0) { throw new Error( `Property 0x${propertyId.toString(16)} is not a u32 vector` ); } const reader = new BinaryReader(bytes, 0, bytes.byteLength, 'u32 vector'); const result: number[] = []; while (reader.remaining > 0) result.push(reader.u32()); return result; } function resolveReferenceSlice( object: StoreObject, ids: { guidIndex: number; value: number }[], offset: number, count: number ): ExGuid[] { if (offset + count > ids.length) { throw new Error( `Reference range ${offset}+${count} exceeds stream length ${ids.length}` ); } return ids.slice(offset, offset + count).map((id) => { const guid = object.mapping.get(id.guidIndex); if (!guid) throw new Error(`GUID mapping ${id.guidIndex} is missing`); return { guid, value: id.value }; }); } function resolvedReferenceSlice( ids: readonly T[], offset: number, count: number ): T[] { if (offset + count > ids.length) { throw new Error( `Reference range ${offset}+${count} exceeds resolved stream length ${ids.length}` ); } return ids.slice(offset, offset + count); } function propertyTypeError(propertyId: number, expected: string): Error { return new Error(`Property 0x${propertyId.toString(16)} is not ${expected}`); }