class Crc32 { constructor() { this.crc = -1 } append(data) { var crc = this.crc | 0; var table = this.table for (var offset = 0, len = data.length | 0; offset < len; offset++) { crc = (crc >>> 8) ^ table[(crc ^ data[offset]) & 0xFF] } this.crc = crc } get() { return ~this.crc } } Crc32.prototype.table = (() => { var i; var j; var t; var table = [] for (i = 0; i < 256; i++) { t = i for (j = 0; j < 8; j++) { t = (t & 1) ? (t >>> 1) ^ 0xEDB88320 : t >>> 1 } table[i] = t } return table })() const getDataHelper = byteLength => { var uint8 = new Uint8Array(byteLength) return { array: uint8, view: new DataView(uint8.buffer) } } const ZIP_SIGNATURE_LOCAL = 0x04034b50 const ZIP_SIGNATURE_CENTRAL = 0x02014b50 const ZIP_SIGNATURE_EOCD = 0x06054b50 const ZIP_SIGNATURE_ZIP64_EOCD = 0x06064b50 const ZIP_SIGNATURE_ZIP64_LOCATOR = 0x07064b50 const ZIP64_MAGIC = 0xFFFFFFFF function u16(view, offset, value) { view.setUint16(offset, value, true) } function u32(view, offset, value) { view.setUint32(offset, value >>> 0, true) } function concatUint8(chunks, total) { const out = new Uint8Array(total) let off = 0 for (const c of chunks) { out.set(c, off) off += c.length } return out } function createWriter(underlyingSource) { const files = Object.create(null) const filenames = [] const encoder = new TextEncoder() // 保持UTF-8编码 let offset = 0 // bytes written to output so far let activeZipIndex = 0 let ctrl let activeZipObject let closed = false function processNextChunk() { if (!activeZipObject) return // directory entry: just local header + immediate footer if (activeZipObject.directory) { if (!activeZipObject.headerWritten) { activeZipObject.writeLocalHeader() activeZipObject.headerWritten = true } if (!activeZipObject.footerWritten) { activeZipObject.writeDataDescriptor() // will be zeros activeZipObject.footerWritten = true next() } return } if (!activeZipObject.reader) { if (!activeZipObject.fileLike || !activeZipObject.fileLike.stream) { next() return } activeZipObject.crc = new Crc32() activeZipObject.reader = activeZipObject.fileLike.stream().getReader() activeZipObject.writeLocalHeader() activeZipObject.headerWritten = true return } return activeZipObject.reader.read().then(({ done, value }) => { if (done) { activeZipObject.writeDataDescriptor() activeZipObject.footerWritten = true next() return } const chunk = value instanceof Uint8Array ? value : new Uint8Array(value) activeZipObject.crc.append(chunk) activeZipObject.uncompressedLength += chunk.length activeZipObject.compressedLength += chunk.length ctrl.enqueue(chunk) offset += chunk.length }) } function next() { activeZipIndex++ activeZipObject = files[filenames[activeZipIndex]] if (activeZipObject) { processNextChunk() } else if (closed) { closeZip() } } function dosDateTime(date) { const dt = new Date(date) const time = ((dt.getHours() << 11) | (dt.getMinutes() << 5) | (dt.getSeconds() / 2)) & 0xFFFF const d = (((dt.getFullYear() - 1980) << 9) | ((dt.getMonth() + 1) << 5) | dt.getDate()) & 0xFFFF return { time, date: d } } const zipWriter = { enqueue(fileLike) { if (closed) throw new TypeError('Cannot enqueue after close()') let name = String(fileLike.name || '').trim() if (!name) throw new Error('Missing file name') if (fileLike.directory && !name.endsWith('/')) name += '/' if (files[name]) throw new Error('File already exists.') const nameBuf = encoder.encode(name) // 保持UTF-8编码 const commentBuf = encoder.encode(fileLike.comment || '') const { time, date } = dosDateTime(typeof fileLike.lastModified === 'undefined' ? Date.now() : fileLike.lastModified) const zipObject = (files[name] = { fileLike, directory: !!fileLike.directory, nameBuf, comment: commentBuf, time, date, // tracked offset: 0, crc: null, compressedLength: 0, uncompressedLength: 0, headerWritten: false, footerWritten: false, reader: null, // whether sizes/offset require ZIP64 needsZip64() { return ( this.uncompressedLength > ZIP64_MAGIC || this.compressedLength > ZIP64_MAGIC || this.offset > ZIP64_MAGIC ) }, writeLocalHeader() { this.offset = offset // 关键修改:设置通用位标记,第11位(0x0800)表示文件名使用UTF-8编码 // 保留原0x0008(数据描述符存在),新增0x0800(UTF-8编码) const generalPurposeBitFlag = 0x0808 const compressionMethod = 0 // store const versionNeeded = this.needsZip64() ? 45 : 20 // 适配ZIP64 // We always use data descriptor; write zeros for crc/sizes in local header const local = getDataHelper(30 + this.nameBuf.length) u32(local.view, 0, ZIP_SIGNATURE_LOCAL) u16(local.view, 4, versionNeeded) u16(local.view, 6, generalPurposeBitFlag) // 应用UTF-8标记 u16(local.view, 8, compressionMethod) u16(local.view, 10, this.time) u16(local.view, 12, this.date) u32(local.view, 14, 0) u32(local.view, 18, 0) u32(local.view, 22, 0) u16(local.view, 26, this.nameBuf.length) u16(local.view, 28, 0) // extra length local.array.set(this.nameBuf, 30) ctrl.enqueue(local.array) offset += local.array.length }, writeDataDescriptor() { // data descriptor: optional signature + crc32 + sizes (32-bit or 64-bit) const crc = this.crc ? this.crc.get() >>> 0 : 0 const useZip64 = this.needsZip64() if (!useZip64) { const dd = getDataHelper(16) u32(dd.view, 0, 0x08074b50) u32(dd.view, 4, crc) u32(dd.view, 8, this.compressedLength) u32(dd.view, 12, this.uncompressedLength) ctrl.enqueue(dd.array) offset += dd.array.length } else { // 24 bytes: sig + crc32 + compSize(8) + uncompSize(8) const dd = getDataHelper(24) u32(dd.view, 0, 0x08074b50) u32(dd.view, 4, crc) // BigInt writes dd.view.setBigUint64(8, BigInt(this.compressedLength), true) dd.view.setBigUint64(16, BigInt(this.uncompressedLength), true) ctrl.enqueue(dd.array) offset += dd.array.length } } }) filenames.push(name) if (!activeZipObject) { activeZipObject = zipObject processNextChunk() } }, close() { if (closed) throw new TypeError('Cannot close twice') closed = true if (!activeZipObject) closeZip() } } function closeZip() { // Build central directory in memory then enqueue once. const cdChunks = [] let cdSize = 0 for (const name of filenames) { const file = files[name] const useZip64 = file.needsZip64() const versionMadeBy = useZip64 ? 45 : 20 const versionNeeded = useZip64 ? 45 : 20 // 关键修改:中央目录也需要设置UTF-8标记 const generalPurposeBitFlag = 0x0808 const compressionMethod = 0 const crc = file.crc ? file.crc.get() >>> 0 : 0 const compressed32 = useZip64 ? ZIP64_MAGIC : file.compressedLength const uncompressed32 = useZip64 ? ZIP64_MAGIC : file.uncompressedLength const offset32 = useZip64 ? ZIP64_MAGIC : file.offset // ZIP64 extra field if needed let extra = new Uint8Array(0) if (useZip64) { // headerId(2)=0x0001, dataSize(2)=24, uncompressed(8), compressed(8), offset(8) extra = new Uint8Array(4 + 24) const ev = new DataView(extra.buffer) u16(ev, 0, 0x0001) u16(ev, 2, 24) ev.setBigUint64(4, BigInt(file.uncompressedLength), true) ev.setBigUint64(12, BigInt(file.compressedLength), true) ev.setBigUint64(20, BigInt(file.offset), true) } const headerLen = 46 + file.nameBuf.length + extra.length + file.comment.length const cd = getDataHelper(headerLen) u32(cd.view, 0, ZIP_SIGNATURE_CENTRAL) u16(cd.view, 4, versionMadeBy) u16(cd.view, 6, versionNeeded) u16(cd.view, 8, generalPurposeBitFlag) // 中央目录应用UTF-8标记 u16(cd.view, 10, compressionMethod) u16(cd.view, 12, file.time) u16(cd.view, 14, file.date) u32(cd.view, 16, crc) u32(cd.view, 20, compressed32) u32(cd.view, 24, uncompressed32) u16(cd.view, 28, file.nameBuf.length) u16(cd.view, 30, extra.length) u16(cd.view, 32, file.comment.length) u16(cd.view, 34, 0) // disk number start u16(cd.view, 36, 0) // internal attrs // external file attrs: mark directory u32(cd.view, 38, file.directory ? 0x10 : 0) u32(cd.view, 42, offset32) cd.array.set(file.nameBuf, 46) if (extra.length) cd.array.set(extra, 46 + file.nameBuf.length) if (file.comment.length) cd.array.set(file.comment, 46 + file.nameBuf.length + extra.length) cdChunks.push(cd.array) cdSize += cd.array.length } const centralDirectoryOffset = offset const centralDirectory = concatUint8(cdChunks, cdSize) ctrl.enqueue(centralDirectory) offset += centralDirectory.length const entries = filenames.length const needsZip64ForArchive = entries > 0xFFFF || centralDirectoryOffset > ZIP64_MAGIC || cdSize > ZIP64_MAGIC || offset > ZIP64_MAGIC const tailChunks = [] let tailSize = 0 if (needsZip64ForArchive) { // ZIP64 EOCD const zip64eocd = getDataHelper(56) u32(zip64eocd.view, 0, ZIP_SIGNATURE_ZIP64_EOCD) zip64eocd.view.setBigUint64(4, BigInt(44), true) // remaining size u16(zip64eocd.view, 12, 45) u16(zip64eocd.view, 14, 45) u32(zip64eocd.view, 16, 0) u32(zip64eocd.view, 20, 0) zip64eocd.view.setBigUint64(24, BigInt(entries), true) zip64eocd.view.setBigUint64(32, BigInt(entries), true) zip64eocd.view.setBigUint64(40, BigInt(cdSize), true) zip64eocd.view.setBigUint64(48, BigInt(centralDirectoryOffset), true) // ZIP64 locator const locator = getDataHelper(20) u32(locator.view, 0, ZIP_SIGNATURE_ZIP64_LOCATOR) u32(locator.view, 4, 0) locator.view.setBigUint64(8, BigInt(centralDirectoryOffset + cdSize), true) // offset of zip64eocd u32(locator.view, 16, 1) tailChunks.push(zip64eocd.array, locator.array) tailSize += zip64eocd.array.length + locator.array.length } // EOCD const eocd = getDataHelper(22) u32(eocd.view, 0, ZIP_SIGNATURE_EOCD) u16(eocd.view, 4, 0) u16(eocd.view, 6, 0) u16(eocd.view, 8, Math.min(entries, 0xFFFF)) u16(eocd.view, 10, Math.min(entries, 0xFFFF)) u32(eocd.view, 12, needsZip64ForArchive ? ZIP64_MAGIC : cdSize) u32(eocd.view, 16, needsZip64ForArchive ? ZIP64_MAGIC : centralDirectoryOffset) u16(eocd.view, 20, 0) // comment length tailChunks.push(eocd.array) tailSize += eocd.array.length ctrl.enqueue(concatUint8(tailChunks, tailSize)) ctrl.close() } return new ReadableStream({ start: c => { ctrl = c underlyingSource.start && Promise.resolve(underlyingSource.start(zipWriter)) }, pull() { return processNextChunk() || ( underlyingSource.pull && Promise.resolve(underlyingSource.pull(zipWriter)) ) } }) } window.ZIP = createWriter