// Reads entries of a ZIP archive without dependencies: the central directory, then one entry, stored or deflated // (node:zlib, built into Bun). Used for an .ipa / .aab (Payload/.app/Info.plist, the manifest) and the Play // financial report exports (earnings_YYYYMM.zip, salesreport_YYYYMM.zip: one CSV inside). No ZIP64, no encryption. import { closeSync, fstatSync, openSync, readSync } from 'node:fs'; import { inflateRawSync } from 'node:zlib'; export interface ZipEntry { name: string; method: number; compressedSize: number; size: number; localOffset: number } function readAt(fd: number, pos: number, len: number): Buffer { const b = Buffer.alloc(len); const n = readSync(fd, b, 0, len, pos); return b.subarray(0, n); } /** Entries of the central directory of a ZIP file (path) or buffer. */ export function zipEntries(src: string | Buffer): ZipEntry[] { const fd = typeof src === 'string' ? openSync(src, 'r') : -1; try { const size = fd >= 0 ? fstatSync(fd).size : (src as Buffer).length; const read = (pos: number, len: number): Buffer => (fd >= 0 ? readAt(fd, pos, len) : (src as Buffer).subarray(pos, pos + len)); const tailLen = Math.min(size, 65_557); const tail = read(size - tailLen, tailLen); let eocd = -1; for (let i = tail.length - 22; i >= 0; i--) if (tail.readUInt32LE(i) === 0x06054b50) { eocd = i; break; } if (eocd < 0) throw new Error('not a ZIP archive (no end of central directory)'); const count = tail.readUInt16LE(eocd + 10), cdSize = tail.readUInt32LE(eocd + 12), cdOff = tail.readUInt32LE(eocd + 16); if (cdOff === 0xffffffff) throw new Error('ZIP64 archives are not supported'); const cd = read(cdOff, cdSize); const out: ZipEntry[] = []; for (let p = 0, k = 0; k < count && p + 46 <= cd.length; k++) { if (cd.readUInt32LE(p) !== 0x02014b50) throw new Error('ZIP: bad central directory entry'); const nameLen = cd.readUInt16LE(p + 28), extraLen = cd.readUInt16LE(p + 30), commentLen = cd.readUInt16LE(p + 32); out.push({ name: cd.subarray(p + 46, p + 46 + nameLen).toString('utf8'), method: cd.readUInt16LE(p + 10), compressedSize: cd.readUInt32LE(p + 20), size: cd.readUInt32LE(p + 24), localOffset: cd.readUInt32LE(p + 42) }); p += 46 + nameLen + extraLen + commentLen; } return out; } finally { if (fd >= 0) closeSync(fd); } } /** The uncompressed bytes of one entry. */ export function zipRead(src: string | Buffer, e: ZipEntry): Buffer { const fd = typeof src === 'string' ? openSync(src, 'r') : -1; try { const read = (pos: number, len: number): Buffer => (fd >= 0 ? readAt(fd, pos, len) : (src as Buffer).subarray(pos, pos + len)); const h = read(e.localOffset, 30); if (h.readUInt32LE(0) !== 0x04034b50) throw new Error(`ZIP: bad local header for ${e.name}`); const data = read(e.localOffset + 30 + h.readUInt16LE(26) + h.readUInt16LE(28), e.compressedSize); if (e.method === 0) return Buffer.from(data); if (e.method === 8) return inflateRawSync(data); throw new Error(`ZIP: compression method ${e.method} not supported (${e.name})`); } finally { if (fd >= 0) closeSync(fd); } } /** Every file entry with its uncompressed bytes, from an in-memory archive (a Play report export). */ export function unzip(buf: Uint8Array): { name: string; data: Uint8Array }[] { const b = Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength); return zipEntries(b).filter(e => !e.name.endsWith('/')).map(e => ({ name: e.name, data: new Uint8Array(zipRead(b, e)) })); }