/* This file is part of the Notesnook project (https://notesnook.com/) Copyright (C) 2023 Streetwriters (Private) Limited This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ import { CompactID } from "../reader"; import { ExtendedGUID, GUID } from "../utils/guid"; import { OneNoteObject, extendedGuidToKey } from "../onestore/object-space"; import { PropertySet as RawPropertySet } from "../onestore/file-node-types/shared/PropertySet"; import { PropertyType } from "./property-type"; import { decode, fromHex, toHex } from "../utils/reader"; export type PropertyData = | boolean | number | { cb: number; Data: Uint8Array } | Uint8Array | CompactID | CompactID[] | { propertyId: number; propertySets: RawPropertySet[] } | RawPropertySet[] | RawPropertySet | null; type IdMapping = Map; function resolveCompactId( mapping: IdMapping, id: CompactID ): ExtendedGUID | undefined { const guid = mapping.get(id.guidIndex); if (!guid) return undefined; return new ExtendedGUID(guid, id.n); } function countReferences(set: RawPropertySet): number { let count = 0; for (let i = 0; i < set.rgPrids.length; ++i) { const value = set.rgData[i]; switch (set.rgPrids[i].type) { case 0x8: // ObjectID count += 1; break; case 0x9: // ArrayOfObjectIDs count += typeof value === "number" ? value : 0; break; case 0x10: // ArrayOfPropertyValues if (value && typeof value === "object" && "propertyId" in value) { for (const sub of value.propertySets) count += countReferences(sub); } break; case 0x11: // PropertySet if (value && typeof value === "object" && "rgData" in value) { count += countReferences(value as RawPropertySet); } break; } } return count; } /** * A wrapper over an object's raw property set that provides typed access to * properties, mirroring the `Object` API of the Rust onenote_parser crate. */ export class ObjectProps { constructor( private readonly object: OneNoteObject, private readonly mapping: IdMapping ) {} private get propSet() { return this.object.propSet; } get jcid() { return this.object.jcid; } get contextId() { return this.object.contextId; } get fileData(): Uint8Array | undefined { return this.object.fileData; } get fileExtension(): string | undefined { return this.object.fileExtension; } /** The index of the property with the given id, if present. */ indexOf(propType: PropertyType): number { const id = propType & 0x3ffffff; return this.propSet.body.rgPrids.findIndex((p) => p.id === id); } /** Returns the raw property value at the given index. */ valueAt(index: number): PropertyData | undefined { if (index < 0) return undefined; return this.propSet.body.rgData[index]; } has(propType: PropertyType): boolean { return this.indexOf(propType) > -1; } getBool(propType: PropertyType): boolean | undefined { const index = this.indexOf(propType); if (index < 0) return undefined; const value = this.propSet.body.rgData[index]; if (typeof value !== "boolean") return undefined; return value; } getBoolOrDefault(propType: PropertyType, def = false): boolean { return this.getBool(propType) ?? def; } getU8(propType: PropertyType): number | undefined { const value = this.getNumber(propType); return value === undefined ? undefined : value; } getU16(propType: PropertyType): number | undefined { return this.getNumber(propType); } getU32(propType: PropertyType): number | undefined { return this.getNumber(propType); } getU64(propType: PropertyType): number | undefined { return this.getNumber(propType); } /** A lossless u8 conversion accepting any integer width. */ getU8Lossless(propType: PropertyType): number | undefined { const value = this.getNumber(propType); if (value === undefined || value < 0 || value > 0xff) return undefined; return value; } private getNumber(propType: PropertyType): number | undefined { const index = this.indexOf(propType); if (index < 0) return undefined; const value = this.propSet.body.rgData[index]; if (typeof value === "number") return value; if (value instanceof Uint8Array) { let result = 0; for (let i = 0; i < value.length && i < 6; ++i) { result += value[i] * 2 ** (i * 8); } if (value.length >= 8) { // 64-bit values beyond 2^53 lose precision but are not used as // exact integers by the parser. let high = 0; for (let i = 6; i < value.length; ++i) { high += value[i] * 2 ** ((i - 6) * 8); } result += high * 2 ** 48; } return result; } return undefined; } /** Parse a 4-byte property as an f32. */ getF32(propType: PropertyType): number | undefined { const index = this.indexOf(propType); if (index < 0) return undefined; const value = this.propSet.body.rgData[index]; if (value instanceof Uint8Array && value.length === 4) { return new DataView( value.buffer, value.byteOffset, value.byteLength ).getFloat32(0, true); } if (typeof value !== "number") return undefined; const buffer = new ArrayBuffer(4); new DataView(buffer).setUint32(0, value, true); return new DataView(buffer).getFloat32(0, true); } getVec(propType: PropertyType): Uint8Array | undefined { const index = this.indexOf(propType); if (index < 0) return undefined; const value = this.propSet.body.rgData[index]; if (value instanceof Uint8Array) return value; if (value && typeof value === "object" && "cb" in value) return (value as { Data: Uint8Array }).Data; return undefined; } getVecU16(propType: PropertyType): number[] | undefined { const data = this.getVec(propType); if (!data) return undefined; const result: number[] = []; for (let i = 0; i + 1 < data.length; i += 2) { result.push(data[i] | (data[i + 1] << 8)); } return result; } getVecU32(propType: PropertyType): number[] | undefined { const data = this.getVec(propType); if (!data) return undefined; const result: number[] = []; for (let i = 0; i + 4 <= data.length; i += 4) { result.push( (data[i] | (data[i + 1] << 8) | (data[i + 2] << 16) | (data[i + 3] << 24)) >>> 0 ); } return result; } getVecI32(propType: PropertyType): number[] | undefined { const data = this.getVec(propType); if (!data) return undefined; const result: number[] = []; for (let i = 0; i + 4 <= data.length; i += 4) { result.push( (data[i] | (data[i + 1] << 8) | (data[i + 2] << 16) | (data[i + 3] << 24)) | 0 ); } return result; } getVecF32(propType: PropertyType): number[] | undefined { const data = this.getVec(propType); if (!data) return undefined; const result: number[] = []; for (let i = 0; i + 4 <= data.length; i += 4) { const view = new DataView( data.buffer, data.byteOffset + i, 4 ); result.push(view.getFloat32(0, true)); } return result; } getString(propType: PropertyType): string | undefined { const data = this.getVec(propType); if (!data) return undefined; // OneNote stores strings as UTF-16 with a trailing NUL terminator. return decode(data, "utf16le").replace(/\0+$/g, ""); } getAscii(propType: PropertyType): string | undefined { const data = this.getVec(propType); if (!data) return undefined; return decode(data, "latin1"); } getGuid(propType: PropertyType): GUID | undefined { const data = this.getVec(propType); if (!data) return undefined; return GUID.fromBuffer(data); } getGuidString(propType: PropertyType): string | undefined { return this.getGuid(propType)?.toString(); } /** A single object reference, resolved to the object's ID. */ objectRef(propType: PropertyType): string | undefined { const offset = this.objectIdOffset(propType); if (offset === undefined) return undefined; const id = this.propSet.OIDs.body[offset]; if (!id) return undefined; const resolved = resolveCompactId(this.mapping, id); return resolved ? extendedGuidToKey(resolved) : undefined; } objectRefs(propType: PropertyType): string[] { const index = this.indexOf(propType); if (index < 0) return []; const value = this.propSet.body.rgData[index]; const count = Array.isArray(value) ? value.length : typeof value === "number" ? value : 0; const offset = this.objectIdOffset(propType) ?? 0; const result: string[] = []; for (let i = 0; i < count; ++i) { const id = this.propSet.OIDs.body[offset + i]; if (!id) continue; const resolved = resolveCompactId(this.mapping, id); if (resolved) result.push(extendedGuidToKey(resolved)); } return result; } objectSpaceRefs(propType: PropertyType): string[] { const index = this.indexOf(propType); if (index < 0) return []; const value = this.propSet.body.rgData[index]; const count = Array.isArray(value) ? value.length : typeof value === "number" ? value : 0; const offset = this.objectSpaceIdOffset(propType) ?? 0; const result: string[] = []; const osids = this.propSet.OSIDs?.body ?? []; for (let i = 0; i < count; ++i) { const id = osids[offset + i]; if (!id) continue; const resolved = resolveCompactId(this.mapping, id); if (resolved) result.push(extendedGuidToKey(resolved)); } return result; } private objectIdOffset(propType: PropertyType): number | undefined { if (!this.has(propType)) return undefined; const index = this.indexOf(propType); let offset = 0; for (let i = 0; i < index; ++i) { const propertyId = this.propSet.body.rgPrids[i]; const value = this.propSet.body.rgData[i]; switch (propertyId.type) { case 0x8: // ObjectID offset += 1; break; case 0x9: // ArrayOfObjectIDs offset += Array.isArray(value) ? value.length : typeof value === "number" ? value : 0; break; case 0x10: // ArrayOfPropertyValues if (value && typeof value === "object" && "propertyId" in value) { for (const set of value.propertySets) { offset += countReferences(set); } } break; case 0x11: // PropertySet if (value && typeof value === "object" && "rgData" in value) { offset += countReferences(value as RawPropertySet); } break; } } return offset; } private objectSpaceIdOffset(propType: PropertyType): number | undefined { if (!this.has(propType)) return undefined; const index = this.indexOf(propType); let offset = 0; for (let i = 0; i < index; ++i) { const propertyId = this.propSet.body.rgPrids[i]; const value = this.propSet.body.rgData[i]; switch (propertyId.type) { case 0xa: // ObjectSpaceID offset += 1; break; case 0xb: // ArrayOfObjectSpaceIDs offset += Array.isArray(value) ? value.length : typeof value === "number" ? value : 0; break; case 0x10: // ArrayOfPropertyValues if (value && typeof value === "object" && "propertyId" in value) { for (const set of value.propertySets) { offset += countPropertySetSpaceRefs(set); } } break; case 0x11: // PropertySet if (value && typeof value === "object" && "rgData" in value) { offset += countPropertySetSpaceRefs(value as RawPropertySet); } break; } } return offset; } /** A property containing an array of property sets (e.g. note tags, text runs). */ propertyValues( propType: PropertyType ): { propertyId: number; propertySets: RawPropertySet[] } | undefined { const index = this.indexOf(propType); if (index < 0) return undefined; const value = this.propSet.body.rgData[index]; if (value && typeof value === "object" && "propertyId" in value) return value as { propertyId: number; propertySets: RawPropertySet[] }; return undefined; } /** * Create a sub-object for a property set (used for text runs, note tags, etc). * The sub-object inherits the context, file data and ID mapping but has the * given property set and a slice of the parent's object ID stream, mirroring * the Rust `parse_object` implementation. */ subObject( parentPropType: PropertyType, propertyId: number, props: RawPropertySet ): ObjectProps { const oidOffset = this.objectIdOffset(parentPropType) ?? 0; const oidCount = countReferences(props); const spaceOffset = this.objectSpaceIdOffset(parentPropType) ?? 0; const spaceCount = countPropertySetSpaceRefs(props); return new ObjectProps( { contextId: this.contextId, jcid: { id: propertyId, index: propertyId & 0xffff, isBinary: ((propertyId >> 16) & 0x1) === 1, isPropertySet: ((propertyId >> 17) & 0x1) === 1, isGraphNode: ((propertyId >> 18) & 0x1) === 1, isFileData: ((propertyId >> 19) & 0x1) === 1, isReadOnly: ((propertyId >> 20) & 0x1) === 1 }, propSet: { OIDs: { header: this.propSet.OIDs.header, body: this.propSet.OIDs.body.slice(oidOffset, oidOffset + oidCount) }, OSIDs: this.propSet.OSIDs ? { header: this.propSet.OSIDs.header, body: this.propSet.OSIDs.body.slice( spaceOffset, spaceOffset + spaceCount ) } : undefined, ContextIDs: this.propSet.ContextIDs, body: props }, mapping: this.mapping, fileData: this.object.fileData, fileDataReference: this.object.fileDataReference, fileExtension: this.object.fileExtension, odcs: 0 }, this.mapping ); } } function countPropertySetSpaceRefs(set: RawPropertySet): number { let count = 0; for (let i = 0; i < set.rgPrids.length; ++i) { const value = set.rgData[i]; switch (set.rgPrids[i].type) { case 0xa: count += 1; break; case 0xb: count += typeof value === "number" ? value : 0; break; case 0x10: if (value && typeof value === "object" && "propertyId" in value) { for (const sub of value.propertySets) { count += countPropertySetSpaceRefs(sub); } } break; case 0x11: if (value && typeof value === "object" && "rgData" in value) { count += countPropertySetSpaceRefs(value as RawPropertySet); } break; } } return count; } export { toHex, fromHex };