feat: parse rich OneNote content resources
This commit is contained in:
262
src/onenote/one/property-access.ts
Normal file
262
src/onenote/one/property-access.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
// 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,
|
||||
} 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
|
||||
);
|
||||
return resolveReferenceSlice(object, object.props.objectIds, offset, count);
|
||||
}
|
||||
|
||||
export function objectSpaceReferences(
|
||||
object: StoreObject,
|
||||
propertyId: number
|
||||
): ExGuid[] {
|
||||
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
|
||||
);
|
||||
return resolveReferenceSlice(
|
||||
object,
|
||||
object.props.objectSpaceIds,
|
||||
offset,
|
||||
count
|
||||
);
|
||||
}
|
||||
|
||||
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 propertyTypeError(propertyId: number, expected: string): Error {
|
||||
return new Error(`Property 0x${propertyId.toString(16)} is not ${expected}`);
|
||||
}
|
||||
Reference in New Issue
Block a user