// The node-side portability zip reader lives in @paperclipai/shared so the // server can consume the same codec (a raw uploaded zip is unzipped into the // exact `{ rootPath, files }` bundle the inline import source carries). This // module re-exports it to keep the CLI's existing import paths stable. export { binaryContentTypeByExtension, bytesToPortableFileEntry, isBlobStorePath, readZipArchive, } from "@paperclipai/shared/portability-zip"; // STORE-only zip writer used to package a local folder in memory for the // chunked import transfer path. STORE keeps the writer dependency-free; the // transfer slices the raw archive bytes, so compression only trades CPU for // part count. Classic zip only: entry counts and offsets past the 16/32-bit // header fields would need zip64, which the reader side does not require and // this writer refuses to emit. const ZIP_MAX_ENTRIES = 0xffff; const ZIP_MAX_OFFSET_BYTES = 0xffffffff; function writeUint16(target: Uint8Array, offset: number, value: number) { target[offset] = value & 0xff; target[offset + 1] = (value >>> 8) & 0xff; } function writeUint32(target: Uint8Array, offset: number, value: number) { target[offset] = value & 0xff; target[offset + 1] = (value >>> 8) & 0xff; target[offset + 2] = (value >>> 16) & 0xff; target[offset + 3] = (value >>> 24) & 0xff; } function crc32(bytes: Uint8Array) { let crc = 0xffffffff; for (const byte of bytes) { crc ^= byte; for (let bit = 0; bit < 8; bit += 1) { crc = (crc & 1) === 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1; } } return (crc ^ 0xffffffff) >>> 0; } /** * Build a stored (uncompressed) zip of `files` under a single `rootPath/` * top-level directory — the layout `readZipArchive` folds back into the * inline `{ rootPath, files }` bundle. Entries are written in sorted path * order and carry no timestamps, so the same content always produces the * same bytes and a re-run resumes its content-addressed transfer. */ export function createStoredZipArchive(files: Record, rootPath: string): Uint8Array { const entries = Object.entries(files).sort(([left], [right]) => left.localeCompare(right)); if (entries.length > ZIP_MAX_ENTRIES) { throw new Error(`Package has too many files to zip (${entries.length}; the zip format caps at ${ZIP_MAX_ENTRIES}).`); } const encoder = new TextEncoder(); const localChunks: Uint8Array[] = []; const centralChunks: Uint8Array[] = []; let localOffset = 0; for (const [relativePath, body] of entries) { const fileName = encoder.encode(`${rootPath}/${relativePath}`); const checksum = crc32(body); const localHeader = new Uint8Array(30 + fileName.length); writeUint32(localHeader, 0, 0x04034b50); writeUint16(localHeader, 4, 20); writeUint16(localHeader, 6, 0x0800); writeUint16(localHeader, 8, 0); writeUint32(localHeader, 14, checksum); writeUint32(localHeader, 18, body.length); writeUint32(localHeader, 22, body.length); writeUint16(localHeader, 26, fileName.length); localHeader.set(fileName, 30); const centralHeader = new Uint8Array(46 + fileName.length); writeUint32(centralHeader, 0, 0x02014b50); writeUint16(centralHeader, 4, 20); writeUint16(centralHeader, 6, 20); writeUint16(centralHeader, 8, 0x0800); writeUint16(centralHeader, 10, 0); writeUint32(centralHeader, 16, checksum); writeUint32(centralHeader, 20, body.length); writeUint32(centralHeader, 24, body.length); writeUint16(centralHeader, 28, fileName.length); writeUint32(centralHeader, 42, localOffset); centralHeader.set(fileName, 46); localChunks.push(localHeader, body); centralChunks.push(centralHeader); localOffset += localHeader.length + body.length; if (body.length > ZIP_MAX_OFFSET_BYTES || localOffset > ZIP_MAX_OFFSET_BYTES) { throw new Error("Package is too large to zip in memory (zip64 archives are not supported)."); } } const centralDirectoryLength = centralChunks.reduce((sum, chunk) => sum + chunk.length, 0); const archive = new Uint8Array(localOffset + centralDirectoryLength + 22); let offset = 0; for (const chunk of localChunks) { archive.set(chunk, offset); offset += chunk.length; } const centralDirectoryOffset = offset; for (const chunk of centralChunks) { archive.set(chunk, offset); offset += chunk.length; } writeUint32(archive, offset, 0x06054b50); writeUint16(archive, offset + 8, entries.length); writeUint16(archive, offset + 10, entries.length); writeUint32(archive, offset + 12, centralDirectoryLength); writeUint32(archive, offset + 16, centralDirectoryOffset); return archive; }