// 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' } ); } }