Server-Bibliothek
vor 18 Std. 98bce1b0da675715b2e5482c0ad61ba21135f62f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
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]);
}