Files
Harlekin e0b03ea336
Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
Gedankenstriche durch Bindestriche ersetzt
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die
README; an keiner Stelle Bedeutung, nur Zeichensetzung.
2026-09-04 08:54:52 +02:00

102 lines
3.5 KiB
TypeScript

import { createHash } from 'node:crypto'
/**
* Ein winziger ZIP-Schreiber, nur „gespeichert" ohne Kompression.
*
* Eigener Code statt einer Abhängigkeit, weil der Inhalt eines Sammelexports
* PDFs und bereits neu kodierte Bilder sind - beides ist komprimiert, ein
* Deflate darüber bringt nichts und kostet nur CPU auf einem Heimserver. Was
* bleibt, ist das Behälterformat, und das sind achtzig Zeilen.
*
* Aufbau nach der ZIP-Spezifikation: je Eintrag ein lokaler Kopf mit den
* Daten, danach das zentrale Verzeichnis und dessen Abschluss.
*/
type Eintrag = { name: string; content: Uint8Array }
/** DOS-Zeitstempel. Die Auflösung beträgt zwei Sekunden, das reicht hier. */
function dosZeit(datum: Date): { zeit: number; datum: number } {
return {
zeit:
(datum.getHours() << 11) | (datum.getMinutes() << 5) | Math.floor(datum.getSeconds() / 2),
datum:
((datum.getFullYear() - 1980) << 9) | ((datum.getMonth() + 1) << 5) | datum.getDate(),
}
}
function crc32(daten: Uint8Array): number {
// Tabelle einmal je Aufruf: bei einer Handvoll Dateien je Export ist das
// billiger als eine Konstante, die immer im Speicher liegt.
let c: number
const tabelle = new Int32Array(256)
for (let n = 0; n < 256; n++) {
c = n
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1
tabelle[n] = c
}
let crc = -1
for (const byte of daten) crc = (crc >>> 8) ^ tabelle[(crc ^ byte) & 0xff]!
return (crc ^ -1) >>> 0
}
export function createZip(eintraege: Eintrag[], now: Date = new Date()): Uint8Array {
const { zeit, datum } = dosZeit(now)
const teile: Buffer[] = []
const verzeichnis: Buffer[] = []
let offset = 0
for (const eintrag of eintraege) {
const name = Buffer.from(eintrag.name, 'utf8')
const inhalt = Buffer.from(eintrag.content)
const pruefsumme = crc32(eintrag.content)
const lokal = Buffer.alloc(30)
lokal.writeUInt32LE(0x04034b50, 0)
lokal.writeUInt16LE(20, 4) // benötigte Version
lokal.writeUInt16LE(0x0800, 6) // Bit 11: Name ist UTF-8
lokal.writeUInt16LE(0, 8) // Verfahren: gespeichert
lokal.writeUInt16LE(zeit, 10)
lokal.writeUInt16LE(datum, 12)
lokal.writeUInt32LE(pruefsumme, 14)
lokal.writeUInt32LE(inhalt.length, 18)
lokal.writeUInt32LE(inhalt.length, 22)
lokal.writeUInt16LE(name.length, 26)
lokal.writeUInt16LE(0, 28)
teile.push(lokal, name, inhalt)
const zentral = Buffer.alloc(46)
zentral.writeUInt32LE(0x02014b50, 0)
zentral.writeUInt16LE(20, 4)
zentral.writeUInt16LE(20, 6)
zentral.writeUInt16LE(0x0800, 8)
zentral.writeUInt16LE(0, 10)
zentral.writeUInt16LE(zeit, 12)
zentral.writeUInt16LE(datum, 14)
zentral.writeUInt32LE(pruefsumme, 16)
zentral.writeUInt32LE(inhalt.length, 20)
zentral.writeUInt32LE(inhalt.length, 24)
zentral.writeUInt16LE(name.length, 28)
zentral.writeUInt32LE(offset, 42)
verzeichnis.push(zentral, name)
offset += lokal.length + name.length + inhalt.length
}
const verzeichnisBytes = Buffer.concat(verzeichnis)
const abschluss = Buffer.alloc(22)
abschluss.writeUInt32LE(0x06054b50, 0)
abschluss.writeUInt16LE(eintraege.length, 8)
abschluss.writeUInt16LE(eintraege.length, 10)
abschluss.writeUInt32LE(verzeichnisBytes.length, 12)
abschluss.writeUInt32LE(offset, 16)
return new Uint8Array(Buffer.concat([...teile, verzeichnisBytes, abschluss]))
}
/** Für das Protokoll: worüber genau wurde ausgeleitet. */
export function digestOf(daten: Uint8Array): string {
return createHash('sha256').update(daten).digest('hex')
}