fix: bound aggregate FSSHTTP references
This commit is contained in:
72
src/onenote/fsshttpb/data-elements.test.ts
Normal file
72
src/onenote/fsshttpb/data-elements.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { BinaryReader } from '../binary/BinaryReader.js';
|
||||
import { resolveParserLimits } from '../parser/limits.js';
|
||||
import { parseFssHttpDataElementPackage } from './data-elements.js';
|
||||
import { FSSHTTP_OBJECT } from './types.js';
|
||||
|
||||
function start(type: number, compound: boolean, body: number[]): number[] {
|
||||
const raw = (body.length << 9) | (type << 3) | (compound ? 0x04 : 0x00);
|
||||
return [raw & 0xff, raw >>> 8, ...body];
|
||||
}
|
||||
|
||||
function end(type: number): number[] {
|
||||
return [(type << 2) | 0x01];
|
||||
}
|
||||
|
||||
function objectGroupDataEntry(): number[] {
|
||||
return start(FSSHTTP_OBJECT.objectGroupDataExcluded, false, [
|
||||
0x03, // One object reference.
|
||||
0x00, // Nil ExGUID.
|
||||
0x03, // One cell reference.
|
||||
0x00, // Nil context ExGUID.
|
||||
0x00, // Nil object-space ExGUID.
|
||||
0x00, // Excluded object data size.
|
||||
]);
|
||||
}
|
||||
|
||||
function packageWithReferenceArrays(entryCount: number): Uint8Array {
|
||||
const dataEntries = Array.from({ length: entryCount }, () =>
|
||||
objectGroupDataEntry()
|
||||
).flat();
|
||||
return Uint8Array.from([
|
||||
...start(FSSHTTP_OBJECT.dataElementPackage, true, [0x00]),
|
||||
...start(FSSHTTP_OBJECT.dataElement, true, [
|
||||
0x00, // Nil data-element ExGUID.
|
||||
0x00, // Nil serial number.
|
||||
0x0b, // Object Group data-element type (5).
|
||||
]),
|
||||
...start(FSSHTTP_OBJECT.objectGroupDeclaration, true, []),
|
||||
...end(FSSHTTP_OBJECT.objectGroupDeclaration),
|
||||
...start(FSSHTTP_OBJECT.objectGroupData, true, []),
|
||||
...dataEntries,
|
||||
...end(FSSHTTP_OBJECT.objectGroupData),
|
||||
...end(FSSHTTP_OBJECT.dataElement),
|
||||
...end(FSSHTTP_OBJECT.dataElementPackage),
|
||||
]);
|
||||
}
|
||||
|
||||
describe('FSSHTTPB data elements', () => {
|
||||
it('enforces one aggregate budget across many reference arrays', () => {
|
||||
const bytes = packageWithReferenceArrays(16);
|
||||
|
||||
expect(() =>
|
||||
parseFssHttpDataElementPackage(
|
||||
// Every array contains one reference, but the package contains 32.
|
||||
new BinaryReader(bytes),
|
||||
resolveParserLimits({ maxObjectReferences: 31 })
|
||||
)
|
||||
).toThrowError(
|
||||
expect.objectContaining({
|
||||
code: 'LIMIT_EXCEEDED',
|
||||
structure: 'FSSHTTPB object and cell references',
|
||||
})
|
||||
);
|
||||
|
||||
const parsed = parseFssHttpDataElementPackage(
|
||||
new BinaryReader(bytes),
|
||||
resolveParserLimits({ maxObjectReferences: 32 })
|
||||
);
|
||||
expect(parsed.objectGroups.values().next().value?.data).toHaveLength(16);
|
||||
});
|
||||
});
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
readFssHttpSerialNumber,
|
||||
readFssHttpStreamHeader,
|
||||
takeFssHttpHeaderBody,
|
||||
type FssHttpReferenceBudget,
|
||||
} from './primitives.js';
|
||||
import {
|
||||
FSSHTTP_OBJECT,
|
||||
@@ -48,6 +49,7 @@ interface DataElementContext {
|
||||
objectCount: number;
|
||||
blobCount: number;
|
||||
totalBlobBytes: number;
|
||||
referenceBudget: FssHttpReferenceBudget;
|
||||
}
|
||||
|
||||
export function parseFssHttpDataElementPackage(
|
||||
@@ -89,6 +91,7 @@ export function parseFssHttpDataElementPackage(
|
||||
objectCount: 0,
|
||||
blobCount: 0,
|
||||
totalBlobBytes: 0,
|
||||
referenceBudget: { limit: limits.maxObjectReferences, used: 0 },
|
||||
};
|
||||
|
||||
while (!hasFssHttpEnd(reader, FSSHTTP_OBJECT.dataElementPackage)) {
|
||||
@@ -533,8 +536,16 @@ function parseObjectGroupData(
|
||||
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);
|
||||
const objectReferences = readFssHttpExGuidArray(
|
||||
body,
|
||||
context.limits,
|
||||
context.referenceBudget
|
||||
);
|
||||
const cellReferences = readFssHttpCellIdArray(
|
||||
body,
|
||||
context.limits,
|
||||
context.referenceBudget
|
||||
);
|
||||
let result: FssHttpObjectGroupData;
|
||||
if (header.type === FSSHTTP_OBJECT.objectGroupDataExcluded) {
|
||||
result = {
|
||||
|
||||
@@ -19,6 +19,11 @@ import type {
|
||||
FssHttpStreamHeader,
|
||||
} from './types.js';
|
||||
|
||||
export interface FssHttpReferenceBudget {
|
||||
readonly limit: number;
|
||||
used: number;
|
||||
}
|
||||
|
||||
export function readCompactU64(reader: BinaryReader): bigint {
|
||||
const offset = reader.offset;
|
||||
const first = reader.u8();
|
||||
@@ -92,13 +97,16 @@ export function readFssHttpExGuid(reader: BinaryReader): ExGuid {
|
||||
|
||||
export function readFssHttpExGuidArray(
|
||||
reader: BinaryReader,
|
||||
limits: OneNoteParserLimits
|
||||
limits: OneNoteParserLimits,
|
||||
budget: FssHttpReferenceBudget
|
||||
): ExGuid[] {
|
||||
const countOffset = reader.offset;
|
||||
const count = readCompactNumber(
|
||||
reader,
|
||||
limits.maxObjectReferences,
|
||||
'Extended GUID array count'
|
||||
);
|
||||
reserveReferenceBudget(budget, count, countOffset);
|
||||
const result: ExGuid[] = [];
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
result.push(readFssHttpExGuid(reader));
|
||||
@@ -115,13 +123,16 @@ export function readFssHttpCellId(reader: BinaryReader): FssHttpCellId {
|
||||
|
||||
export function readFssHttpCellIdArray(
|
||||
reader: BinaryReader,
|
||||
limits: OneNoteParserLimits
|
||||
limits: OneNoteParserLimits,
|
||||
budget: FssHttpReferenceBudget
|
||||
): FssHttpCellId[] {
|
||||
const countOffset = reader.offset;
|
||||
const count = readCompactNumber(
|
||||
reader,
|
||||
limits.maxObjectReferences,
|
||||
'Cell ID array count'
|
||||
);
|
||||
reserveReferenceBudget(budget, count, countOffset);
|
||||
const result: FssHttpCellId[] = [];
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
result.push(readFssHttpCellId(reader));
|
||||
@@ -129,6 +140,21 @@ export function readFssHttpCellIdArray(
|
||||
return result;
|
||||
}
|
||||
|
||||
function reserveReferenceBudget(
|
||||
budget: FssHttpReferenceBudget,
|
||||
count: number,
|
||||
offset: number
|
||||
): void {
|
||||
if (count > budget.limit - budget.used) {
|
||||
throw new OneNoteParserError(
|
||||
'LIMIT_EXCEEDED',
|
||||
`FSSHTTPB object and cell reference count exceeds ${budget.limit}`,
|
||||
{ offset, structure: 'FSSHTTPB object and cell references' }
|
||||
);
|
||||
}
|
||||
budget.used += count;
|
||||
}
|
||||
|
||||
export function readFssHttpSerialNumber(
|
||||
reader: BinaryReader
|
||||
): FssHttpSerialNumber {
|
||||
|
||||
Reference in New Issue
Block a user