94 lines
3.4 KiB
TypeScript
94 lines
3.4 KiB
TypeScript
import { translate } from '../i18n'
|
|
|
|
export interface ZipArchiveFile {
|
|
name: string
|
|
data: Uint8Array
|
|
}
|
|
|
|
const crcTable = 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: Uint8Array) {
|
|
let crc = 0xffffffff
|
|
for (const byte of data) crc = crcTable[(crc ^ byte) & 0xff] ^ (crc >>> 8)
|
|
return (crc ^ 0xffffffff) >>> 0
|
|
}
|
|
|
|
function safeArchivePath(value: string) {
|
|
const parts = String(value || '').replace(/\\/g, '/').split('/').filter((part) => part && part !== '.')
|
|
if (!parts.length || parts.some((part) => part === '..')) throw new Error(translate('errors.invalidZipPath'))
|
|
return parts.join('/')
|
|
}
|
|
|
|
function dateTime(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(),
|
|
}
|
|
}
|
|
|
|
function header(size: number) {
|
|
const bytes = new Uint8Array(size)
|
|
return { bytes, view: new DataView(bytes.buffer) }
|
|
}
|
|
|
|
function combine(chunks: Uint8Array[]) {
|
|
const output = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.length, 0))
|
|
let offset = 0
|
|
for (const chunk of chunks) { output.set(chunk, offset); offset += chunk.length }
|
|
return output
|
|
}
|
|
|
|
/** 创建 Store 模式 ZIP;图片本身已经压缩,避免引入额外打包依赖。 */
|
|
export function createZipArchive(files: ZipArchiveFile[]) {
|
|
const encoder = new TextEncoder()
|
|
const localChunks: Uint8Array[] = []
|
|
const centralChunks: Uint8Array[] = []
|
|
const timestamp = dateTime()
|
|
let offset = 0
|
|
|
|
for (const file of files) {
|
|
const name = encoder.encode(safeArchivePath(file.name))
|
|
const crc = crc32(file.data)
|
|
const local = header(30)
|
|
local.view.setUint32(0, 0x04034b50, true)
|
|
local.view.setUint16(4, 20, true)
|
|
local.view.setUint16(6, 0x0800, true)
|
|
local.view.setUint16(10, timestamp.time, true)
|
|
local.view.setUint16(12, timestamp.date, true)
|
|
local.view.setUint32(14, crc, true)
|
|
local.view.setUint32(18, file.data.length, true)
|
|
local.view.setUint32(22, file.data.length, true)
|
|
local.view.setUint16(26, name.length, true)
|
|
localChunks.push(local.bytes, name, file.data)
|
|
|
|
const central = header(46)
|
|
central.view.setUint32(0, 0x02014b50, true)
|
|
central.view.setUint16(4, 20, true)
|
|
central.view.setUint16(6, 20, true)
|
|
central.view.setUint16(8, 0x0800, true)
|
|
central.view.setUint16(12, timestamp.time, true)
|
|
central.view.setUint16(14, timestamp.date, true)
|
|
central.view.setUint32(16, crc, true)
|
|
central.view.setUint32(20, file.data.length, true)
|
|
central.view.setUint32(24, file.data.length, true)
|
|
central.view.setUint16(28, name.length, true)
|
|
central.view.setUint32(42, offset, true)
|
|
centralChunks.push(central.bytes, name)
|
|
offset += local.bytes.length + name.length + file.data.length
|
|
}
|
|
|
|
const centralDirectory = combine(centralChunks)
|
|
const end = header(22)
|
|
end.view.setUint32(0, 0x06054b50, true)
|
|
end.view.setUint16(8, files.length, true)
|
|
end.view.setUint16(10, files.length, true)
|
|
end.view.setUint32(12, centralDirectory.length, true)
|
|
end.view.setUint32(16, offset, true)
|
|
return new Blob([combine([...localChunks, centralDirectory, end.bytes])], { type: 'application/zip' })
|
|
}
|