diff --git a/src/onenote/image-safety.test.ts b/src/onenote/image-safety.test.ts new file mode 100644 index 0000000..3d1c640 --- /dev/null +++ b/src/onenote/image-safety.test.ts @@ -0,0 +1,159 @@ +import { describe, expect, it } from 'vitest'; + +import { + DEFAULT_BROWSER_IMAGE_SAFETY_LIMITS, + inspectBrowserImage, +} from './image-safety.js'; + +describe('browser image safety inspection', () => { + it('accepts bounded PNG and JPEG dimensions', () => { + expect(inspectBrowserImage(png(640, 480))).toMatchObject({ + format: 'png', + extension: 'png', + mediaType: 'image/png', + width: 640, + height: 480, + pixels: 307_200, + browserRenderable: true, + }); + expect(inspectBrowserImage(jpeg(1_920, 1_080))).toMatchObject({ + format: 'jpeg', + extension: 'jpg', + mediaType: 'image/jpeg', + width: 1_920, + height: 1_080, + pixels: 2_073_600, + browserRenderable: true, + }); + }); + + it('rejects huge PNG and JPEG dimensions without losing their type', () => { + expect(inspectBrowserImage(png(0xffff_ffff, 1))).toMatchObject({ + format: 'png', + mediaType: 'image/png', + width: 0xffff_ffff, + height: 1, + browserRenderable: false, + rejectionReason: 'dimension-limit', + }); + expect(inspectBrowserImage(jpeg(0xffff, 0xffff))).toMatchObject({ + format: 'jpeg', + mediaType: 'image/jpeg', + width: 0xffff, + height: 0xffff, + browserRenderable: false, + rejectionReason: 'dimension-limit', + }); + }); + + it('enforces decoded pixel and compressed-byte limits independently', () => { + expect( + inspectBrowserImage(png(8_192, 8_193), { + ...DEFAULT_BROWSER_IMAGE_SAFETY_LIMITS, + maxDimension: 10_000, + }) + ).toMatchObject({ + pixels: 67_117_056, + browserRenderable: false, + rejectionReason: 'pixel-limit', + }); + expect( + inspectBrowserImage(png(1, 1), { + ...DEFAULT_BROWSER_IMAGE_SAFETY_LIMITS, + maxBytes: 32, + }) + ).toMatchObject({ + width: 1, + height: 1, + browserRenderable: false, + rejectionReason: 'byte-limit', + }); + }); + + it('rejects malformed PNG IHDR and JPEG marker streams', () => { + const malformedPng = png(16, 16); + malformedPng[11] = 12; + const malformedJpeg = jpeg(16, 16); + malformedJpeg[4] = 0; + malformedJpeg[5] = 1; + + expect(inspectBrowserImage(malformedPng)).toMatchObject({ + format: 'png', + browserRenderable: false, + rejectionReason: 'invalid-header', + }); + expect(inspectBrowserImage(malformedJpeg)).toMatchObject({ + format: 'jpeg', + browserRenderable: false, + rejectionReason: 'invalid-header', + }); + expect(inspectBrowserImage(Uint8Array.of(0xff, 0xd8, 0xff))).toMatchObject({ + format: 'jpeg', + browserRenderable: false, + rejectionReason: 'invalid-header', + }); + }); + + it('caps a JPEG marker walk that never reaches a frame header', () => { + const bytes = new Uint8Array(2 + 5_000 * 2); + bytes.set([0xff, 0xd8], 0); + for (let offset = 2; offset < bytes.length; offset += 2) { + bytes[offset] = 0xff; + bytes[offset + 1] = 0xd0; + } + + expect(inspectBrowserImage(bytes)).toMatchObject({ + format: 'jpeg', + browserRenderable: false, + rejectionReason: 'invalid-header', + }); + }); +}); + +function png(width: number, height: number): Uint8Array { + const bytes = new Uint8Array(33); + bytes.set([0x89, 0x50, 0x4e, 0x47, 13, 10, 26, 10], 0); + writeU32Be(bytes, 8, 13); + bytes.set([0x49, 0x48, 0x44, 0x52], 12); + writeU32Be(bytes, 16, width); + writeU32Be(bytes, 20, height); + bytes.set([8, 6, 0, 0, 0], 24); + return bytes; +} + +function jpeg(width: number, height: number): Uint8Array { + return Uint8Array.of( + 0xff, + 0xd8, + 0xff, + 0xe0, + 0x00, + 0x02, + 0xff, + 0xc0, + 0x00, + 0x11, + 0x08, + (height >>> 8) & 0xff, + height & 0xff, + (width >>> 8) & 0xff, + width & 0xff, + 0x03, + 0x01, + 0x11, + 0x00, + 0x02, + 0x11, + 0x00, + 0x03, + 0x11, + 0x00 + ); +} + +function writeU32Be(bytes: Uint8Array, offset: number, value: number): void { + bytes[offset] = (value >>> 24) & 0xff; + bytes[offset + 1] = (value >>> 16) & 0xff; + bytes[offset + 2] = (value >>> 8) & 0xff; + bytes[offset + 3] = value & 0xff; +} diff --git a/src/onenote/image-safety.ts b/src/onenote/image-safety.ts new file mode 100644 index 0000000..5101cc0 --- /dev/null +++ b/src/onenote/image-safety.ts @@ -0,0 +1,273 @@ +// 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/. + +export interface BrowserImageSafetyLimits { + /** Maximum compressed resource size eligible for automatic rendering. */ + maxBytes: number; + /** Maximum supported width or height in pixels. */ + maxDimension: number; + /** Maximum supported decoded pixel count. */ + maxPixels: number; +} + +export const DEFAULT_BROWSER_IMAGE_SAFETY_LIMITS: Readonly = + { + maxBytes: 32 * 1024 * 1024, + maxDimension: 16_384, + maxPixels: 64 * 1024 * 1024, + }; + +export type BrowserImageRejectionReason = + | 'unsupported-format' + | 'byte-limit' + | 'invalid-header' + | 'dimension-limit' + | 'pixel-limit'; + +export interface BrowserImageInspection { + format?: 'png' | 'jpeg'; + extension?: 'png' | 'jpg'; + mediaType?: 'image/png' | 'image/jpeg'; + width?: number; + height?: number; + pixels?: number; + browserRenderable: boolean; + rejectionReason?: BrowserImageRejectionReason; +} + +interface ImageDimensions { + width: number; + height: number; +} + +const PNG_SIGNATURE = Uint8Array.of( + 0x89, + 0x50, + 0x4e, + 0x47, + 0x0d, + 0x0a, + 0x1a, + 0x0a +); +const PNG_IHDR_SIZE = 33; +const JPEG_HEADER_SCAN_BYTES = 1024 * 1024; +const JPEG_MARKER_LIMIT = 4_096; + +/** + * Inspect untrusted PNG/JPEG bytes without decoding pixels. The JPEG walk is + * capped independently of the resource size so malformed marker streams + * cannot turn preview eligibility checks into an unbounded scan. + */ +export function inspectBrowserImage( + bytes: Uint8Array, + limits: BrowserImageSafetyLimits = DEFAULT_BROWSER_IMAGE_SAFETY_LIMITS +): BrowserImageInspection { + validateLimits(limits); + + let identity: + | { + format: 'png' | 'jpeg'; + extension: 'png' | 'jpg'; + mediaType: 'image/png' | 'image/jpeg'; + } + | undefined; + let dimensions: ImageDimensions | undefined; + + if (hasPngSignature(bytes)) { + identity = { + format: 'png', + extension: 'png', + mediaType: 'image/png', + }; + dimensions = readPngDimensions(bytes); + } else if (hasJpegSignature(bytes)) { + identity = { + format: 'jpeg', + extension: 'jpg', + mediaType: 'image/jpeg', + }; + dimensions = readJpegDimensions(bytes); + } else { + return { + browserRenderable: false, + rejectionReason: 'unsupported-format', + }; + } + + if (!dimensions) { + return { + ...identity, + browserRenderable: false, + rejectionReason: 'invalid-header', + }; + } + + const { width, height } = dimensions; + const pixels = + width <= Math.floor(Number.MAX_SAFE_INTEGER / height) + ? width * height + : undefined; + const details = { ...identity, width, height, pixels }; + + if (bytes.byteLength > limits.maxBytes) { + return { + ...details, + browserRenderable: false, + rejectionReason: 'byte-limit', + }; + } + if (width > limits.maxDimension || height > limits.maxDimension) { + return { + ...details, + browserRenderable: false, + rejectionReason: 'dimension-limit', + }; + } + if (pixels === undefined || pixels > limits.maxPixels) { + return { + ...details, + browserRenderable: false, + rejectionReason: 'pixel-limit', + }; + } + return { ...details, browserRenderable: true }; +} + +function readPngDimensions(bytes: Uint8Array): ImageDimensions | undefined { + if (bytes.byteLength < PNG_IHDR_SIZE) return undefined; + if ( + readU32Be(bytes, 8) !== 13 || + bytes[12] !== 0x49 || + bytes[13] !== 0x48 || + bytes[14] !== 0x44 || + bytes[15] !== 0x52 + ) { + return undefined; + } + const width = readU32Be(bytes, 16); + const height = readU32Be(bytes, 20); + const bitDepth = bytes[24]!; + const colorType = bytes[25]!; + if ( + width === 0 || + height === 0 || + !validPngBitDepth(bitDepth, colorType) || + bytes[26] !== 0 || + bytes[27] !== 0 || + (bytes[28] !== 0 && bytes[28] !== 1) + ) { + return undefined; + } + return { width, height }; +} + +function validPngBitDepth(bitDepth: number, colorType: number): boolean { + switch (colorType) { + case 0: + return [1, 2, 4, 8, 16].includes(bitDepth); + case 2: + case 4: + case 6: + return bitDepth === 8 || bitDepth === 16; + case 3: + return [1, 2, 4, 8].includes(bitDepth); + default: + return false; + } +} + +function readJpegDimensions(bytes: Uint8Array): ImageDimensions | undefined { + const scanEnd = Math.min(bytes.byteLength, JPEG_HEADER_SCAN_BYTES); + let offset = 2; + let markers = 0; + + while (offset < scanEnd && markers < JPEG_MARKER_LIMIT) { + if (bytes[offset] !== 0xff) return undefined; + while (offset < scanEnd && bytes[offset] === 0xff) offset += 1; + if (offset >= scanEnd) return undefined; + const marker = bytes[offset++]!; + markers += 1; + + if (marker === 0x00) return undefined; + if (marker === 0xd9 || marker === 0xda) return undefined; + if (isStandaloneJpegMarker(marker)) continue; + if (offset + 2 > scanEnd) return undefined; + + const segmentLength = readU16Be(bytes, offset); + if (segmentLength < 2 || offset + segmentLength > scanEnd) { + return undefined; + } + if (isStartOfFrame(marker)) { + if (segmentLength < 8) return undefined; + const height = readU16Be(bytes, offset + 3); + const width = readU16Be(bytes, offset + 5); + const componentCount = bytes[offset + 7]!; + if ( + width === 0 || + height === 0 || + componentCount === 0 || + segmentLength < 8 + componentCount * 3 + ) { + return undefined; + } + return { width, height }; + } + offset += segmentLength; + } + return undefined; +} + +function isStandaloneJpegMarker(marker: number): boolean { + return ( + marker === 0x01 || marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd7) + ); +} + +function isStartOfFrame(marker: number): boolean { + return ( + marker >= 0xc0 && + marker <= 0xcf && + marker !== 0xc4 && + marker !== 0xc8 && + marker !== 0xcc + ); +} + +function hasPngSignature(bytes: Uint8Array): boolean { + return ( + bytes.byteLength >= PNG_SIGNATURE.byteLength && + PNG_SIGNATURE.every((value, index) => bytes[index] === value) + ); +} + +function hasJpegSignature(bytes: Uint8Array): boolean { + return ( + bytes.byteLength >= 3 && + bytes[0] === 0xff && + bytes[1] === 0xd8 && + bytes[2] === 0xff + ); +} + +function readU16Be(bytes: Uint8Array, offset: number): number { + return bytes[offset]! * 0x100 + bytes[offset + 1]!; +} + +function readU32Be(bytes: Uint8Array, offset: number): number { + return ( + bytes[offset]! * 0x100_0000 + + bytes[offset + 1]! * 0x1_0000 + + bytes[offset + 2]! * 0x100 + + bytes[offset + 3]! + ); +} + +function validateLimits(limits: BrowserImageSafetyLimits): void { + for (const [name, value] of Object.entries(limits)) { + if (!Number.isSafeInteger(value) || value <= 0) { + throw new TypeError(`${name} must be a positive safe integer`); + } + } +} diff --git a/src/onenote/index.ts b/src/onenote/index.ts index c23a8e5..2dd0e24 100644 --- a/src/onenote/index.ts +++ b/src/onenote/index.ts @@ -4,6 +4,13 @@ export { type OneNoteFormatKind, } from './parser/detect-format.js'; export { OneNoteParserError } from './parser/error.js'; +export { + DEFAULT_BROWSER_IMAGE_SAFETY_LIMITS, + inspectBrowserImage, + type BrowserImageInspection, + type BrowserImageRejectionReason, + type BrowserImageSafetyLimits, +} from './image-safety.js'; export { DEFAULT_ONENOTE_PARSER_LIMITS, type OneNoteParserLimits, diff --git a/src/onenote/one/content-image-safety.test.ts b/src/onenote/one/content-image-safety.test.ts new file mode 100644 index 0000000..edb7ee9 --- /dev/null +++ b/src/onenote/one/content-image-safety.test.ts @@ -0,0 +1,119 @@ +import { describe, expect, it } from 'vitest'; + +import { DEFAULT_ONENOTE_PARSER_LIMITS } from '../parser/limits.js'; +import type { + ExGuid, + ObjectPropSet, + ObjectSpace, + StoreObject, +} from '../onestore/types.js'; +import { exGuidKey } from '../onestore/types.js'; +import { parseContentObjects } from './content.js'; +import { JCID, PROPERTY } from './constants.js'; + +const GUID = '{44444444-4444-4444-4444-444444444444}'; + +describe('content image preview safety', () => { + it('keeps an oversized image downloadable while disabling auto-rendering', () => { + const imageId = id(1); + const containerId = id(2); + const payload = hugePng(); + const image = object(imageId, JCID.ImageNode, { + ...emptyProps(), + objectIds: [{ guidIndex: 1, value: 2 }], + properties: { + entries: [ + { + rawId: PROPERTY.PictureContainer, + id: PROPERTY.PictureContainer & 0x03ff_ffff, + type: 0x8, + value: { kind: 'objectId' }, + }, + ], + }, + }); + const container = object(containerId, JCID.PictureContainer, emptyProps()); + container.fileData = { + referenceKind: 'embedded', + dataReference: '{44444444-4444-4444-4444-444444444444}', + extension: '.png', + blob: { source: payload, offset: 0, size: payload.byteLength }, + }; + const resources = new Map(); + const space: ObjectSpace = { + id: id(0), + roots: new Map(), + objects: new Map([ + [exGuidKey(image.id), image], + [exGuidKey(container.id), container], + ]), + }; + + const blocks = parseContentObjects([imageId], { + space, + limits: { ...DEFAULT_ONENOTE_PARSER_LIMITS }, + diagnostics: [], + resources, + }); + + expect(blocks).toEqual([ + expect.objectContaining({ + kind: 'image', + resourceId: exGuidKey(containerId), + }), + ]); + expect(resources.get(exGuidKey(containerId))).toMatchObject({ + kind: 'image', + mediaType: 'image/png', + browserRenderable: false, + availability: 'available', + size: payload.byteLength, + blob: { source: payload, offset: 0, size: payload.byteLength }, + }); + }); +}); + +function id(value: number): ExGuid { + return { guid: GUID, value }; +} + +function object( + objectId: ExGuid, + jcid: number, + props: ObjectPropSet +): StoreObject { + return { + id: objectId, + jcid, + props, + mapping: new Map([[1, GUID]]), + contextId: id(0), + }; +} + +function emptyProps(): ObjectPropSet { + return { + objectIds: [], + objectSpaceIds: [], + contextIds: [], + properties: { entries: [] }, + }; +} + +function hugePng(): Uint8Array { + const bytes = new Uint8Array(33); + bytes.set([0x89, 0x50, 0x4e, 0x47, 13, 10, 26, 10], 0); + writeU32Be(bytes, 8, 13); + bytes.set([0x49, 0x48, 0x44, 0x52], 12); + writeU32Be(bytes, 16, 0xffff_ffff); + writeU32Be(bytes, 20, 1); + bytes.set([8, 6, 0, 0, 0], 24); + return bytes; +} + +function writeU32Be(bytes: Uint8Array, offset: number, value: number): void { + bytes[offset] = (value >>> 24) & 0xff; + bytes[offset + 1] = (value >>> 16) & 0xff; + bytes[offset + 2] = (value >>> 8) & 0xff; + bytes[offset + 3] = value & 0xff; +} diff --git a/src/onenote/one/content-table-safety.test.ts b/src/onenote/one/content-table-safety.test.ts new file mode 100644 index 0000000..899cf81 --- /dev/null +++ b/src/onenote/one/content-table-safety.test.ts @@ -0,0 +1,124 @@ +import { describe, expect, it } from 'vitest'; + +import type { ParserDiagnostic } from '../model/diagnostics.js'; +import { DEFAULT_ONENOTE_PARSER_LIMITS } from '../parser/limits.js'; +import type { + ExGuid, + ObjectPropSet, + ObjectSpace, + PropertyEntry, + StoreObject, +} from '../onestore/types.js'; +import { exGuidKey } from '../onestore/types.js'; +import { parseContentObjects } from './content.js'; +import { JCID, PROPERTY } from './constants.js'; + +const GUID = '{55555555-5555-5555-5555-555555555555}'; + +describe('content table allocation safety', () => { + it('rejects a max-u32 declared column count before bitmap allocation', () => { + const tableId: ExGuid = { guid: GUID, value: 1 }; + const table = object( + tableId, + JCID.TableNode, + props( + u32Entry(PROPERTY.RowCount, 1), + u32Entry(PROPERTY.ColumnCount, 0xffff_ffff), + bytesEntry(PROPERTY.TableColumnsLocked, Uint8Array.of(1, 1)) + ) + ); + const diagnostics: ParserDiagnostic[] = []; + const space: ObjectSpace = { + id: { guid: GUID, value: 0 }, + roots: new Map(), + objects: new Map([[exGuidKey(table.id), table]]), + }; + + expect( + parseContentObjects([tableId], { + space, + limits: { ...DEFAULT_ONENOTE_PARSER_LIMITS }, + diagnostics, + resources: new Map(), + }) + ).toEqual([]); + expect(diagnostics).toContainEqual( + expect.objectContaining({ + code: 'CONTENT_PARSE_FAILED', + message: expect.stringContaining( + 'Declared table column count exceeds 1000000' + ), + }) + ); + }); + + it('rejects declared table dimensions whose product exceeds the cell limit', () => { + const tableId: ExGuid = { guid: GUID, value: 2 }; + const table = object( + tableId, + JCID.TableNode, + props( + u32Entry(PROPERTY.RowCount, 1_001), + u32Entry(PROPERTY.ColumnCount, 1_000) + ) + ); + const diagnostics: ParserDiagnostic[] = []; + + expect( + parseContentObjects([tableId], { + space: { + id: { guid: GUID, value: 0 }, + roots: new Map(), + objects: new Map([[exGuidKey(table.id), table]]), + }, + limits: { ...DEFAULT_ONENOTE_PARSER_LIMITS }, + diagnostics, + resources: new Map(), + }) + ).toEqual([]); + expect(diagnostics[0]?.message).toContain( + 'Declared table cell count exceeds 1000000' + ); + }); +}); + +function object( + id: ExGuid, + jcid: number, + objectProps: ObjectPropSet +): StoreObject { + return { + id, + jcid, + props: objectProps, + mapping: new Map(), + contextId: { guid: GUID, value: 0 }, + }; +} + +function props(...entries: PropertyEntry[]): ObjectPropSet { + return { + objectIds: [], + objectSpaceIds: [], + contextIds: [], + properties: { entries }, + }; +} + +function u32Entry(rawId: number, value: number): PropertyEntry { + return { + rawId, + id: rawId & 0x03ff_ffff, + type: 0x4, + value: { kind: 'u32', value }, + }; +} + +function bytesEntry(rawId: number, value: Uint8Array): PropertyEntry { + return { + rawId, + id: rawId & 0x03ff_ffff, + type: 0x7, + value: { kind: 'bytes', value }, + }; +} diff --git a/src/onenote/one/content.ts b/src/onenote/one/content.ts index 06a8702..63ac079 100644 --- a/src/onenote/one/content.ts +++ b/src/onenote/one/content.ts @@ -11,6 +11,7 @@ import { BinaryReader } from '../binary/BinaryReader.js'; import { readGuid } from '../binary/guid.js'; +import { inspectBrowserImage } from '../image-safety.js'; import type { ParserDiagnostic } from '../model/diagnostics.js'; import type { OneNoteParserLimits } from '../parser/limits.js'; import type { @@ -504,7 +505,11 @@ function registerResource( const extension = normalizeExtension( object.fileData?.extension ?? extensionFromFilename(filename) ); - const sniffed = sniffResource(object.fileData?.blob, extension); + const sniffed = sniffResource( + object.fileData?.blob, + extension, + context.limits + ); const availability = resourceAvailability(object); context.resources.set(key, { id: key, @@ -529,6 +534,11 @@ function parseTable( ): OneNoteTableBlock { const declaredRowCount = optionalU32(object, PROPERTY.RowCount); const declaredColumnCount = optionalU32(object, PROPERTY.ColumnCount); + validateDeclaredTableCounts( + declaredRowCount, + declaredColumnCount, + context.limits.maxTableCells + ); const rowIds = objectReferences(object, PROPERTY.ElementChildNodes); if (declaredRowCount !== undefined && declaredRowCount !== rowIds.length) { addDiagnostic( @@ -1014,7 +1024,7 @@ function parseLockedColumns( ); } const bitmap = bytes.subarray(1, Math.min(bytes.length, byteCount + 1)); - const count = columnCount ?? bitCount; + const count = Math.min(columnCount ?? bitCount, context.limits.maxTableCells); return Array.from( { length: count }, (_, index) => @@ -1022,26 +1032,46 @@ function parseLockedColumns( ); } +function validateDeclaredTableCounts( + rowCount: number | undefined, + columnCount: number | undefined, + maxTableCells: number +): void { + if (rowCount !== undefined && rowCount > maxTableCells) { + throw new Error(`Declared table row count exceeds ${maxTableCells}`); + } + if (columnCount !== undefined && columnCount > maxTableCells) { + throw new Error(`Declared table column count exceeds ${maxTableCells}`); + } + if ( + rowCount !== undefined && + columnCount !== undefined && + columnCount > 0 && + rowCount > Math.floor(maxTableCells / columnCount) + ) { + throw new Error(`Declared table cell count exceeds ${maxTableCells}`); + } +} + function sniffResource( blob: FileDataBlobReference | undefined, - extension: string | undefined + extension: string | undefined, + limits: OneNoteParserLimits ): { mediaType: string; browserRenderable: boolean } { - if (blob && blob.size >= 8) { - const bytes = blob.source.subarray(blob.offset, blob.offset + 12); - if ( - 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 { mediaType: 'image/png', browserRenderable: true }; - } - if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) { - return { mediaType: 'image/jpeg', browserRenderable: true }; + if (blob) { + const inspected = inspectBrowserImage( + blob.source.subarray(blob.offset, blob.offset + blob.size), + { + maxBytes: limits.maxAutoRenderImageBytes, + maxDimension: limits.maxAutoRenderImageDimension, + maxPixels: limits.maxAutoRenderImagePixels, + } + ); + if (inspected.mediaType) { + return { + mediaType: inspected.mediaType, + browserRenderable: inspected.browserRenderable, + }; } } return { diff --git a/src/onenote/parser/limits.ts b/src/onenote/parser/limits.ts index 9a30be9..6f6ade3 100644 --- a/src/onenote/parser/limits.ts +++ b/src/onenote/parser/limits.ts @@ -2,6 +2,8 @@ // 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/. +import { DEFAULT_BROWSER_IMAGE_SAFETY_LIMITS } from '../image-safety.js'; + export interface OneNoteParserLimits { maxFileBytes: number; maxTransactionFragments: number; @@ -18,6 +20,9 @@ export interface OneNoteParserLimits { maxFileDataObjects: number; maxFileDataBytes: number; maxTotalFileDataBytes: number; + maxAutoRenderImageBytes: number; + maxAutoRenderImageDimension: number; + maxAutoRenderImagePixels: number; maxGraphDepth: number; maxContentBlocks: number; maxTextRuns: number; @@ -44,6 +49,9 @@ export const DEFAULT_ONENOTE_PARSER_LIMITS: Readonly = { maxFileDataObjects: 10_000, maxFileDataBytes: 256 * 1024 * 1024, maxTotalFileDataBytes: 512 * 1024 * 1024, + maxAutoRenderImageBytes: DEFAULT_BROWSER_IMAGE_SAFETY_LIMITS.maxBytes, + maxAutoRenderImageDimension: DEFAULT_BROWSER_IMAGE_SAFETY_LIMITS.maxDimension, + maxAutoRenderImagePixels: DEFAULT_BROWSER_IMAGE_SAFETY_LIMITS.maxPixels, maxGraphDepth: 256, maxContentBlocks: 1_000_000, maxTextRuns: 1_000_000,