import { normalize } from 'node:path' const CRC_TABLE = 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) { let crc = 0xffffffff for (const byte of data) crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8) return (crc ^ 0xffffffff) >>> 0 } function archivePath(value) { const path = normalize(String(value || '').replace(/\\/g, '/')).replace(/\\/g, '/').replace(/^\.\//, '') if (!path || path.startsWith('/') || path === '..' || path.startsWith('../')) throw new Error('ZIP 文件路径无效') return path } function dosDateTime(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(), } } /** 创建无额外依赖的 ZIP(Store 模式);PNG 和 .spine 本身已压缩,无需重复压缩。 */ export function createZipArchive(files) { const localParts = [] const centralParts = [] let offset = 0 const timestamp = dosDateTime() for (const file of files) { const name = Buffer.from(archivePath(file.name), 'utf8') const data = Buffer.isBuffer(file.data) ? file.data : Buffer.from(file.data) const crc = crc32(data) const local = Buffer.alloc(30) local.writeUInt32LE(0x04034b50, 0) local.writeUInt16LE(20, 4) local.writeUInt16LE(0x0800, 6) local.writeUInt16LE(0, 8) local.writeUInt16LE(timestamp.time, 10) local.writeUInt16LE(timestamp.date, 12) local.writeUInt32LE(crc, 14) local.writeUInt32LE(data.length, 18) local.writeUInt32LE(data.length, 22) local.writeUInt16LE(name.length, 26) localParts.push(local, name, data) const central = Buffer.alloc(46) central.writeUInt32LE(0x02014b50, 0) central.writeUInt16LE(20, 4) central.writeUInt16LE(20, 6) central.writeUInt16LE(0x0800, 8) central.writeUInt16LE(0, 10) central.writeUInt16LE(timestamp.time, 12) central.writeUInt16LE(timestamp.date, 14) central.writeUInt32LE(crc, 16) central.writeUInt32LE(data.length, 20) central.writeUInt32LE(data.length, 24) central.writeUInt16LE(name.length, 28) central.writeUInt32LE(offset, 42) centralParts.push(central, name) offset += local.length + name.length + data.length } const centralDirectory = Buffer.concat(centralParts) const end = Buffer.alloc(22) end.writeUInt32LE(0x06054b50, 0) end.writeUInt16LE(files.length, 8) end.writeUInt16LE(files.length, 10) end.writeUInt32LE(centralDirectory.length, 12) end.writeUInt32LE(offset, 16) return Buffer.concat([...localParts, centralDirectory, end]) }