Conflicts resolved for both sides: - cli/lib/plist.ts, cli/lib/zip.ts: one implementation each. plist reads XML (text or bytes, attributes tolerated) and binary plists; ads scan now reads bytes, so a binary Info.plist works too. zip gains unzip() for the Play report exports on top of zipEntries / zipRead. - apple upload waits with testflight wait's loop (waitForBuild) instead of its own copy; BUILD_DONE removed. - config, credentials (profiles, keystore, ASC_VENDOR_NUMBER, now also a profile key), command table, MCP tools, credentials template, README, CHANGELOG: union. - MCP: --write stays forbidden; the three pull tools are the listed local-file exception (LOCAL_WRITE_TOOLS), so scan --write remains unreachable. - Adapters and API coverage tables regenerated.
59 lines
3.5 KiB
TypeScript
59 lines
3.5 KiB
TypeScript
// 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>.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)) }));
|
|
}
|