diff --git a/src/onenote/export/export.test.ts b/src/onenote/export/export.test.ts
index 5964694..7249427 100644
--- a/src/onenote/export/export.test.ts
+++ b/src/onenote/export/export.test.ts
@@ -8,6 +8,7 @@ import type {
OneNoteTextStyle,
} from '../one/content-model.js';
import {
+ createOneNoteExportPlan,
createOneNoteExportSnapshot,
createOneNoteStaticNotebookZip,
OneNoteExportError,
@@ -19,11 +20,17 @@ import {
serializeOneNoteStructuredJson,
type OneNoteExportSnapshot,
} from './index.js';
+import { utf8ByteLength } from './plan.js';
const PNG = Uint8Array.from([
- 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00,
+ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49,
+ 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+]);
+const JPEG = Uint8Array.from([
+ 0xff, 0xd8, 0xff, 0xc0, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01,
+ 0x11, 0x00, 0xff, 0xd9,
]);
-const JPEG = Uint8Array.from([0xff, 0xd8, 0xff, 0xd9]);
const SOURCE_MARKUP = '
';
describe('OneNote export serializers', () => {
@@ -260,6 +267,158 @@ describe('OneNote export serializers', () => {
});
});
+describe('OneNote export aggregate budgets', () => {
+ it('counts repeated rich-text representations against one text budget', () => {
+ const baseline = minimalSnapshot([textBlock('text', '')]);
+ const baselineCharacters =
+ createOneNoteExportPlan(baseline).textCharacterCount;
+ const snapshot = minimalSnapshot([textBlock('text', '0123456789')]);
+
+ expect(() =>
+ serializeOneNoteStructuredJson(snapshot, {
+ limits: { maxTextCharacters: baselineCharacters + 29 },
+ })
+ ).toThrowError(
+ expect.objectContaining>({
+ code: 'limit-exceeded',
+ message: expect.stringContaining('text character'),
+ })
+ );
+ });
+
+ it('counts ink points across blocks and strokes', () => {
+ const ink = (id: string): OneNoteContentBlock => ({
+ kind: 'ink',
+ id,
+ strokes: [
+ {
+ points: [
+ { x: 0, y: 0 },
+ { x: 1, y: 1 },
+ ],
+ },
+ ],
+ children: [],
+ layout: {},
+ });
+ const snapshot = minimalSnapshot([ink('ink-1'), ink('ink-2')]);
+
+ expect(() =>
+ serializeOneNoteSemanticHtml(snapshot, {
+ limits: { maxInkPoints: 3 },
+ })
+ ).toThrowError(
+ expect.objectContaining>({
+ code: 'limit-exceeded',
+ message: expect.stringContaining('ink point'),
+ })
+ );
+ });
+
+ it('preflights standalone output and measures exact UTF-8 bytes', () => {
+ const encodedSample = 'ASCII · 😀 · \ud800';
+ expect(utf8ByteLength(encodedSample)).toBe(
+ new TextEncoder().encode(encodedSample).byteLength
+ );
+
+ expect(() =>
+ serializeOneNoteMarkdown(minimalSnapshot(), {
+ limits: { maxOutputBytes: 8 * 1024 },
+ })
+ ).toThrowError(
+ expect.objectContaining>({
+ code: 'limit-exceeded',
+ message: expect.stringContaining('Projected standalone'),
+ })
+ );
+ });
+
+ it('includes resource members in the static ZIP output budget', () => {
+ const resourceBytes = new Uint8Array(64);
+ const snapshot = minimalSnapshot(
+ [],
+ [
+ {
+ sectionId: 's',
+ id: 'attachment',
+ kind: 'attachment',
+ filename: 'attachment.bin',
+ extension: 'bin',
+ mediaType: 'application/octet-stream',
+ browserRenderable: false,
+ bytes: resourceBytes,
+ },
+ ]
+ );
+ const resourceLimits = {
+ maxResourceBytes: 128,
+ maxTotalResourceBytes: 128,
+ };
+ const plan = createOneNoteExportPlan(snapshot, {
+ limits: { ...resourceLimits, maxOutputBytes: 1_000_000 },
+ });
+ const justTooSmall =
+ plan.projectedOutputBytes * 6 + resourceBytes.byteLength - 1;
+
+ expect(() =>
+ createOneNoteStaticNotebookZip(snapshot, {
+ limits: { ...resourceLimits, maxOutputBytes: justTooSmall },
+ })
+ ).toThrowError(
+ expect.objectContaining>({
+ code: 'limit-exceeded',
+ message: expect.stringContaining('Projected static notebook'),
+ })
+ );
+ });
+
+ it('never auto-embeds parser-disabled or oversized-pixel images', () => {
+ const imageResource = (
+ id: string,
+ bytes: Uint8Array,
+ browserRenderable = true
+ ): OneNoteExportSnapshot['resources'][number] => ({
+ sectionId: 's',
+ id,
+ kind: 'image',
+ filename: `${id}.png`,
+ extension: 'png',
+ mediaType: 'image/png',
+ browserRenderable,
+ bytes,
+ });
+ const snapshot = minimalSnapshot(
+ [],
+ [
+ imageResource('parser-disabled', PNG, false),
+ imageResource('dimension-bomb', pngWithDimensions(16_385, 1)),
+ imageResource('pixel-bomb', pngWithDimensions(8_193, 8_193)),
+ ]
+ );
+
+ const plan = createOneNoteExportPlan(snapshot);
+ expect(plan.resources).toHaveLength(0);
+ expect(plan.warnings).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ code: 'IMAGE_RESOURCE_NOT_RENDERABLE',
+ resourceId: 'parser-disabled',
+ }),
+ expect.objectContaining({
+ code: 'UNSAFE_IMAGE_RESOURCE',
+ resourceId: 'dimension-bomb',
+ message: expect.stringContaining('dimension-limit'),
+ }),
+ expect.objectContaining({
+ code: 'UNSAFE_IMAGE_RESOURCE',
+ resourceId: 'pixel-bomb',
+ message: expect.stringContaining('pixel-limit'),
+ }),
+ ])
+ );
+ });
+});
+
describe('OneNote export filenames', () => {
it('removes traversal, reserved names, controls and directional overrides', () => {
expect(sanitizeExportFilename('../../CON')).toBe('_CON');
@@ -451,6 +610,51 @@ function fixtureSnapshot(): OneNoteExportSnapshot {
});
}
+function minimalSnapshot(
+ blocks: OneNoteContentBlock[] = [],
+ resources: OneNoteExportSnapshot['resources'] = []
+): OneNoteExportSnapshot {
+ return createOneNoteExportSnapshot({
+ sourceName: 's.one',
+ sourceFormat: 'one',
+ notebookName: 'n',
+ appName: 'a',
+ appVersion: '1',
+ parser: {
+ name: 'p',
+ version: '1',
+ implementation: 't',
+ supportedVariants: [],
+ },
+ sections: [
+ {
+ id: 's',
+ name: 's',
+ pages: [
+ {
+ id: 'p',
+ title: 'p',
+ text: '',
+ textPreview: '',
+ blocks,
+ diagnostics: [],
+ },
+ ],
+ diagnostics: [],
+ },
+ ],
+ resources,
+ });
+}
+
+function pngWithDimensions(width: number, height: number): Uint8Array {
+ const bytes = PNG.slice();
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
+ view.setUint32(16, width);
+ view.setUint32(20, height);
+ return bytes;
+}
+
function textBlock(
id: string,
text: string,
diff --git a/src/onenote/export/plan.ts b/src/onenote/export/plan.ts
index 0e4c384..956d4c1 100644
--- a/src/onenote/export/plan.ts
+++ b/src/onenote/export/plan.ts
@@ -4,7 +4,15 @@
// SPDX-License-Identifier: MPL-2.0
import type { ParserDiagnostic } from '../model/diagnostics.js';
-import type { OneNoteContentBlock } from '../one/content-model.js';
+import type { OneNoteNotebookNodeDto } from '../model/dto.js';
+import {
+ DEFAULT_BROWSER_IMAGE_SAFETY_LIMITS,
+ inspectBrowserImage,
+} from '../image-safety.js';
+import type {
+ OneNoteContentBlock,
+ OneNoteTextStyle,
+} from '../one/content-model.js';
import {
filenameExtension,
forceFilenameExtension,
@@ -61,6 +69,18 @@ export interface OneNoteExportPlan {
diagnostics: ParserDiagnostic[];
blockCount: number;
pageCount: number;
+ textCharacterCount: number;
+ inkPointCount: number;
+ totalResourceBytes: number;
+ projectedOutputBytes: number;
+}
+
+interface ExportBudget {
+ textCharacterCount: number;
+ inkPointCount: number;
+ modelEntryCount: number;
+ tableCellSlotCount: number;
+ numericArrayValueCount: number;
}
export function exportResourceKey(
@@ -82,6 +102,14 @@ export function createOneNoteExportPlan(
const warnings: OneNoteExportWarning[] = [];
const unsupportedContent: OneNoteUnsupportedContentRecord[] = [];
const diagnostics = collectDiagnostics(snapshot, limits);
+ const budget: ExportBudget = {
+ textCharacterCount: 0,
+ inkPointCount: 0,
+ modelEntryCount: 1,
+ tableCellSlotCount: 0,
+ numericArrayValueCount: 0,
+ };
+ countSnapshotMetadata(snapshot, budget, limits);
const sectionIds = new Set(snapshot.sections.map(({ id }) => id));
const sectionNames = new Map();
const sectionAllocator = new SafeExportNameAllocator();
@@ -144,7 +172,14 @@ export function createOneNoteExportPlan(
resourceAllocators.get(resource.sectionId) ??
new SafeExportNameAllocator();
resourceAllocators.set(resource.sectionId, allocator);
- const planned = planResource(resource, bytes, allocator, index, warnings);
+ const planned = planResource(
+ resource,
+ bytes,
+ allocator,
+ index,
+ warnings,
+ limits
+ );
if (!planned) continue;
const sectionDirectory = sectionNames.get(resource.sectionId)!;
planned.path = `assets/${sectionDirectory}/${planned.filename}`;
@@ -179,6 +214,7 @@ export function createOneNoteExportPlan(
if (blockCount > limits.maxBlocks) {
throw limitError('content block', blockCount, limits.maxBlocks);
}
+ countBlockMetadata(block, budget, limits);
inspectBlock(
block,
section.id,
@@ -202,22 +238,340 @@ export function createOneNoteExportPlan(
diagnostics,
blockCount,
pageCount,
+ textCharacterCount: budget.textCharacterCount,
+ inkPointCount: budget.inkPointCount,
+ totalResourceBytes,
+ projectedOutputBytes: projectedOutputBytes(budget, limits),
};
}
+function countSnapshotMetadata(
+ snapshot: OneNoteExportSnapshot,
+ budget: ExportBudget,
+ limits: OneNoteExportLimits
+): void {
+ addText(
+ budget,
+ limits,
+ snapshot.app.name,
+ snapshot.app.version,
+ snapshot.parser.name,
+ snapshot.parser.version,
+ snapshot.parser.implementation,
+ snapshot.parser.referenceRevision,
+ snapshot.supportLevel,
+ snapshot.source.name,
+ snapshot.source.format,
+ snapshot.notebookName
+ );
+ for (const variant of snapshot.parser.supportedVariants) {
+ addModelEntries(budget, 1);
+ addText(budget, limits, variant);
+ }
+ for (const note of snapshot.supportNotes) {
+ addModelEntries(budget, 1);
+ addText(budget, limits, note);
+ }
+ countDiagnosticsMetadata(snapshot.diagnostics, budget, limits);
+
+ for (const section of snapshot.sections) {
+ addModelEntries(budget, 1);
+ addText(budget, limits, section.id, section.name, section.sourcePath);
+ countDiagnosticsMetadata(section.diagnostics, budget, limits);
+ for (const page of section.pages) {
+ addModelEntries(budget, 1);
+ addText(
+ budget,
+ limits,
+ page.id,
+ page.title,
+ page.createdAt,
+ page.updatedAt,
+ page.text,
+ page.textPreview
+ );
+ countDiagnosticsMetadata(page.diagnostics, budget, limits);
+ }
+ }
+
+ for (const resource of snapshot.resources) {
+ addModelEntries(budget, 1);
+ addText(
+ budget,
+ limits,
+ resource.sectionId,
+ resource.id,
+ resource.kind,
+ resource.filename,
+ resource.extension,
+ resource.mediaType
+ );
+ }
+
+ if (snapshot.tree) {
+ addModelEntries(budget, 1);
+ addText(budget, limits, snapshot.tree.tocPath);
+ countTreeNodes(
+ snapshot.tree.nodes,
+ 0,
+ new Set(),
+ budget,
+ limits
+ );
+ }
+}
+
+function countTreeNodes(
+ nodes: readonly OneNoteNotebookNodeDto[],
+ depth: number,
+ active: Set,
+ budget: ExportBudget,
+ limits: OneNoteExportLimits
+): void {
+ if (depth > limits.maxDepth) {
+ throw new OneNoteExportError(
+ 'limit-exceeded',
+ `Export notebook tree depth exceeds ${limits.maxDepth}`
+ );
+ }
+ for (const node of nodes) {
+ if (active.has(node)) {
+ throw new OneNoteExportError(
+ 'invalid-input',
+ 'Export notebook tree contains an object cycle'
+ );
+ }
+ active.add(node);
+ addModelEntries(budget, 1);
+ addText(
+ budget,
+ limits,
+ node.kind,
+ node.id,
+ node.displayName,
+ node.kind === 'section' ? node.sectionId : undefined
+ );
+ if (node.kind === 'section-group') {
+ countTreeNodes(node.children, depth + 1, active, budget, limits);
+ }
+ active.delete(node);
+ }
+}
+
+function countDiagnosticsMetadata(
+ diagnostics: readonly ParserDiagnostic[],
+ budget: ExportBudget,
+ limits: OneNoteExportLimits
+): void {
+ for (const diagnostic of diagnostics) {
+ addModelEntries(budget, 1);
+ addText(
+ budget,
+ limits,
+ diagnostic.severity,
+ diagnostic.code,
+ diagnostic.message,
+ diagnostic.structure,
+ diagnostic.propertyId
+ );
+ }
+}
+
+function countBlockMetadata(
+ block: OneNoteContentBlock,
+ budget: ExportBudget,
+ limits: OneNoteExportLimits
+): void {
+ addModelEntries(budget, 1);
+ addText(budget, limits, block.kind, block.id);
+ switch (block.kind) {
+ case 'text':
+ addText(
+ budget,
+ limits,
+ block.sourceText,
+ block.text,
+ block.paragraphAlignment
+ );
+ countTextStyle(block.paragraphStyle, budget, limits);
+ for (const run of block.runs) {
+ addModelEntries(budget, 1);
+ addText(budget, limits, run.text, run.href);
+ countTextStyle(run.style, budget, limits);
+ }
+ return;
+ case 'image':
+ addText(
+ budget,
+ limits,
+ block.resourceId,
+ block.filename,
+ block.altText,
+ block.ocrText,
+ block.href
+ );
+ return;
+ case 'attachment':
+ addText(
+ budget,
+ limits,
+ block.resourceId,
+ block.iconResourceId,
+ block.filename
+ );
+ return;
+ case 'table': {
+ addNumericArrayValues(
+ budget,
+ block.columnWidths.length + block.lockedColumns.length
+ );
+ let columnCount = 0;
+ if (block.declaredColumnCount !== undefined) {
+ if (
+ !Number.isSafeInteger(block.declaredColumnCount) ||
+ block.declaredColumnCount < 0
+ ) {
+ throw new OneNoteExportError(
+ 'invalid-input',
+ 'Export table column count must be a non-negative safe integer'
+ );
+ }
+ columnCount = block.declaredColumnCount;
+ }
+ for (const row of block.rows) {
+ addModelEntries(budget, 1);
+ addText(budget, limits, row.id);
+ columnCount = Math.max(columnCount, row.cells.length);
+ for (const cell of row.cells) {
+ addModelEntries(budget, 1);
+ addText(budget, limits, cell.id, cell.backgroundColor?.kind);
+ }
+ }
+ addTableCellSlots(budget, columnCount, block.rows.length + 2);
+ return;
+ }
+ case 'outline':
+ addNumericArrayValues(budget, block.indents.length);
+ return;
+ case 'outline-element':
+ case 'outline-group':
+ return;
+ case 'ink':
+ for (const stroke of block.strokes) {
+ addModelEntries(budget, 1);
+ addText(budget, limits, stroke.bias);
+ addInkPoints(budget, limits, stroke.points.length);
+ }
+ return;
+ case 'unsupported':
+ addText(budget, limits, block.description);
+ }
+}
+
+function countTextStyle(
+ style: OneNoteTextStyle,
+ budget: ExportBudget,
+ limits: OneNoteExportLimits
+): void {
+ addText(
+ budget,
+ limits,
+ style.font,
+ style.fontColor?.kind,
+ style.highlight?.kind
+ );
+}
+
+function addText(
+ budget: ExportBudget,
+ limits: OneNoteExportLimits,
+ ...values: Array
+): void {
+ for (const value of values) {
+ if (value === undefined) continue;
+ if (value.length > limits.maxTextCharacters - budget.textCharacterCount) {
+ throw limitError(
+ 'text character',
+ limits.maxTextCharacters + 1,
+ limits.maxTextCharacters
+ );
+ }
+ budget.textCharacterCount += value.length;
+ }
+}
+
+function addInkPoints(
+ budget: ExportBudget,
+ limits: OneNoteExportLimits,
+ count: number
+): void {
+ if (count > limits.maxInkPoints - budget.inkPointCount) {
+ throw limitError('ink point', limits.maxInkPoints + 1, limits.maxInkPoints);
+ }
+ budget.inkPointCount += count;
+}
+
+function addModelEntries(budget: ExportBudget, count: number): void {
+ budget.modelEntryCount = saturatingAdd(budget.modelEntryCount, count);
+}
+
+function addNumericArrayValues(budget: ExportBudget, count: number): void {
+ budget.numericArrayValueCount = saturatingAdd(
+ budget.numericArrayValueCount,
+ count
+ );
+}
+
+function addTableCellSlots(
+ budget: ExportBudget,
+ columns: number,
+ rows: number
+): void {
+ budget.tableCellSlotCount = saturatingAdd(
+ budget.tableCellSlotCount,
+ saturatingMultiply(columns, rows)
+ );
+}
+
function planResource(
resource: OneNoteExportResource,
bytes: Uint8Array,
allocator: SafeExportNameAllocator,
index: number,
- warnings: OneNoteExportWarning[]
+ warnings: OneNoteExportWarning[],
+ limits: OneNoteExportLimits
): PlannedOneNoteResource | undefined {
if (resource.kind === 'image') {
- const image = sniffBrowserImage(bytes);
- if (!image) {
+ if (!resource.browserRenderable) {
warnings.push({
- code: 'UNSUPPORTED_IMAGE_RESOURCE',
- message: 'Only signature-verified PNG and JPEG images are exported',
+ code: 'IMAGE_RESOURCE_NOT_RENDERABLE',
+ message:
+ 'Image resource was omitted because the parser did not mark it safe for browser rendering',
+ sectionId: resource.sectionId,
+ resourceId: resource.id,
+ });
+ return undefined;
+ }
+ const inspected = inspectBrowserImage(bytes, {
+ maxBytes: Math.min(
+ limits.maxResourceBytes,
+ DEFAULT_BROWSER_IMAGE_SAFETY_LIMITS.maxBytes
+ ),
+ maxDimension: DEFAULT_BROWSER_IMAGE_SAFETY_LIMITS.maxDimension,
+ maxPixels: DEFAULT_BROWSER_IMAGE_SAFETY_LIMITS.maxPixels,
+ });
+ if (
+ !inspected.browserRenderable ||
+ !inspected.extension ||
+ !inspected.mediaType
+ ) {
+ const reason = inspected.rejectionReason ?? 'invalid-header';
+ warnings.push({
+ code:
+ reason === 'unsupported-format'
+ ? 'UNSUPPORTED_IMAGE_RESOURCE'
+ : 'UNSAFE_IMAGE_RESOURCE',
+ message: `Image resource was omitted by bounded inspection (${reason})`,
sectionId: resource.sectionId,
resourceId: resource.id,
});
@@ -225,11 +579,11 @@ function planResource(
}
const safe = sanitizeExportFilename(
resource.filename,
- `image-${index + 1}.${image.extension}`
+ `image-${index + 1}.${inspected.extension}`
);
const filename = allocator.allocate(
- forceFilenameExtension(safe, image.extension),
- `image-${index + 1}.${image.extension}`
+ forceFilenameExtension(safe, inspected.extension),
+ `image-${index + 1}.${inspected.extension}`
);
return {
sectionId: resource.sectionId,
@@ -237,8 +591,8 @@ function planResource(
kind: resource.kind,
path: '',
filename,
- extension: image.extension,
- mediaType: image.mediaType,
+ extension: inspected.extension,
+ mediaType: inspected.mediaType,
browserRenderable: true,
size: bytes.byteLength,
bytes,
@@ -382,35 +736,6 @@ function collectDiagnostics(
}
}
-function sniffBrowserImage(
- bytes: Uint8Array
-):
- | { extension: 'png' | 'jpg'; mediaType: 'image/png' | 'image/jpeg' }
- | undefined {
- if (
- bytes.length >= 8 &&
- bytes[0] === 0x89 &&
- bytes[1] === 0x50 &&
- bytes[2] === 0x4e &&
- bytes[3] === 0x47 &&
- bytes[4] === 0x0d &&
- bytes[5] === 0x0a &&
- bytes[6] === 0x1a &&
- bytes[7] === 0x0a
- ) {
- return { extension: 'png', mediaType: 'image/png' };
- }
- if (
- bytes.length >= 3 &&
- bytes[0] === 0xff &&
- bytes[1] === 0xd8 &&
- bytes[2] === 0xff
- ) {
- return { extension: 'jpg', mediaType: 'image/jpeg' };
- }
- return undefined;
-}
-
function safeMediaType(value: string): string {
return /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/iu.test(
value
@@ -419,6 +744,55 @@ function safeMediaType(value: string): string {
: 'application/octet-stream';
}
+/** Enforce the exact UTF-8 size of one completed standalone member. */
+export function enforceSerializedTextOutput(
+ value: string,
+ plan: OneNoteExportPlan,
+ subject: string
+): string {
+ enforceOutputByteLength(plan, utf8ByteLength(value), subject);
+ return value;
+}
+
+export function enforceOutputByteLength(
+ plan: OneNoteExportPlan,
+ byteLength: number,
+ subject: string
+): void {
+ if (byteLength > plan.limits.maxOutputBytes) {
+ throw new OneNoteExportError(
+ 'limit-exceeded',
+ `${subject} is ${byteLength} UTF-8 bytes; the export limit is ${plan.limits.maxOutputBytes}`
+ );
+ }
+}
+
+/** Exact TextEncoder-compatible byte length without allocating encoded bytes. */
+export function utf8ByteLength(value: string): number {
+ let bytes = 0;
+ for (let index = 0; index < value.length; index += 1) {
+ const code = value.charCodeAt(index);
+ if (code <= 0x7f) {
+ bytes += 1;
+ } else if (code <= 0x7ff) {
+ bytes += 2;
+ } else if (
+ code >= 0xd800 &&
+ code <= 0xdbff &&
+ index + 1 < value.length &&
+ value.charCodeAt(index + 1) >= 0xdc00 &&
+ value.charCodeAt(index + 1) <= 0xdfff
+ ) {
+ bytes += 4;
+ index += 1;
+ } else {
+ // TextEncoder replaces unpaired surrogates with U+FFFD (three bytes).
+ bytes += 3;
+ }
+ }
+ return bytes;
+}
+
function resourceBytes(value: ArrayBuffer | Uint8Array): Uint8Array {
return value instanceof Uint8Array ? value : new Uint8Array(value);
}
@@ -438,6 +812,80 @@ function resolveExportLimits(
return result;
}
+function projectedOutputBytes(
+ budget: ExportBudget,
+ limits: OneNoteExportLimits
+): number {
+ let result = 8 * 1024;
+ result = addProjectedBytes(
+ result,
+ budget.textCharacterCount,
+ 12,
+ limits.maxOutputBytes
+ );
+ result = addProjectedBytes(
+ result,
+ budget.inkPointCount,
+ 96,
+ limits.maxOutputBytes
+ );
+ result = addProjectedBytes(
+ result,
+ budget.modelEntryCount,
+ 512,
+ limits.maxOutputBytes
+ );
+ result = addProjectedBytes(
+ result,
+ budget.tableCellSlotCount,
+ 64,
+ limits.maxOutputBytes
+ );
+ result = addProjectedBytes(
+ result,
+ budget.numericArrayValueCount,
+ 32,
+ limits.maxOutputBytes
+ );
+ if (result > limits.maxOutputBytes) {
+ throw new OneNoteExportError(
+ 'limit-exceeded',
+ `Projected standalone export output exceeds ${limits.maxOutputBytes} bytes`
+ );
+ }
+ return result;
+}
+
+function addProjectedBytes(
+ current: number,
+ count: number,
+ bytesPerItem: number,
+ limit: number
+): number {
+ if (current > limit || count > Math.floor((limit - current) / bytesPerItem)) {
+ return Number.POSITIVE_INFINITY;
+ }
+ return current + count * bytesPerItem;
+}
+
+function saturatingAdd(left: number, right: number): number {
+ return !Number.isSafeInteger(right) ||
+ right < 0 ||
+ left > Number.MAX_SAFE_INTEGER - right
+ ? Number.POSITIVE_INFINITY
+ : left + right;
+}
+
+function saturatingMultiply(left: number, right: number): number {
+ return !Number.isSafeInteger(left) ||
+ !Number.isSafeInteger(right) ||
+ left < 0 ||
+ right < 0 ||
+ (left !== 0 && right > Number.MAX_SAFE_INTEGER / left)
+ ? Number.POSITIVE_INFINITY
+ : left * right;
+}
+
function limitError(
subject: string,
actual: number,
diff --git a/src/onenote/export/serializers.ts b/src/onenote/export/serializers.ts
index 69967f4..b56c287 100644
--- a/src/onenote/export/serializers.ts
+++ b/src/onenote/export/serializers.ts
@@ -13,6 +13,7 @@ import type {
} from '../one/content-model.js';
import {
createOneNoteExportPlan,
+ enforceSerializedTextOutput,
exportResourceKey,
type OneNoteExportPlan,
type PlannedOneNoteResource,
@@ -91,7 +92,11 @@ export function serializeStructuredJsonWithPlan(
warnings: plan.warnings,
unsupportedContent: plan.unsupportedContent,
};
- return `${JSON.stringify(value, null, 2)}\n`;
+ return enforceSerializedTextOutput(
+ `${JSON.stringify(value, null, 2)}\n`,
+ plan,
+ 'Structured JSON export'
+ );
}
export function serializePlainTextWithPlan(
@@ -138,10 +143,14 @@ export function serializePlainTextWithPlan(
}
}
appendPlainWarnings(lines, plan);
- return `${lines
- .join('\n')
- .replace(/\n{4,}/gu, '\n\n\n')
- .trimEnd()}\n`;
+ return enforceSerializedTextOutput(
+ `${lines
+ .join('\n')
+ .replace(/\n{4,}/gu, '\n\n\n')
+ .trimEnd()}\n`,
+ plan,
+ 'Plain-text export'
+ );
}
export function serializeMarkdownWithPlan(
@@ -192,10 +201,14 @@ export function serializeMarkdownWithPlan(
}
}
appendMarkdownWarnings(lines, plan);
- return `${lines
- .join('\n')
- .replace(/\n{4,}/gu, '\n\n\n')
- .trimEnd()}\n`;
+ return enforceSerializedTextOutput(
+ `${lines
+ .join('\n')
+ .replace(/\n{4,}/gu, '\n\n\n')
+ .trimEnd()}\n`,
+ plan,
+ 'Markdown export'
+ );
}
export function serializeHtmlWithPlan(
@@ -214,7 +227,8 @@ export function serializeHtmlWithPlan(
.map((section) => htmlSection(section, context))
.join('\n');
const warnings = htmlWarnings(plan);
- return `
+ return enforceSerializedTextOutput(
+ `
@@ -246,7 +260,10 @@ ${sections}
${warnings}
-`;
+`,
+ plan,
+ 'Semantic HTML export'
+ );
}
function htmlSection(
diff --git a/src/onenote/export/types.ts b/src/onenote/export/types.ts
index 21d6903..8004972 100644
--- a/src/onenote/export/types.ts
+++ b/src/onenote/export/types.ts
@@ -82,8 +82,14 @@ export interface OneNoteExportLimits {
maxBlocks: number;
maxDepth: number;
maxDiagnostics: number;
+ /** Aggregate UTF-16 code units across all snapshot metadata and content. */
+ maxTextCharacters: number;
+ /** Aggregate point records across all ink strokes. */
+ maxInkPoints: number;
maxResourceBytes: number;
maxTotalResourceBytes: number;
+ /** UTF-8 bytes for one standalone export or all uncompressed ZIP members. */
+ maxOutputBytes: number;
}
export const DEFAULT_ONENOTE_EXPORT_LIMITS: Readonly = {
@@ -92,8 +98,11 @@ export const DEFAULT_ONENOTE_EXPORT_LIMITS: Readonly = {
maxBlocks: 1_000_000,
maxDepth: 256,
maxDiagnostics: 100_000,
- maxResourceBytes: 256 * 1024 * 1024,
- maxTotalResourceBytes: 512 * 1024 * 1024,
+ maxTextCharacters: 8_000_000,
+ maxInkPoints: 500_000,
+ maxResourceBytes: 64 * 1024 * 1024,
+ maxTotalResourceBytes: 64 * 1024 * 1024,
+ maxOutputBytes: 128 * 1024 * 1024,
};
export interface OneNoteExportOptions {
diff --git a/src/onenote/export/zip.ts b/src/onenote/export/zip.ts
index 8fa0fbc..b44ed64 100644
--- a/src/onenote/export/zip.ts
+++ b/src/onenote/export/zip.ts
@@ -5,14 +5,23 @@
import { strToU8, zipSync, type Zippable } from 'fflate';
import { forceFilenameExtension, sanitizeExportFilename } from './naming.js';
-import { createOneNoteExportPlan, type OneNoteExportPlan } from './plan.js';
+import {
+ createOneNoteExportPlan,
+ enforceOutputByteLength,
+ utf8ByteLength,
+ type OneNoteExportPlan,
+} from './plan.js';
import {
serializeHtmlWithPlan,
serializeMarkdownWithPlan,
serializePlainTextWithPlan,
serializeStructuredJsonWithPlan,
} from './serializers.js';
-import type { OneNoteExportOptions, OneNoteExportSnapshot } from './types.js';
+import {
+ OneNoteExportError,
+ type OneNoteExportOptions,
+ type OneNoteExportSnapshot,
+} from './types.js';
export interface OneNoteStaticNotebookManifest {
schemaVersion: number;
@@ -83,6 +92,7 @@ export function createOneNoteStaticNotebookZip(
options: OneNoteExportOptions = {}
): OneNoteStaticNotebookZipArtifact {
const plan = createOneNoteExportPlan(snapshot, options);
+ enforceProjectedStaticNotebookSize(plan);
const manifest = createStaticManifest(snapshot, plan);
const warnings: OneNoteExportWarningsManifest = {
schemaVersion: snapshot.schemaVersion,
@@ -96,17 +106,26 @@ export function createOneNoteStaticNotebookZip(
};
const files: Zippable = Object.create(null) as Zippable;
- files['index.html'] = strToU8(serializeHtmlWithPlan(snapshot, plan, true));
- files['notebook.json'] = strToU8(
+ const members: Array<{ path: string; byteLength: number }> = [];
+ let memberBytes = 0;
+ addTextMember('index.html', serializeHtmlWithPlan(snapshot, plan, true));
+ addTextMember(
+ 'notebook.json',
serializeStructuredJsonWithPlan(snapshot, plan)
);
- files['notebook.txt'] = strToU8(serializePlainTextWithPlan(snapshot, plan));
- files['notebook.md'] = strToU8(
- serializeMarkdownWithPlan(snapshot, plan, true)
+ addTextMember('notebook.txt', serializePlainTextWithPlan(snapshot, plan));
+ addTextMember('notebook.md', serializeMarkdownWithPlan(snapshot, plan, true));
+ addTextMember('manifest.json', jsonText(manifest));
+ addTextMember('warnings.json', jsonText(warnings));
+ for (const resource of plan.resources) {
+ addBinaryMember(resource.path, resource.bytes);
+ }
+
+ enforceOutputByteLength(
+ plan,
+ projectedZipByteLength(members),
+ 'Projected static notebook ZIP'
);
- files['manifest.json'] = jsonBytes(manifest);
- files['warnings.json'] = jsonBytes(warnings);
- for (const resource of plan.resources) files[resource.path] = resource.bytes;
const bytes = zipSync(files, {
level: 6,
@@ -114,6 +133,7 @@ export function createOneNoteStaticNotebookZip(
os: 3,
attrs: 0o644 << 16,
});
+ enforceOutputByteLength(plan, bytes.byteLength, 'Static notebook ZIP');
const safeName = sanitizeExportFilename(
snapshot.notebookName,
'onenote-export'
@@ -124,6 +144,37 @@ export function createOneNoteStaticNotebookZip(
bytes,
manifest,
};
+
+ function addTextMember(path: string, value: string): void {
+ ensureMemberCapacity(utf8ByteLength(value));
+ const bytes = strToU8(value);
+ addMember(path, bytes, bytes.byteLength);
+ }
+
+ function addBinaryMember(path: string, value: Uint8Array): void {
+ ensureMemberCapacity(value.byteLength);
+ addMember(path, value, value.byteLength);
+ }
+
+ function addMember(
+ path: string,
+ value: Uint8Array,
+ byteLength: number
+ ): void {
+ ensureMemberCapacity(byteLength);
+ memberBytes += byteLength;
+ members.push({ path, byteLength });
+ files[path] = value;
+ }
+
+ function ensureMemberCapacity(byteLength: number): void {
+ if (byteLength > plan.limits.maxOutputBytes - memberBytes) {
+ throw new OneNoteExportError(
+ 'limit-exceeded',
+ `Static notebook members exceed ${plan.limits.maxOutputBytes} bytes`
+ );
+ }
+ }
}
function createStaticManifest(
@@ -173,6 +224,48 @@ function createStaticManifest(
};
}
-function jsonBytes(value: unknown): Uint8Array {
- return strToU8(`${JSON.stringify(value, null, 2)}\n`);
+function jsonText(value: unknown): string {
+ return `${JSON.stringify(value, null, 2)}\n`;
+}
+
+function enforceProjectedStaticNotebookSize(plan: OneNoteExportPlan): void {
+ const available = plan.limits.maxOutputBytes - plan.totalResourceBytes;
+ if (available < 0 || plan.projectedOutputBytes > Math.floor(available / 6)) {
+ throw new OneNoteExportError(
+ 'limit-exceeded',
+ `Projected static notebook members exceed ${plan.limits.maxOutputBytes} bytes`
+ );
+ }
+}
+
+function projectedZipByteLength(
+ members: readonly { path: string; byteLength: number }[]
+): number {
+ let result = 22;
+ for (const member of members) {
+ const pathBytes = utf8ByteLength(member.path);
+ const deflateOverhead = Math.ceil(member.byteLength / 16_384) * 16 + 128;
+ result = safeByteSum(
+ result,
+ member.byteLength,
+ deflateOverhead,
+ 76,
+ pathBytes * 2
+ );
+ }
+ return result;
+}
+
+function safeByteSum(...values: number[]): number {
+ let total = 0;
+ for (const value of values) {
+ if (!Number.isSafeInteger(value) || value < 0) {
+ return Number.POSITIVE_INFINITY;
+ }
+ if (total > Number.MAX_SAFE_INTEGER - value) {
+ return Number.POSITIVE_INFINITY;
+ }
+ total += value;
+ }
+ return total;
}
diff --git a/src/worker/export-adapter.test.ts b/src/worker/export-adapter.test.ts
index 12d5e12..85b8572 100644
--- a/src/worker/export-adapter.test.ts
+++ b/src/worker/export-adapter.test.ts
@@ -16,10 +16,44 @@ import {
} from './export-adapter.js';
import { workerPageKey, workerResourceKey } from './parser-adapter.js';
-const PNG = Uint8Array.of(0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00);
+const PNG = Uint8Array.of(
+ 0x89,
+ 0x50,
+ 0x4e,
+ 0x47,
+ 0x0d,
+ 0x0a,
+ 0x1a,
+ 0x0a,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x0d,
+ 0x49,
+ 0x48,
+ 0x44,
+ 0x52,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x01,
+ 0x08,
+ 0x06,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00
+);
describe('worker export adapter', () => {
- it('builds a partial section snapshot from full session pages and copied resources', () => {
+ it('builds a partial section snapshot with bounded zero-copy resources', () => {
const pageDiagnostic = diagnostic('page-warning', 'page page-1');
const sectionDiagnostic = diagnostic('section-warning');
const source = sectionDto('Unsafe/Section.one', 'Section name', [
@@ -80,21 +114,46 @@ describe('worker export adapter', () => {
mediaType: 'image/png',
size: PNG.length,
});
- expect(new Uint8Array(snapshot.resources[0]!.bytes)).toEqual(PNG);
+ const imageBytes = snapshot.resources[0]!.bytes;
+ expect(imageBytes).toBeInstanceOf(Uint8Array);
+ if (!(imageBytes instanceof Uint8Array)) {
+ throw new Error('Expected a zero-copy image view');
+ }
+ expect(imageBytes).toEqual(PNG);
+ expect(imageBytes.buffer).toBe(resourceSource.buffer);
expect(snapshot.resources[1]).toMatchObject({
sectionId: 'section',
id: 'attachment-1',
kind: 'attachment',
size: 2,
});
- expect(new Uint8Array(snapshot.resources[1]!.bytes)).toEqual(
- Uint8Array.of(5, 4)
- );
- resourceSource.fill(0);
- attachmentSource.fill(0);
- expect(new Uint8Array(snapshot.resources[0]!.bytes)).toEqual(PNG);
- expect(new Uint8Array(snapshot.resources[1]!.bytes)).toEqual(
- Uint8Array.of(5, 4)
+ const attachmentBytes = snapshot.resources[1]!.bytes;
+ expect(attachmentBytes).toBeInstanceOf(Uint8Array);
+ if (!(attachmentBytes instanceof Uint8Array)) {
+ throw new Error('Expected a zero-copy attachment view');
+ }
+ expect(attachmentBytes).toEqual(Uint8Array.of(5, 4));
+ expect(attachmentBytes.buffer).toBe(attachmentSource.buffer);
+ attachmentSource[1] = 8;
+ expect(attachmentBytes[0]).toBe(8);
+ });
+
+ it('caps synthesized missing-page diagnostics without variadic appends', () => {
+ const source = sectionDto('Section.one', 'Section', []);
+ source.pages = [
+ { id: 'missing-1', title: 'Missing 1', text: '', textPreview: '' },
+ { id: 'missing-2', title: 'Missing 2', text: '', textPreview: '' },
+ ];
+
+ expect(() =>
+ createWorkerExportSnapshot(source, new Map(), new Map(), {
+ limits: { maxDiagnostics: 1 },
+ })
+ ).toThrowError(
+ expect.objectContaining({
+ code: 'limit-exceeded',
+ message: expect.stringContaining('diagnostic count'),
+ })
);
});
diff --git a/src/worker/export-adapter.ts b/src/worker/export-adapter.ts
index 9b3b672..4045ad2 100644
--- a/src/worker/export-adapter.ts
+++ b/src/worker/export-adapter.ts
@@ -6,7 +6,9 @@
import {
createOneNoteExportSnapshot,
createOneNoteStaticNotebookZip,
+ DEFAULT_ONENOTE_EXPORT_LIMITS,
forceFilenameExtension,
+ OneNoteExportError,
sanitizeExportFilename,
serializeOneNoteMarkdown,
serializeOneNotePlainText,
@@ -26,11 +28,7 @@ import type {
OneNoteSectionDto,
} from '../onenote/model/dto.js';
import type { OneNoteResource } from '../onenote/one/content-model.js';
-import {
- resourcePayload,
- workerPageKey,
- workerResourceKey,
-} from './parser-adapter.js';
+import { workerPageKey, workerResourceKey } from './parser-adapter.js';
export type WorkerExportFormat =
'static-zip' | 'json' | 'text' | 'markdown' | 'html';
@@ -57,11 +55,13 @@ interface CollectedPages {
export function createWorkerExportSnapshot(
source: OneNoteSectionDto | OneNotePackageDto,
pages: ReadonlyMap,
- resources: ReadonlyMap
+ resources: ReadonlyMap,
+ options: OneNoteExportOptions = {}
): OneNoteExportSnapshot {
+ const maxDiagnostics = diagnosticLimit(options);
return source.format === 'one'
- ? sectionSnapshot(source, pages, resources)
- : packageSnapshot(source, pages, resources);
+ ? sectionSnapshot(source, pages, resources, maxDiagnostics)
+ : packageSnapshot(source, pages, resources, maxDiagnostics);
}
/** Serialize one parser session into a browser-downloadable export artifact. */
@@ -72,13 +72,18 @@ export function createWorkerExportArtifact(
format: WorkerExportFormat,
options: OneNoteExportOptions = {}
): WorkerExportArtifact {
- const snapshot = createWorkerExportSnapshot(source, pages, resources);
+ const snapshot = createWorkerExportSnapshot(
+ source,
+ pages,
+ resources,
+ options
+ );
if (format === 'static-zip') {
const artifact = createOneNoteStaticNotebookZip(snapshot, options);
return {
filename: artifact.filename,
mediaType: artifact.mediaType,
- bytes: copyToArrayBuffer(artifact.bytes),
+ bytes: transferableArrayBuffer(artifact.bytes),
};
}
@@ -90,7 +95,7 @@ export function createWorkerExportArtifact(
return {
filename,
mediaType: serialization.mediaType,
- bytes: copyToArrayBuffer(
+ bytes: transferableArrayBuffer(
new TextEncoder().encode(serialization.serialize(snapshot, options))
),
};
@@ -132,9 +137,16 @@ const TEXT_EXPORTS: Record<
function sectionSnapshot(
source: OneNoteSectionDto,
sessionPages: ReadonlyMap,
- sessionResources: ReadonlyMap
+ sessionResources: ReadonlyMap,
+ maxDiagnostics: number
): OneNoteExportSnapshot {
- const collected = collectPages(source.pages, sessionPages);
+ const collected = collectPages(
+ source.pages,
+ sessionPages,
+ undefined,
+ undefined,
+ maxDiagnostics
+ );
const context: ExportedSectionContext = {
parserSection: source,
exportSection: {
@@ -170,13 +182,23 @@ function sectionSnapshot(
function packageSnapshot(
source: OneNotePackageDto,
sessionPages: ReadonlyMap,
- sessionResources: ReadonlyMap
+ sessionResources: ReadonlyMap,
+ maxDiagnostics: number
): OneNoteExportSnapshot {
const contexts: ExportedSectionContext[] = [];
- const globalDiagnostics = [
- ...source.diagnostics,
- ...source.sections.flatMap((section) => section.diagnostics),
- ];
+ const globalDiagnostics: ParserDiagnostic[] = [];
+ appendDiagnosticsBounded(
+ globalDiagnostics,
+ source.diagnostics,
+ maxDiagnostics
+ );
+ for (const section of source.sections) {
+ appendDiagnosticsBounded(
+ globalDiagnostics,
+ section.diagnostics,
+ maxDiagnostics
+ );
+ }
let missingPageCount = 0;
for (const packaged of source.sections) {
@@ -185,9 +207,14 @@ function packageSnapshot(
packaged.section.pages,
sessionPages,
packaged.id,
- packaged.path
+ packaged.path,
+ maxDiagnostics
+ );
+ appendDiagnosticsBounded(
+ globalDiagnostics,
+ collected.diagnostics,
+ maxDiagnostics
);
- globalDiagnostics.push(...collected.diagnostics);
missingPageCount += collected.missingCount;
contexts.push({
sessionSectionId: packaged.id,
@@ -261,7 +288,8 @@ function collectPages(
summaries: readonly OneNotePageSummaryDto[],
sessionPages: ReadonlyMap,
sessionSectionId?: string,
- sectionPath?: string
+ sectionPath?: string,
+ maxDiagnostics = DEFAULT_ONENOTE_EXPORT_LIMITS.maxDiagnostics
): CollectedPages {
const fullPages: OneNotePageDto[] = [];
const diagnostics: ParserDiagnostic[] = [];
@@ -271,13 +299,17 @@ function collectPages(
fullPages.push(page);
continue;
}
- diagnostics.push({
- severity: 'warning',
- code: 'export-page-unavailable',
- message: `Page ${summary.title || summary.id} was omitted because its full parsed content is no longer available in this session.`,
- structure: `${sectionPath ? `${sectionPath}/` : ''}page ${summary.id}`,
- recoverable: true,
- });
+ appendDiagnosticBounded(
+ diagnostics,
+ {
+ severity: 'warning',
+ code: 'export-page-unavailable',
+ message: `Page ${summary.title || summary.id} was omitted because its full parsed content is no longer available in this session.`,
+ structure: `${sectionPath ? `${sectionPath}/` : ''}page ${summary.id}`,
+ recoverable: true,
+ },
+ maxDiagnostics
+ );
}
return {
pages: fullPages,
@@ -291,9 +323,11 @@ function subtractNestedDiagnostics(
pages: readonly OneNotePageDto[]
): ParserDiagnostic[] {
const nested = new Map();
- for (const diagnostic of pages.flatMap((page) => page.diagnostics)) {
- const key = diagnosticKey(diagnostic);
- nested.set(key, (nested.get(key) ?? 0) + 1);
+ for (const page of pages) {
+ for (const diagnostic of page.diagnostics) {
+ const key = diagnosticKey(diagnostic);
+ nested.set(key, (nested.get(key) ?? 0) + 1);
+ }
}
return sectionDiagnostics.filter((diagnostic) => {
const key = diagnosticKey(diagnostic);
@@ -304,6 +338,43 @@ function subtractNestedDiagnostics(
});
}
+function diagnosticLimit(options: OneNoteExportOptions): number {
+ const value =
+ options.limits?.maxDiagnostics ??
+ DEFAULT_ONENOTE_EXPORT_LIMITS.maxDiagnostics;
+ if (!Number.isSafeInteger(value) || value <= 0) {
+ throw new OneNoteExportError(
+ 'invalid-input',
+ 'maxDiagnostics must be a positive safe integer'
+ );
+ }
+ return value;
+}
+
+function appendDiagnosticsBounded(
+ target: ParserDiagnostic[],
+ values: readonly ParserDiagnostic[],
+ limit: number
+): void {
+ for (const diagnostic of values) {
+ appendDiagnosticBounded(target, diagnostic, limit);
+ }
+}
+
+function appendDiagnosticBounded(
+ target: ParserDiagnostic[],
+ diagnostic: ParserDiagnostic,
+ limit: number
+): void {
+ if (target.length >= limit) {
+ throw new OneNoteExportError(
+ 'limit-exceeded',
+ `Export diagnostic count exceeds ${limit}`
+ );
+ }
+ target.push(diagnostic);
+}
+
function diagnosticKey(diagnostic: ParserDiagnostic): string {
return JSON.stringify([
diagnostic.severity,
@@ -329,23 +400,43 @@ function collectResources(
if (sessionSectionId === INVALID_RESOURCE_SECTION) continue;
const context = sectionBySessionId.get(sessionSectionId);
if (!context) continue;
- const payload = resourcePayload(resource);
- if (!payload) continue;
+ const blob = boundedResourceBlob(resource);
+ if (!blob) continue;
result.push({
sectionId: context.exportSection.id,
- id: payload.id,
- kind: payload.kind,
- filename: payload.filename,
- extension: payload.extension,
- mediaType: payload.mediaType,
- browserRenderable: payload.browserRenderable,
- size: payload.size,
- bytes: payload.bytes,
+ id: resource.id,
+ kind: resource.kind,
+ filename: resource.filename,
+ extension: resource.extension,
+ mediaType: resource.mediaType,
+ browserRenderable: resource.browserRenderable,
+ size: blob.byteLength,
+ // The export planner applies per-resource and aggregate limits before
+ // any serializer or ZIP compressor copies this bounded source view.
+ bytes: blob,
});
}
return result;
}
+function boundedResourceBlob(
+ resource: OneNoteResource
+): Uint8Array | undefined {
+ const blob = resource.blob;
+ if (
+ resource.availability !== 'available' ||
+ !blob ||
+ !Number.isSafeInteger(blob.offset) ||
+ !Number.isSafeInteger(blob.size) ||
+ blob.offset < 0 ||
+ blob.size < 0 ||
+ blob.offset > blob.source.byteLength - blob.size
+ ) {
+ return undefined;
+ }
+ return blob.source.subarray(blob.offset, blob.offset + blob.size);
+}
+
const INVALID_RESOURCE_SECTION = Symbol('invalid-resource-section');
function resourceSectionId(
@@ -390,7 +481,14 @@ function unique(values: readonly string[]): string[] {
return [...new Set(values.filter((value) => value.trim() !== ''))];
}
-function copyToArrayBuffer(bytes: Uint8Array): ArrayBuffer {
+function transferableArrayBuffer(bytes: Uint8Array): ArrayBuffer {
+ if (
+ bytes.buffer instanceof ArrayBuffer &&
+ bytes.byteOffset === 0 &&
+ bytes.byteLength === bytes.buffer.byteLength
+ ) {
+ return bytes.buffer;
+ }
const result = new ArrayBuffer(bytes.byteLength);
new Uint8Array(result).set(bytes);
return result;