From 30b0357530d1b76e7037e70ca7dbf0925b8138e2 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Wed, 22 Jul 2026 19:15:51 +0200 Subject: [PATCH] feat: parse bounded FSSHTTPB packages --- THIRD_PARTY_NOTICES.md | 6 + src/onenote/fsshttpb/data-elements.ts | 723 ++++++++++++++++++ src/onenote/fsshttpb/index.ts | 5 + src/onenote/fsshttpb/package.test.ts | 111 +++ src/onenote/fsshttpb/package.ts | 128 ++++ src/onenote/fsshttpb/primitives.test.ts | 47 ++ src/onenote/fsshttpb/primitives.ts | 279 +++++++ src/onenote/fsshttpb/types.ts | 173 +++++ tests/fixtures/README.md | 21 + .../joplin-quick-notes-fsshttp.one.b64 | 302 ++++++++ 10 files changed, 1795 insertions(+) create mode 100644 src/onenote/fsshttpb/data-elements.ts create mode 100644 src/onenote/fsshttpb/index.ts create mode 100644 src/onenote/fsshttpb/package.test.ts create mode 100644 src/onenote/fsshttpb/package.ts create mode 100644 src/onenote/fsshttpb/primitives.test.ts create mode 100644 src/onenote/fsshttpb/primitives.ts create mode 100644 src/onenote/fsshttpb/types.ts create mode 100644 tests/fixtures/joplin-quick-notes-fsshttp.one.b64 diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 7546b4f..2cfc741 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -34,6 +34,12 @@ The Base64-encoded Joplin `test.onepkg` fixture is pinned at commit `1e73aad7eb08fde5d9e4cb533df40052a5cd32d7` and is likewise test-only data distributed under AGPL-3.0-or-later. +The Base64-encoded Joplin `Simple notebook/Quick Notes.one` fixture is copied +from the pinned `onenote.rs` test corpus at revision +`5138a39a3f4e72b840932f9872fecde52fa9da60`. Its adjacent upstream notice +attributes the data to Joplin under AGPL-3.0-or-later; it is test-only and its +hash and provenance are recorded in `tests/fixtures/README.md`. + The encoded fixture bytes are excluded from `dist` and the release ZIP. The release retains the fixture provenance document and AGPL text so these notices remain complete. diff --git a/src/onenote/fsshttpb/data-elements.ts b/src/onenote/fsshttpb/data-elements.ts new file mode 100644 index 0000000..7f54071 --- /dev/null +++ b/src/onenote/fsshttpb/data-elements.ts @@ -0,0 +1,723 @@ +// 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_element/*`. +// Deviations: immutable zero-copy payload views, duplicate rejection, exact +// header-length validation, explicit count/byte limits, and offset-aware errors. + +import { BinaryReader } from '../binary/BinaryReader.js'; +import { readGuid } from '../binary/guid.js'; +import type { ExGuid } from '../onestore/types.js'; +import { exGuidKey } from '../onestore/types.js'; +import { OneNoteParserError } from '../parser/error.js'; +import type { OneNoteParserLimits } from '../parser/limits.js'; +import { + expectFssHttpEnd, + expectFssHttpStart, + finishFssHttpBody, + hasFssHttpEnd, + readCompactNumber, + readFssHttpCellId, + readFssHttpCellIdArray, + readFssHttpExGuid, + readFssHttpExGuidArray, + readFssHttpSerialNumber, + readFssHttpStreamHeader, + takeFssHttpHeaderBody, +} from './primitives.js'; +import { + FSSHTTP_OBJECT, + fssHttpCellKey, + type FssHttpDataElementFragment, + type FssHttpDataElementPackage, + type FssHttpObjectDeclaration, + type FssHttpObjectGroup, + type FssHttpObjectGroupData, + type FssHttpRevisionManifest, + type FssHttpStorageIndex, + type FssHttpStorageManifest, + type FssHttpStreamHeader, +} from './types.js'; + +interface DataElementContext { + limits: OneNoteParserLimits; + elementCount: number; + objectCount: number; + blobCount: number; + totalBlobBytes: number; +} + +export function parseFssHttpDataElementPackage( + reader: BinaryReader, + limits: OneNoteParserLimits +): FssHttpDataElementPackage { + const header = expectFssHttpStart( + reader, + FSSHTTP_OBJECT.dataElementPackage, + true, + 2 + ); + const body = takeFssHttpHeaderBody( + reader, + header, + 'Data Element Package start' + ); + if (body.u8() !== 0) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + 'Data Element Package reserved byte is not zero', + { offset: body.offset - 1, structure: 'Data Element Package' } + ); + } + finishFssHttpBody(body, 'Data Element Package start'); + + const result: FssHttpDataElementPackage = { + storageIndexes: new Map(), + storageManifests: new Map(), + cellManifests: new Map(), + revisionManifests: new Map(), + objectGroups: new Map(), + fragments: new Map(), + blobs: new Map(), + }; + const context: DataElementContext = { + limits, + elementCount: 0, + objectCount: 0, + blobCount: 0, + totalBlobBytes: 0, + }; + + while (!hasFssHttpEnd(reader, FSSHTTP_OBJECT.dataElementPackage)) { + context.elementCount += 1; + if (context.elementCount > limits.maxStreamIds) { + throw new OneNoteParserError( + 'LIMIT_EXCEEDED', + `Data element count exceeds ${limits.maxStreamIds}`, + { offset: reader.offset, structure: 'Data Element Package' } + ); + } + parseDataElement(reader, result, context); + } + expectFssHttpEnd(reader, FSSHTTP_OBJECT.dataElementPackage); + return result; +} + +function parseDataElement( + reader: BinaryReader, + result: FssHttpDataElementPackage, + context: DataElementContext +): void { + const header = expectFssHttpStart( + reader, + FSSHTTP_OBJECT.dataElement, + true, + 2 + ); + const body = takeFssHttpHeaderBody(reader, header, 'Data Element start'); + const id = readFssHttpExGuid(body); + readFssHttpSerialNumber(body); + const type = readCompactNumber(body, 0xff, 'Data element type'); + finishFssHttpBody(body, 'Data Element start'); + + switch (type) { + case 0x01: + insertUnique( + result.storageIndexes, + id, + parseStorageIndex(reader, context), + header.offset, + 'Storage Index' + ); + break; + case 0x02: + insertUnique( + result.storageManifests, + id, + parseStorageManifest(reader, context), + header.offset, + 'Storage Manifest' + ); + break; + case 0x03: + insertUnique( + result.cellManifests, + id, + parseCellManifest(reader), + header.offset, + 'Cell Manifest' + ); + break; + case 0x04: + insertUnique( + result.revisionManifests, + id, + parseRevisionManifest(reader, context), + header.offset, + 'Revision Manifest' + ); + break; + case 0x05: + insertUnique( + result.objectGroups, + id, + parseObjectGroup(reader, context), + header.offset, + 'Object Group' + ); + break; + case 0x06: + insertUnique( + result.fragments, + id, + parseDataElementFragment(reader, context), + header.offset, + 'Data Element Fragment' + ); + break; + case 0x0a: + insertUnique( + result.blobs, + id, + parseObjectDataBlob(reader, context), + header.offset, + 'Object Data BLOB' + ); + break; + default: + throw new OneNoteParserError( + 'UNSUPPORTED_FORMAT', + `Unsupported FSSHTTPB data element type 0x${type.toString(16)}`, + { offset: header.offset, structure: 'Data Element' } + ); + } +} + +function parseStorageIndex( + reader: BinaryReader, + context: DataElementContext +): FssHttpStorageIndex { + const manifestMappings: FssHttpStorageIndex['manifestMappings'] = []; + const cellMappings: FssHttpStorageIndex['cellMappings'] = new Map(); + const revisionMappings: FssHttpStorageIndex['revisionMappings'] = new Map(); + let count = 0; + + while (!hasFssHttpEnd(reader, FSSHTTP_OBJECT.dataElement)) { + count = boundedIncrement( + count, + context.limits.maxStreamIds, + reader, + 'Storage Index mappings' + ); + const header = readFssHttpStreamHeader(reader, 2); + if (header.compound) unexpectedCompound(header, 'Storage Index mapping'); + const body = takeFssHttpHeaderBody(reader, header, 'Storage Index mapping'); + if (header.type === FSSHTTP_OBJECT.storageIndexManifestMapping) { + manifestMappings.push({ + id: readFssHttpExGuid(body), + serial: readFssHttpSerialNumber(body), + }); + } else if (header.type === FSSHTTP_OBJECT.storageIndexCellMapping) { + const cellId = readFssHttpCellId(body); + const mapping = { + cellId, + id: readFssHttpExGuid(body), + serial: readFssHttpSerialNumber(body), + }; + insertUniqueKey( + cellMappings, + fssHttpCellKey(cellId), + mapping, + header.offset, + 'Storage Index Cell Mapping' + ); + } else if (header.type === FSSHTTP_OBJECT.storageIndexRevisionMapping) { + const id = readFssHttpExGuid(body); + const mapping = { + id, + revision: readFssHttpExGuid(body), + serial: readFssHttpSerialNumber(body), + }; + insertUnique( + revisionMappings, + id, + mapping, + header.offset, + 'Storage Index Revision Mapping' + ); + } else { + unexpectedType(header, 'Storage Index'); + } + finishFssHttpBody(body, 'Storage Index mapping'); + } + expectFssHttpEnd(reader, FSSHTTP_OBJECT.dataElement); + return { manifestMappings, cellMappings, revisionMappings }; +} + +function parseStorageManifest( + reader: BinaryReader, + context: DataElementContext +): FssHttpStorageManifest { + const schemaHeader = expectFssHttpStart( + reader, + FSSHTTP_OBJECT.storageManifest, + false, + 2 + ); + const schemaBody = takeFssHttpHeaderBody( + reader, + schemaHeader, + 'Storage Manifest schema' + ); + const schema = readGuid(schemaBody); + finishFssHttpBody(schemaBody, 'Storage Manifest schema'); + + const roots = new Map>(); + let count = 0; + while (!hasFssHttpEnd(reader, FSSHTTP_OBJECT.dataElement)) { + count = boundedIncrement( + count, + context.limits.maxStreamIds, + reader, + 'Storage Manifest roots' + ); + const header = expectFssHttpStart( + reader, + FSSHTTP_OBJECT.storageManifestRoot, + false, + 2 + ); + const body = takeFssHttpHeaderBody(reader, header, 'Storage Manifest root'); + const rootId = readFssHttpExGuid(body); + const cell = readFssHttpCellId(body); + finishFssHttpBody(body, 'Storage Manifest root'); + insertUniqueKey( + roots, + exGuidKey(rootId), + cell, + header.offset, + 'Storage Manifest root' + ); + } + expectFssHttpEnd(reader, FSSHTTP_OBJECT.dataElement); + return { schema, roots }; +} + +function parseCellManifest(reader: BinaryReader): ExGuid { + const header = expectFssHttpStart( + reader, + FSSHTTP_OBJECT.cellManifest, + false, + 2 + ); + const body = takeFssHttpHeaderBody(reader, header, 'Cell Manifest'); + const revisionId = readFssHttpExGuid(body); + finishFssHttpBody(body, 'Cell Manifest'); + expectFssHttpEnd(reader, FSSHTTP_OBJECT.dataElement); + return revisionId; +} + +function parseRevisionManifest( + reader: BinaryReader, + context: DataElementContext +): FssHttpRevisionManifest { + const manifestHeader = expectFssHttpStart( + reader, + FSSHTTP_OBJECT.revisionManifest, + false, + 2 + ); + const manifestBody = takeFssHttpHeaderBody( + reader, + manifestHeader, + 'Revision Manifest' + ); + const revisionId = readFssHttpExGuid(manifestBody); + const baseRevisionId = readFssHttpExGuid(manifestBody); + finishFssHttpBody(manifestBody, 'Revision Manifest'); + + const roots: FssHttpRevisionManifest['roots'] = []; + const objectGroupIds: ExGuid[] = []; + let count = 0; + while (!hasFssHttpEnd(reader, FSSHTTP_OBJECT.dataElement)) { + count = boundedIncrement( + count, + context.limits.maxStreamIds, + reader, + 'Revision Manifest entries' + ); + const header = readFssHttpStreamHeader(reader, 2); + if (header.compound) unexpectedCompound(header, 'Revision Manifest entry'); + const body = takeFssHttpHeaderBody( + reader, + header, + 'Revision Manifest entry' + ); + if (header.type === FSSHTTP_OBJECT.revisionManifestRoot) { + roots.push({ + role: readFssHttpExGuid(body), + objectId: readFssHttpExGuid(body), + }); + } else if (header.type === FSSHTTP_OBJECT.revisionManifestGroupReference) { + objectGroupIds.push(readFssHttpExGuid(body)); + } else { + unexpectedType(header, 'Revision Manifest'); + } + finishFssHttpBody(body, 'Revision Manifest entry'); + } + expectFssHttpEnd(reader, FSSHTTP_OBJECT.dataElement); + return { revisionId, baseRevisionId, roots, objectGroupIds }; +} + +function parseObjectGroup( + reader: BinaryReader, + context: DataElementContext +): FssHttpObjectGroup { + const declarationsHeader = expectFssHttpStart( + reader, + FSSHTTP_OBJECT.objectGroupDeclaration, + true + ); + const declarationsBody = takeFssHttpHeaderBody( + reader, + declarationsHeader, + 'Object Group Declarations start' + ); + finishFssHttpBody(declarationsBody, 'Object Group Declarations start'); + + const declarations: FssHttpObjectDeclaration[] = []; + while (!hasFssHttpEnd(reader, FSSHTTP_OBJECT.objectGroupDeclaration)) { + context.objectCount += 1; + if (context.objectCount > context.limits.maxObjects) { + throw new OneNoteParserError( + 'LIMIT_EXCEEDED', + `FSSHTTPB object declaration count exceeds ${context.limits.maxObjects}`, + { offset: reader.offset, structure: 'Object Group Declarations' } + ); + } + declarations.push(parseObjectDeclaration(reader, context)); + } + expectFssHttpEnd(reader, FSSHTTP_OBJECT.objectGroupDeclaration); + + const changeFrequencies: number[] = []; + let dataHeader = readFssHttpStreamHeader(reader); + if (dataHeader.type === FSSHTTP_OBJECT.objectGroupMetadataBlock) { + if (!dataHeader.compound) + unexpectedCompound(dataHeader, 'Object Group Metadata Block', true); + const metadataBody = takeFssHttpHeaderBody( + reader, + dataHeader, + 'Object Group Metadata Block start' + ); + finishFssHttpBody(metadataBody, 'Object Group Metadata Block start'); + while (!hasFssHttpEnd(reader, FSSHTTP_OBJECT.objectGroupMetadataBlock)) { + if (changeFrequencies.length >= context.limits.maxObjects) { + throw new OneNoteParserError( + 'LIMIT_EXCEEDED', + `Object Group metadata count exceeds ${context.limits.maxObjects}`, + { offset: reader.offset, structure: 'Object Group Metadata Block' } + ); + } + const header = expectFssHttpStart( + reader, + FSSHTTP_OBJECT.objectGroupMetadata, + false, + 4 + ); + const body = takeFssHttpHeaderBody( + reader, + header, + 'Object Group Metadata' + ); + const frequency = readCompactNumber(body, 4, 'Object change frequency'); + changeFrequencies.push(frequency); + finishFssHttpBody(body, 'Object Group Metadata'); + } + expectFssHttpEnd(reader, FSSHTTP_OBJECT.objectGroupMetadataBlock); + dataHeader = readFssHttpStreamHeader(reader); + } + if ( + dataHeader.type !== FSSHTTP_OBJECT.objectGroupData || + !dataHeader.compound + ) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + `Expected Object Group Data, found type 0x${dataHeader.type.toString(16)}`, + { offset: dataHeader.offset, structure: 'Object Group' } + ); + } + const dataStartBody = takeFssHttpHeaderBody( + reader, + dataHeader, + 'Object Group Data start' + ); + finishFssHttpBody(dataStartBody, 'Object Group Data start'); + + const data: FssHttpObjectGroupData[] = []; + while (!hasFssHttpEnd(reader, FSSHTTP_OBJECT.objectGroupData)) { + if (data.length >= context.limits.maxObjects) { + throw new OneNoteParserError( + 'LIMIT_EXCEEDED', + `Object Group data count exceeds ${context.limits.maxObjects}`, + { offset: reader.offset, structure: 'Object Group Data' } + ); + } + data.push(parseObjectGroupData(reader, context)); + } + expectFssHttpEnd(reader, FSSHTTP_OBJECT.objectGroupData); + expectFssHttpEnd(reader, FSSHTTP_OBJECT.dataElement); + return { declarations, changeFrequencies, data }; +} + +function parseObjectDeclaration( + reader: BinaryReader, + context: DataElementContext +): FssHttpObjectDeclaration { + const header = readFssHttpStreamHeader(reader); + if (header.compound) unexpectedCompound(header, 'Object declaration'); + const body = takeFssHttpHeaderBody(reader, header, 'Object declaration'); + const objectId = readFssHttpExGuid(body); + let result: FssHttpObjectDeclaration; + if (header.type === FSSHTTP_OBJECT.objectGroupObject) { + result = { + kind: 'object', + objectId, + partitionId: readCompactNumber(body, 4, 'Object partition ID'), + dataSize: readCompactNumber( + body, + context.limits.maxVectorBytes, + 'Object data size' + ), + objectReferenceCount: readCompactNumber( + body, + context.limits.maxObjectReferences, + 'Object reference count' + ), + cellReferenceCount: readCompactNumber( + body, + context.limits.maxObjectReferences, + 'Cell reference count' + ), + }; + } else if (header.type === FSSHTTP_OBJECT.objectGroupDataBlob) { + result = { + kind: 'blob', + objectId, + blobId: readFssHttpExGuid(body), + partitionId: readCompactNumber(body, 4, 'Object partition ID'), + objectReferenceCount: readCompactNumber( + body, + context.limits.maxObjectReferences, + 'Object reference count' + ), + cellReferenceCount: readCompactNumber( + body, + context.limits.maxObjectReferences, + 'Cell reference count' + ), + }; + } else { + unexpectedType(header, 'Object Group Declarations'); + } + finishFssHttpBody(body, 'Object declaration'); + return result; +} + +function parseObjectGroupData( + reader: BinaryReader, + context: DataElementContext +): FssHttpObjectGroupData { + 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); + let result: FssHttpObjectGroupData; + if (header.type === FSSHTTP_OBJECT.objectGroupDataExcluded) { + result = { + kind: 'excluded', + objectReferences, + cellReferences, + dataSize: readCompactNumber( + body, + context.limits.maxVectorBytes, + 'Excluded object data size' + ), + }; + } else if (header.type === FSSHTTP_OBJECT.objectGroupDataObject) { + const size = readCompactNumber( + body, + context.limits.maxVectorBytes, + 'Object binary item size' + ); + result = { + kind: 'object', + objectReferences, + cellReferences, + data: body.read(size), + }; + } else if (header.type === FSSHTTP_OBJECT.objectGroupBlobReference) { + result = { + kind: 'blob-reference', + objectReferences, + cellReferences, + blobId: readFssHttpExGuid(body), + }; + } else { + unexpectedType(header, 'Object Group Data'); + } + finishFssHttpBody(body, 'Object Group data entry'); + return result; +} + +function parseObjectDataBlob( + reader: BinaryReader, + context: DataElementContext +) { + const header = expectFssHttpStart( + reader, + FSSHTTP_OBJECT.objectDataBlob, + false + ); + const body = takeFssHttpHeaderBody(reader, header, 'Object Data BLOB'); + const size = readCompactNumber( + body, + context.limits.maxFileDataBytes, + 'Object Data BLOB size' + ); + context.blobCount += 1; + context.totalBlobBytes += size; + if (context.blobCount > context.limits.maxFileDataObjects) { + throw new OneNoteParserError( + 'LIMIT_EXCEEDED', + `Object Data BLOB count exceeds ${context.limits.maxFileDataObjects}`, + { offset: header.offset, structure: 'Object Data BLOB' } + ); + } + if (context.totalBlobBytes > context.limits.maxTotalFileDataBytes) { + throw new OneNoteParserError( + 'LIMIT_EXCEEDED', + `Object Data BLOB bytes exceed ${context.limits.maxTotalFileDataBytes}`, + { offset: header.offset, structure: 'Object Data BLOB' } + ); + } + const offset = body.offset; + body.skip(size); + finishFssHttpBody(body, 'Object Data BLOB'); + expectFssHttpEnd(reader, FSSHTTP_OBJECT.dataElement); + return { source: reader.bytes, offset, size }; +} + +function parseDataElementFragment( + reader: BinaryReader, + context: DataElementContext +): FssHttpDataElementFragment { + const header = expectFssHttpStart( + reader, + FSSHTTP_OBJECT.dataElementFragment, + false, + 4 + ); + const body = takeFssHttpHeaderBody(reader, header, 'Data Element Fragment'); + const dataElementId = readFssHttpExGuid(body); + const fragmentSize = readCompactNumber( + body, + context.limits.maxFileBytes, + 'Fragmented data element size' + ); + const chunkOffset = readCompactNumber( + body, + fragmentSize, + 'Fragment chunk offset' + ); + const chunkLength = readCompactNumber( + body, + context.limits.maxVectorBytes, + 'Fragment chunk length' + ); + if (chunkOffset + chunkLength > fragmentSize) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + 'Fragment chunk range exceeds the fragmented data element size', + { offset: header.offset, structure: 'Data Element Fragment' } + ); + } + if (body.remaining !== chunkLength) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + `Fragment header declares ${chunkLength} data bytes; ${body.remaining} remain`, + { offset: header.offset, structure: 'Data Element Fragment' } + ); + } + const data = body.read(chunkLength); + finishFssHttpBody(body, 'Data Element Fragment'); + expectFssHttpEnd(reader, FSSHTTP_OBJECT.dataElement); + return { dataElementId, fragmentSize, chunkOffset, chunkLength, data }; +} + +function boundedIncrement( + current: number, + limit: number, + reader: BinaryReader, + structure: string +): number { + const next = current + 1; + if (next > limit) { + throw new OneNoteParserError( + 'LIMIT_EXCEEDED', + `${structure} count exceeds ${limit}`, + { offset: reader.offset, structure } + ); + } + return next; +} + +function insertUnique( + map: Map, + id: ExGuid, + value: T, + offset: number, + structure: string +): void { + insertUniqueKey(map, exGuidKey(id), value, offset, structure); +} + +function insertUniqueKey( + map: Map, + key: string, + value: T, + offset: number, + structure: string +): void { + if (map.has(key)) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + `${structure} identifier ${key} is duplicated`, + { offset, structure } + ); + } + map.set(key, value); +} + +function unexpectedType(header: FssHttpStreamHeader, structure: string): never { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + `Unexpected stream object type 0x${header.type.toString(16)}`, + { offset: header.offset, structure } + ); +} + +function unexpectedCompound( + header: FssHttpStreamHeader, + structure: string, + expected = false +): never { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + `${structure} must be ${expected ? 'compound' : 'single'}`, + { offset: header.offset, structure } + ); +} diff --git a/src/onenote/fsshttpb/index.ts b/src/onenote/fsshttpb/index.ts new file mode 100644 index 0000000..3b2d7fe --- /dev/null +++ b/src/onenote/fsshttpb/index.ts @@ -0,0 +1,5 @@ +export { parseFssHttpOneStorePackage } from './package.js'; +export type { + FssHttpDataElementPackage, + FssHttpOneStorePackage, +} from './types.js'; diff --git a/src/onenote/fsshttpb/package.test.ts b/src/onenote/fsshttpb/package.test.ts new file mode 100644 index 0000000..08011b0 --- /dev/null +++ b/src/onenote/fsshttpb/package.test.ts @@ -0,0 +1,111 @@ +import { createHash } from 'node:crypto'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +import { ONE_SECTION_FILE_TYPE } from '../parser/detect-format.js'; +import { resolveParserLimits } from '../parser/limits.js'; +import { + FSSHTTP_SECTION_SCHEMA, + parseFssHttpOneStorePackage, +} from './package.js'; + +function fixtureBytes(): Uint8Array { + const encoded = readFileSync( + resolve(process.cwd(), 'tests/fixtures/joplin-quick-notes-fsshttp.one.b64'), + 'utf8' + ); + return Uint8Array.from(Buffer.from(encoded.replaceAll(/\s/gu, ''), 'base64')); +} + +describe('FSSHTTPB OneStore packaging', () => { + it('retains and parses the pinned Joplin Quick Notes fixture', () => { + const bytes = fixtureBytes(); + expect(bytes).toHaveLength(17_197); + expect(createHash('sha256').update(bytes).digest('hex')).toBe( + '5e501ef5f539cd2f149786f57659be9d0762913d863a69867deb9c8e8c43efe2' + ); + + const parsed = parseFssHttpOneStorePackage(bytes, resolveParserLimits()); + expect(parsed).toMatchObject({ + fileType: ONE_SECTION_FILE_TYPE, + cellSchema: FSSHTTP_SECTION_SCHEMA, + }); + expect({ + indexes: parsed.dataElements.storageIndexes.size, + manifests: parsed.dataElements.storageManifests.size, + cells: parsed.dataElements.cellManifests.size, + revisions: parsed.dataElements.revisionManifests.size, + groups: parsed.dataElements.objectGroups.size, + fragments: parsed.dataElements.fragments.size, + blobs: parsed.dataElements.blobs.size, + }).toEqual({ + indexes: 1, + manifests: 1, + cells: 4, + revisions: 9, + groups: 9, + fragments: 0, + blobs: 0, + }); + }); + + it('rejects malformed reserved bytes with an absolute offset', () => { + const bytes = fixtureBytes(); + bytes[64] = 1; + expect(() => + parseFssHttpOneStorePackage(bytes, resolveParserLimits()) + ).toThrowError( + expect.objectContaining({ + code: 'INVALID_STRUCTURE', + offset: 64, + structure: 'OneStore Packaging Structure', + }) + ); + }); + + it('rejects a truncated package as a bounded parse failure', () => { + const bytes = fixtureBytes().subarray(0, 1_000); + expect(() => + parseFssHttpOneStorePackage(bytes, resolveParserLimits()) + ).toThrowError( + expect.objectContaining({ + code: 'BOUNDS_EXCEEDED', + offset: expect.any(Number), + }) + ); + }); + + it('enforces source and data-element count limits', () => { + const bytes = fixtureBytes(); + expect(() => + parseFssHttpOneStorePackage( + bytes, + resolveParserLimits({ maxFileBytes: bytes.byteLength - 1 }) + ) + ).toThrowError( + expect.objectContaining({ code: 'LIMIT_EXCEEDED', offset: 0 }) + ); + expect(() => + parseFssHttpOneStorePackage( + bytes, + resolveParserLimits({ maxStreamIds: 1 }) + ) + ).toThrowError(expect.objectContaining({ code: 'LIMIT_EXCEEDED' })); + }); + + it('allows only zero bytes after the packaging end marker', () => { + const bytes = fixtureBytes(); + bytes[bytes.length - 1] = 1; + expect(() => + parseFssHttpOneStorePackage(bytes, resolveParserLimits()) + ).toThrowError( + expect.objectContaining({ + code: 'INVALID_STRUCTURE', + offset: bytes.length - 1, + structure: 'OneStore Packaging Structure padding', + }) + ); + }); +}); diff --git a/src/onenote/fsshttpb/package.ts b/src/onenote/fsshttpb/package.ts new file mode 100644 index 0000000..c0bd95a --- /dev/null +++ b/src/onenote/fsshttpb/package.ts @@ -0,0 +1,128 @@ +// 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/packaging.rs`. +// Deviations: direct immutable browser bytes, exact envelope consumption, +// explicit bounds, and offset-aware failures. + +import { BinaryReader } from '../binary/BinaryReader.js'; +import { readGuid } from '../binary/guid.js'; +import { + FSSHTTP_PACKAGING_FORMAT, + ONE_SECTION_FILE_TYPE, + ONE_TOC_FILE_TYPE, +} from '../parser/detect-format.js'; +import { OneNoteParserError } from '../parser/error.js'; +import type { OneNoteParserLimits } from '../parser/limits.js'; +import { parseFssHttpDataElementPackage } from './data-elements.js'; +import { + expectFssHttpEnd, + expectFssHttpStart, + finishFssHttpBody, + readFssHttpExGuid, + takeFssHttpHeaderBody, +} from './primitives.js'; +import { FSSHTTP_OBJECT, type FssHttpOneStorePackage } from './types.js'; + +export const FSSHTTP_SECTION_SCHEMA = '{1F937CB4-B26F-445F-B9F8-17E20160E461}'; +export const FSSHTTP_TOC_SCHEMA = '{E4DBFD38-E5C7-408B-A8A1-0E7B421E1F5F}'; + +/** Parse the MS-ONESTORE 2.8.1 package envelope and its data elements. */ +export function parseFssHttpOneStorePackage( + bytes: Uint8Array, + limits: OneNoteParserLimits +): FssHttpOneStorePackage { + if (bytes.byteLength > limits.maxFileBytes) { + throw new OneNoteParserError( + 'LIMIT_EXCEEDED', + `FSSHTTP-packaged OneNote source is ${bytes.byteLength} bytes (limit ${limits.maxFileBytes})`, + { offset: 0, structure: 'OneStore Packaging Structure' } + ); + } + const reader = new BinaryReader( + bytes, + 0, + bytes.byteLength, + 'OneStore Packaging Structure' + ); + const fileType = readGuid(reader); + const fileIdentity = readGuid(reader); + const legacyFileVersion = readGuid(reader); + const fileFormat = readGuid(reader); + if (fileType !== ONE_SECTION_FILE_TYPE && fileType !== ONE_TOC_FILE_TYPE) { + throw new OneNoteParserError( + 'INVALID_FORMAT', + `Unsupported package-store file type ${fileType}`, + { offset: 0, structure: 'OneStore Packaging Structure' } + ); + } + if (fileFormat !== FSSHTTP_PACKAGING_FORMAT) { + throw new OneNoteParserError( + 'INVALID_FORMAT', + `Expected package-store format ${FSSHTTP_PACKAGING_FORMAT}, found ${fileFormat}`, + { offset: 48, structure: 'OneStore Packaging Structure' } + ); + } + if (reader.u32() !== 0) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + 'Package-store reserved bytes are not zero', + { offset: 64, structure: 'OneStore Packaging Structure' } + ); + } + + const packagingHeader = expectFssHttpStart( + reader, + FSSHTTP_OBJECT.oneNotePackaging, + true, + 4 + ); + const packagingBody = takeFssHttpHeaderBody( + reader, + packagingHeader, + 'OneNote Packaging start' + ); + const storageIndexId = readFssHttpExGuid(packagingBody); + const cellSchema = readGuid(packagingBody); + finishFssHttpBody(packagingBody, 'OneNote Packaging start'); + if ( + (fileType === ONE_SECTION_FILE_TYPE && + cellSchema !== FSSHTTP_SECTION_SCHEMA) || + (fileType === ONE_TOC_FILE_TYPE && cellSchema !== FSSHTTP_TOC_SCHEMA) + ) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + `Cell schema ${cellSchema} does not match package file type ${fileType}`, + { offset: packagingHeader.bodyOffset, structure: 'guidCellSchemaId' } + ); + } + + const dataElements = parseFssHttpDataElementPackage(reader, limits); + expectFssHttpEnd(reader, FSSHTTP_OBJECT.oneNotePackaging); + // OneDrive exports observed in the pinned corpus pad the package to the + // physical file size. The packaging end is authoritative, but non-zero + // trailing data is never accepted as padding. + while (reader.remaining > 0) { + const offset = reader.offset; + if (reader.u8() !== 0) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + 'OneStore Packaging Structure has non-zero trailing data', + { offset, structure: 'OneStore Packaging Structure padding' } + ); + } + } + + return { + fileType, + fileIdentity, + legacyFileVersion, + fileFormat, + storageIndexId, + cellSchema, + dataElements, + }; +} diff --git a/src/onenote/fsshttpb/primitives.test.ts b/src/onenote/fsshttpb/primitives.test.ts new file mode 100644 index 0000000..99a0555 --- /dev/null +++ b/src/onenote/fsshttpb/primitives.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest'; + +import { BinaryReader } from '../binary/BinaryReader.js'; +import { + readCompactU64, + readFssHttpExGuid, + readFssHttpStreamHeader, +} from './primitives.js'; + +describe('FSSHTTPB primitives', () => { + it.each([ + [[0x00], 0n], + [[(0x5a << 1) | 1], 0x5an], + [[0xd2, 0x48], 0x1234n], + [ + [0x80, 0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01], + 0x0123_4567_89ab_cdefn, + ], + ] as const)('decodes compact unsigned integer %j', (input, expected) => { + const reader = new BinaryReader(Uint8Array.from(input)); + expect(readCompactU64(reader)).toBe(expected); + expect(reader.remaining).toBe(0); + }); + + it('rejects a truncated extended GUID at the original absolute offset', () => { + const reader = new BinaryReader( + Uint8Array.from([0, 0, 0x80, 1, 2]), + 2, + 3, + 'test ExGuid' + ); + expect(() => readFssHttpExGuid(reader)).toThrowError( + expect.objectContaining({ code: 'BOUNDS_EXCEEDED', offset: 3 }) + ); + }); + + it('rejects stream-object end markers where a start is required', () => { + const reader = new BinaryReader(Uint8Array.from([0x05])); + expect(() => readFssHttpStreamHeader(reader)).toThrowError( + expect.objectContaining({ + code: 'INVALID_STRUCTURE', + offset: 0, + structure: 'FSSHTTPB stream object header', + }) + ); + }); +}); diff --git a/src/onenote/fsshttpb/primitives.ts b/src/onenote/fsshttpb/primitives.ts new file mode 100644 index 0000000..d8b0ac2 --- /dev/null +++ b/src/onenote/fsshttpb/primitives.ts @@ -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' } + ); + } +} diff --git a/src/onenote/fsshttpb/types.ts b/src/onenote/fsshttpb/types.ts new file mode 100644 index 0000000..69aabcd --- /dev/null +++ b/src/onenote/fsshttpb/types.ts @@ -0,0 +1,173 @@ +// 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, +// data_element,packaging}.rs`. Deviations: serializable TypeScript records, +// zero-copy byte ranges, checked integer conversion, and explicit limits. + +import type { ExGuid, FileDataBlobReference } from '../onestore/types.js'; + +export const FSSHTTP_OBJECT = { + dataElement: 0x01, + objectDataBlob: 0x02, + objectGroupDataExcluded: 0x03, + objectGroupDataBlob: 0x05, + storageManifestRoot: 0x07, + revisionManifestRoot: 0x0a, + cellManifest: 0x0b, + storageManifest: 0x0c, + storageIndexRevisionMapping: 0x0d, + storageIndexCellMapping: 0x0e, + storageIndexManifestMapping: 0x11, + dataElementPackage: 0x15, + objectGroupDataObject: 0x16, + objectGroupObject: 0x18, + revisionManifestGroupReference: 0x19, + revisionManifest: 0x1a, + objectGroupBlobReference: 0x1c, + objectGroupDeclaration: 0x1d, + objectGroupData: 0x1e, + dataElementFragment: 0x6a, + objectGroupMetadata: 0x78, + objectGroupMetadataBlock: 0x79, + oneNotePackaging: 0x7a, +} as const; + +export interface FssHttpStreamHeader { + offset: number; + width: 2 | 4; + compound: boolean; + type: number; + length: number; + bodyOffset: number; + bodyEnd: number; +} + +export interface FssHttpSerialNumber { + guid: string; + value: bigint; +} + +export interface FssHttpCellId { + context: ExGuid; + objectSpace: ExGuid; +} + +export interface FssHttpStorageIndexManifestMapping { + id: ExGuid; + serial: FssHttpSerialNumber; +} + +export interface FssHttpStorageIndexCellMapping { + cellId: FssHttpCellId; + id: ExGuid; + serial: FssHttpSerialNumber; +} + +export interface FssHttpStorageIndexRevisionMapping { + id: ExGuid; + revision: ExGuid; + serial: FssHttpSerialNumber; +} + +export interface FssHttpStorageIndex { + manifestMappings: FssHttpStorageIndexManifestMapping[]; + cellMappings: Map; + revisionMappings: Map; +} + +export interface FssHttpStorageManifest { + schema: string; + roots: Map; +} + +export interface FssHttpRevisionRoot { + role: ExGuid; + objectId: ExGuid; +} + +export interface FssHttpRevisionManifest { + revisionId: ExGuid; + baseRevisionId: ExGuid; + roots: FssHttpRevisionRoot[]; + objectGroupIds: ExGuid[]; +} + +export type FssHttpObjectDeclaration = + | { + kind: 'object'; + objectId: ExGuid; + partitionId: number; + dataSize: number; + objectReferenceCount: number; + cellReferenceCount: number; + } + | { + kind: 'blob'; + objectId: ExGuid; + blobId: ExGuid; + partitionId: number; + objectReferenceCount: number; + cellReferenceCount: number; + }; + +export type FssHttpObjectGroupData = + | { + kind: 'object'; + objectReferences: ExGuid[]; + cellReferences: FssHttpCellId[]; + data: Uint8Array; + } + | { + kind: 'excluded'; + objectReferences: ExGuid[]; + cellReferences: FssHttpCellId[]; + dataSize: number; + } + | { + kind: 'blob-reference'; + objectReferences: ExGuid[]; + cellReferences: FssHttpCellId[]; + blobId: ExGuid; + }; + +export interface FssHttpObjectGroup { + declarations: FssHttpObjectDeclaration[]; + changeFrequencies: number[]; + data: FssHttpObjectGroupData[]; +} + +export interface FssHttpDataElementFragment { + dataElementId: ExGuid; + fragmentSize: number; + chunkOffset: number; + chunkLength: number; + data: Uint8Array; +} + +export interface FssHttpDataElementPackage { + storageIndexes: Map; + storageManifests: Map; + cellManifests: Map; + revisionManifests: Map; + objectGroups: Map; + fragments: Map; + blobs: Map; +} + +export interface FssHttpOneStorePackage { + fileType: string; + fileIdentity: string; + legacyFileVersion: string; + fileFormat: string; + storageIndexId: ExGuid; + cellSchema: string; + dataElements: FssHttpDataElementPackage; +} + +export function fssHttpCellKey(cell: FssHttpCellId): string { + return `${cell.context.guid}:${cell.context.value}/${cell.objectSpace.guid}:${cell.objectSpace.value}`; +} diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md index dd3d33a..1e9544e 100644 --- a/tests/fixtures/README.md +++ b/tests/fixtures/README.md @@ -38,6 +38,27 @@ and does not change the underlying file's licence. Tests verify the package hash, all seven paths, the native `.one` magic for every section, section parse statuses, and representative title/text output. +## `joplin-quick-notes-fsshttp.one.b64` + +- Immediate reviewed source: `onenote.rs` + [`crates/parser/tests/samples/joplin/Simple notebook/Quick Notes.one`](https://github.com/msiemens/onenote.rs/blob/5138a39a3f4e72b840932f9872fecde52fa9da60/crates/parser/tests/samples/joplin/Simple%20notebook/Quick%20Notes.one) + at the parser reference revision + `5138a39a3f4e72b840932f9872fecde52fa9da60`; introduced there by commit + `3011a1f0f2a13288aa8ac366f783adc0673e4989` as test data from Joplin. +- Upstream attribution recorded alongside the source fixture: Joplin, + copyright Laurent Cozic, AGPL-3.0-or-later. The full licence is retained in + `LICENSES/AGPL-3.0.txt`. +- Decoded size: 17,197 bytes. +- Decoded SHA-256: + `5e501ef5f539cd2f149786f57659be9d0762913d863a69867deb9c8e8c43efe2`. +- Golden package expectation: FSSHTTPB package-store section with one Storage + Index, one Storage Manifest, four Cell Manifests, nine Revision Manifests, + and nine Object Groups. This fixture contains no Data Element Fragments or + Object Data BLOB elements. + +Tests retain the exact hash, validate the package envelope and element counts, +and exercise truncated, malformed, padding, and configured-limit failures. + ## Inline rust-cab CAB fixtures `src/onenote/onepkg/cabinet.test.ts` contains byte-for-byte test arrays copied diff --git a/tests/fixtures/joplin-quick-notes-fsshttp.one.b64 b/tests/fixtures/joplin-quick-notes-fsshttp.one.b64 new file mode 100644 index 0000000..d39e4b8 --- /dev/null +++ b/tests/fixtures/joplin-quick-notes-fsshttp.one.b64 @@ -0,0 +1,302 @@ +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 +AQAAALAOAAAJCwAGALIAhAIbZKENJlHyi/hNkYTusdymUy1soQ0mUfKL+E2RhO6x3KZTLXShDSZR +8ov4TZGE7rHcplMtfKENJlHyi/hNkYTusdymUy2EoQ0mUfKL+E2RhO6x3KZTLYyhDSZR8ov4TZGE +7rHcplMtlKENJlHyi/hNkYTusdymUy2coQ0mUfKL+E2RhO6x3KZTLaShDSZR8ov4TZGE7rHcplMt +rKENJlHyi/hNkYTusdymUy20oQ0mUfKL+E2RhO6x3KZTLbyhDSZR8ov4TZGE7rHcplMtRKENJlHy +i/hNkYTusdymUy0AxQ0AAIAMAAAADQAAAA4AAAAPAAAAEAAAABEAAAASAAAAEwAAABQAAAAVAAAA +FgAAABcAAAAIAAAABgCSHAAI3BwAiA8dABQQHQAU2DQAJF8dACQAAIA/zczMPgwAAAABAAAAsA4A +AAksAAYAsMYFTKENJlHyi/hNkYTusdymUy3UoQ0mUfKL+E2RhO6x3KZTLQB9AgAAgAkAAAAaAAAA +BgAUHAAUFRwAFD4cABSEHAAUIBwAJHodABQAAAAAAAAAAAwACQAAAAAAAgAAAFyxZlOwDgAACQwA +BgCyAAgBA1ShDSZR8ov4TZGE7rHcplMtAOEBAACACgAAAA4AAxwADBMcAAwbHAAUEhwAHIQcABSy +HACI4BwAiPkcAIiRHACI/xwAiOwcABS0HACIIBwAJHodABQBAAAAUEEUAAAABPX19QAAAD8AAAAA +AABAPwAAQD8MAAAAAACQQAEAAABcsWZTsA4AAAkNAAYAsPIHxKENJlHyi/hNkYTusdymUy3EoQ0m +UfKL+E2RhO6x3KZTLVyhDSZR8ov4TZGE7rHcplMtAIcDAACAGAAAABgAAAALAAAACQADHAAMCR0A +FHodABR5HQAgeB0AILIcAIjgHACItBwAiB8cACQBXLFmU1yxZlMBAAAAsA4AAAkOAAYAsLYFnKEN +JlHyi/hNkYTusdymUy3MoQ0mUfKL+E2RhO6x3KZTLQBtAgAAgBMAAAAZAAAABgAiHAAc/hwAECw0 +ACC0HACIEx4AJHodABQCAAAAAAAAAAEAAABcsWZTsA4AAAlNABIAsPAAAOsAAACACABaNAAc2jQA +DAscABAMHAAUijQAHC80ABQuNAAUChwAHBQAAABQAGEAZwBlAFQAaQB0AGwAZQAAAAIoAAAAAP8E +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 +Gk64guHj3tTFWAkJAADAKoT5pCo/dOwaTriC4ePe1MVYA60FAMAq/KENJlHyi/hNkYTusdymUy0J +CQAAwCz8oQ0mUfKL+E2RhO6x3KZTLQMaAgAAdfQAsA4AAAlEAAIAsFwDnPmkKj907BpOuILh497U +xVgANQEAAIATAQAAAgB3HQAYeR0AIABVgMihndoBsA4AAAkNAAYAsNoHxKENJlHyi/hNkYTusdym +Uy3EoQ0mUfKL+E2RhO6x3KZTLez5pCo/dOwaTriC4ePe1MVYAG8DAACAGAIAABgCAAAdAQAABgAD +HAAMCR0AFHodABR4HQAgeR0AIB8cACQBjrJmU5KyZlMBAAAAsA4AAAkOAAYAsPYFZKENJlHyi/hN +kYTusdymUy3MoQ0mUfKL+E2RhO6x3KZTLQCtAgAAgAwCAAAZAgAABgD+HAAQeh0AFCIcABxbHgAc +LDQAIBMeACQJBJKyZlMaAAAAUABhAGcAZQAgAGMAbwBuAHQAZQBuAHQAAAAEAAAAMQAAAAEAAACw +DgAACQwABgCw6AP0+aQqP3TsGk64guHj3tTFWADBAQAAgB4BAAAJAAMcAAwTHAAMFBwAFBUcABQb +HAAUhBwAFHodABQSHAAcIBwAJAEAAACAP5qZGUAAAFBBCQAAAI6yZlMUAAAABPX19QAAAD8AAAAA +AABAPwAAQD8BAAAAsA4AAAkLAAYAsgC+Ah3M+aQqP3TsGk64guHj3tTFWEShDSZR8ov4TZGE7rHc +plMtZKENJlHyi/hNkYTusdymUy1soQ0mUfKL+E2RhO6x3KZTLXShDSZR8ov4TZGE7rHcplMtfKEN +JlHyi/hNkYTusdymUy2EoQ0mUfKL+E2RhO6x3KZTLYyhDSZR8ov4TZGE7rHcplMtlKENJlHyi/hN +kYTusdymUy2coQ0mUfKL+E2RhO6x3KZTLaShDSZR8ov4TZGE7rHcplMtrKENJlHyi/hNkYTusdym +Uy20oQ0mUfKL+E2RhO6x3KZTLbyhDSZR8ov4TZGE7rHcplMtAN0OAACAGQEAAAgCAAAMAgAADQIA +AA4CAAAPAgAAEAIAABECAAASAgAAEwIAABQCAAAVAgAAFgIAABcCAAAHAJIcAAjcHACIDx0AFBAd +ABQgHAAkXx0AJNg0ACQAAIA/zczMPgEAAAABAAAADAAAALAOAAAJDQAGALDyB8ShDSZR8ov4TZGE +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 +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==