fix: bound OneNote export output
This commit is contained in:
@@ -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 = '<img src=x onerror=alert(1)><script>bad()</script>';
|
||||
|
||||
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<Partial<OneNoteExportError>>({
|
||||
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<Partial<OneNoteExportError>>({
|
||||
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<Partial<OneNoteExportError>>({
|
||||
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<Partial<OneNoteExportError>>({
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user