import { translate } from '../i18n' export interface ZipArchiveFile { name: string data: Uint8Array } const crcTable = Array.from({ length: 256 }, (_, value) => { let crc = value for (let bit = 0; bit < 8; bit++) crc = (crc & 1) ? 0xedb88320 ^ (crc >>> 1) : crc >>> 1 return crc >>> 0 }) function crc32(data: Uint8Array) { let crc = 0xffffffff for (const byte of data) crc = crcTable[(crc ^ byte) & 0xff] ^ (crc >>> 8) return (crc ^ 0xffffffff) >>> 0 } function safeArchivePath(value: string) { const parts = String(value || '').replace(/\\/g, '/').split('/').filter((part) => part && part !== '.') if (!parts.length || parts.some((part) => part === '..')) throw new Error(translate('errors.invalidZipPath')) return parts.join('/') } function dateTime(date = new Date()) { const year = Math.max(1980, date.getFullYear()) return { time: (date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2), date: ((year - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate(), } } function header(size: number) { const bytes = new Uint8Array(size) return { bytes, view: new DataView(bytes.buffer) } } function combine(chunks: Uint8Array[]) { const output = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.length, 0)) let offset = 0 for (const chunk of chunks) { output.set(chunk, offset); offset += chunk.length } return output } /** 创建 Store 模式 ZIP;图片本身已经压缩,避免引入额外打包依赖。 */ export function createZipArchive(files: ZipArchiveFile[]) { const encoder = new TextEncoder() const localChunks: Uint8Array[] = [] const centralChunks: Uint8Array[] = [] const timestamp = dateTime() let offset = 0 for (const file of files) { const name = encoder.encode(safeArchivePath(file.name)) const crc = crc32(file.data) const local = header(30) local.view.setUint32(0, 0x04034b50, true) local.view.setUint16(4, 20, true) local.view.setUint16(6, 0x0800, true) local.view.setUint16(10, timestamp.time, true) local.view.setUint16(12, timestamp.date, true) local.view.setUint32(14, crc, true) local.view.setUint32(18, file.data.length, true) local.view.setUint32(22, file.data.length, true) local.view.setUint16(26, name.length, true) localChunks.push(local.bytes, name, file.data) const central = header(46) central.view.setUint32(0, 0x02014b50, true) central.view.setUint16(4, 20, true) central.view.setUint16(6, 20, true) central.view.setUint16(8, 0x0800, true) central.view.setUint16(12, timestamp.time, true) central.view.setUint16(14, timestamp.date, true) central.view.setUint32(16, crc, true) central.view.setUint32(20, file.data.length, true) central.view.setUint32(24, file.data.length, true) central.view.setUint16(28, name.length, true) central.view.setUint32(42, offset, true) centralChunks.push(central.bytes, name) offset += local.bytes.length + name.length + file.data.length } const centralDirectory = combine(centralChunks) const end = header(22) end.view.setUint32(0, 0x06054b50, true) end.view.setUint16(8, files.length, true) end.view.setUint16(10, files.length, true) end.view.setUint32(12, centralDirectory.length, true) end.view.setUint32(16, offset, true) return new Blob([combine([...localChunks, centralDirectory, end.bytes])], { type: 'application/zip' }) }