feat: parse bounded FSSHTTPB packages

This commit is contained in:
2026-07-22 19:15:51 +02:00
parent c99615f5e6
commit 30b0357530
10 changed files with 1795 additions and 0 deletions

View File

@@ -0,0 +1,279 @@
// 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 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
): ExGuid[] {
const count = readCompactNumber(
reader,
limits.maxObjectReferences,
'Extended GUID array count'
);
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
): FssHttpCellId[] {
const count = readCompactNumber(
reader,
limits.maxObjectReferences,
'Cell ID array count'
);
const result: FssHttpCellId[] = [];
for (let index = 0; index < count; index += 1) {
result.push(readFssHttpCellId(reader));
}
return result;
}
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' }
);
}
}