// ZIP lesen. Node bringt zlib mit, aber kein Archivformat, und das Projekt // kommt ohne Abhaengigkeiten aus (ADR-0001) - wie beim YAML-Parser und beim // obs-websocket-Client also selbst gebaut. // // Gebraucht wird das an zwei Stellen: beim Paketbau kommen ffmpeg, // whisper.cpp und Node als ZIP von ihren Herausgebern, und der Updater auf // Abdus Rechner packt damit das neue Paket aus - ohne PowerShell, ohne // externes Programm. import zlib from 'node:zlib'; const EOCD = 0x06054b50; // End of Central Directory const CENTRAL = 0x02014b50; // Eintrag im Inhaltsverzeichnis const LOCAL = 0x04034b50; // Kopf vor den eigentlichen Daten export type ZipEntry = { name: string; /** Unkomprimierte Groesse in Bytes. */ size: number; /** Verzeichniseintraege haben keine Daten. */ isDirectory: boolean; data: () => Buffer; }; /** * Inhaltsverzeichnis eines ZIP lesen. * * Die Daten werden erst beim Aufruf von `data()` entpackt - beim Paketbau * werden von 800 Eintraegen keine 30 gebraucht. */ export function readZip(buf: Buffer): ZipEntry[] { const eocd = findEocd(buf); const count = buf.readUInt16LE(eocd + 10); const dirSize = buf.readUInt32LE(eocd + 12); const dirStart = buf.readUInt32LE(eocd + 16); if (dirStart === 0xffffffff || dirSize === 0xffffffff || count === 0xffff) { throw new Error('ZIP64 wird nicht unterstuetzt - das Archiv ist zu gross.'); } const entries: ZipEntry[] = []; let p = dirStart; for (let i = 0; i < count; i++) { if (buf.readUInt32LE(p) !== CENTRAL) { throw new Error(`Beschaedigtes Inhaltsverzeichnis bei Eintrag ${i}`); } const method = buf.readUInt16LE(p + 10); const compressed = buf.readUInt32LE(p + 20); const size = buf.readUInt32LE(p + 24); const nameLen = buf.readUInt16LE(p + 28); const extraLen = buf.readUInt16LE(p + 30); const commentLen = buf.readUInt16LE(p + 32); const localOffset = buf.readUInt32LE(p + 42); const name = buf.toString('utf8', p + 46, p + 46 + nameLen); entries.push({ name, size, isDirectory: name.endsWith('/'), data: () => extract(buf, localOffset, method, compressed, size, name), }); p += 46 + nameLen + extraLen + commentLen; } return entries; } function extract( buf: Buffer, localOffset: number, method: number, compressed: number, size: number, name: string ): Buffer { if (buf.readUInt32LE(localOffset) !== LOCAL) { throw new Error(`Kein lokaler Dateikopf fuer ${name}`); } // Der lokale Kopf hat eigene Laengen fuer Name und Extra-Feld; sie koennen // von denen im Inhaltsverzeichnis abweichen und muessen von hier kommen. const nameLen = buf.readUInt16LE(localOffset + 26); const extraLen = buf.readUInt16LE(localOffset + 28); const start = localOffset + 30 + nameLen + extraLen; const raw = buf.subarray(start, start + compressed); if (method === 0) return Buffer.from(raw); if (method === 8) { const out = zlib.inflateRawSync(raw); if (out.length !== size) { throw new Error(`${name}: ${out.length} Bytes entpackt, ${size} erwartet`); } return out; } throw new Error(`${name}: unbekanntes Kompressionsverfahren ${method}`); } /** * Das Inhaltsverzeichnis steht am Dateiende, hinter einem Kommentar * beliebiger Laenge. Also von hinten suchen - hoechstens 64 KB weit, mehr * darf der Kommentar nicht sein. */ function findEocd(buf: Buffer): number { const min = Math.max(0, buf.length - 0x10000 - 22); for (let i = buf.length - 22; i >= min; i--) { if (buf.readUInt32LE(i) === EOCD) return i; } throw new Error('Kein ZIP-Inhaltsverzeichnis gefunden.'); } /** * ZIP schreiben. Nur so viel Format, wie der Windows-Explorer und * `Expand-Archive` verlangen - insbesondere die CRC32-Pruefsumme, sonst * halten beide das Archiv fuer beschaedigt. */ export function writeZip(files: Array<{ name: string; data: Buffer }>): Buffer { const locals: Buffer[] = []; const centrals: Buffer[] = []; let offset = 0; for (const f of files) { const name = Buffer.from(f.name.replace(/\\/g, '/'), 'utf8'); const packed = zlib.deflateRawSync(f.data, { level: 9 }); // Groesser als das Original kommt bei schon komprimierten Dateien vor - // dann unveraendert ablegen. const useDeflate = packed.length < f.data.length; const body = useDeflate ? packed : f.data; const crc = zlib.crc32(f.data); const local = Buffer.alloc(30 + name.length); local.writeUInt32LE(LOCAL, 0); local.writeUInt16LE(20, 4); // benoetigte Version: 2.0 local.writeUInt16LE(useDeflate ? 8 : 0, 8); local.writeUInt32LE(crc, 14); local.writeUInt32LE(body.length, 18); local.writeUInt32LE(f.data.length, 22); local.writeUInt16LE(name.length, 26); name.copy(local, 30); locals.push(local, body); const central = Buffer.alloc(46 + name.length); central.writeUInt32LE(CENTRAL, 0); central.writeUInt16LE(20, 4); central.writeUInt16LE(20, 6); central.writeUInt16LE(useDeflate ? 8 : 0, 10); central.writeUInt32LE(crc, 16); central.writeUInt32LE(body.length, 20); central.writeUInt32LE(f.data.length, 24); central.writeUInt16LE(name.length, 28); central.writeUInt32LE(offset, 42); name.copy(central, 46); centrals.push(central); offset += local.length + body.length; } const dir = Buffer.concat(centrals); const eocd = Buffer.alloc(22); eocd.writeUInt32LE(EOCD, 0); eocd.writeUInt16LE(files.length, 8); eocd.writeUInt16LE(files.length, 10); eocd.writeUInt32LE(dir.length, 12); eocd.writeUInt32LE(offset, 16); const out = Buffer.concat([...locals, dir, eocd]); if (out.length > 0xffffffff) throw new Error('Ueber 4 GB braeuchte ZIP64.'); return out; }