283 lines
7.9 KiB
TypeScript
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}`);
|
|
}
|