feat: parse bounded FSSHTTPB packages
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
723
src/onenote/fsshttpb/data-elements.ts
Normal file
723
src/onenote/fsshttpb/data-elements.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
5
src/onenote/fsshttpb/index.ts
Normal file
5
src/onenote/fsshttpb/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export { parseFssHttpOneStorePackage } from './package.js';
|
||||||
|
export type {
|
||||||
|
FssHttpDataElementPackage,
|
||||||
|
FssHttpOneStorePackage,
|
||||||
|
} from './types.js';
|
||||||
111
src/onenote/fsshttpb/package.test.ts
Normal file
111
src/onenote/fsshttpb/package.test.ts
Normal 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',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
128
src/onenote/fsshttpb/package.ts
Normal file
128
src/onenote/fsshttpb/package.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
47
src/onenote/fsshttpb/primitives.test.ts
Normal file
47
src/onenote/fsshttpb/primitives.test.ts
Normal 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',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
279
src/onenote/fsshttpb/primitives.ts
Normal file
279
src/onenote/fsshttpb/primitives.ts
Normal 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' }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
173
src/onenote/fsshttpb/types.ts
Normal file
173
src/onenote/fsshttpb/types.ts
Normal 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}`;
|
||||||
|
}
|
||||||
21
tests/fixtures/README.md
vendored
21
tests/fixtures/README.md
vendored
@@ -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
|
||||||
|
|||||||
302
tests/fixtures/joplin-quick-notes-fsshttp.one.b64
vendored
Normal file
302
tests/fixtures/joplin-quick-notes-fsshttp.one.b64
vendored
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
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
|
||||||
|
nwMG1+3zQ9akaQkJAADAKmzXmXaeYrKfAwbX7fND1qRpAy0AAMAq/KENJlHyi/hNkYTusdymUy0J
|
||||||
|
CQAAwCr8oQ0mUfKL+E2RhO6x3KZTLQPRAAB19ACwDgAACVEAEgCwdgAAcQAAAIABAHUdABwqAAAA
|
||||||
|
UABlAGQAcgBvACAATAB1AGkAegAgAEYAZQByAG4AYQBuAGQAZQBzAAAAsA4AAAlEAAIAsF4D4Amh
|
||||||
|
DSZR8ov4TZGE7rHcplMtADUBAACAJwAAAAIAeR0AIHcdABgAFroPoZ3aAbAOAAAJNwAGALDyBWzX
|
||||||
|
mXaeYrKfAwbX7fND1qRpPKENJlHyi/hNkYTusdymUy0DDH9JEXFrGwlClJHJiwTPTFps5lnevGKE
|
||||||
|
cUGxwzpfmfKRgWUCAABADQEAAAcAAAAAAABAAQAAAAECAAADAEI0ACQfHAAkezQANAEAAAABAAAA
|
||||||
|
AQAAALAOAAAJCwAGALIAhAIbZKENJlHyi/hNkYTusdymUy1soQ0mUfKL+E2RhO6x3KZTLXShDSZR
|
||||||
|
8ov4TZGE7rHcplMtfKENJlHyi/hNkYTusdymUy2EoQ0mUfKL+E2RhO6x3KZTLYyhDSZR8ov4TZGE
|
||||||
|
7rHcplMtlKENJlHyi/hNkYTusdymUy2coQ0mUfKL+E2RhO6x3KZTLaShDSZR8ov4TZGE7rHcplMt
|
||||||
|
rKENJlHyi/hNkYTusdymUy20oQ0mUfKL+E2RhO6x3KZTLbyhDSZR8ov4TZGE7rHcplMtRKENJlHy
|
||||||
|
i/hNkYTusdymUy0AxQ0AAIAMAAAADQAAAA4AAAAPAAAAEAAAABEAAAASAAAAEwAAABQAAAAVAAAA
|
||||||
|
FgAAABcAAAAIAAAABgCSHAAI3BwAiA8dABQQHQAU2DQAJF8dACQAAIA/zczMPgwAAAABAAAAsA4A
|
||||||
|
AAksAAYAsMYFTKENJlHyi/hNkYTusdymUy3UoQ0mUfKL+E2RhO6x3KZTLQB9AgAAgAkAAAAaAAAA
|
||||||
|
BgAUHAAUFRwAFD4cABSEHAAUIBwAJHodABQAAAAAAAAAAAwACQAAAAAAAgAAAFyxZlOwDgAACQwA
|
||||||
|
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
|
||||||
|
plMtZKENJlHyi/hNkYTusdymUy1soQ0mUfKL+E2RhO6x3KZTLXShDSZR8ov4TZGE7rHcplMtfKEN
|
||||||
|
JlHyi/hNkYTusdymUy2EoQ0mUfKL+E2RhO6x3KZTLYyhDSZR8ov4TZGE7rHcplMtlKENJlHyi/hN
|
||||||
|
kYTusdymUy2coQ0mUfKL+E2RhO6x3KZTLaShDSZR8ov4TZGE7rHcplMtrKENJlHyi/hNkYTusdym
|
||||||
|
Uy20oQ0mUfKL+E2RhO6x3KZTLbyhDSZR8ov4TZGE7rHcplMtAN0OAACAGQEAAAgCAAAMAgAADQIA
|
||||||
|
AA4CAAAPAgAAEAIAABECAAASAgAAEwIAABQCAAAVAgAAFgIAABcCAAAHAJIcAAjcHACIDx0AFBAd
|
||||||
|
ABQgHAAkXx0AJNg0ACQAAIA/zczMPgEAAAABAAAADAAAALAOAAAJDQAGALDyB8ShDSZR8ov4TZGE
|
||||||
|
7rHcplMtxKENJlHyi/hNkYTusdymUy2E+aQqP3TsGk64guHj3tTFWACHAwAAgBgCAAAYAgAAEAEA
|
||||||
|
AAkAshwAiLQcAIjgHACIAxwADAkdABR6HQAUeB0AIHkdACAfHAAkAVyxZlOOsmZTAQAAALAOAAAJ
|
||||||
|
DgAGALD2BZyhDSZR8ov4TZGE7rHcplMtzKENJlHyi/hNkYTusdymUy0ArQIAAIATAgAAGQIAAAcA
|
||||||
|
tBwAiP4cABB6HQAUIhwAHFseABwsNAAgEx4AJAAAjrJmUxYAAABQAGEAZwBlACAAdABpAHQAbABl
|
||||||
|
AAAABAAAADEAAAABAAAAsA4AAAkwAAIAsgAUAQAAGgIAAACABwCCHQAU/x0AFIs0ABRlHAAYMBwA
|
||||||
|
HPMcABwAUAAcKAAAAAEAAAAoAAAAgEfU6Yed2gEQAAAAOVY11yvHTUmbcHdKAGD9EBYAAABQAGEA
|
||||||
|
ZwBlACAAdABpAHQAbABlAAAAHgAAAE8ATgBEAEMAIABOAG8AdABlAGIAbwBvAGsAcwAAAHkFDFrA
|
||||||
|
ISp4+tBVNxh6SZ6QuYk3ln+WgA+0bGBO0xE/Puh9GRsVNkoMAAAAAAAAAAnQSOAK5lnevGKEcUGx
|
||||||
|
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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==
|
||||||
Reference in New Issue
Block a user