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,
|
readFssHttpSerialNumber,
|
||||||
readFssHttpStreamHeader,
|
readFssHttpStreamHeader,
|
||||||
takeFssHttpHeaderBody,
|
takeFssHttpHeaderBody,
|
||||||
|
type FssHttpReferenceBudget,
|
||||||
} from './primitives.js';
|
} from './primitives.js';
|
||||||
import {
|
import {
|
||||||
FSSHTTP_OBJECT,
|
FSSHTTP_OBJECT,
|
||||||
@@ -48,6 +49,7 @@ interface DataElementContext {
|
|||||||
objectCount: number;
|
objectCount: number;
|
||||||
blobCount: number;
|
blobCount: number;
|
||||||
totalBlobBytes: number;
|
totalBlobBytes: number;
|
||||||
|
referenceBudget: FssHttpReferenceBudget;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseFssHttpDataElementPackage(
|
export function parseFssHttpDataElementPackage(
|
||||||
@@ -89,6 +91,7 @@ export function parseFssHttpDataElementPackage(
|
|||||||
objectCount: 0,
|
objectCount: 0,
|
||||||
blobCount: 0,
|
blobCount: 0,
|
||||||
totalBlobBytes: 0,
|
totalBlobBytes: 0,
|
||||||
|
referenceBudget: { limit: limits.maxObjectReferences, used: 0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
while (!hasFssHttpEnd(reader, FSSHTTP_OBJECT.dataElementPackage)) {
|
while (!hasFssHttpEnd(reader, FSSHTTP_OBJECT.dataElementPackage)) {
|
||||||
@@ -533,8 +536,16 @@ function parseObjectGroupData(
|
|||||||
const header = readFssHttpStreamHeader(reader);
|
const header = readFssHttpStreamHeader(reader);
|
||||||
if (header.compound) unexpectedCompound(header, 'Object Group data entry');
|
if (header.compound) unexpectedCompound(header, 'Object Group data entry');
|
||||||
const body = takeFssHttpHeaderBody(reader, header, 'Object Group data entry');
|
const body = takeFssHttpHeaderBody(reader, header, 'Object Group data entry');
|
||||||
const objectReferences = readFssHttpExGuidArray(body, context.limits);
|
const objectReferences = readFssHttpExGuidArray(
|
||||||
const cellReferences = readFssHttpCellIdArray(body, context.limits);
|
body,
|
||||||
|
context.limits,
|
||||||
|
context.referenceBudget
|
||||||
|
);
|
||||||
|
const cellReferences = readFssHttpCellIdArray(
|
||||||
|
body,
|
||||||
|
context.limits,
|
||||||
|
context.referenceBudget
|
||||||
|
);
|
||||||
let result: FssHttpObjectGroupData;
|
let result: FssHttpObjectGroupData;
|
||||||
if (header.type === FSSHTTP_OBJECT.objectGroupDataExcluded) {
|
if (header.type === FSSHTTP_OBJECT.objectGroupDataExcluded) {
|
||||||
result = {
|
result = {
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ import type {
|
|||||||
FssHttpStreamHeader,
|
FssHttpStreamHeader,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
|
export interface FssHttpReferenceBudget {
|
||||||
|
readonly limit: number;
|
||||||
|
used: number;
|
||||||
|
}
|
||||||
|
|
||||||
export function readCompactU64(reader: BinaryReader): bigint {
|
export function readCompactU64(reader: BinaryReader): bigint {
|
||||||
const offset = reader.offset;
|
const offset = reader.offset;
|
||||||
const first = reader.u8();
|
const first = reader.u8();
|
||||||
@@ -92,13 +97,16 @@ export function readFssHttpExGuid(reader: BinaryReader): ExGuid {
|
|||||||
|
|
||||||
export function readFssHttpExGuidArray(
|
export function readFssHttpExGuidArray(
|
||||||
reader: BinaryReader,
|
reader: BinaryReader,
|
||||||
limits: OneNoteParserLimits
|
limits: OneNoteParserLimits,
|
||||||
|
budget: FssHttpReferenceBudget
|
||||||
): ExGuid[] {
|
): ExGuid[] {
|
||||||
|
const countOffset = reader.offset;
|
||||||
const count = readCompactNumber(
|
const count = readCompactNumber(
|
||||||
reader,
|
reader,
|
||||||
limits.maxObjectReferences,
|
limits.maxObjectReferences,
|
||||||
'Extended GUID array count'
|
'Extended GUID array count'
|
||||||
);
|
);
|
||||||
|
reserveReferenceBudget(budget, count, countOffset);
|
||||||
const result: ExGuid[] = [];
|
const result: ExGuid[] = [];
|
||||||
for (let index = 0; index < count; index += 1) {
|
for (let index = 0; index < count; index += 1) {
|
||||||
result.push(readFssHttpExGuid(reader));
|
result.push(readFssHttpExGuid(reader));
|
||||||
@@ -115,13 +123,16 @@ export function readFssHttpCellId(reader: BinaryReader): FssHttpCellId {
|
|||||||
|
|
||||||
export function readFssHttpCellIdArray(
|
export function readFssHttpCellIdArray(
|
||||||
reader: BinaryReader,
|
reader: BinaryReader,
|
||||||
limits: OneNoteParserLimits
|
limits: OneNoteParserLimits,
|
||||||
|
budget: FssHttpReferenceBudget
|
||||||
): FssHttpCellId[] {
|
): FssHttpCellId[] {
|
||||||
|
const countOffset = reader.offset;
|
||||||
const count = readCompactNumber(
|
const count = readCompactNumber(
|
||||||
reader,
|
reader,
|
||||||
limits.maxObjectReferences,
|
limits.maxObjectReferences,
|
||||||
'Cell ID array count'
|
'Cell ID array count'
|
||||||
);
|
);
|
||||||
|
reserveReferenceBudget(budget, count, countOffset);
|
||||||
const result: FssHttpCellId[] = [];
|
const result: FssHttpCellId[] = [];
|
||||||
for (let index = 0; index < count; index += 1) {
|
for (let index = 0; index < count; index += 1) {
|
||||||
result.push(readFssHttpCellId(reader));
|
result.push(readFssHttpCellId(reader));
|
||||||
@@ -129,6 +140,21 @@ export function readFssHttpCellIdArray(
|
|||||||
return result;
|
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(
|
export function readFssHttpSerialNumber(
|
||||||
reader: BinaryReader
|
reader: BinaryReader
|
||||||
): FssHttpSerialNumber {
|
): FssHttpSerialNumber {
|
||||||
|
|||||||
Reference in New Issue
Block a user