/**
|
* 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]);
|
}
|