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

@@ -34,6 +34,12 @@ The Base64-encoded Joplin `test.onepkg` fixture is pinned at commit
`1e73aad7eb08fde5d9e4cb533df40052a5cd32d7` and is likewise test-only data `1e73aad7eb08fde5d9e4cb533df40052a5cd32d7` and is likewise test-only data
distributed under AGPL-3.0-or-later. 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 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 release retains the fixture provenance document and AGPL text so these notices
remain complete. remain complete.

View File

@@ -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<string, ReturnType<typeof readFssHttpCellId>>();
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<T>(
map: Map<string, T>,
id: ExGuid,
value: T,
offset: number,
structure: string
): void {
insertUniqueKey(map, exGuidKey(id), value, offset, structure);
}
function insertUniqueKey<T>(
map: Map<string, T>,
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 }
);
}

View File

@@ -0,0 +1,5 @@
export { parseFssHttpOneStorePackage } from './package.js';
export type {
FssHttpDataElementPackage,
FssHttpOneStorePackage,
} from './types.js';

View File

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

View File

@@ -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,
};
}

View File

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

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

View File

@@ -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<string, FssHttpStorageIndexCellMapping>;
revisionMappings: Map<string, FssHttpStorageIndexRevisionMapping>;
}
export interface FssHttpStorageManifest {
schema: string;
roots: Map<string, FssHttpCellId>;
}
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<string, FssHttpStorageIndex>;
storageManifests: Map<string, FssHttpStorageManifest>;
cellManifests: Map<string, ExGuid>;
revisionManifests: Map<string, FssHttpRevisionManifest>;
objectGroups: Map<string, FssHttpObjectGroup>;
fragments: Map<string, FssHttpDataElementFragment>;
blobs: Map<string, FileDataBlobReference>;
}
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}`;
}

View File

@@ -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 Tests verify the package hash, all seven paths, the native `.one` magic for
every section, section parse statuses, and representative title/text output. 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 ## Inline rust-cab CAB fixtures
`src/onenote/onepkg/cabinet.test.ts` contains byte-for-byte test arrays copied `src/onenote/onepkg/cabinet.test.ts` contains byte-for-byte test arrays copied

View File

@@ -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==