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 }
|
||||
);
|
||||
}
|
||||
5
src/onenote/fsshttpb/index.ts
Normal file
5
src/onenote/fsshttpb/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { parseFssHttpOneStorePackage } from './package.js';
|
||||
export type {
|
||||
FssHttpDataElementPackage,
|
||||
FssHttpOneStorePackage,
|
||||
} from './types.js';
|
||||
111
src/onenote/fsshttpb/package.test.ts
Normal file
111
src/onenote/fsshttpb/package.test.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { ONE_SECTION_FILE_TYPE } from '../parser/detect-format.js';
|
||||
import { resolveParserLimits } from '../parser/limits.js';
|
||||
import {
|
||||
FSSHTTP_SECTION_SCHEMA,
|
||||
parseFssHttpOneStorePackage,
|
||||
} from './package.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 packaging', () => {
|
||||
it('retains and parses the pinned Joplin Quick Notes fixture', () => {
|
||||
const bytes = fixtureBytes();
|
||||
expect(bytes).toHaveLength(17_197);
|
||||
expect(createHash('sha256').update(bytes).digest('hex')).toBe(
|
||||
'5e501ef5f539cd2f149786f57659be9d0762913d863a69867deb9c8e8c43efe2'
|
||||
);
|
||||
|
||||
const parsed = parseFssHttpOneStorePackage(bytes, resolveParserLimits());
|
||||
expect(parsed).toMatchObject({
|
||||
fileType: ONE_SECTION_FILE_TYPE,
|
||||
cellSchema: FSSHTTP_SECTION_SCHEMA,
|
||||
});
|
||||
expect({
|
||||
indexes: parsed.dataElements.storageIndexes.size,
|
||||
manifests: parsed.dataElements.storageManifests.size,
|
||||
cells: parsed.dataElements.cellManifests.size,
|
||||
revisions: parsed.dataElements.revisionManifests.size,
|
||||
groups: parsed.dataElements.objectGroups.size,
|
||||
fragments: parsed.dataElements.fragments.size,
|
||||
blobs: parsed.dataElements.blobs.size,
|
||||
}).toEqual({
|
||||
indexes: 1,
|
||||
manifests: 1,
|
||||
cells: 4,
|
||||
revisions: 9,
|
||||
groups: 9,
|
||||
fragments: 0,
|
||||
blobs: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects malformed reserved bytes with an absolute offset', () => {
|
||||
const bytes = fixtureBytes();
|
||||
bytes[64] = 1;
|
||||
expect(() =>
|
||||
parseFssHttpOneStorePackage(bytes, resolveParserLimits())
|
||||
).toThrowError(
|
||||
expect.objectContaining({
|
||||
code: 'INVALID_STRUCTURE',
|
||||
offset: 64,
|
||||
structure: 'OneStore Packaging Structure',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a truncated package as a bounded parse failure', () => {
|
||||
const bytes = fixtureBytes().subarray(0, 1_000);
|
||||
expect(() =>
|
||||
parseFssHttpOneStorePackage(bytes, resolveParserLimits())
|
||||
).toThrowError(
|
||||
expect.objectContaining({
|
||||
code: 'BOUNDS_EXCEEDED',
|
||||
offset: expect.any(Number),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('enforces source and data-element count limits', () => {
|
||||
const bytes = fixtureBytes();
|
||||
expect(() =>
|
||||
parseFssHttpOneStorePackage(
|
||||
bytes,
|
||||
resolveParserLimits({ maxFileBytes: bytes.byteLength - 1 })
|
||||
)
|
||||
).toThrowError(
|
||||
expect.objectContaining({ code: 'LIMIT_EXCEEDED', offset: 0 })
|
||||
);
|
||||
expect(() =>
|
||||
parseFssHttpOneStorePackage(
|
||||
bytes,
|
||||
resolveParserLimits({ maxStreamIds: 1 })
|
||||
)
|
||||
).toThrowError(expect.objectContaining({ code: 'LIMIT_EXCEEDED' }));
|
||||
});
|
||||
|
||||
it('allows only zero bytes after the packaging end marker', () => {
|
||||
const bytes = fixtureBytes();
|
||||
bytes[bytes.length - 1] = 1;
|
||||
expect(() =>
|
||||
parseFssHttpOneStorePackage(bytes, resolveParserLimits())
|
||||
).toThrowError(
|
||||
expect.objectContaining({
|
||||
code: 'INVALID_STRUCTURE',
|
||||
offset: bytes.length - 1,
|
||||
structure: 'OneStore Packaging Structure padding',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
128
src/onenote/fsshttpb/package.ts
Normal file
128
src/onenote/fsshttpb/package.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
// 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/packaging.rs`.
|
||||
// Deviations: direct immutable browser bytes, exact envelope consumption,
|
||||
// explicit bounds, and offset-aware failures.
|
||||
|
||||
import { BinaryReader } from '../binary/BinaryReader.js';
|
||||
import { readGuid } from '../binary/guid.js';
|
||||
import {
|
||||
FSSHTTP_PACKAGING_FORMAT,
|
||||
ONE_SECTION_FILE_TYPE,
|
||||
ONE_TOC_FILE_TYPE,
|
||||
} from '../parser/detect-format.js';
|
||||
import { OneNoteParserError } from '../parser/error.js';
|
||||
import type { OneNoteParserLimits } from '../parser/limits.js';
|
||||
import { parseFssHttpDataElementPackage } from './data-elements.js';
|
||||
import {
|
||||
expectFssHttpEnd,
|
||||
expectFssHttpStart,
|
||||
finishFssHttpBody,
|
||||
readFssHttpExGuid,
|
||||
takeFssHttpHeaderBody,
|
||||
} from './primitives.js';
|
||||
import { FSSHTTP_OBJECT, type FssHttpOneStorePackage } from './types.js';
|
||||
|
||||
export const FSSHTTP_SECTION_SCHEMA = '{1F937CB4-B26F-445F-B9F8-17E20160E461}';
|
||||
export const FSSHTTP_TOC_SCHEMA = '{E4DBFD38-E5C7-408B-A8A1-0E7B421E1F5F}';
|
||||
|
||||
/** Parse the MS-ONESTORE 2.8.1 package envelope and its data elements. */
|
||||
export function parseFssHttpOneStorePackage(
|
||||
bytes: Uint8Array,
|
||||
limits: OneNoteParserLimits
|
||||
): FssHttpOneStorePackage {
|
||||
if (bytes.byteLength > limits.maxFileBytes) {
|
||||
throw new OneNoteParserError(
|
||||
'LIMIT_EXCEEDED',
|
||||
`FSSHTTP-packaged OneNote source is ${bytes.byteLength} bytes (limit ${limits.maxFileBytes})`,
|
||||
{ offset: 0, structure: 'OneStore Packaging Structure' }
|
||||
);
|
||||
}
|
||||
const reader = new BinaryReader(
|
||||
bytes,
|
||||
0,
|
||||
bytes.byteLength,
|
||||
'OneStore Packaging Structure'
|
||||
);
|
||||
const fileType = readGuid(reader);
|
||||
const fileIdentity = readGuid(reader);
|
||||
const legacyFileVersion = readGuid(reader);
|
||||
const fileFormat = readGuid(reader);
|
||||
if (fileType !== ONE_SECTION_FILE_TYPE && fileType !== ONE_TOC_FILE_TYPE) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_FORMAT',
|
||||
`Unsupported package-store file type ${fileType}`,
|
||||
{ offset: 0, structure: 'OneStore Packaging Structure' }
|
||||
);
|
||||
}
|
||||
if (fileFormat !== FSSHTTP_PACKAGING_FORMAT) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_FORMAT',
|
||||
`Expected package-store format ${FSSHTTP_PACKAGING_FORMAT}, found ${fileFormat}`,
|
||||
{ offset: 48, structure: 'OneStore Packaging Structure' }
|
||||
);
|
||||
}
|
||||
if (reader.u32() !== 0) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
'Package-store reserved bytes are not zero',
|
||||
{ offset: 64, structure: 'OneStore Packaging Structure' }
|
||||
);
|
||||
}
|
||||
|
||||
const packagingHeader = expectFssHttpStart(
|
||||
reader,
|
||||
FSSHTTP_OBJECT.oneNotePackaging,
|
||||
true,
|
||||
4
|
||||
);
|
||||
const packagingBody = takeFssHttpHeaderBody(
|
||||
reader,
|
||||
packagingHeader,
|
||||
'OneNote Packaging start'
|
||||
);
|
||||
const storageIndexId = readFssHttpExGuid(packagingBody);
|
||||
const cellSchema = readGuid(packagingBody);
|
||||
finishFssHttpBody(packagingBody, 'OneNote Packaging start');
|
||||
if (
|
||||
(fileType === ONE_SECTION_FILE_TYPE &&
|
||||
cellSchema !== FSSHTTP_SECTION_SCHEMA) ||
|
||||
(fileType === ONE_TOC_FILE_TYPE && cellSchema !== FSSHTTP_TOC_SCHEMA)
|
||||
) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`Cell schema ${cellSchema} does not match package file type ${fileType}`,
|
||||
{ offset: packagingHeader.bodyOffset, structure: 'guidCellSchemaId' }
|
||||
);
|
||||
}
|
||||
|
||||
const dataElements = parseFssHttpDataElementPackage(reader, limits);
|
||||
expectFssHttpEnd(reader, FSSHTTP_OBJECT.oneNotePackaging);
|
||||
// OneDrive exports observed in the pinned corpus pad the package to the
|
||||
// physical file size. The packaging end is authoritative, but non-zero
|
||||
// trailing data is never accepted as padding.
|
||||
while (reader.remaining > 0) {
|
||||
const offset = reader.offset;
|
||||
if (reader.u8() !== 0) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
'OneStore Packaging Structure has non-zero trailing data',
|
||||
{ offset, structure: 'OneStore Packaging Structure padding' }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
fileType,
|
||||
fileIdentity,
|
||||
legacyFileVersion,
|
||||
fileFormat,
|
||||
storageIndexId,
|
||||
cellSchema,
|
||||
dataElements,
|
||||
};
|
||||
}
|
||||
47
src/onenote/fsshttpb/primitives.test.ts
Normal file
47
src/onenote/fsshttpb/primitives.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { BinaryReader } from '../binary/BinaryReader.js';
|
||||
import {
|
||||
readCompactU64,
|
||||
readFssHttpExGuid,
|
||||
readFssHttpStreamHeader,
|
||||
} from './primitives.js';
|
||||
|
||||
describe('FSSHTTPB primitives', () => {
|
||||
it.each([
|
||||
[[0x00], 0n],
|
||||
[[(0x5a << 1) | 1], 0x5an],
|
||||
[[0xd2, 0x48], 0x1234n],
|
||||
[
|
||||
[0x80, 0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01],
|
||||
0x0123_4567_89ab_cdefn,
|
||||
],
|
||||
] as const)('decodes compact unsigned integer %j', (input, expected) => {
|
||||
const reader = new BinaryReader(Uint8Array.from(input));
|
||||
expect(readCompactU64(reader)).toBe(expected);
|
||||
expect(reader.remaining).toBe(0);
|
||||
});
|
||||
|
||||
it('rejects a truncated extended GUID at the original absolute offset', () => {
|
||||
const reader = new BinaryReader(
|
||||
Uint8Array.from([0, 0, 0x80, 1, 2]),
|
||||
2,
|
||||
3,
|
||||
'test ExGuid'
|
||||
);
|
||||
expect(() => readFssHttpExGuid(reader)).toThrowError(
|
||||
expect.objectContaining({ code: 'BOUNDS_EXCEEDED', offset: 3 })
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects stream-object end markers where a start is required', () => {
|
||||
const reader = new BinaryReader(Uint8Array.from([0x05]));
|
||||
expect(() => readFssHttpStreamHeader(reader)).toThrowError(
|
||||
expect.objectContaining({
|
||||
code: 'INVALID_STRUCTURE',
|
||||
offset: 0,
|
||||
structure: 'FSSHTTPB stream object header',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
279
src/onenote/fsshttpb/primitives.ts
Normal file
279
src/onenote/fsshttpb/primitives.ts
Normal file
@@ -0,0 +1,279 @@
|
||||
// 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/
|
||||
// {cell_id,compact_u64,exguid,serial_number,stream_object}.rs`. Deviations:
|
||||
// all ranges and integer conversions are checked with offset-aware errors.
|
||||
|
||||
import { BinaryReader } from '../binary/BinaryReader.js';
|
||||
import { NIL_GUID, readGuid } from '../binary/guid.js';
|
||||
import type { ExGuid } from '../onestore/types.js';
|
||||
import { OneNoteParserError } from '../parser/error.js';
|
||||
import type { OneNoteParserLimits } from '../parser/limits.js';
|
||||
import type {
|
||||
FssHttpCellId,
|
||||
FssHttpSerialNumber,
|
||||
FssHttpStreamHeader,
|
||||
} from './types.js';
|
||||
|
||||
export function readCompactU64(reader: BinaryReader): bigint {
|
||||
const offset = reader.offset;
|
||||
const first = reader.u8();
|
||||
if (first === 0) return 0n;
|
||||
if ((first & 0x01) !== 0) return BigInt(first >>> 1);
|
||||
|
||||
let width: number;
|
||||
if ((first & 0x02) !== 0) width = 2;
|
||||
else if ((first & 0x04) !== 0) width = 3;
|
||||
else if ((first & 0x08) !== 0) width = 4;
|
||||
else if ((first & 0x10) !== 0) width = 5;
|
||||
else if ((first & 0x20) !== 0) width = 6;
|
||||
else if ((first & 0x40) !== 0) width = 7;
|
||||
else if ((first & 0x80) !== 0) return reader.u64();
|
||||
else {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`Invalid compact unsigned integer marker 0x${first.toString(16)}`,
|
||||
{ offset, structure: 'Compact unsigned 64-bit integer' }
|
||||
);
|
||||
}
|
||||
|
||||
const rest = reader.read(width - 1);
|
||||
let encoded = BigInt(first);
|
||||
for (let index = 0; index < rest.length; index += 1) {
|
||||
encoded |= BigInt(rest[index]!) << BigInt((index + 1) * 8);
|
||||
}
|
||||
return encoded >> BigInt(width);
|
||||
}
|
||||
|
||||
export function readCompactNumber(
|
||||
reader: BinaryReader,
|
||||
limit: number,
|
||||
structure: string
|
||||
): number {
|
||||
const offset = reader.offset;
|
||||
const value = readCompactU64(reader);
|
||||
if (value > BigInt(Number.MAX_SAFE_INTEGER) || value > BigInt(limit)) {
|
||||
throw new OneNoteParserError(
|
||||
'LIMIT_EXCEEDED',
|
||||
`${structure} value ${value.toString()} exceeds limit ${limit}`,
|
||||
{ offset, structure }
|
||||
);
|
||||
}
|
||||
return Number(value);
|
||||
}
|
||||
|
||||
export function readFssHttpExGuid(reader: BinaryReader): ExGuid {
|
||||
const offset = reader.offset;
|
||||
const first = reader.u8();
|
||||
if (first === 0) return { guid: NIL_GUID, value: 0 };
|
||||
|
||||
let value: number;
|
||||
if ((first & 0x07) === 0x04) {
|
||||
value = first >>> 3;
|
||||
} else if ((first & 0x3f) === 0x20) {
|
||||
value = (reader.u8() << 2) | (first >>> 6);
|
||||
} else if ((first & 0x7f) === 0x40) {
|
||||
value = (reader.u16() << 1) | (first >>> 7);
|
||||
} else if (first === 0x80) {
|
||||
value = reader.u32();
|
||||
} else {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`Invalid extended GUID marker 0x${first.toString(16)}`,
|
||||
{ offset, structure: 'Extended GUID' }
|
||||
);
|
||||
}
|
||||
return { guid: readGuid(reader), value };
|
||||
}
|
||||
|
||||
export function readFssHttpExGuidArray(
|
||||
reader: BinaryReader,
|
||||
limits: OneNoteParserLimits
|
||||
): ExGuid[] {
|
||||
const count = readCompactNumber(
|
||||
reader,
|
||||
limits.maxObjectReferences,
|
||||
'Extended GUID array count'
|
||||
);
|
||||
const result: ExGuid[] = [];
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
result.push(readFssHttpExGuid(reader));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function readFssHttpCellId(reader: BinaryReader): FssHttpCellId {
|
||||
return {
|
||||
context: readFssHttpExGuid(reader),
|
||||
objectSpace: readFssHttpExGuid(reader),
|
||||
};
|
||||
}
|
||||
|
||||
export function readFssHttpCellIdArray(
|
||||
reader: BinaryReader,
|
||||
limits: OneNoteParserLimits
|
||||
): FssHttpCellId[] {
|
||||
const count = readCompactNumber(
|
||||
reader,
|
||||
limits.maxObjectReferences,
|
||||
'Cell ID array count'
|
||||
);
|
||||
const result: FssHttpCellId[] = [];
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
result.push(readFssHttpCellId(reader));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function readFssHttpSerialNumber(
|
||||
reader: BinaryReader
|
||||
): FssHttpSerialNumber {
|
||||
const kind = reader.u8();
|
||||
if (kind === 0) return { guid: NIL_GUID, value: 0n };
|
||||
return { guid: readGuid(reader), value: reader.u64() };
|
||||
}
|
||||
|
||||
export function readFssHttpStreamHeader(
|
||||
reader: BinaryReader,
|
||||
requiredWidth?: 2 | 4
|
||||
): FssHttpStreamHeader {
|
||||
const offset = reader.offset;
|
||||
const marker = reader.bytes[offset]! & 0x03;
|
||||
let width: 2 | 4;
|
||||
let raw: number;
|
||||
if (marker === 0) {
|
||||
width = 2;
|
||||
raw = reader.u16();
|
||||
} else if (marker === 2) {
|
||||
width = 4;
|
||||
raw = reader.u32();
|
||||
} else {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`Unexpected stream object start marker ${marker}`,
|
||||
{ offset, structure: 'FSSHTTPB stream object header' }
|
||||
);
|
||||
}
|
||||
if (requiredWidth !== undefined && width !== requiredWidth) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`Expected a ${requiredWidth * 8}-bit stream object header`,
|
||||
{ offset, structure: 'FSSHTTPB stream object header' }
|
||||
);
|
||||
}
|
||||
|
||||
const compound = (raw & 0x04) !== 0;
|
||||
const type = width === 2 ? (raw >>> 3) & 0x3f : (raw >>> 3) & 0x3fff;
|
||||
let lengthValue = BigInt(width === 2 ? raw >>> 9 : raw >>> 17);
|
||||
if (width === 4 && lengthValue === 0x7fffn) {
|
||||
lengthValue = readCompactU64(reader);
|
||||
}
|
||||
if (lengthValue > BigInt(Number.MAX_SAFE_INTEGER)) {
|
||||
throw new OneNoteParserError(
|
||||
'LIMIT_EXCEEDED',
|
||||
`Stream object length ${lengthValue.toString()} is not a safe integer`,
|
||||
{ offset, structure: 'FSSHTTPB stream object header' }
|
||||
);
|
||||
}
|
||||
const length = Number(lengthValue);
|
||||
const bodyOffset = reader.offset;
|
||||
const bodyEnd = bodyOffset + length;
|
||||
if (!Number.isSafeInteger(bodyEnd) || bodyEnd > reader.end) {
|
||||
throw new OneNoteParserError(
|
||||
'BOUNDS_EXCEEDED',
|
||||
`Stream object body (${bodyOffset}, ${length}) exceeds its containing data`,
|
||||
{ offset, structure: 'FSSHTTPB stream object header' }
|
||||
);
|
||||
}
|
||||
return { offset, width, compound, type, length, bodyOffset, bodyEnd };
|
||||
}
|
||||
|
||||
export function expectFssHttpStart(
|
||||
reader: BinaryReader,
|
||||
type: number,
|
||||
compound: boolean,
|
||||
requiredWidth?: 2 | 4
|
||||
): FssHttpStreamHeader {
|
||||
const header = readFssHttpStreamHeader(reader, requiredWidth);
|
||||
if (header.type !== type || header.compound !== compound) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`Expected stream object 0x${type.toString(16)} (${compound ? 'compound' : 'single'}), found 0x${header.type.toString(16)} (${header.compound ? 'compound' : 'single'})`,
|
||||
{ offset: header.offset, structure: 'FSSHTTPB stream object header' }
|
||||
);
|
||||
}
|
||||
return header;
|
||||
}
|
||||
|
||||
export function finishFssHttpHeaderBody(
|
||||
reader: BinaryReader,
|
||||
header: FssHttpStreamHeader,
|
||||
structure: string
|
||||
): void {
|
||||
if (reader.offset !== header.bodyEnd) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`${structure} consumed ${reader.offset - header.bodyOffset} bytes; its header declares ${header.length}`,
|
||||
{ offset: header.offset, structure }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function takeFssHttpHeaderBody(
|
||||
reader: BinaryReader,
|
||||
header: FssHttpStreamHeader,
|
||||
structure: string
|
||||
): BinaryReader {
|
||||
if (reader.offset !== header.bodyOffset) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`${structure} body is not read immediately after its header`,
|
||||
{ offset: header.offset, structure }
|
||||
);
|
||||
}
|
||||
return reader.subview(header.length, structure);
|
||||
}
|
||||
|
||||
export function finishFssHttpBody(body: BinaryReader, structure: string): void {
|
||||
if (body.remaining !== 0) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`${structure} has ${body.remaining} unread declared bytes`,
|
||||
{ offset: body.offset, structure }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function hasFssHttpEnd(reader: BinaryReader, type: number): boolean {
|
||||
if (type <= 0x3f) {
|
||||
if (reader.remaining < 1) return false;
|
||||
const raw = reader.bytes[reader.offset]!;
|
||||
return (raw & 0x03) === 1 && raw >>> 2 === type;
|
||||
}
|
||||
if (reader.remaining < 2) return false;
|
||||
const raw = new DataView(
|
||||
reader.bytes.buffer,
|
||||
reader.bytes.byteOffset + reader.offset,
|
||||
2
|
||||
).getUint16(0, true);
|
||||
return (raw & 0x03) === 3 && raw >>> 2 === type;
|
||||
}
|
||||
|
||||
export function expectFssHttpEnd(reader: BinaryReader, type: number): void {
|
||||
const offset = reader.offset;
|
||||
const actual = type <= 0x3f ? reader.u8() : reader.u16();
|
||||
const marker = actual & 0x03;
|
||||
const actualType = actual >>> 2;
|
||||
const expectedMarker = type <= 0x3f ? 1 : 3;
|
||||
if (marker !== expectedMarker || actualType !== type) {
|
||||
throw new OneNoteParserError(
|
||||
'INVALID_STRUCTURE',
|
||||
`Expected stream object end 0x${type.toString(16)}, found marker ${marker} type 0x${actualType.toString(16)}`,
|
||||
{ offset, structure: 'FSSHTTPB stream object end' }
|
||||
);
|
||||
}
|
||||
}
|
||||
173
src/onenote/fsshttpb/types.ts
Normal file
173
src/onenote/fsshttpb/types.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
// 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,
|
||||
// data_element,packaging}.rs`. Deviations: serializable TypeScript records,
|
||||
// zero-copy byte ranges, checked integer conversion, and explicit limits.
|
||||
|
||||
import type { ExGuid, FileDataBlobReference } from '../onestore/types.js';
|
||||
|
||||
export const FSSHTTP_OBJECT = {
|
||||
dataElement: 0x01,
|
||||
objectDataBlob: 0x02,
|
||||
objectGroupDataExcluded: 0x03,
|
||||
objectGroupDataBlob: 0x05,
|
||||
storageManifestRoot: 0x07,
|
||||
revisionManifestRoot: 0x0a,
|
||||
cellManifest: 0x0b,
|
||||
storageManifest: 0x0c,
|
||||
storageIndexRevisionMapping: 0x0d,
|
||||
storageIndexCellMapping: 0x0e,
|
||||
storageIndexManifestMapping: 0x11,
|
||||
dataElementPackage: 0x15,
|
||||
objectGroupDataObject: 0x16,
|
||||
objectGroupObject: 0x18,
|
||||
revisionManifestGroupReference: 0x19,
|
||||
revisionManifest: 0x1a,
|
||||
objectGroupBlobReference: 0x1c,
|
||||
objectGroupDeclaration: 0x1d,
|
||||
objectGroupData: 0x1e,
|
||||
dataElementFragment: 0x6a,
|
||||
objectGroupMetadata: 0x78,
|
||||
objectGroupMetadataBlock: 0x79,
|
||||
oneNotePackaging: 0x7a,
|
||||
} as const;
|
||||
|
||||
export interface FssHttpStreamHeader {
|
||||
offset: number;
|
||||
width: 2 | 4;
|
||||
compound: boolean;
|
||||
type: number;
|
||||
length: number;
|
||||
bodyOffset: number;
|
||||
bodyEnd: number;
|
||||
}
|
||||
|
||||
export interface FssHttpSerialNumber {
|
||||
guid: string;
|
||||
value: bigint;
|
||||
}
|
||||
|
||||
export interface FssHttpCellId {
|
||||
context: ExGuid;
|
||||
objectSpace: ExGuid;
|
||||
}
|
||||
|
||||
export interface FssHttpStorageIndexManifestMapping {
|
||||
id: ExGuid;
|
||||
serial: FssHttpSerialNumber;
|
||||
}
|
||||
|
||||
export interface FssHttpStorageIndexCellMapping {
|
||||
cellId: FssHttpCellId;
|
||||
id: ExGuid;
|
||||
serial: FssHttpSerialNumber;
|
||||
}
|
||||
|
||||
export interface FssHttpStorageIndexRevisionMapping {
|
||||
id: ExGuid;
|
||||
revision: ExGuid;
|
||||
serial: FssHttpSerialNumber;
|
||||
}
|
||||
|
||||
export interface FssHttpStorageIndex {
|
||||
manifestMappings: FssHttpStorageIndexManifestMapping[];
|
||||
cellMappings: Map<string, FssHttpStorageIndexCellMapping>;
|
||||
revisionMappings: Map<string, FssHttpStorageIndexRevisionMapping>;
|
||||
}
|
||||
|
||||
export interface FssHttpStorageManifest {
|
||||
schema: string;
|
||||
roots: Map<string, FssHttpCellId>;
|
||||
}
|
||||
|
||||
export interface FssHttpRevisionRoot {
|
||||
role: ExGuid;
|
||||
objectId: ExGuid;
|
||||
}
|
||||
|
||||
export interface FssHttpRevisionManifest {
|
||||
revisionId: ExGuid;
|
||||
baseRevisionId: ExGuid;
|
||||
roots: FssHttpRevisionRoot[];
|
||||
objectGroupIds: ExGuid[];
|
||||
}
|
||||
|
||||
export type FssHttpObjectDeclaration =
|
||||
| {
|
||||
kind: 'object';
|
||||
objectId: ExGuid;
|
||||
partitionId: number;
|
||||
dataSize: number;
|
||||
objectReferenceCount: number;
|
||||
cellReferenceCount: number;
|
||||
}
|
||||
| {
|
||||
kind: 'blob';
|
||||
objectId: ExGuid;
|
||||
blobId: ExGuid;
|
||||
partitionId: number;
|
||||
objectReferenceCount: number;
|
||||
cellReferenceCount: number;
|
||||
};
|
||||
|
||||
export type FssHttpObjectGroupData =
|
||||
| {
|
||||
kind: 'object';
|
||||
objectReferences: ExGuid[];
|
||||
cellReferences: FssHttpCellId[];
|
||||
data: Uint8Array;
|
||||
}
|
||||
| {
|
||||
kind: 'excluded';
|
||||
objectReferences: ExGuid[];
|
||||
cellReferences: FssHttpCellId[];
|
||||
dataSize: number;
|
||||
}
|
||||
| {
|
||||
kind: 'blob-reference';
|
||||
objectReferences: ExGuid[];
|
||||
cellReferences: FssHttpCellId[];
|
||||
blobId: ExGuid;
|
||||
};
|
||||
|
||||
export interface FssHttpObjectGroup {
|
||||
declarations: FssHttpObjectDeclaration[];
|
||||
changeFrequencies: number[];
|
||||
data: FssHttpObjectGroupData[];
|
||||
}
|
||||
|
||||
export interface FssHttpDataElementFragment {
|
||||
dataElementId: ExGuid;
|
||||
fragmentSize: number;
|
||||
chunkOffset: number;
|
||||
chunkLength: number;
|
||||
data: Uint8Array;
|
||||
}
|
||||
|
||||
export interface FssHttpDataElementPackage {
|
||||
storageIndexes: Map<string, FssHttpStorageIndex>;
|
||||
storageManifests: Map<string, FssHttpStorageManifest>;
|
||||
cellManifests: Map<string, ExGuid>;
|
||||
revisionManifests: Map<string, FssHttpRevisionManifest>;
|
||||
objectGroups: Map<string, FssHttpObjectGroup>;
|
||||
fragments: Map<string, FssHttpDataElementFragment>;
|
||||
blobs: Map<string, FileDataBlobReference>;
|
||||
}
|
||||
|
||||
export interface FssHttpOneStorePackage {
|
||||
fileType: string;
|
||||
fileIdentity: string;
|
||||
legacyFileVersion: string;
|
||||
fileFormat: string;
|
||||
storageIndexId: ExGuid;
|
||||
cellSchema: string;
|
||||
dataElements: FssHttpDataElementPackage;
|
||||
}
|
||||
|
||||
export function fssHttpCellKey(cell: FssHttpCellId): string {
|
||||
return `${cell.context.guid}:${cell.context.value}/${cell.objectSpace.guid}:${cell.objectSpace.value}`;
|
||||
}
|
||||
Reference in New Issue
Block a user