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
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die README; an keiner Stelle Bedeutung, nur Zeichensetzung.
102 lines
3.5 KiB
TypeScript
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')
|
|
}
|