fix: bound OneNote export output

This commit is contained in:
2026-07-22 20:03:55 +02:00
parent 45880fcf2b
commit 572a67148a
7 changed files with 1049 additions and 121 deletions

View File

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