/** * The native `.num` document file: a ZIP container around the same JSON the * `.json` export writes (json.ts), so it's smaller on disk and leaves room * for more parts later (images, attachments) without changing the format. * * Layout, modelled on OpenDocument: * mimetype "application/x-numolus", stored uncompressed as the * first entry, so a file sniffer can identify the format * from a fixed offset * meta.json { format, formatVersion, created (ISO 8601) } * document.json the document, exactly as `serializeDocument()` writes it * * Reading only needs `document.json`; `mimetype` is checked when present so * a random ZIP renamed to `.num` fails with a clear message. */ import { strFromU8, strToU8, unzipSync, zipSync } from 'fflate'; export const NUM_MIMETYPE = 'application/x-numolus'; export const NUM_FORMAT_VERSION = 1; export class NumFileError extends Error {} export function packNum(documentJson: string, now: Date = new Date()): Uint8Array { const meta = { format: 'numolus', formatVersion: NUM_FORMAT_VERSION, created: now.toISOString() }; return zipSync({ mimetype: [strToU8(NUM_MIMETYPE), { level: 0 }], 'meta.json': strToU8(JSON.stringify(meta, null, 2)), 'document.json': strToU8(documentJson), }); } /** The `document.json` text inside a `.num` file. */ export function unpackNum(bytes: Uint8Array): string { let files: Record; try { files = unzipSync(bytes); } catch { throw new NumFileError('Keine gültige .num-Datei (kein ZIP-Archiv).'); } const mimetype = files.mimetype ? strFromU8(files.mimetype).trim() : null; if (mimetype !== null && mimetype !== NUM_MIMETYPE) { throw new NumFileError(`Keine numolus-Datei (Typ "${mimetype}").`); } const doc = files['document.json']; if (!doc) throw new NumFileError('Die .num-Datei enthält kein document.json.'); const meta = files['meta.json'] ? safeJson(strFromU8(files['meta.json'])) : null; const version = meta && typeof meta === 'object' ? (meta as { formatVersion?: unknown }).formatVersion : undefined; if (typeof version === 'number' && version > NUM_FORMAT_VERSION) { throw new NumFileError( `Die Datei stammt aus einer neueren numolus-Version (Format ${version}); bitte die App aktualisieren.`, ); } return strFromU8(doc); } function safeJson(text: string): unknown { try { return JSON.parse(text); } catch { return null; } }