79 lines
2.7 KiB
JavaScript
79 lines
2.7 KiB
JavaScript
import { normalize } from 'node:path'
|
||
|
||
const CRC_TABLE = 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) {
|
||
let crc = 0xffffffff
|
||
for (const byte of data) crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8)
|
||
return (crc ^ 0xffffffff) >>> 0
|
||
}
|
||
|
||
function archivePath(value) {
|
||
const path = normalize(String(value || '').replace(/\\/g, '/')).replace(/\\/g, '/').replace(/^\.\//, '')
|
||
if (!path || path.startsWith('/') || path === '..' || path.startsWith('../')) throw new Error('ZIP 文件路径无效')
|
||
return path
|
||
}
|
||
|
||
function dosDateTime(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(),
|
||
}
|
||
}
|
||
|
||
/** 创建无额外依赖的 ZIP(Store 模式);PNG 和 .spine 本身已压缩,无需重复压缩。 */
|
||
export function createZipArchive(files) {
|
||
const localParts = []
|
||
const centralParts = []
|
||
let offset = 0
|
||
const timestamp = dosDateTime()
|
||
|
||
for (const file of files) {
|
||
const name = Buffer.from(archivePath(file.name), 'utf8')
|
||
const data = Buffer.isBuffer(file.data) ? file.data : Buffer.from(file.data)
|
||
const crc = crc32(data)
|
||
const local = Buffer.alloc(30)
|
||
local.writeUInt32LE(0x04034b50, 0)
|
||
local.writeUInt16LE(20, 4)
|
||
local.writeUInt16LE(0x0800, 6)
|
||
local.writeUInt16LE(0, 8)
|
||
local.writeUInt16LE(timestamp.time, 10)
|
||
local.writeUInt16LE(timestamp.date, 12)
|
||
local.writeUInt32LE(crc, 14)
|
||
local.writeUInt32LE(data.length, 18)
|
||
local.writeUInt32LE(data.length, 22)
|
||
local.writeUInt16LE(name.length, 26)
|
||
localParts.push(local, name, data)
|
||
|
||
const central = Buffer.alloc(46)
|
||
central.writeUInt32LE(0x02014b50, 0)
|
||
central.writeUInt16LE(20, 4)
|
||
central.writeUInt16LE(20, 6)
|
||
central.writeUInt16LE(0x0800, 8)
|
||
central.writeUInt16LE(0, 10)
|
||
central.writeUInt16LE(timestamp.time, 12)
|
||
central.writeUInt16LE(timestamp.date, 14)
|
||
central.writeUInt32LE(crc, 16)
|
||
central.writeUInt32LE(data.length, 20)
|
||
central.writeUInt32LE(data.length, 24)
|
||
central.writeUInt16LE(name.length, 28)
|
||
central.writeUInt32LE(offset, 42)
|
||
centralParts.push(central, name)
|
||
offset += local.length + name.length + data.length
|
||
}
|
||
|
||
const centralDirectory = Buffer.concat(centralParts)
|
||
const end = Buffer.alloc(22)
|
||
end.writeUInt32LE(0x06054b50, 0)
|
||
end.writeUInt16LE(files.length, 8)
|
||
end.writeUInt16LE(files.length, 10)
|
||
end.writeUInt32LE(centralDirectory.length, 12)
|
||
end.writeUInt32LE(offset, 16)
|
||
return Buffer.concat([...localParts, centralDirectory, end])
|
||
}
|