From ec8360913910c5419174fd7a7638cc4040d39e1c Mon Sep 17 00:00:00 2001 From: wangxiaoshuang <825034831@qq.com> Date: Mon, 19 Jan 2026 15:53:11 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E6=96=87=E4=BB=B6=E6=94=AF?= =?UTF-8?q?=E6=8C=81zip64?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/stream.js | 2 +- src/utils/zip-stream.js | 379 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 380 insertions(+), 1 deletion(-) create mode 100644 src/utils/zip-stream.js diff --git a/src/utils/stream.js b/src/utils/stream.js index 4b139653..dac09607 100644 --- a/src/utils/stream.js +++ b/src/utils/stream.js @@ -1,5 +1,5 @@ import streamSaver from "streamsaver"; -import "streamsaver/examples/zip-stream.js"; +import "./zip-stream.js"; import store from '@/store' import dcmjs from './dcmUpload/dcmjs' streamSaver.mitm = `${window.location.origin}/mitm.html?version=2.0.0` diff --git a/src/utils/zip-stream.js b/src/utils/zip-stream.js new file mode 100644 index 00000000..4c3066b2 --- /dev/null +++ b/src/utils/zip-stream.js @@ -0,0 +1,379 @@ +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() + + 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) + 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 + + const generalPurposeBitFlag = 0x0008 // data descriptor present + const compressionMethod = 0 // store + const versionNeeded = 20 // 2.0 (ZIP64 switches this to 45) + + // 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) + 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 + const generalPurposeBitFlag = 0x0008 + 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) + 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