/** * The entries a ZIP's start shows: its local file headers, one before each entry's data, as far as the data * holds them (the first segment of a .zip; REBUILD.md, phase 2). The full list is in the central directory at * the end; the start names the first entries and says whether they're encrypted, as a RAR's first volume does. */ export interface ZipStart { files: { name: string; size: number }[] encrypted: boolean } const LOCAL = 0x04034b50 /** Traditional PKWARE encryption (bit 0 of the flags), or WinZip AES (method 99). */ const ENCRYPTED_FLAG = 0x1 const DATA_DESCRIPTOR_FLAG = 0x8 const AES_METHOD = 99 const MAX_ENTRIES = 10_000 export function parseZipStart(data: Buffer): ZipStart | null { if (data.length < 30 || data.readUInt32LE(0) !== LOCAL) return null const result: ZipStart = { files: [], encrypted: false } let pos = 0 while (pos + 30 <= data.length && data.readUInt32LE(pos) === LOCAL && result.files.length < MAX_ENTRIES) { const flags = data.readUInt16LE(pos + 6) const method = data.readUInt16LE(pos + 8) let compressed = data.readUInt32LE(pos + 18) let size = data.readUInt32LE(pos + 22) const nameLength = data.readUInt16LE(pos + 26) const extraLength = data.readUInt16LE(pos + 28) const nameEnd = pos + 30 + nameLength if (nameEnd > data.length) break // Bit 11: UTF-8 names; others are mostly CP437, read as latin1. const name = data.toString(flags & 0x800 ? 'utf8' : 'latin1', pos + 30, nameEnd) // Zip64: the real sizes in extra field 0x0001. for (let e = nameEnd; e + 4 <= Math.min(nameEnd + extraLength, data.length); ) { const id = data.readUInt16LE(e) const length = data.readUInt16LE(e + 2) if (id === 0x0001 && e + 4 + length <= data.length && length >= 16) { size = Number(data.readBigUInt64LE(e + 4)) compressed = Number(data.readBigUInt64LE(e + 12)) } e += 4 + length } if (flags & ENCRYPTED_FLAG || method === AES_METHOD) result.encrypted = true if (!name.endsWith('/')) result.files.push({ name, size }) // Sizes after the data (a streamed zip): where the next header starts isn't known. if (flags & DATA_DESCRIPTOR_FLAG) break pos = nameEnd + extraLength + compressed } return result.files.length > 0 || result.encrypted ? result : null }