306 lines
9.4 KiB
TypeScript
306 lines
9.4 KiB
TypeScript
// 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 interface FssHttpReferenceBudget {
|
|
readonly limit: number;
|
|
used: number;
|
|
}
|
|
|
|
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,
|
|
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));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function readFssHttpCellId(reader: BinaryReader): FssHttpCellId {
|
|
return {
|
|
context: readFssHttpExGuid(reader),
|
|
objectSpace: readFssHttpExGuid(reader),
|
|
};
|
|
}
|
|
|
|
export function readFssHttpCellIdArray(
|
|
reader: BinaryReader,
|
|
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));
|
|
}
|
|
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 {
|
|
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' }
|
|
);
|
|
}
|
|
}
|