From 98bce1b0da675715b2e5482c0ad61ba21135f62f Mon Sep 17 00:00:00 2001
From: Server-Bibliothek <server-bibliothek@lokal>
Date: Di, 04 Aug 2026 07:00:04 +0200
Subject: [PATCH] Add a desktop client that knows what is installed locally

---
 packages/bibliothek-server/src/zip.ts |  107 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 107 insertions(+), 0 deletions(-)

diff --git a/packages/bibliothek-server/src/zip.ts b/packages/bibliothek-server/src/zip.ts
new file mode 100644
index 0000000..0ed22aa
--- /dev/null
+++ b/packages/bibliothek-server/src/zip.ts
@@ -0,0 +1,107 @@
+/**
+ * Minimaler ZIP-Schreiber (Speicher-Methode, ohne Komprimierung).
+ *
+ * Bewusst ohne Fremdbibliothek: Installer und Archive sind bereits komprimiert,
+ * erneutes Packen brächte nichts und kostet nur Zeit. Damit bleibt der Server
+ * ohne Zusatz-Abhängigkeit.
+ *
+ * Grenze: Das klassische ZIP-Format adressiert mit 32 Bit, ein Archiv (bzw. eine
+ * Einzeldatei darin) darf also 4 GB nicht überschreiten — ZIP64 ist nicht umgesetzt.
+ */
+
+const CRC_TABELLE = erzeugeCrcTabelle();
+
+function erzeugeCrcTabelle(): Uint32Array {
+  const tabelle = new Uint32Array(256);
+  for (let index = 0; index < 256; index++) {
+    let wert = index;
+    for (let bit = 0; bit < 8; bit++) {
+      wert = wert & 1 ? 0xedb88320 ^ (wert >>> 1) : wert >>> 1;
+    }
+    tabelle[index] = wert >>> 0;
+  }
+  return tabelle;
+}
+
+function crc32(daten: Buffer): number {
+  let pruefsumme = 0xffffffff;
+  for (const byte of daten) {
+    pruefsumme = CRC_TABELLE[(pruefsumme ^ byte) & 0xff] ^ (pruefsumme >>> 8);
+  }
+  return (pruefsumme ^ 0xffffffff) >>> 0;
+}
+
+/** Wandelt einen Zeitpunkt in das von ZIP verwendete DOS-Format (Datum + Uhrzeit). */
+function dosZeitstempel(zeitpunkt: Date): { zeit: number; datum: number } {
+  const zeit = (zeitpunkt.getHours() << 11) | (zeitpunkt.getMinutes() << 5) | (Math.floor(zeitpunkt.getSeconds() / 2) & 0x1f);
+  const datum = ((zeitpunkt.getFullYear() - 1980) << 9) | ((zeitpunkt.getMonth() + 1) << 5) | zeitpunkt.getDate();
+  return { zeit, datum };
+}
+
+export interface ZipEintrag {
+  dateiname: string;
+  inhalt: Buffer;
+}
+
+/** Packt die Einträge zu einem ZIP-Archiv (ohne Komprimierung). */
+export function erzeugeZip(eintraege: ZipEintrag[], zeitpunkt: Date = new Date()): Buffer {
+  const { zeit, datum } = dosZeitstempel(zeitpunkt);
+  const lokaleBloecke: Buffer[] = [];
+  const zentralBloecke: Buffer[] = [];
+  let versatz = 0;
+
+  for (const eintrag of eintraege) {
+    const nameBytes = Buffer.from(eintrag.dateiname, 'utf8');
+    const pruefsumme = crc32(eintrag.inhalt);
+    const groesse = eintrag.inhalt.length;
+
+    const lokalerKopf = Buffer.alloc(30);
+    lokalerKopf.writeUInt32LE(0x04034b50, 0); // Signatur
+    lokalerKopf.writeUInt16LE(20, 4); // benötigte Version
+    lokalerKopf.writeUInt16LE(0x0800, 6); // Flags: Dateiname ist UTF-8
+    lokalerKopf.writeUInt16LE(0, 8); // Methode 0 = gespeichert
+    lokalerKopf.writeUInt16LE(zeit, 10);
+    lokalerKopf.writeUInt16LE(datum, 12);
+    lokalerKopf.writeUInt32LE(pruefsumme, 14);
+    lokalerKopf.writeUInt32LE(groesse, 18); // komprimierte Größe
+    lokalerKopf.writeUInt32LE(groesse, 22); // Originalgröße
+    lokalerKopf.writeUInt16LE(nameBytes.length, 26);
+    lokalerKopf.writeUInt16LE(0, 28); // keine Extra-Felder
+    lokaleBloecke.push(lokalerKopf, nameBytes, eintrag.inhalt);
+
+    const zentralKopf = Buffer.alloc(46);
+    zentralKopf.writeUInt32LE(0x02014b50, 0); // Signatur
+    zentralKopf.writeUInt16LE(20, 4); // erstellt mit Version
+    zentralKopf.writeUInt16LE(20, 6); // benötigte Version
+    zentralKopf.writeUInt16LE(0x0800, 8);
+    zentralKopf.writeUInt16LE(0, 10);
+    zentralKopf.writeUInt16LE(zeit, 12);
+    zentralKopf.writeUInt16LE(datum, 14);
+    zentralKopf.writeUInt32LE(pruefsumme, 16);
+    zentralKopf.writeUInt32LE(groesse, 20);
+    zentralKopf.writeUInt32LE(groesse, 24);
+    zentralKopf.writeUInt16LE(nameBytes.length, 28);
+    zentralKopf.writeUInt16LE(0, 30); // Extra
+    zentralKopf.writeUInt16LE(0, 32); // Kommentar
+    zentralKopf.writeUInt16LE(0, 34); // Datenträger
+    zentralKopf.writeUInt16LE(0, 36); // interne Attribute
+    zentralKopf.writeUInt32LE(0, 38); // externe Attribute
+    zentralKopf.writeUInt32LE(versatz, 42); // Position des lokalen Kopfes
+    zentralBloecke.push(zentralKopf, nameBytes);
+
+    versatz += lokalerKopf.length + nameBytes.length + groesse;
+  }
+
+  const zentralVerzeichnis = Buffer.concat(zentralBloecke);
+  const abschluss = Buffer.alloc(22);
+  abschluss.writeUInt32LE(0x06054b50, 0); // Signatur
+  abschluss.writeUInt16LE(0, 4); // Datenträger
+  abschluss.writeUInt16LE(0, 6); // Datenträger mit Verzeichnis
+  abschluss.writeUInt16LE(eintraege.length, 8);
+  abschluss.writeUInt16LE(eintraege.length, 10);
+  abschluss.writeUInt32LE(zentralVerzeichnis.length, 12);
+  abschluss.writeUInt32LE(versatz, 16); // Beginn des Zentralverzeichnisses
+  abschluss.writeUInt16LE(0, 20); // kein Kommentar
+
+  return Buffer.concat([...lokaleBloecke, zentralVerzeichnis, abschluss]);
+}

--
Gitblit v1.9.3