/* ===================================================================== EIN ZIP- UND XLSX-SCHREIBER FUER PRUEFUNGEN (14.09.2026) Er steht hier und nicht in einer der Pruefdateien, weil ihn seit heute ZWEI brauchen: pruef-xlsx (der Leser) und pruef-backstage-import (der Weg durch die Oberflaeche). Zwei Abschriften waeren zwei Gelegenheiten, dass die eine ein Feld anders schreibt als die andere -- und dann misst man zwei verschiedene Dinge und nennt beide "eine Excel-Datei". Gebaut wird nach der echten Backstage-Form: alles in der Zeichenkettentabelle (`t="s"`), ein Blatt, keine Formate. `crc32` gibt es in node:zlib seit Node 22. Fehlt es, sagen die Pruefungen das -- der dritte Ausgang, kein stiller Aussetzer. ===================================================================== */ import { deflateRawSync, crc32 } from "node:zlib"; export const kannXlsxBauen = typeof crc32 === "function"; export function zipBauen(eintraege) { const lokale = []; const zentrale = []; let stelle = 0; for (const [name, inhalt] of eintraege) { const roh = Buffer.from(inhalt, "utf8"); const gepackt = deflateRawSync(roh); const pruef = crc32(roh) >>> 0; const nameB = Buffer.from(name, "utf8"); const lokal = Buffer.alloc(30); lokal.writeUInt32LE(0x04034b50, 0); lokal.writeUInt16LE(20, 4); // Version lokal.writeUInt16LE(0, 6); // Merker lokal.writeUInt16LE(8, 8); // Deflate lokal.writeUInt32LE(pruef, 14); lokal.writeUInt32LE(gepackt.length, 18); lokal.writeUInt32LE(roh.length, 22); lokal.writeUInt16LE(nameB.length, 26); lokale.push(lokal, nameB, gepackt); const zentral = Buffer.alloc(46); zentral.writeUInt32LE(0x02014b50, 0); zentral.writeUInt16LE(20, 4); zentral.writeUInt16LE(20, 6); zentral.writeUInt16LE(8, 10); zentral.writeUInt32LE(pruef, 16); zentral.writeUInt32LE(gepackt.length, 20); zentral.writeUInt32LE(roh.length, 24); zentral.writeUInt16LE(nameB.length, 28); zentral.writeUInt32LE(stelle, 42); zentrale.push(zentral, nameB); stelle += lokal.length + nameB.length + gepackt.length; } const koerper = Buffer.concat(lokale); const verzeichnis = Buffer.concat(zentrale); const ende = Buffer.alloc(22); ende.writeUInt32LE(0x06054b50, 0); ende.writeUInt16LE(eintraege.length, 8); ende.writeUInt16LE(eintraege.length, 10); ende.writeUInt32LE(verzeichnis.length, 12); ende.writeUInt32LE(koerper.length, 16); return Buffer.concat([koerper, verzeichnis, ende]); } export const RELS = ` `; /** Baut eine .xlsx aus Zeilen. Alles wandert in die * Zeichenkettentabelle – genau wie Backstage es tut. */ export function xlsxBauen(zeilen, { blattName = "Sheet1", extra = "" } = {}) { const texte = []; const nummer = (w) => { const i = texte.indexOf(w); if (i >= 0) return i; texte.push(w); return texte.length - 1; }; const buchstabe = (n) => { let s = ""; n += 1; while (n > 0) { const r = (n - 1) % 26; s = String.fromCharCode(65 + r) + s; n = (n - r - 1) / 26; } return s; }; const zeilenXml = zeilen.map((z, r) => { const zellen = z.map((w, c) => { if (w === null || w === undefined || w === "") return ""; return `${nummer(String(w))}`; }).join(""); return `${zellen}`; }).join(""); const blatt = ` ${zeilenXml}`; const strings = ` ` + texte.map((t) => `${t.replace(/&/g, "&").replace(/`).join("") + ""; const mappe = ` `; return zipBauen([ ["xl/workbook.xml", mappe], ["xl/_rels/workbook.xml.rels", RELS], ["xl/sharedStrings.xml", strings], ["xl/worksheets/sheet1.xml", blatt], ]); }