Files
vendoo/lib/archive.mjs

79 lines
3.7 KiB
JavaScript

import { inflateRawSync } from 'zlib';
const EOCD_SIGNATURE = 0x06054b50;
const CENTRAL_SIGNATURE = 0x02014b50;
const LOCAL_SIGNATURE = 0x04034b50;
function normalizeEntryName(name) {
const normalized = String(name || '').replace(/\\/g, '/').replace(/^\/+/, '');
if (!normalized || normalized.endsWith('/')) return normalized;
if (normalized.includes('\0') || normalized.split('/').some(part => part === '..')) {
throw new Error(`Unsicherer ZIP-Pfad: ${name}`);
}
return normalized;
}
function findEndOfCentralDirectory(buffer) {
const min = Math.max(0, buffer.length - 0xffff - 22);
for (let offset = buffer.length - 22; offset >= min; offset--) {
if (buffer.readUInt32LE(offset) === EOCD_SIGNATURE) return offset;
}
throw new Error('ZIP-Endverzeichnis wurde nicht gefunden.');
}
export function readZipEntries(buffer, { maxEntries = 10000, maxUncompressedBytes = 8 * 1024 * 1024 * 1024 } = {}) {
if (!Buffer.isBuffer(buffer)) buffer = Buffer.from(buffer || []);
const eocd = findEndOfCentralDirectory(buffer);
const entryCount = buffer.readUInt16LE(eocd + 10);
const centralSize = buffer.readUInt32LE(eocd + 12);
const centralOffset = buffer.readUInt32LE(eocd + 16);
if (entryCount > maxEntries) throw new Error(`ZIP enthält zu viele Dateien (${entryCount}).`);
if (centralOffset + centralSize > buffer.length) throw new Error('ZIP-Zentralverzeichnis ist beschädigt.');
const entries = [];
let offset = centralOffset;
let totalUncompressed = 0;
for (let index = 0; index < entryCount; index++) {
if (buffer.readUInt32LE(offset) !== CENTRAL_SIGNATURE) throw new Error('ZIP-Zentralverzeichnis ist ungültig.');
const flags = buffer.readUInt16LE(offset + 8);
const method = buffer.readUInt16LE(offset + 10);
const crc = buffer.readUInt32LE(offset + 16);
const compressedSize = buffer.readUInt32LE(offset + 20);
const uncompressedSize = buffer.readUInt32LE(offset + 24);
const nameLength = buffer.readUInt16LE(offset + 28);
const extraLength = buffer.readUInt16LE(offset + 30);
const commentLength = buffer.readUInt16LE(offset + 32);
const localOffset = buffer.readUInt32LE(offset + 42);
const nameStart = offset + 46;
const name = normalizeEntryName(buffer.subarray(nameStart, nameStart + nameLength).toString((flags & 0x0800) ? 'utf8' : 'utf8'));
totalUncompressed += uncompressedSize;
if (totalUncompressed > maxUncompressedBytes) throw new Error('ZIP ist nach dem Entpacken zu groß.');
entries.push({ name, method, crc, compressedSize, uncompressedSize, localOffset, directory: name.endsWith('/') });
offset = nameStart + nameLength + extraLength + commentLength;
}
return entries;
}
export function extractZipEntry(buffer, entry) {
if (entry.directory) return Buffer.alloc(0);
const offset = entry.localOffset;
if (buffer.readUInt32LE(offset) !== LOCAL_SIGNATURE) throw new Error(`Lokaler ZIP-Header fehlt: ${entry.name}`);
const nameLength = buffer.readUInt16LE(offset + 26);
const extraLength = buffer.readUInt16LE(offset + 28);
const dataStart = offset + 30 + nameLength + extraLength;
const compressed = buffer.subarray(dataStart, dataStart + entry.compressedSize);
let data;
if (entry.method === 0) data = Buffer.from(compressed);
else if (entry.method === 8) data = inflateRawSync(compressed);
else throw new Error(`Nicht unterstützte ZIP-Kompression (${entry.method}) für ${entry.name}.`);
if (data.length !== entry.uncompressedSize) throw new Error(`ZIP-Größe stimmt nicht: ${entry.name}`);
return data;
}
export function readZip(buffer, options) {
const entries = readZipEntries(buffer, options);
const files = new Map();
for (const entry of entries) if (!entry.directory) files.set(entry.name, extractZipEntry(buffer, entry));
return { entries, files };
}