export interface TransferArchiveEntry { name: string; relativePath?: string; data: Uint8Array | ArrayBuffer | Blob; lastModified?: number; } const ZIP_UTF8_FLAG = 0x0800; const CRC32_TABLE = createCrc32Table(); function createCrc32Table() { const table = new Uint32Array(256); for (let index = 0; index < 256; index += 1) { let value = index; for (let bit = 0; bit < 8; bit += 1) { value = (value & 1) === 1 ? (0xEDB88320 ^ (value >>> 1)) : (value >>> 1); } table[index] = value >>> 0; } return table; } function sanitizeArchivePath(entry: TransferArchiveEntry) { const rawPath = entry.relativePath?.trim() || entry.name; const normalizedPath = rawPath .replaceAll('\\', '/') .split('/') .map((segment) => segment.trim()) .filter(Boolean) .join('/'); return normalizedPath || entry.name; } function crc32(bytes: Uint8Array) { let value = 0xFFFFFFFF; for (const byte of bytes) { value = CRC32_TABLE[(value ^ byte) & 0xFF] ^ (value >>> 8); } return (value ^ 0xFFFFFFFF) >>> 0; } function toDosDateTime(timestamp: number) { const date = new Date(timestamp); const year = Math.max(1980, date.getFullYear()); const month = date.getMonth() + 1; const day = date.getDate(); const hours = date.getHours(); const minutes = date.getMinutes(); const seconds = Math.floor(date.getSeconds() / 2); return { time: (hours << 11) | (minutes << 5) | seconds, date: ((year - 1980) << 9) | (month << 5) | day, }; } function writeUint16(view: DataView, offset: number, value: number) { view.setUint16(offset, value, true); } function writeUint32(view: DataView, offset: number, value: number) { view.setUint32(offset, value >>> 0, true); } function concatUint8Arrays(chunks: Uint8Array[]) { const totalLength = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0); const output = new Uint8Array(totalLength); let offset = 0; for (const chunk of chunks) { output.set(chunk, offset); offset += chunk.byteLength; } return output; } async function normalizeArchiveData(data: TransferArchiveEntry['data']) { if (data instanceof Uint8Array) { return data; } if (data instanceof Blob) { return new Uint8Array(await data.arrayBuffer()); } return new Uint8Array(data); } export function buildTransferArchiveFileName(baseName: string) { return baseName.toLowerCase().endsWith('.zip') ? baseName : `${baseName}.zip`; } export async function createTransferZipArchive(entries: TransferArchiveEntry[]) { const encoder = new TextEncoder(); const fileSections: Uint8Array[] = []; const centralDirectorySections: Uint8Array[] = []; let offset = 0; for (const entry of entries) { const fileName = sanitizeArchivePath(entry); const fileNameBytes = encoder.encode(fileName); const fileData = await normalizeArchiveData(entry.data); const checksum = crc32(fileData); const {time, date} = toDosDateTime(entry.lastModified ?? Date.now()); const localHeader = new Uint8Array(30); const localHeaderView = new DataView(localHeader.buffer); writeUint32(localHeaderView, 0, 0x04034B50); writeUint16(localHeaderView, 4, 20); writeUint16(localHeaderView, 6, ZIP_UTF8_FLAG); writeUint16(localHeaderView, 8, 0); writeUint16(localHeaderView, 10, time); writeUint16(localHeaderView, 12, date); writeUint32(localHeaderView, 14, checksum); writeUint32(localHeaderView, 18, fileData.byteLength); writeUint32(localHeaderView, 22, fileData.byteLength); writeUint16(localHeaderView, 26, fileNameBytes.byteLength); writeUint16(localHeaderView, 28, 0); fileSections.push(localHeader, fileNameBytes, fileData); const centralHeader = new Uint8Array(46); const centralHeaderView = new DataView(centralHeader.buffer); writeUint32(centralHeaderView, 0, 0x02014B50); writeUint16(centralHeaderView, 4, 20); writeUint16(centralHeaderView, 6, 20); writeUint16(centralHeaderView, 8, ZIP_UTF8_FLAG); writeUint16(centralHeaderView, 10, 0); writeUint16(centralHeaderView, 12, time); writeUint16(centralHeaderView, 14, date); writeUint32(centralHeaderView, 16, checksum); writeUint32(centralHeaderView, 20, fileData.byteLength); writeUint32(centralHeaderView, 24, fileData.byteLength); writeUint16(centralHeaderView, 28, fileNameBytes.byteLength); writeUint16(centralHeaderView, 30, 0); writeUint16(centralHeaderView, 32, 0); writeUint16(centralHeaderView, 34, 0); writeUint16(centralHeaderView, 36, 0); writeUint32(centralHeaderView, 38, 0); writeUint32(centralHeaderView, 42, offset); centralDirectorySections.push(centralHeader, fileNameBytes); offset += localHeader.byteLength + fileNameBytes.byteLength + fileData.byteLength; } const centralDirectory = concatUint8Arrays(centralDirectorySections); const endRecord = new Uint8Array(22); const endRecordView = new DataView(endRecord.buffer); writeUint32(endRecordView, 0, 0x06054B50); writeUint16(endRecordView, 4, 0); writeUint16(endRecordView, 6, 0); writeUint16(endRecordView, 8, entries.length); writeUint16(endRecordView, 10, entries.length); writeUint32(endRecordView, 12, centralDirectory.byteLength); writeUint32(endRecordView, 16, offset); writeUint16(endRecordView, 20, 0); return new Blob([ concatUint8Arrays(fileSections), centralDirectory, endRecord, ], { type: 'application/zip', }); }