irc_web/src/utils/zip-stream.js

382 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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数据描述符存在新增0x0800UTF-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