Files
onenote-tools/src/onenote/one/content-table-safety.test.ts

170 lines
4.5 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import type { ParserDiagnostic } from '../model/diagnostics.js';
import { DEFAULT_ONENOTE_PARSER_LIMITS } from '../parser/limits.js';
import type {
ExGuid,
ObjectPropSet,
ObjectSpace,
PropertyEntry,
StoreObject,
} from '../onestore/types.js';
import { exGuidKey } from '../onestore/types.js';
import { parseContentObjects } from './content.js';
import { JCID, PROPERTY } from './constants.js';
const GUID = '{55555555-5555-5555-5555-555555555555}';
describe('content table allocation safety', () => {
it('rejects a max-u32 declared column count before bitmap allocation', () => {
const tableId: ExGuid = { guid: GUID, value: 1 };
const table = object(
tableId,
JCID.TableNode,
props(
u32Entry(PROPERTY.RowCount, 1),
u32Entry(PROPERTY.ColumnCount, 0xffff_ffff),
bytesEntry(PROPERTY.TableColumnsLocked, Uint8Array.of(1, 1))
)
);
const diagnostics: ParserDiagnostic[] = [];
const space: ObjectSpace = {
id: { guid: GUID, value: 0 },
roots: new Map(),
objects: new Map([[exGuidKey(table.id), table]]),
};
expect(
parseContentObjects([tableId], {
space,
limits: { ...DEFAULT_ONENOTE_PARSER_LIMITS },
diagnostics,
resources: new Map(),
})
).toEqual([]);
expect(diagnostics).toContainEqual(
expect.objectContaining({
code: 'CONTENT_PARSE_FAILED',
message: expect.stringContaining(
`Declared table column count exceeds ${DEFAULT_ONENOTE_PARSER_LIMITS.maxTableCells}`
),
})
);
});
it('rejects declared table dimensions whose product exceeds the cell limit', () => {
const tableId: ExGuid = { guid: GUID, value: 2 };
const table = object(
tableId,
JCID.TableNode,
props(
u32Entry(PROPERTY.RowCount, 1_001),
u32Entry(PROPERTY.ColumnCount, 1_000)
)
);
const diagnostics: ParserDiagnostic[] = [];
expect(
parseContentObjects([tableId], {
space: {
id: { guid: GUID, value: 0 },
roots: new Map(),
objects: new Map([[exGuidKey(table.id), table]]),
},
limits: { ...DEFAULT_ONENOTE_PARSER_LIMITS },
diagnostics,
resources: new Map(),
})
).toEqual([]);
expect(diagnostics[0]?.message).toContain(
`Declared table cell count exceeds ${DEFAULT_ONENOTE_PARSER_LIMITS.maxTableCells}`
);
});
it('rejects actual row references before mapping an oversized array', () => {
const tableId: ExGuid = { guid: GUID, value: 3 };
const firstRowId: ExGuid = { guid: GUID, value: 4 };
const secondRowId: ExGuid = { guid: GUID, value: 5 };
const table = object(
tableId,
JCID.TableNode,
props(referenceEntry(PROPERTY.ElementChildNodes, 2))
);
table.resolvedObjectIds = [firstRowId, secondRowId];
const firstRow = object(firstRowId, JCID.TableRowNode, props());
const secondRow = object(secondRowId, JCID.TableRowNode, props());
const diagnostics: ParserDiagnostic[] = [];
expect(
parseContentObjects([tableId], {
space: {
id: { guid: GUID, value: 0 },
roots: new Map(),
objects: new Map(
[table, firstRow, secondRow].map((value) => [
exGuidKey(value.id),
value,
])
),
},
limits: { ...DEFAULT_ONENOTE_PARSER_LIMITS, maxTableCells: 1 },
diagnostics,
resources: new Map(),
})
).toEqual([]);
expect(diagnostics[0]?.message).toContain(
'Table row reference count exceeds 1'
);
});
});
function object(
id: ExGuid,
jcid: number,
objectProps: ObjectPropSet
): StoreObject {
return {
id,
jcid,
props: objectProps,
mapping: new Map(),
contextId: { guid: GUID, value: 0 },
};
}
function props(...entries: PropertyEntry[]): ObjectPropSet {
return {
objectIds: [],
objectSpaceIds: [],
contextIds: [],
properties: { entries },
};
}
function u32Entry(rawId: number, value: number): PropertyEntry {
return {
rawId,
id: rawId & 0x03ff_ffff,
type: 0x4,
value: { kind: 'u32', value },
};
}
function bytesEntry(rawId: number, value: Uint8Array): PropertyEntry {
return {
rawId,
id: rawId & 0x03ff_ffff,
type: 0x7,
value: { kind: 'bytes', value },
};
}
function referenceEntry(rawId: number, count: number): PropertyEntry {
return {
rawId,
id: rawId & 0x03ff_ffff,
type: 0x9,
value: { kind: 'objectIds', count },
};
}