diff --git a/src/onenote/fsshttpb/data-elements.test.ts b/src/onenote/fsshttpb/data-elements.test.ts new file mode 100644 index 0000000..f459249 --- /dev/null +++ b/src/onenote/fsshttpb/data-elements.test.ts @@ -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); + }); +}); diff --git a/src/onenote/fsshttpb/data-elements.ts b/src/onenote/fsshttpb/data-elements.ts index edcf40f..3cd053a 100644 --- a/src/onenote/fsshttpb/data-elements.ts +++ b/src/onenote/fsshttpb/data-elements.ts @@ -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 = { diff --git a/src/onenote/fsshttpb/primitives.ts b/src/onenote/fsshttpb/primitives.ts index d8b0ac2..c262204 100644 --- a/src/onenote/fsshttpb/primitives.ts +++ b/src/onenote/fsshttpb/primitives.ts @@ -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 {