feat: parse bounded FSSHTTPB packages
This commit is contained in:
723
src/onenote/fsshttpb/data-elements.ts
Normal file
723
src/onenote/fsshttpb/data-elements.ts
Normal file
@@ -0,0 +1,723 @@
|
||||
// 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: `fsshttpb/data_element/*`.
|
||||
// Deviations: immutable zero-copy payload views, duplicate rejection, exact
|
||||
// header-length validation, explicit count/byte limits, and offset-aware errors.
|
||||
|
||||
import { BinaryReader } from '../binary/BinaryReader.js';
|
||||
import { readGuid } from '../binary/guid.js';
|
||||
import type { ExGuid } from '../onestore/types.js';
|
||||
import { exGuidKey } from '../onestore/types.js';
|
||||
import { OneNoteParserError } from '../parser/error.js';
|
||||
import type { OneNoteParserLimits } from '../parser/limits.js';
|
||||
import {
|
||||
expectFssHttpEnd,
|
||||
expectFssHttpStart,
|
||||
finishFssHttpBody,
|
||||
hasFssHttpEnd,
|
||||
readCompactNumber,
|
||||
readFssHttpCellId,
|
||||
readFssHttpCellIdArray,
|
||||
readFssHttpExGuid,
|
||||
readFssHttpExGuidArray,
|
||||
readFssHttpSerialNumber,
|
||||
readFssHttpStreamHeader,
|
||||
takeFssHttpHeaderBody,
|
||||
} from './primitives.js';
|
||||
import {
|
||||
FSSHTTP_OBJECT,
|
||||
fssHttpCellKey,
|
||||
type FssHttpDataElementFragment,
|
||||
type FssHttpDataElementPackage,
|
||||
type FssHttpObjectDeclaration,
|
||||
type FssHttpObjectGroup,
|
||||
type FssHttpObjectGroupData,
|
||||
type FssHttpRevisionManifest,
|
||||
type FssHttpStorageIndex,
|
||||
type FssHttpStorageManifest,
|
||||
type FssHttpStreamHeader,
|
||||
} from './types.js';
|
||||
|
||||
interface DataElementContext {
|
||||
limits: OneNoteParserLimits;
|
||||
elementCount: number;
|
||||
objectCount: number;
|
||||
blobCount: number;
|
||||
totalBlobBytes: number;
|
||||
}
|
||||
|
||||
export function parseFssHttpDataElementPackage(
|
||||
reader: BinaryReader,
|
||||
limits: OneNoteParserLimits
|
||||
): FssHttpDataElementPackage {
|
||||
const header = expectFssHttpStart(
|
||||
reader,
|
||||
FSSHTTP_OBJECT.dataElementPackage,
|
||||
true,
|
||||
2
|
||||
);
|
||||
const body = takeFssHttpHeaderBody(
|
||||
reader,
|
||||
header,
|
||||
'Data Element Package start'
|
||||
);
|
||||
if (body.u8() !== 0) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
'Data Element Package reserved byte is not zero',
|
||||
{ offset: body.offset - 1, structure: 'Data Element Package' }
|
||||
);
|
||||
}
|
||||
finishFssHttpBody(body, 'Data Element Package start');
|
||||
|
||||
const result: FssHttpDataElementPackage = {
|
||||
storageIndexes: new Map(),
|
||||
storageManifests: new Map(),
|
||||
cellManifests: new Map(),
|
||||
revisionManifests: new Map(),
|
||||
objectGroups: new Map(),
|
||||
fragments: new Map(),
|
||||
blobs: new Map(),
|
||||
};
|
||||
const context: DataElementContext = {
|
||||
limits,
|
||||
elementCount: 0,
|
||||
objectCount: 0,
|
||||
blobCount: 0,
|
||||
totalBlobBytes: 0,
|
||||
};
|
||||
|
||||
while (!hasFssHttpEnd(reader, FSSHTTP_OBJECT.dataElementPackage)) {
|
||||
context.elementCount += 1;
|
||||
if (context.elementCount > limits.maxStreamIds) {
|
||||
throw new OneNoteParserError(
|
||||
'LIMIT_EXCEEDED',
|
||||
`Data element count exceeds ${limits.maxStreamIds}`,
|
||||
{ offset: reader.offset, structure: 'Data Element Package' }
|
||||
);
|
||||
}
|
||||
parseDataElement(reader, result, context);
|
||||
}
|
||||
expectFssHttpEnd(reader, FSSHTTP_OBJECT.dataElementPackage);
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseDataElement(
|
||||
reader: BinaryReader,
|
||||
result: FssHttpDataElementPackage,
|
||||
context: DataElementContext
|
||||
): void {
|
||||
const header = expectFssHttpStart(
|
||||
reader,
|
||||
FSSHTTP_OBJECT.dataElement,
|
||||
true,
|
||||
2
|
||||
);
|
||||
const body = takeFssHttpHeaderBody(reader, header, 'Data Element start');
|
||||
const id = readFssHttpExGuid(body);
|
||||
readFssHttpSerialNumber(body);
|
||||
const type = readCompactNumber(body, 0xff, 'Data element type');
|
||||
finishFssHttpBody(body, 'Data Element start');
|
||||
|
||||
switch (type) {
|
||||
case 0x01:
|
||||
insertUnique(
|
||||
result.storageIndexes,
|
||||
id,
|
||||
parseStorageIndex(reader, context),
|
||||
header.offset,
|
||||
'Storage Index'
|
||||
);
|
||||
break;
|
||||
case 0x02:
|
||||
insertUnique(
|
||||
result.storageManifests,
|
||||
id,
|
||||
parseStorageManifest(reader, context),
|
||||
header.offset,
|
||||
'Storage Manifest'
|
||||
);
|
||||
break;
|
||||
case 0x03:
|
||||
insertUnique(
|
||||
result.cellManifests,
|
||||
id,
|
||||
parseCellManifest(reader),
|
||||
header.offset,
|
||||
'Cell Manifest'
|
||||
);
|
||||
break;
|
||||
case 0x04:
|
||||
insertUnique(
|
||||
result.revisionManifests,
|
||||
id,
|
||||
parseRevisionManifest(reader, context),
|
||||
header.offset,
|
||||
'Revision Manifest'
|
||||
);
|
||||
break;
|
||||
case 0x05:
|
||||
insertUnique(
|
||||
result.objectGroups,
|
||||
id,
|
||||
parseObjectGroup(reader, context),
|
||||
header.offset,
|
||||
'Object Group'
|
||||
);
|
||||
break;
|
||||
case 0x06:
|
||||
insertUnique(
|
||||
result.fragments,
|
||||
id,
|
||||
parseDataElementFragment(reader, context),
|
||||
header.offset,
|
||||
'Data Element Fragment'
|
||||
);
|
||||
break;
|
||||
case 0x0a:
|
||||
insertUnique(
|
||||
result.blobs,
|
||||
id,
|
||||
parseObjectDataBlob(reader, context),
|
||||
header.offset,
|
||||
'Object Data BLOB'
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new OneNoteParserError(
|
||||
'UNSUPPORTED_FORMAT',
|
||||
`Unsupported FSSHTTPB data element type 0x${type.toString(16)}`,
|
||||
{ offset: header.offset, structure: 'Data Element' }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function parseStorageIndex(
|
||||
reader: BinaryReader,
|
||||
context: DataElementContext
|
||||
): FssHttpStorageIndex {
|
||||
const manifestMappings: FssHttpStorageIndex['manifestMappings'] = [];
|
||||
const cellMappings: FssHttpStorageIndex['cellMappings'] = new Map();
|
||||
const revisionMappings: FssHttpStorageIndex['revisionMappings'] = new Map();
|
||||
let count = 0;
|
||||
|
||||
while (!hasFssHttpEnd(reader, FSSHTTP_OBJECT.dataElement)) {
|
||||
count = boundedIncrement(
|
||||
count,
|
||||
context.limits.maxStreamIds,
|
||||
reader,
|
||||
'Storage Index mappings'
|
||||
);
|
||||
const header = readFssHttpStreamHeader(reader, 2);
|
||||
if (header.compound) unexpectedCompound(header, 'Storage Index mapping');
|
||||
const body = takeFssHttpHeaderBody(reader, header, 'Storage Index mapping');
|
||||
if (header.type === FSSHTTP_OBJECT.storageIndexManifestMapping) {
|
||||
manifestMappings.push({
|
||||
id: readFssHttpExGuid(body),
|
||||
serial: readFssHttpSerialNumber(body),
|
||||
});
|
||||
} else if (header.type === FSSHTTP_OBJECT.storageIndexCellMapping) {
|
||||
const cellId = readFssHttpCellId(body);
|
||||
const mapping = {
|
||||
cellId,
|
||||
id: readFssHttpExGuid(body),
|
||||
serial: readFssHttpSerialNumber(body),
|
||||
};
|
||||
insertUniqueKey(
|
||||
cellMappings,
|
||||
fssHttpCellKey(cellId),
|
||||
mapping,
|
||||
header.offset,
|
||||
'Storage Index Cell Mapping'
|
||||
);
|
||||
} else if (header.type === FSSHTTP_OBJECT.storageIndexRevisionMapping) {
|
||||
const id = readFssHttpExGuid(body);
|
||||
const mapping = {
|
||||
id,
|
||||
revision: readFssHttpExGuid(body),
|
||||
serial: readFssHttpSerialNumber(body),
|
||||
};
|
||||
insertUnique(
|
||||
revisionMappings,
|
||||
id,
|
||||
mapping,
|
||||
header.offset,
|
||||
'Storage Index Revision Mapping'
|
||||
);
|
||||
} else {
|
||||
unexpectedType(header, 'Storage Index');
|
||||
}
|
||||
finishFssHttpBody(body, 'Storage Index mapping');
|
||||
}
|
||||
expectFssHttpEnd(reader, FSSHTTP_OBJECT.dataElement);
|
||||
return { manifestMappings, cellMappings, revisionMappings };
|
||||
}
|
||||
|
||||
function parseStorageManifest(
|
||||
reader: BinaryReader,
|
||||
context: DataElementContext
|
||||
): FssHttpStorageManifest {
|
||||
const schemaHeader = expectFssHttpStart(
|
||||
reader,
|
||||
FSSHTTP_OBJECT.storageManifest,
|
||||
false,
|
||||
2
|
||||
);
|
||||
const schemaBody = takeFssHttpHeaderBody(
|
||||
reader,
|
||||
schemaHeader,
|
||||
'Storage Manifest schema'
|
||||
);
|
||||
const schema = readGuid(schemaBody);
|
||||
finishFssHttpBody(schemaBody, 'Storage Manifest schema');
|
||||
|
||||
const roots = new Map<string, ReturnType<typeof readFssHttpCellId>>();
|
||||
let count = 0;
|
||||
while (!hasFssHttpEnd(reader, FSSHTTP_OBJECT.dataElement)) {
|
||||
count = boundedIncrement(
|
||||
count,
|
||||
context.limits.maxStreamIds,
|
||||
reader,
|
||||
'Storage Manifest roots'
|
||||
);
|
||||
const header = expectFssHttpStart(
|
||||
reader,
|
||||
FSSHTTP_OBJECT.storageManifestRoot,
|
||||
false,
|
||||
2
|
||||
);
|
||||
const body = takeFssHttpHeaderBody(reader, header, 'Storage Manifest root');
|
||||
const rootId = readFssHttpExGuid(body);
|
||||
const cell = readFssHttpCellId(body);
|
||||
finishFssHttpBody(body, 'Storage Manifest root');
|
||||
insertUniqueKey(
|
||||
roots,
|
||||
exGuidKey(rootId),
|
||||
cell,
|
||||
header.offset,
|
||||
'Storage Manifest root'
|
||||
);
|
||||
}
|
||||
expectFssHttpEnd(reader, FSSHTTP_OBJECT.dataElement);
|
||||
return { schema, roots };
|
||||
}
|
||||
|
||||
function parseCellManifest(reader: BinaryReader): ExGuid {
|
||||
const header = expectFssHttpStart(
|
||||
reader,
|
||||
FSSHTTP_OBJECT.cellManifest,
|
||||
false,
|
||||
2
|
||||
);
|
||||
const body = takeFssHttpHeaderBody(reader, header, 'Cell Manifest');
|
||||
const revisionId = readFssHttpExGuid(body);
|
||||
finishFssHttpBody(body, 'Cell Manifest');
|
||||
expectFssHttpEnd(reader, FSSHTTP_OBJECT.dataElement);
|
||||
return revisionId;
|
||||
}
|
||||
|
||||
function parseRevisionManifest(
|
||||
reader: BinaryReader,
|
||||
context: DataElementContext
|
||||
): FssHttpRevisionManifest {
|
||||
const manifestHeader = expectFssHttpStart(
|
||||
reader,
|
||||
FSSHTTP_OBJECT.revisionManifest,
|
||||
false,
|
||||
2
|
||||
);
|
||||
const manifestBody = takeFssHttpHeaderBody(
|
||||
reader,
|
||||
manifestHeader,
|
||||
'Revision Manifest'
|
||||
);
|
||||
const revisionId = readFssHttpExGuid(manifestBody);
|
||||
const baseRevisionId = readFssHttpExGuid(manifestBody);
|
||||
finishFssHttpBody(manifestBody, 'Revision Manifest');
|
||||
|
||||
const roots: FssHttpRevisionManifest['roots'] = [];
|
||||
const objectGroupIds: ExGuid[] = [];
|
||||
let count = 0;
|
||||
while (!hasFssHttpEnd(reader, FSSHTTP_OBJECT.dataElement)) {
|
||||
count = boundedIncrement(
|
||||
count,
|
||||
context.limits.maxStreamIds,
|
||||
reader,
|
||||
'Revision Manifest entries'
|
||||
);
|
||||
const header = readFssHttpStreamHeader(reader, 2);
|
||||
if (header.compound) unexpectedCompound(header, 'Revision Manifest entry');
|
||||
const body = takeFssHttpHeaderBody(
|
||||
reader,
|
||||
header,
|
||||
'Revision Manifest entry'
|
||||
);
|
||||
if (header.type === FSSHTTP_OBJECT.revisionManifestRoot) {
|
||||
roots.push({
|
||||
role: readFssHttpExGuid(body),
|
||||
objectId: readFssHttpExGuid(body),
|
||||
});
|
||||
} else if (header.type === FSSHTTP_OBJECT.revisionManifestGroupReference) {
|
||||
objectGroupIds.push(readFssHttpExGuid(body));
|
||||
} else {
|
||||
unexpectedType(header, 'Revision Manifest');
|
||||
}
|
||||
finishFssHttpBody(body, 'Revision Manifest entry');
|
||||
}
|
||||
expectFssHttpEnd(reader, FSSHTTP_OBJECT.dataElement);
|
||||
return { revisionId, baseRevisionId, roots, objectGroupIds };
|
||||
}
|
||||
|
||||
function parseObjectGroup(
|
||||
reader: BinaryReader,
|
||||
context: DataElementContext
|
||||
): FssHttpObjectGroup {
|
||||
const declarationsHeader = expectFssHttpStart(
|
||||
reader,
|
||||
FSSHTTP_OBJECT.objectGroupDeclaration,
|
||||
true
|
||||
);
|
||||
const declarationsBody = takeFssHttpHeaderBody(
|
||||
reader,
|
||||
declarationsHeader,
|
||||
'Object Group Declarations start'
|
||||
);
|
||||
finishFssHttpBody(declarationsBody, 'Object Group Declarations start');
|
||||
|
||||
const declarations: FssHttpObjectDeclaration[] = [];
|
||||
while (!hasFssHttpEnd(reader, FSSHTTP_OBJECT.objectGroupDeclaration)) {
|
||||
context.objectCount += 1;
|
||||
if (context.objectCount > context.limits.maxObjects) {
|
||||
throw new OneNoteParserError(
|
||||
'LIMIT_EXCEEDED',
|
||||
`FSSHTTPB object declaration count exceeds ${context.limits.maxObjects}`,
|
||||
{ offset: reader.offset, structure: 'Object Group Declarations' }
|
||||
);
|
||||
}
|
||||
declarations.push(parseObjectDeclaration(reader, context));
|
||||
}
|
||||
expectFssHttpEnd(reader, FSSHTTP_OBJECT.objectGroupDeclaration);
|
||||
|
||||
const changeFrequencies: number[] = [];
|
||||
let dataHeader = readFssHttpStreamHeader(reader);
|
||||
if (dataHeader.type === FSSHTTP_OBJECT.objectGroupMetadataBlock) {
|
||||
if (!dataHeader.compound)
|
||||
unexpectedCompound(dataHeader, 'Object Group Metadata Block', true);
|
||||
const metadataBody = takeFssHttpHeaderBody(
|
||||
reader,
|
||||
dataHeader,
|
||||
'Object Group Metadata Block start'
|
||||
);
|
||||
finishFssHttpBody(metadataBody, 'Object Group Metadata Block start');
|
||||
while (!hasFssHttpEnd(reader, FSSHTTP_OBJECT.objectGroupMetadataBlock)) {
|
||||
if (changeFrequencies.length >= context.limits.maxObjects) {
|
||||
throw new OneNoteParserError(
|
||||
'LIMIT_EXCEEDED',
|
||||
`Object Group metadata count exceeds ${context.limits.maxObjects}`,
|
||||
{ offset: reader.offset, structure: 'Object Group Metadata Block' }
|
||||
);
|
||||
}
|
||||
const header = expectFssHttpStart(
|
||||
reader,
|
||||
FSSHTTP_OBJECT.objectGroupMetadata,
|
||||
false,
|
||||
4
|
||||
);
|
||||
const body = takeFssHttpHeaderBody(
|
||||
reader,
|
||||
header,
|
||||
'Object Group Metadata'
|
||||
);
|
||||
const frequency = readCompactNumber(body, 4, 'Object change frequency');
|
||||
changeFrequencies.push(frequency);
|
||||
finishFssHttpBody(body, 'Object Group Metadata');
|
||||
}
|
||||
expectFssHttpEnd(reader, FSSHTTP_OBJECT.objectGroupMetadataBlock);
|
||||
dataHeader = readFssHttpStreamHeader(reader);
|
||||
}
|
||||
if (
|
||||
dataHeader.type !== FSSHTTP_OBJECT.objectGroupData ||
|
||||
!dataHeader.compound
|
||||
) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`Expected Object Group Data, found type 0x${dataHeader.type.toString(16)}`,
|
||||
{ offset: dataHeader.offset, structure: 'Object Group' }
|
||||
);
|
||||
}
|
||||
const dataStartBody = takeFssHttpHeaderBody(
|
||||
reader,
|
||||
dataHeader,
|
||||
'Object Group Data start'
|
||||
);
|
||||
finishFssHttpBody(dataStartBody, 'Object Group Data start');
|
||||
|
||||
const data: FssHttpObjectGroupData[] = [];
|
||||
while (!hasFssHttpEnd(reader, FSSHTTP_OBJECT.objectGroupData)) {
|
||||
if (data.length >= context.limits.maxObjects) {
|
||||
throw new OneNoteParserError(
|
||||
'LIMIT_EXCEEDED',
|
||||
`Object Group data count exceeds ${context.limits.maxObjects}`,
|
||||
{ offset: reader.offset, structure: 'Object Group Data' }
|
||||
);
|
||||
}
|
||||
data.push(parseObjectGroupData(reader, context));
|
||||
}
|
||||
expectFssHttpEnd(reader, FSSHTTP_OBJECT.objectGroupData);
|
||||
expectFssHttpEnd(reader, FSSHTTP_OBJECT.dataElement);
|
||||
return { declarations, changeFrequencies, data };
|
||||
}
|
||||
|
||||
function parseObjectDeclaration(
|
||||
reader: BinaryReader,
|
||||
context: DataElementContext
|
||||
): FssHttpObjectDeclaration {
|
||||
const header = readFssHttpStreamHeader(reader);
|
||||
if (header.compound) unexpectedCompound(header, 'Object declaration');
|
||||
const body = takeFssHttpHeaderBody(reader, header, 'Object declaration');
|
||||
const objectId = readFssHttpExGuid(body);
|
||||
let result: FssHttpObjectDeclaration;
|
||||
if (header.type === FSSHTTP_OBJECT.objectGroupObject) {
|
||||
result = {
|
||||
kind: 'object',
|
||||
objectId,
|
||||
partitionId: readCompactNumber(body, 4, 'Object partition ID'),
|
||||
dataSize: readCompactNumber(
|
||||
body,
|
||||
context.limits.maxVectorBytes,
|
||||
'Object data size'
|
||||
),
|
||||
objectReferenceCount: readCompactNumber(
|
||||
body,
|
||||
context.limits.maxObjectReferences,
|
||||
'Object reference count'
|
||||
),
|
||||
cellReferenceCount: readCompactNumber(
|
||||
body,
|
||||
context.limits.maxObjectReferences,
|
||||
'Cell reference count'
|
||||
),
|
||||
};
|
||||
} else if (header.type === FSSHTTP_OBJECT.objectGroupDataBlob) {
|
||||
result = {
|
||||
kind: 'blob',
|
||||
objectId,
|
||||
blobId: readFssHttpExGuid(body),
|
||||
partitionId: readCompactNumber(body, 4, 'Object partition ID'),
|
||||
objectReferenceCount: readCompactNumber(
|
||||
body,
|
||||
context.limits.maxObjectReferences,
|
||||
'Object reference count'
|
||||
),
|
||||
cellReferenceCount: readCompactNumber(
|
||||
body,
|
||||
context.limits.maxObjectReferences,
|
||||
'Cell reference count'
|
||||
),
|
||||
};
|
||||
} else {
|
||||
unexpectedType(header, 'Object Group Declarations');
|
||||
}
|
||||
finishFssHttpBody(body, 'Object declaration');
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseObjectGroupData(
|
||||
reader: BinaryReader,
|
||||
context: DataElementContext
|
||||
): FssHttpObjectGroupData {
|
||||
const header = readFssHttpStreamHeader(reader);
|
||||
if (header.compound) unexpectedCompound(header, 'Object Group data entry');
|
||||
const body = takeFssHttpHeaderBody(reader, header, 'Object Group data entry');
|
||||
const objectReferences = readFssHttpExGuidArray(body, context.limits);
|
||||
const cellReferences = readFssHttpCellIdArray(body, context.limits);
|
||||
let result: FssHttpObjectGroupData;
|
||||
if (header.type === FSSHTTP_OBJECT.objectGroupDataExcluded) {
|
||||
result = {
|
||||
kind: 'excluded',
|
||||
objectReferences,
|
||||
cellReferences,
|
||||
dataSize: readCompactNumber(
|
||||
body,
|
||||
context.limits.maxVectorBytes,
|
||||
'Excluded object data size'
|
||||
),
|
||||
};
|
||||
} else if (header.type === FSSHTTP_OBJECT.objectGroupDataObject) {
|
||||
const size = readCompactNumber(
|
||||
body,
|
||||
context.limits.maxVectorBytes,
|
||||
'Object binary item size'
|
||||
);
|
||||
result = {
|
||||
kind: 'object',
|
||||
objectReferences,
|
||||
cellReferences,
|
||||
data: body.read(size),
|
||||
};
|
||||
} else if (header.type === FSSHTTP_OBJECT.objectGroupBlobReference) {
|
||||
result = {
|
||||
kind: 'blob-reference',
|
||||
objectReferences,
|
||||
cellReferences,
|
||||
blobId: readFssHttpExGuid(body),
|
||||
};
|
||||
} else {
|
||||
unexpectedType(header, 'Object Group Data');
|
||||
}
|
||||
finishFssHttpBody(body, 'Object Group data entry');
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseObjectDataBlob(
|
||||
reader: BinaryReader,
|
||||
context: DataElementContext
|
||||
) {
|
||||
const header = expectFssHttpStart(
|
||||
reader,
|
||||
FSSHTTP_OBJECT.objectDataBlob,
|
||||
false
|
||||
);
|
||||
const body = takeFssHttpHeaderBody(reader, header, 'Object Data BLOB');
|
||||
const size = readCompactNumber(
|
||||
body,
|
||||
context.limits.maxFileDataBytes,
|
||||
'Object Data BLOB size'
|
||||
);
|
||||
context.blobCount += 1;
|
||||
context.totalBlobBytes += size;
|
||||
if (context.blobCount > context.limits.maxFileDataObjects) {
|
||||
throw new OneNoteParserError(
|
||||
'LIMIT_EXCEEDED',
|
||||
`Object Data BLOB count exceeds ${context.limits.maxFileDataObjects}`,
|
||||
{ offset: header.offset, structure: 'Object Data BLOB' }
|
||||
);
|
||||
}
|
||||
if (context.totalBlobBytes > context.limits.maxTotalFileDataBytes) {
|
||||
throw new OneNoteParserError(
|
||||
'LIMIT_EXCEEDED',
|
||||
`Object Data BLOB bytes exceed ${context.limits.maxTotalFileDataBytes}`,
|
||||
{ offset: header.offset, structure: 'Object Data BLOB' }
|
||||
);
|
||||
}
|
||||
const offset = body.offset;
|
||||
body.skip(size);
|
||||
finishFssHttpBody(body, 'Object Data BLOB');
|
||||
expectFssHttpEnd(reader, FSSHTTP_OBJECT.dataElement);
|
||||
return { source: reader.bytes, offset, size };
|
||||
}
|
||||
|
||||
function parseDataElementFragment(
|
||||
reader: BinaryReader,
|
||||
context: DataElementContext
|
||||
): FssHttpDataElementFragment {
|
||||
const header = expectFssHttpStart(
|
||||
reader,
|
||||
FSSHTTP_OBJECT.dataElementFragment,
|
||||
false,
|
||||
4
|
||||
);
|
||||
const body = takeFssHttpHeaderBody(reader, header, 'Data Element Fragment');
|
||||
const dataElementId = readFssHttpExGuid(body);
|
||||
const fragmentSize = readCompactNumber(
|
||||
body,
|
||||
context.limits.maxFileBytes,
|
||||
'Fragmented data element size'
|
||||
);
|
||||
const chunkOffset = readCompactNumber(
|
||||
body,
|
||||
fragmentSize,
|
||||
'Fragment chunk offset'
|
||||
);
|
||||
const chunkLength = readCompactNumber(
|
||||
body,
|
||||
context.limits.maxVectorBytes,
|
||||
'Fragment chunk length'
|
||||
);
|
||||
if (chunkOffset + chunkLength > fragmentSize) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
'Fragment chunk range exceeds the fragmented data element size',
|
||||
{ offset: header.offset, structure: 'Data Element Fragment' }
|
||||
);
|
||||
}
|
||||
if (body.remaining !== chunkLength) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`Fragment header declares ${chunkLength} data bytes; ${body.remaining} remain`,
|
||||
{ offset: header.offset, structure: 'Data Element Fragment' }
|
||||
);
|
||||
}
|
||||
const data = body.read(chunkLength);
|
||||
finishFssHttpBody(body, 'Data Element Fragment');
|
||||
expectFssHttpEnd(reader, FSSHTTP_OBJECT.dataElement);
|
||||
return { dataElementId, fragmentSize, chunkOffset, chunkLength, data };
|
||||
}
|
||||
|
||||
function boundedIncrement(
|
||||
current: number,
|
||||
limit: number,
|
||||
reader: BinaryReader,
|
||||
structure: string
|
||||
): number {
|
||||
const next = current + 1;
|
||||
if (next > limit) {
|
||||
throw new OneNoteParserError(
|
||||
'LIMIT_EXCEEDED',
|
||||
`${structure} count exceeds ${limit}`,
|
||||
{ offset: reader.offset, structure }
|
||||
);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function insertUnique<T>(
|
||||
map: Map<string, T>,
|
||||
id: ExGuid,
|
||||
value: T,
|
||||
offset: number,
|
||||
structure: string
|
||||
): void {
|
||||
insertUniqueKey(map, exGuidKey(id), value, offset, structure);
|
||||
}
|
||||
|
||||
function insertUniqueKey<T>(
|
||||
map: Map<string, T>,
|
||||
key: string,
|
||||
value: T,
|
||||
offset: number,
|
||||
structure: string
|
||||
): void {
|
||||
if (map.has(key)) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`${structure} identifier ${key} is duplicated`,
|
||||
{ offset, structure }
|
||||
);
|
||||
}
|
||||
map.set(key, value);
|
||||
}
|
||||
|
||||
function unexpectedType(header: FssHttpStreamHeader, structure: string): never {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`Unexpected stream object type 0x${header.type.toString(16)}`,
|
||||
{ offset: header.offset, structure }
|
||||
);
|
||||
}
|
||||
|
||||
function unexpectedCompound(
|
||||
header: FssHttpStreamHeader,
|
||||
structure: string,
|
||||
expected = false
|
||||
): never {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`${structure} must be ${expected ? 'compound' : 'single'}`,
|
||||
{ offset: header.offset, structure }
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user