fix: bound OneNote export output
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user