382 lines
12 KiB
JavaScript
382 lines
12 KiB
JavaScript
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 |