Files
onenote-tools/src/onenote/one/property-access.ts

283 lines
7.9 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
//
// 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<T>(
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}`);
}