import type { OneNotePackageDto, OneNotePackageEntryDto, OneNotePackagedSectionDto, OneNoteSectionDto, } from '../model/dto.js'; import type { ParserDiagnostic } from '../model/diagnostics.js'; import { checkCancellation } from './cancellation.js'; import { extractCabinet } from './cabinet.js'; import { extractCabinetSet } from './cabinet-set.js'; import { diagnosticFromUnknown, OnePkgError, warning } from './errors.js'; import { buildOneNotePackageTree } from './toc-hierarchy.js'; import type { CabOpenOptions, CabExtractionResult, CabSetPart, ExtractedCabFile, OnePkgCancellation, } from './types.js'; export interface OneNotePackageSectionParseRequest { sourceName: string; path: string; normalizedPath: string; bytes: Uint8Array; cancellation?: OnePkgCancellation; } export type OneNotePackageSectionParseOutcome = | { status: 'parsed'; value: OneNoteSectionDto; diagnostics?: ParserDiagnostic[]; } | { status: 'unsupported'; diagnostics?: ParserDiagnostic[] }; /** * Deliberately independent from the CAB implementation: a `.one` parser is * injected and receives only one extracted section plus cancellation context. */ export type OneNotePackageSectionParser = ( request: OneNotePackageSectionParseRequest ) => | OneNotePackageSectionParseOutcome | Promise; export interface OneNotePackageOpenOptions extends CabOpenOptions { sourceName?: string; parseSection?: OneNotePackageSectionParser; } export interface ParsedOneNotePackageSection { normalizedPath: string; value: OneNoteSectionDto; } export interface OneNotePackageOpenResult { package: OneNotePackageDto; /** Extracted in memory only. CAB paths are never written to the filesystem. */ extractedEntries: ExtractedCabFile[]; parsedSections: ParsedOneNotePackageSection[]; } function entryKind(path: string): OneNotePackageEntryDto['kind'] { const lower = path.toLowerCase(); if (lower.endsWith('.one')) return 'section'; if (lower.endsWith('.onetoc2')) return 'table-of-contents'; return 'other'; } function compressionLabel(entry: ExtractedCabFile): string { return entry.compression.kind === 'lzx' ? `${entry.compression.label} 2^${entry.compression.windowBits}` : entry.compression.label; } function sectionDisplayName(path: string): string { const fileName = path.slice(path.lastIndexOf('/') + 1); return fileName.toLowerCase().endsWith('.one') ? fileName.slice(0, -4) : fileName; } export async function openOneNotePackage( bytes: Uint8Array, options: OneNotePackageOpenOptions = {} ): Promise { const sourceName = options.sourceName ?? 'notebook.onepkg'; const extraction = await extractCabinet(bytes, options); return buildOpenedPackage(extraction, sourceName, bytes.byteLength, options); } export async function openOneNotePackageSet( parts: CabSetPart[], options: OneNotePackageOpenOptions = {} ): Promise { const extraction = await extractCabinetSet(parts, options); const sourceName = options.sourceName ?? parts.find((part) => part.fileName.toLowerCase().endsWith('.onepkg')) ?.fileName ?? extraction.parts[0]?.fileName ?? 'notebook.onepkg'; return buildOpenedPackage( extraction, sourceName, parts.reduce((sum, part) => sum + part.bytes.byteLength, 0), options ); } async function buildOpenedPackage( extraction: CabExtractionResult, sourceName: string, sourceSize: number, options: OneNotePackageOpenOptions ): Promise { const diagnostics = [...extraction.diagnostics]; const packageEntries: OneNotePackageEntryDto[] = extraction.extractedEntries.map((entry) => ({ path: entry.path, normalizedPath: entry.normalizedPath, kind: entryKind(entry.normalizedPath), uncompressedSize: entry.uncompressedSize, compressionMethod: compressionLabel(entry), parseStatus: 'not-requested', })); const parsedSections: ParsedOneNotePackageSection[] = []; const packagedSections: OneNotePackagedSectionDto[] = []; for (let index = 0; index < extraction.extractedEntries.length; index += 1) { const entry = extraction.extractedEntries[index]!; const packageEntry = packageEntries[index]!; if (packageEntry.kind !== 'section') continue; const sectionDiagnostics: ParserDiagnostic[] = []; let parsedSection: OneNoteSectionDto | undefined; if (options.parseSection !== undefined) { checkCancellation(options.cancellation); try { const outcome = await options.parseSection({ sourceName: entry.normalizedPath, path: entry.path, normalizedPath: entry.normalizedPath, bytes: entry.bytes, cancellation: options.cancellation, }); if (outcome.status === 'parsed') { packageEntry.parseStatus = 'parsed'; parsedSection = outcome.value; parsedSections.push({ normalizedPath: entry.normalizedPath, value: outcome.value, }); } else { packageEntry.parseStatus = 'unsupported'; if (outcome.diagnostics === undefined) { sectionDiagnostics.push( warning( 'one-section-unsupported', `Section parser does not support ${entry.normalizedPath}.`, { structure: entry.normalizedPath } ) ); } } sectionDiagnostics.push(...(outcome.diagnostics ?? [])); } catch (error) { if ( error instanceof OnePkgError && error.diagnostic.code === 'onepkg-cancelled' ) { throw error; } packageEntry.parseStatus = 'failed'; sectionDiagnostics.push({ ...diagnosticFromUnknown( error, 'one-section-parse-failed', `Could not parse ${entry.normalizedPath}`, entry.normalizedPath ), recoverable: true, }); } } packagedSections.push({ id: entry.normalizedPath, path: entry.normalizedPath, displayName: sectionDisplayName(entry.normalizedPath), parseStatus: packageEntry.parseStatus, section: parsedSection, diagnostics: sectionDiagnostics, }); } const treeResult = await buildOneNotePackageTree( extraction.extractedEntries, packagedSections, options ); diagnostics.push(...treeResult.diagnostics); return { package: { format: 'onepkg', sourceName, sourceSize, entries: packageEntries, sections: packagedSections, tree: treeResult.tree, diagnostics, }, extractedEntries: extraction.extractedEntries, parsedSections, }; }