feat: open FSSHTTP OneNote sections
This commit is contained in:
@@ -553,11 +553,14 @@ function parseObjectGroupData(
|
||||
context.limits.maxVectorBytes,
|
||||
'Object binary item size'
|
||||
);
|
||||
const offset = body.offset;
|
||||
const data = body.read(size);
|
||||
result = {
|
||||
kind: 'object',
|
||||
objectReferences,
|
||||
cellReferences,
|
||||
data: body.read(size),
|
||||
data,
|
||||
dataReference: { source: body.bytes, offset, size },
|
||||
};
|
||||
} else if (header.type === FSSHTTP_OBJECT.objectGroupBlobReference) {
|
||||
result = {
|
||||
|
||||
@@ -120,6 +120,7 @@ export type FssHttpObjectGroupData =
|
||||
objectReferences: ExGuid[];
|
||||
cellReferences: FssHttpCellId[];
|
||||
data: Uint8Array;
|
||||
dataReference: FileDataBlobReference;
|
||||
}
|
||||
| {
|
||||
kind: 'excluded';
|
||||
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
ObjectSpace,
|
||||
PropertyEntry,
|
||||
StoreObject,
|
||||
StoreObjectSpaceReference,
|
||||
} from '../onestore/types.js';
|
||||
import { exGuidKey } from '../onestore/types.js';
|
||||
|
||||
@@ -40,13 +41,16 @@ export function objectReferences(
|
||||
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
|
||||
): ExGuid[] {
|
||||
): StoreObjectSpaceReference[] {
|
||||
const property = findProperty(object.props, propertyId);
|
||||
if (!property) return [];
|
||||
let count: number;
|
||||
@@ -60,12 +64,15 @@ export function objectSpaceReferences(
|
||||
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(
|
||||
@@ -257,6 +264,19 @@ function resolveReferenceSlice(
|
||||
});
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
|
||||
@@ -83,14 +83,14 @@ export function buildSectionContentModel(
|
||||
series,
|
||||
PROPERTY.ChildGraphSpaceElementNodes
|
||||
);
|
||||
for (const pageSpaceId of pageSpaceIds) {
|
||||
for (const pageSpaceReference of pageSpaceIds) {
|
||||
context.currentPage = undefined;
|
||||
const pageSpace = store.objectSpaces.get(exGuidKey(pageSpaceId));
|
||||
const pageSpace = store.objectSpaces.get(pageSpaceReference.key);
|
||||
if (!pageSpace) {
|
||||
addDiagnostic(
|
||||
context,
|
||||
'MISSING_PAGE_SPACE',
|
||||
`Page object space ${exGuidKey(pageSpaceId)} is missing`,
|
||||
`Page object space ${pageSpaceReference.key} is missing`,
|
||||
'PageSeriesNode'
|
||||
);
|
||||
continue;
|
||||
@@ -111,7 +111,7 @@ export function buildSectionContentModel(
|
||||
addDiagnostic(
|
||||
context,
|
||||
'PAGE_PARSE_FAILED',
|
||||
`Could not parse page ${exGuidKey(pageSpaceId)}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
`Could not parse page ${pageSpaceReference.key}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
'Page',
|
||||
'error'
|
||||
);
|
||||
|
||||
@@ -26,6 +26,7 @@ import type {
|
||||
ObjectSpace,
|
||||
PropertyEntry,
|
||||
StoreObject,
|
||||
StoreObjectSpaceReference,
|
||||
} from '../onestore/types.js';
|
||||
import { exGuidKey } from '../onestore/types.js';
|
||||
import { JCID, PROPERTY } from './constants.js';
|
||||
@@ -55,7 +56,9 @@ export function buildSectionDto(
|
||||
const root = store.rootObjectSpace;
|
||||
const metadata = rootObject(root, 2, 'section metadata root');
|
||||
assertJcid(metadata, JCID.SectionMetadata, 'SectionMetadata');
|
||||
const displayName = optionalString(metadata, PROPERTY.SectionDisplayName);
|
||||
const displayName = cleanMetadataString(
|
||||
optionalString(metadata, PROPERTY.SectionDisplayName)
|
||||
);
|
||||
|
||||
const section = rootObject(root, 1, 'section content root');
|
||||
assertJcid(section, JCID.SectionNode, 'SectionNode');
|
||||
@@ -69,14 +72,14 @@ export function buildSectionDto(
|
||||
series,
|
||||
PROPERTY.ChildGraphSpaceElementNodes
|
||||
);
|
||||
for (const pageSpaceId of pageSpaceIds) {
|
||||
for (const pageSpaceReference of pageSpaceIds) {
|
||||
context.currentPage = undefined;
|
||||
const pageSpace = store.objectSpaces.get(exGuidKey(pageSpaceId));
|
||||
const pageSpace = store.objectSpaces.get(pageSpaceReference.key);
|
||||
if (!pageSpace) {
|
||||
addDiagnostic(
|
||||
context,
|
||||
'MISSING_PAGE_SPACE',
|
||||
`Page object space ${exGuidKey(pageSpaceId)} is missing`,
|
||||
`Page object space ${pageSpaceReference.key} is missing`,
|
||||
'PageSeriesNode'
|
||||
);
|
||||
continue;
|
||||
@@ -99,7 +102,7 @@ export function buildSectionDto(
|
||||
addDiagnostic(
|
||||
context,
|
||||
'PAGE_PARSE_FAILED',
|
||||
`Could not parse page ${exGuidKey(pageSpaceId)}: ${message}`,
|
||||
`Could not parse page ${pageSpaceReference.key}: ${message}`,
|
||||
'Page',
|
||||
'error'
|
||||
);
|
||||
@@ -116,7 +119,7 @@ export function buildSectionDto(
|
||||
diagnostics,
|
||||
parserInfo: {
|
||||
implementation: 'typescript',
|
||||
supportedVariant: 'desktop-revision-store',
|
||||
supportedVariant: store.variant,
|
||||
referenceRevision: 'onenote.rs@5138a39a3f4e72b840932f9872fecde52fa9da60',
|
||||
},
|
||||
};
|
||||
@@ -398,13 +401,16 @@ function objectReferences(object: StoreObject, propertyId: number): ExGuid[] {
|
||||
propertyId,
|
||||
countObjectReferences
|
||||
);
|
||||
if (object.resolvedObjectIds) {
|
||||
return resolvedReferenceSlice(object.resolvedObjectIds, offset, count);
|
||||
}
|
||||
return resolveReferenceSlice(object, object.props.objectIds, offset, count);
|
||||
}
|
||||
|
||||
function objectSpaceReferences(
|
||||
object: StoreObject,
|
||||
propertyId: number
|
||||
): ExGuid[] {
|
||||
): StoreObjectSpaceReference[] {
|
||||
const property = findProperty(object.props, propertyId);
|
||||
if (!property) return [];
|
||||
let count: number;
|
||||
@@ -421,12 +427,15 @@ function objectSpaceReferences(
|
||||
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) }));
|
||||
}
|
||||
|
||||
function resolveReferenceSlice(
|
||||
@@ -447,6 +456,19 @@ function resolveReferenceSlice(
|
||||
});
|
||||
}
|
||||
|
||||
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 rootObject(
|
||||
space: ObjectSpace,
|
||||
role: number,
|
||||
@@ -659,3 +681,8 @@ function addDiagnostic(
|
||||
function stripOneExtension(filename: string): string {
|
||||
return (filename.split(/[\\/]/).at(-1) ?? filename).replace(/\.one$/i, '');
|
||||
}
|
||||
|
||||
function cleanMetadataString(value: string | undefined): string | undefined {
|
||||
const result = value?.split('\0').join('').trim();
|
||||
return result || undefined;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ import { exGuidKey } from './types.js';
|
||||
|
||||
export interface DesktopOneStore {
|
||||
kind: 'section' | 'table-of-contents';
|
||||
variant: 'desktop-revision-store' | 'fsshttpb-package-store';
|
||||
fileIdentity: string;
|
||||
rootObjectSpace: ObjectSpace;
|
||||
objectSpaces: Map<string, ObjectSpace>;
|
||||
@@ -185,6 +186,7 @@ function parseDesktopRevisionStore(
|
||||
|
||||
return {
|
||||
kind: expectedKind === 'desktop-one' ? 'section' : 'table-of-contents',
|
||||
variant: 'desktop-revision-store',
|
||||
fileIdentity,
|
||||
rootObjectSpace,
|
||||
objectSpaces,
|
||||
|
||||
48
src/onenote/onestore/fsshttp-store.test.ts
Normal file
48
src/onenote/onestore/fsshttp-store.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
parseOneNoteSection,
|
||||
parseOneNoteSectionContent,
|
||||
} from '../parser/parse-section.js';
|
||||
|
||||
function fixtureBytes(): Uint8Array {
|
||||
const encoded = readFileSync(
|
||||
resolve(process.cwd(), 'tests/fixtures/joplin-quick-notes-fsshttp.one.b64'),
|
||||
'utf8'
|
||||
);
|
||||
return Uint8Array.from(Buffer.from(encoded.replaceAll(/\s/gu, ''), 'base64'));
|
||||
}
|
||||
|
||||
describe('FSSHTTPB OneStore adapter', () => {
|
||||
it('opens the pinned Joplin section through the public parser', () => {
|
||||
const parsed = parseOneNoteSection(fixtureBytes(), {
|
||||
sourceName: 'Quick Notes.one',
|
||||
});
|
||||
|
||||
expect(parsed).toMatchObject({
|
||||
format: 'one',
|
||||
sourceName: 'Quick Notes.one',
|
||||
sectionName: 'Section title.one',
|
||||
parserInfo: { supportedVariant: 'fsshttpb-package-store' },
|
||||
});
|
||||
expect(parsed.pages).toHaveLength(1);
|
||||
expect(parsed.pages[0]).toMatchObject({
|
||||
title: 'Page title',
|
||||
text: expect.stringContaining('Page content'),
|
||||
});
|
||||
});
|
||||
|
||||
it('feeds FSSHTTPB objects into the rich-content model', () => {
|
||||
const parsed = parseOneNoteSectionContent(fixtureBytes(), {
|
||||
sourceName: 'Quick Notes.one',
|
||||
});
|
||||
|
||||
expect(parsed.sectionName).toBe('Section title.one');
|
||||
expect(parsed.pages).toHaveLength(1);
|
||||
expect(parsed.pages[0]).toMatchObject({ title: 'Page title' });
|
||||
expect(parsed.pages[0]?.text).toContain('Page content');
|
||||
});
|
||||
});
|
||||
694
src/onenote/onestore/fsshttp-store.ts
Normal file
694
src/onenote/onestore/fsshttp-store.ts
Normal file
@@ -0,0 +1,694 @@
|
||||
// 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: `onestore/fsshttpb/*`.
|
||||
// Deviations: immutable browser-native records, explicit cycle/count bounds,
|
||||
// newest-revision precedence, and store-specific resolved reference arrays.
|
||||
|
||||
import { BinaryReader } from '../binary/BinaryReader.js';
|
||||
import { NIL_GUID, readGuid } from '../binary/guid.js';
|
||||
import {
|
||||
fssHttpCellKey,
|
||||
type FssHttpCellId,
|
||||
type FssHttpObjectDeclaration,
|
||||
type FssHttpObjectGroup,
|
||||
type FssHttpObjectGroupData,
|
||||
type FssHttpOneStorePackage,
|
||||
type FssHttpRevisionManifest,
|
||||
type FssHttpSerialNumber,
|
||||
type FssHttpStorageIndex,
|
||||
type FssHttpStorageIndexCellMapping,
|
||||
type FssHttpStorageManifest,
|
||||
} from '../fsshttpb/types.js';
|
||||
import type { ParserDiagnostic } from '../model/diagnostics.js';
|
||||
import { ONE_SECTION_FILE_TYPE } from '../parser/detect-format.js';
|
||||
import { OneNoteParserError } from '../parser/error.js';
|
||||
import type { OneNoteParserLimits } from '../parser/limits.js';
|
||||
import { parseFssHttpOneStorePackage } from '../fsshttpb/package.js';
|
||||
import type { DesktopOneStore } from './desktop-store.js';
|
||||
import { findProperty, parseObjectPropSetRange } from './property-set.js';
|
||||
import type {
|
||||
ExGuid,
|
||||
ObjectSpace,
|
||||
PropertyEntry,
|
||||
StoreFileData,
|
||||
StoreObject,
|
||||
} from './types.js';
|
||||
import { exGuidKey } from './types.js';
|
||||
|
||||
const HEADER_ROOT = exGuidKey({
|
||||
guid: '{1A5A319C-C26B-41AA-B9C5-9BD8C44E07D4}',
|
||||
value: 1,
|
||||
});
|
||||
const DATA_ROOT = exGuidKey({
|
||||
guid: '{84DEFAB9-AAA3-4A0D-A3A8-520C77AC7073}',
|
||||
value: 2,
|
||||
});
|
||||
const REVISION_ROLE_GUID = '{4A3717F8-1C14-49E7-9526-81D942DE1741}';
|
||||
const PROPERTY_FILE_IDENTITY_GUID = 0x1c001d94;
|
||||
const PROPERTY_FILE_ANCESTOR_IDENTITY_GUID = 0x1c001d95;
|
||||
const PROPERTY_FILE_NAME_CRC = 0x14001d93;
|
||||
|
||||
interface StoreContext {
|
||||
package: FssHttpOneStorePackage;
|
||||
index: FssHttpStorageIndex;
|
||||
limits: OneNoteParserLimits;
|
||||
diagnostics: ParserDiagnostic[];
|
||||
objectCount: number;
|
||||
}
|
||||
|
||||
interface GroupEntry {
|
||||
declaration: FssHttpObjectDeclaration;
|
||||
data: FssHttpObjectGroupData;
|
||||
}
|
||||
|
||||
/** Parse an unfragmented FSSHTTPB package into the shared OneStore model. */
|
||||
export function parseFssHttpOneStore(
|
||||
bytes: Uint8Array,
|
||||
limits: OneNoteParserLimits,
|
||||
diagnostics: ParserDiagnostic[]
|
||||
): DesktopOneStore {
|
||||
const packageStore = parseFssHttpOneStorePackage(bytes, limits);
|
||||
if (packageStore.dataElements.fragments.size > 0) {
|
||||
throw new OneNoteParserError(
|
||||
'UNSUPPORTED_FORMAT',
|
||||
'FSSHTTPB Data Element Fragment reconstruction is not supported',
|
||||
{ structure: 'Data Element Fragment' }
|
||||
);
|
||||
}
|
||||
|
||||
const index = selectStorageIndex(packageStore, limits, diagnostics);
|
||||
const context: StoreContext = {
|
||||
package: packageStore,
|
||||
index,
|
||||
limits,
|
||||
diagnostics,
|
||||
objectCount: 0,
|
||||
};
|
||||
const manifest = selectStorageManifest(context);
|
||||
if (manifest.schema !== packageStore.cellSchema) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`Storage Manifest schema ${manifest.schema} does not match package schema ${packageStore.cellSchema}`,
|
||||
{ structure: 'Storage Manifest' }
|
||||
);
|
||||
}
|
||||
|
||||
const headerCell = requiredManifestRoot(manifest, HEADER_ROOT, 'header');
|
||||
const dataRootCell = requiredManifestRoot(manifest, DATA_ROOT, 'data');
|
||||
validateHeaderCell(context, headerCell);
|
||||
|
||||
const rootObjectSpace = parseObjectSpace(context, dataRootCell);
|
||||
const objectSpaces = new Map<string, ObjectSpace>();
|
||||
objectSpaces.set(fssHttpCellKey(dataRootCell), rootObjectSpace);
|
||||
|
||||
const skippedCells = new Set([
|
||||
fssHttpCellKey(headerCell),
|
||||
fssHttpCellKey(dataRootCell),
|
||||
]);
|
||||
for (const mapping of index.cellMappings.values()) {
|
||||
const key = fssHttpCellKey(mapping.cellId);
|
||||
if (skippedCells.has(key) || isNilExGuid(mapping.id)) continue;
|
||||
objectSpaces.set(key, parseObjectSpace(context, mapping.cellId));
|
||||
}
|
||||
|
||||
return {
|
||||
kind:
|
||||
packageStore.fileType === ONE_SECTION_FILE_TYPE
|
||||
? 'section'
|
||||
: 'table-of-contents',
|
||||
variant: 'fsshttpb-package-store',
|
||||
fileIdentity: packageStore.fileIdentity,
|
||||
rootObjectSpace,
|
||||
objectSpaces,
|
||||
};
|
||||
}
|
||||
|
||||
function selectStorageIndex(
|
||||
packageStore: FssHttpOneStorePackage,
|
||||
limits: OneNoteParserLimits,
|
||||
diagnostics: ParserDiagnostic[]
|
||||
): FssHttpStorageIndex {
|
||||
const indexes = packageStore.dataElements.storageIndexes;
|
||||
const requested = indexes.get(exGuidKey(packageStore.storageIndexId));
|
||||
if (requested) return requested;
|
||||
if (indexes.size !== 1) {
|
||||
throw new OneNoteParserError(
|
||||
'MISSING_REFERENCE',
|
||||
`Storage Index ${exGuidKey(packageStore.storageIndexId)} is absent`,
|
||||
{ structure: 'Storage Index' }
|
||||
);
|
||||
}
|
||||
addDiagnostic(
|
||||
diagnostics,
|
||||
limits,
|
||||
'FSSHTTP_STORAGE_INDEX_FALLBACK',
|
||||
`Storage Index ${exGuidKey(packageStore.storageIndexId)} was absent; used the package's only Storage Index`,
|
||||
'Storage Index'
|
||||
);
|
||||
return indexes.values().next().value!;
|
||||
}
|
||||
|
||||
function selectStorageManifest(context: StoreContext): FssHttpStorageManifest {
|
||||
const manifests = context.package.dataElements.storageManifests;
|
||||
for (const mapping of context.index.manifestMappings) {
|
||||
const manifest = manifests.get(exGuidKey(mapping.id));
|
||||
if (manifest) return manifest;
|
||||
}
|
||||
if (manifests.size !== 1) {
|
||||
throw new OneNoteParserError(
|
||||
'MISSING_REFERENCE',
|
||||
context.index.manifestMappings.length === 0
|
||||
? 'Storage Index has no resolvable Storage Manifest mapping'
|
||||
: 'Storage Index manifest mappings do not resolve uniquely',
|
||||
{ structure: 'Storage Manifest' }
|
||||
);
|
||||
}
|
||||
addDiagnostic(
|
||||
context.diagnostics,
|
||||
context.limits,
|
||||
'FSSHTTP_STORAGE_MANIFEST_FALLBACK',
|
||||
"Storage Index manifest mappings did not resolve; used the package's only Storage Manifest",
|
||||
'Storage Manifest'
|
||||
);
|
||||
return manifests.values().next().value!;
|
||||
}
|
||||
|
||||
function requiredManifestRoot(
|
||||
manifest: FssHttpStorageManifest,
|
||||
key: string,
|
||||
name: string
|
||||
): FssHttpCellId {
|
||||
const cell = manifest.roots.get(key);
|
||||
if (!cell) {
|
||||
throw new OneNoteParserError(
|
||||
'MISSING_REFERENCE',
|
||||
`Storage Manifest has no ${name} cell root`,
|
||||
{ structure: 'Storage Manifest root' }
|
||||
);
|
||||
}
|
||||
return cell;
|
||||
}
|
||||
|
||||
function validateHeaderCell(context: StoreContext, cell: FssHttpCellId): void {
|
||||
const mapping = requiredCellMapping(context, cell);
|
||||
const revisionId = resolveCurrentRevisionId(context, mapping);
|
||||
if (!revisionId) {
|
||||
throw new OneNoteParserError(
|
||||
'MISSING_REFERENCE',
|
||||
'Header cell has no current revision',
|
||||
{ structure: 'OneStore package header cell' }
|
||||
);
|
||||
}
|
||||
const revision = requiredRevisionManifest(context, revisionId);
|
||||
const entries = revision.objectGroupIds.flatMap((groupId) =>
|
||||
pairedGroupEntries(context, groupId)
|
||||
);
|
||||
const headerEntry = entries.find(
|
||||
({ declaration }) => declaration.partitionId === 1
|
||||
);
|
||||
if (!headerEntry || headerEntry.data.kind !== 'object') {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
'Header cell has no object-data property set',
|
||||
{ structure: 'OneStore package header cell' }
|
||||
);
|
||||
}
|
||||
const props = parseObjectPropSetRange(
|
||||
headerEntry.data.dataReference.source,
|
||||
headerEntry.data.dataReference.offset,
|
||||
headerEntry.data.dataReference.size,
|
||||
context.limits
|
||||
);
|
||||
requiredBytesProperty(
|
||||
props.properties.entries,
|
||||
PROPERTY_FILE_IDENTITY_GUID,
|
||||
16
|
||||
);
|
||||
requiredBytesProperty(
|
||||
props.properties.entries,
|
||||
PROPERTY_FILE_ANCESTOR_IDENTITY_GUID,
|
||||
16
|
||||
);
|
||||
const crc = findProperty(props, PROPERTY_FILE_NAME_CRC);
|
||||
if (!crc || crc.value.kind !== 'u32') {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
'Header cell FileNameCrc property is absent or malformed',
|
||||
{ structure: 'OneStore package header cell' }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function requiredBytesProperty(
|
||||
entries: PropertyEntry[],
|
||||
propertyId: number,
|
||||
length: number
|
||||
): Uint8Array {
|
||||
const entry = entries.find(
|
||||
(candidate) => candidate.id === (propertyId & 0x03ffffff)
|
||||
);
|
||||
if (
|
||||
!entry ||
|
||||
entry.value.kind !== 'bytes' ||
|
||||
entry.value.value.length !== length
|
||||
) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`Header property 0x${propertyId.toString(16)} is absent or malformed`,
|
||||
{ structure: 'OneStore package header cell' }
|
||||
);
|
||||
}
|
||||
// Exercise the exact mixed-endian GUID range while preserving bounded reads.
|
||||
readGuid(new BinaryReader(entry.value.value, 0, length, 'Header GUID'));
|
||||
return entry.value.value;
|
||||
}
|
||||
|
||||
function parseObjectSpace(
|
||||
context: StoreContext,
|
||||
cell: FssHttpCellId
|
||||
): ObjectSpace {
|
||||
const mapping = requiredCellMapping(context, cell);
|
||||
const revisionId = resolveCurrentRevisionId(context, mapping);
|
||||
if (!revisionId) {
|
||||
return {
|
||||
id: cell.objectSpace,
|
||||
roots: new Map(),
|
||||
objects: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
const roots = new Map<number, ExGuid>();
|
||||
const objects = new Map<string, StoreObject>();
|
||||
for (const revision of revisionChain(context, revisionId)) {
|
||||
for (const root of revision.roots) {
|
||||
const role = parseRevisionRole(root.role);
|
||||
if (!roots.has(role)) roots.set(role, root.objectId);
|
||||
}
|
||||
for (const groupId of revision.objectGroupIds) {
|
||||
parseObjectGroup(context, cell, groupId, objects);
|
||||
}
|
||||
}
|
||||
return { id: cell.objectSpace, roots, objects };
|
||||
}
|
||||
|
||||
function revisionChain(
|
||||
context: StoreContext,
|
||||
firstRevisionId: ExGuid
|
||||
): FssHttpRevisionManifest[] {
|
||||
const result: FssHttpRevisionManifest[] = [];
|
||||
const seen = new Set<string>();
|
||||
let revisionId: ExGuid | undefined = firstRevisionId;
|
||||
while (revisionId) {
|
||||
if (result.length >= context.limits.maxTransactionFragments) {
|
||||
throw new OneNoteParserError(
|
||||
'LIMIT_EXCEEDED',
|
||||
`Revision chain exceeds ${context.limits.maxTransactionFragments} entries`,
|
||||
{ structure: 'FSSHTTPB revision chain' }
|
||||
);
|
||||
}
|
||||
const key = exGuidKey(revisionId);
|
||||
if (seen.has(key)) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`Revision chain contains a cycle at ${key}`,
|
||||
{ structure: 'FSSHTTPB revision chain' }
|
||||
);
|
||||
}
|
||||
seen.add(key);
|
||||
const revision = requiredRevisionManifest(context, revisionId);
|
||||
result.push(revision);
|
||||
if (isNilExGuid(revision.baseRevisionId)) break;
|
||||
revisionId = resolveRevisionId(context, revision.baseRevisionId);
|
||||
if (!revisionId) {
|
||||
throw new OneNoteParserError(
|
||||
'MISSING_REFERENCE',
|
||||
`Base revision ${exGuidKey(revision.baseRevisionId)} is unresolved`,
|
||||
{ structure: 'Revision Manifest' }
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseObjectGroup(
|
||||
context: StoreContext,
|
||||
cell: FssHttpCellId,
|
||||
groupId: ExGuid,
|
||||
objects: Map<string, StoreObject>
|
||||
): void {
|
||||
const entries = pairedGroupEntries(context, groupId);
|
||||
const partitions = new Map<string, Map<number, GroupEntry>>();
|
||||
for (const entry of entries) {
|
||||
const objectKey = exGuidKey(entry.declaration.objectId);
|
||||
let objectPartitions = partitions.get(objectKey);
|
||||
if (!objectPartitions) {
|
||||
objectPartitions = new Map();
|
||||
partitions.set(objectKey, objectPartitions);
|
||||
}
|
||||
if (objectPartitions.has(entry.declaration.partitionId)) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`Object ${objectKey} repeats partition ${entry.declaration.partitionId}`,
|
||||
{ structure: 'Object Group' }
|
||||
);
|
||||
}
|
||||
objectPartitions.set(entry.declaration.partitionId, entry);
|
||||
}
|
||||
|
||||
for (const [objectKey, objectPartitions] of partitions) {
|
||||
if (objects.has(objectKey)) continue;
|
||||
context.objectCount += 1;
|
||||
if (context.objectCount > context.limits.maxObjects) {
|
||||
throw new OneNoteParserError(
|
||||
'LIMIT_EXCEEDED',
|
||||
`Resolved object count exceeds ${context.limits.maxObjects}`,
|
||||
{ structure: 'FSSHTTPB Object Group' }
|
||||
);
|
||||
}
|
||||
objects.set(
|
||||
objectKey,
|
||||
parseStoreObject(context, cell, objectPartitions, objectKey)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function pairedGroupEntries(
|
||||
context: StoreContext,
|
||||
groupId: ExGuid
|
||||
): GroupEntry[] {
|
||||
const group = context.package.dataElements.objectGroups.get(
|
||||
exGuidKey(groupId)
|
||||
);
|
||||
if (!group) {
|
||||
throw new OneNoteParserError(
|
||||
'MISSING_REFERENCE',
|
||||
`Object Group ${exGuidKey(groupId)} is absent`,
|
||||
{ structure: 'Object Group' }
|
||||
);
|
||||
}
|
||||
validateGroupCounts(group, groupId);
|
||||
return group.declarations.map((declaration, index) => {
|
||||
const data = group.data[index]!;
|
||||
validateDeclarationData(declaration, data);
|
||||
return { declaration, data };
|
||||
});
|
||||
}
|
||||
|
||||
function validateGroupCounts(group: FssHttpObjectGroup, groupId: ExGuid): void {
|
||||
if (group.declarations.length !== group.data.length) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`Object Group ${exGuidKey(groupId)} has ${group.declarations.length} declarations and ${group.data.length} data entries`,
|
||||
{ structure: 'Object Group' }
|
||||
);
|
||||
}
|
||||
if (
|
||||
group.changeFrequencies.length !== 0 &&
|
||||
group.changeFrequencies.length !== group.declarations.length
|
||||
) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`Object Group ${exGuidKey(groupId)} metadata count does not match its declarations`,
|
||||
{ structure: 'Object Group Metadata Block' }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateDeclarationData(
|
||||
declaration: FssHttpObjectDeclaration,
|
||||
data: FssHttpObjectGroupData
|
||||
): void {
|
||||
if (
|
||||
declaration.objectReferenceCount !== data.objectReferences.length ||
|
||||
declaration.cellReferenceCount !== data.cellReferences.length
|
||||
) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`Object ${exGuidKey(declaration.objectId)} declaration/reference counts do not match its data`,
|
||||
{ structure: 'Object Group' }
|
||||
);
|
||||
}
|
||||
if (declaration.kind === 'object') {
|
||||
const actualSize =
|
||||
data.kind === 'object'
|
||||
? data.data.byteLength
|
||||
: data.kind === 'excluded'
|
||||
? data.dataSize
|
||||
: undefined;
|
||||
if (actualSize === undefined || declaration.dataSize !== actualSize) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`Object ${exGuidKey(declaration.objectId)} declaration/data sizes do not match`,
|
||||
{ structure: 'Object Group' }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseStoreObject(
|
||||
context: StoreContext,
|
||||
cell: FssHttpCellId,
|
||||
partitions: ReadonlyMap<number, GroupEntry>,
|
||||
objectKey: string
|
||||
): StoreObject {
|
||||
const metadata = requiredObjectPartition(
|
||||
partitions,
|
||||
4,
|
||||
objectKey,
|
||||
'metadata'
|
||||
);
|
||||
const objectData = requiredObjectPartition(partitions, 1, objectKey, 'data');
|
||||
const metadataReader = new BinaryReader(
|
||||
metadata.dataReference.source,
|
||||
metadata.dataReference.offset,
|
||||
metadata.dataReference.size,
|
||||
'FSSHTTPB object metadata'
|
||||
);
|
||||
const jcid = metadataReader.u32();
|
||||
const props = parseObjectPropSetRange(
|
||||
objectData.dataReference.source,
|
||||
objectData.dataReference.offset,
|
||||
objectData.dataReference.size,
|
||||
context.limits
|
||||
);
|
||||
|
||||
const contextCells = objectData.cellReferences.filter(
|
||||
(reference) =>
|
||||
exGuidKey(reference.objectSpace) === exGuidKey(cell.objectSpace)
|
||||
);
|
||||
const objectSpaceCells = objectData.cellReferences.filter(
|
||||
(reference) =>
|
||||
exGuidKey(reference.objectSpace) !== exGuidKey(cell.objectSpace)
|
||||
);
|
||||
if (props.objectIds.length < objectData.objectReferences.length) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`Object ${objectKey} has fewer property-set object IDs than resolved references`,
|
||||
{ structure: 'ObjectPropSet mapping table' }
|
||||
);
|
||||
}
|
||||
if (
|
||||
props.contextIds.length !== contextCells.length ||
|
||||
props.objectSpaceIds.length !== objectSpaceCells.length
|
||||
) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`Object ${objectKey} property-set cell reference counts do not match`,
|
||||
{ structure: 'ObjectPropSet mapping table' }
|
||||
);
|
||||
}
|
||||
|
||||
const objectId = partitions.values().next().value!.declaration.objectId;
|
||||
return {
|
||||
id: objectId,
|
||||
jcid,
|
||||
props,
|
||||
mapping: new Map(),
|
||||
contextId: cell.context,
|
||||
resolvedObjectIds: objectData.objectReferences,
|
||||
resolvedObjectSpaceIds: objectSpaceCells.map((reference) => ({
|
||||
id: reference.objectSpace,
|
||||
key: fssHttpCellKey(reference),
|
||||
})),
|
||||
resolvedContextIds: contextCells.map((reference) => reference.context),
|
||||
fileData: parseFileData(context, partitions, objectKey),
|
||||
};
|
||||
}
|
||||
|
||||
function requiredObjectPartition(
|
||||
partitions: ReadonlyMap<number, GroupEntry>,
|
||||
partitionId: number,
|
||||
objectKey: string,
|
||||
name: string
|
||||
): Extract<FssHttpObjectGroupData, { kind: 'object' }> {
|
||||
const entry = partitions.get(partitionId);
|
||||
if (!entry) {
|
||||
throw new OneNoteParserError(
|
||||
'MISSING_REFERENCE',
|
||||
`Object ${objectKey} ${name} partition is absent`,
|
||||
{ structure: 'Object Group' }
|
||||
);
|
||||
}
|
||||
if (entry.data.kind === 'excluded') {
|
||||
throw new OneNoteParserError(
|
||||
'UNSUPPORTED_FORMAT',
|
||||
`Object ${objectKey} ${name} partition is excluded from this package`,
|
||||
{ structure: 'Object Group Data Excluded' }
|
||||
);
|
||||
}
|
||||
if (entry.data.kind !== 'object') {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`Object ${objectKey} ${name} partition is not object data`,
|
||||
{ structure: 'Object Group' }
|
||||
);
|
||||
}
|
||||
return entry.data;
|
||||
}
|
||||
|
||||
function parseFileData(
|
||||
context: StoreContext,
|
||||
partitions: ReadonlyMap<number, GroupEntry>,
|
||||
objectKey: string
|
||||
): StoreFileData | undefined {
|
||||
const entry = partitions.get(2);
|
||||
if (!entry) return undefined;
|
||||
if (entry.data.kind !== 'blob-reference') {
|
||||
throw new OneNoteParserError(
|
||||
entry.data.kind === 'excluded'
|
||||
? 'UNSUPPORTED_FORMAT'
|
||||
: 'INVALID_STRUCTURE',
|
||||
`Object ${objectKey} file-data partition is not an available BLOB reference`,
|
||||
{ structure: 'Object Group BLOB reference' }
|
||||
);
|
||||
}
|
||||
const blob = context.package.dataElements.blobs.get(
|
||||
exGuidKey(entry.data.blobId)
|
||||
);
|
||||
if (!blob) {
|
||||
throw new OneNoteParserError(
|
||||
'MISSING_REFERENCE',
|
||||
`Object Data BLOB ${exGuidKey(entry.data.blobId)} is absent`,
|
||||
{ structure: 'Object Data BLOB' }
|
||||
);
|
||||
}
|
||||
return {
|
||||
referenceKind: 'embedded',
|
||||
dataReference: `<fsshttpb>${exGuidKey(entry.data.blobId)}`,
|
||||
extension: '',
|
||||
storageGuid: entry.data.blobId.guid,
|
||||
blob,
|
||||
};
|
||||
}
|
||||
|
||||
function requiredCellMapping(
|
||||
context: StoreContext,
|
||||
cell: FssHttpCellId
|
||||
): FssHttpStorageIndexCellMapping {
|
||||
const mapping = context.index.cellMappings.get(fssHttpCellKey(cell));
|
||||
if (!mapping) {
|
||||
throw new OneNoteParserError(
|
||||
'MISSING_REFERENCE',
|
||||
`Cell mapping ${fssHttpCellKey(cell)} is absent`,
|
||||
{ structure: 'Storage Index Cell Mapping' }
|
||||
);
|
||||
}
|
||||
return mapping;
|
||||
}
|
||||
|
||||
function resolveCurrentRevisionId(
|
||||
context: StoreContext,
|
||||
mapping: FssHttpStorageIndexCellMapping
|
||||
): ExGuid | undefined {
|
||||
const cellRevision = context.package.dataElements.cellManifests.get(
|
||||
exGuidKey(mapping.id)
|
||||
);
|
||||
if (cellRevision && isNilExGuid(cellRevision)) return undefined;
|
||||
const resolved = cellRevision
|
||||
? resolveRevisionId(context, cellRevision)
|
||||
: resolveRevisionId(context, mapping.id);
|
||||
if (resolved) return resolved;
|
||||
|
||||
const serialMatches = [...context.index.revisionMappings.values()].filter(
|
||||
(candidate) => sameSerial(candidate.serial, mapping.serial)
|
||||
);
|
||||
if (serialMatches.length > 1) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
'Cell mapping serial number resolves to multiple revision mappings',
|
||||
{ structure: 'Storage Index Revision Mapping' }
|
||||
);
|
||||
}
|
||||
return serialMatches[0]?.revision;
|
||||
}
|
||||
|
||||
function resolveRevisionId(
|
||||
context: StoreContext,
|
||||
id: ExGuid
|
||||
): ExGuid | undefined {
|
||||
const mapped = context.index.revisionMappings.get(exGuidKey(id));
|
||||
if (mapped) return mapped.revision;
|
||||
return context.package.dataElements.revisionManifests.has(exGuidKey(id))
|
||||
? id
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function requiredRevisionManifest(
|
||||
context: StoreContext,
|
||||
id: ExGuid
|
||||
): FssHttpRevisionManifest {
|
||||
const revision = context.package.dataElements.revisionManifests.get(
|
||||
exGuidKey(id)
|
||||
);
|
||||
if (!revision) {
|
||||
throw new OneNoteParserError(
|
||||
'MISSING_REFERENCE',
|
||||
`Revision Manifest ${exGuidKey(id)} is absent`,
|
||||
{ structure: 'Revision Manifest' }
|
||||
);
|
||||
}
|
||||
return revision;
|
||||
}
|
||||
|
||||
function parseRevisionRole(role: ExGuid): number {
|
||||
if (role.guid !== REVISION_ROLE_GUID || role.value < 1 || role.value > 4) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`Invalid revision root role ${exGuidKey(role)}`,
|
||||
{ structure: 'Revision Manifest root declaration' }
|
||||
);
|
||||
}
|
||||
return role.value;
|
||||
}
|
||||
|
||||
function isNilExGuid(id: ExGuid): boolean {
|
||||
return id.guid === NIL_GUID && id.value === 0;
|
||||
}
|
||||
|
||||
function sameSerial(
|
||||
left: FssHttpSerialNumber,
|
||||
right: FssHttpSerialNumber
|
||||
): boolean {
|
||||
return left.guid === right.guid && left.value === right.value;
|
||||
}
|
||||
|
||||
function addDiagnostic(
|
||||
diagnostics: ParserDiagnostic[],
|
||||
limits: OneNoteParserLimits,
|
||||
code: string,
|
||||
message: string,
|
||||
structure: string
|
||||
): void {
|
||||
if (diagnostics.length >= limits.maxDiagnostics) return;
|
||||
diagnostics.push({
|
||||
severity: 'warning',
|
||||
code,
|
||||
message,
|
||||
structure,
|
||||
recoverable: true,
|
||||
});
|
||||
}
|
||||
@@ -39,13 +39,30 @@ export function parseObjectPropSet(
|
||||
{ offset: reference.offset, structure: 'ObjectPropSet' }
|
||||
);
|
||||
}
|
||||
const reader = new BinaryReader(
|
||||
return parseObjectPropSetRange(
|
||||
bytes,
|
||||
reference.offset,
|
||||
reference.size,
|
||||
'ObjectPropSet'
|
||||
limits
|
||||
);
|
||||
}
|
||||
|
||||
/** Parse a property set from an exact bounded range in the source file. */
|
||||
export function parseObjectPropSetRange(
|
||||
bytes: Uint8Array,
|
||||
offset: number,
|
||||
size: number,
|
||||
limits: OneNoteParserLimits
|
||||
): ObjectPropSet {
|
||||
const reader = new BinaryReader(bytes, offset, size, 'ObjectPropSet');
|
||||
|
||||
return parseObjectPropSetReader(reader, limits);
|
||||
}
|
||||
|
||||
function parseObjectPropSetReader(
|
||||
reader: BinaryReader,
|
||||
limits: OneNoteParserLimits
|
||||
): ObjectPropSet {
|
||||
const oidHeader = readStreamHeader(reader, limits);
|
||||
const objectIds = readCompactIds(reader, oidHeader.count);
|
||||
let objectSpaceIds: CompactId[] = [];
|
||||
|
||||
@@ -92,12 +92,24 @@ export interface StoreFileData {
|
||||
blob?: FileDataBlobReference;
|
||||
}
|
||||
|
||||
/** A resolved object-space reference plus the store-specific lookup key. */
|
||||
export interface StoreObjectSpaceReference {
|
||||
id: ExGuid;
|
||||
key: string;
|
||||
}
|
||||
|
||||
export interface StoreObject {
|
||||
id: ExGuid;
|
||||
jcid: number;
|
||||
props: ObjectPropSet;
|
||||
mapping: ReadonlyMap<number, string>;
|
||||
contextId: ExGuid;
|
||||
/** Pre-resolved FSSHTTPB object stream, indexed like `props.objectIds`. */
|
||||
resolvedObjectIds?: readonly ExGuid[];
|
||||
/** Pre-resolved FSSHTTPB object-space stream, indexed like `props.objectSpaceIds`. */
|
||||
resolvedObjectSpaceIds?: readonly StoreObjectSpaceReference[];
|
||||
/** Pre-resolved FSSHTTPB context stream, indexed like `props.contextIds`. */
|
||||
resolvedContextIds?: readonly ExGuid[];
|
||||
fileData?: StoreFileData;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,10 @@ import type { OneNoteSectionContentModel } from '../one/content-model.js';
|
||||
import { buildSectionDto } from '../one/section.js';
|
||||
import { buildSectionContentModel } from '../one/section-content.js';
|
||||
import { parseDesktopOneStore } from '../onestore/desktop-store.js';
|
||||
import { toBytes } from './detect-format.js';
|
||||
import type { DesktopOneStore } from '../onestore/desktop-store.js';
|
||||
import { parseFssHttpOneStore } from '../onestore/fsshttp-store.js';
|
||||
import { detectOneNoteFormat, toBytes } from './detect-format.js';
|
||||
import { OneNoteParserError } from './error.js';
|
||||
import type { OneNoteParserLimits } from './limits.js';
|
||||
import { resolveParserLimits } from './limits.js';
|
||||
|
||||
@@ -16,7 +19,7 @@ export interface ParseOneNoteSectionOptions {
|
||||
limits?: Partial<OneNoteParserLimits>;
|
||||
}
|
||||
|
||||
/** Parse a desktop revision-store `.one` section without network access. */
|
||||
/** Parse a supported desktop or FSSHTTPB `.one` section without network access. */
|
||||
export function parseOneNoteSection(
|
||||
input: ArrayBuffer | Uint8Array,
|
||||
options: ParseOneNoteSectionOptions = {}
|
||||
@@ -24,7 +27,7 @@ export function parseOneNoteSection(
|
||||
const bytes = toBytes(input);
|
||||
const limits = resolveParserLimits(options.limits);
|
||||
const diagnostics: OneNoteSectionDto['diagnostics'] = [];
|
||||
const store = parseDesktopOneStore(bytes, limits, diagnostics);
|
||||
const store = parseSectionStore(bytes, limits, diagnostics);
|
||||
return buildSectionDto(
|
||||
store,
|
||||
options.sourceName ?? 'Untitled.one',
|
||||
@@ -41,7 +44,7 @@ export function parseOneNoteSectionContent(
|
||||
const bytes = toBytes(input);
|
||||
const limits = resolveParserLimits(options.limits);
|
||||
const diagnostics: OneNoteSectionContentModel['diagnostics'] = [];
|
||||
const store = parseDesktopOneStore(bytes, limits, diagnostics);
|
||||
const store = parseSectionStore(bytes, limits, diagnostics);
|
||||
return buildSectionContentModel(
|
||||
store,
|
||||
options.sourceName ?? 'Untitled.one',
|
||||
@@ -49,3 +52,22 @@ export function parseOneNoteSectionContent(
|
||||
diagnostics
|
||||
);
|
||||
}
|
||||
|
||||
function parseSectionStore(
|
||||
bytes: Uint8Array,
|
||||
limits: OneNoteParserLimits,
|
||||
diagnostics: OneNoteSectionDto['diagnostics']
|
||||
): DesktopOneStore {
|
||||
if (detectOneNoteFormat(bytes).kind !== 'fsshttp-one') {
|
||||
return parseDesktopOneStore(bytes, limits, diagnostics);
|
||||
}
|
||||
const store = parseFssHttpOneStore(bytes, limits, diagnostics);
|
||||
if (store.kind !== 'section') {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_FORMAT',
|
||||
'Expected an FSSHTTPB OneNote section, found a table of contents',
|
||||
{ structure: 'OneStore Packaging Structure' }
|
||||
);
|
||||
}
|
||||
return store;
|
||||
}
|
||||
|
||||
@@ -187,6 +187,16 @@ function objectReferences(object: StoreObject, propertyId: number): ExGuid[] {
|
||||
propertyId,
|
||||
countObjectReferences
|
||||
);
|
||||
if (object.resolvedObjectIds) {
|
||||
if (offset + count > object.resolvedObjectIds.length) {
|
||||
throw new OneNoteParserError(
|
||||
'MISSING_REFERENCE',
|
||||
'TOC object-reference range exceeds its resolved OID stream',
|
||||
{ structure: 'TOCEntryIndex_OidIndex' }
|
||||
);
|
||||
}
|
||||
return object.resolvedObjectIds.slice(offset, offset + count);
|
||||
}
|
||||
if (offset + count > object.props.objectIds.length) {
|
||||
throw new OneNoteParserError(
|
||||
'MISSING_REFERENCE',
|
||||
|
||||
Reference in New Issue
Block a user