148 lines
5.9 KiB
JavaScript
148 lines
5.9 KiB
JavaScript
// zip.js —— 零依赖 ZIP 解压(stored/deflate),供技能安装与热更新复用。
|
||
"use strict";
|
||
const fs = require("fs");
|
||
const path = require("path");
|
||
const zlib = require("zlib");
|
||
|
||
// 解析中央目录,把 zip 内容写入 destDir;返回写出的文件数。
|
||
function extractZip(buf, destDir) {
|
||
// 找 EOCD(0x06054b50),从尾部回扫
|
||
let eocd = -1;
|
||
for (let i = buf.length - 22; i >= 0 && i >= buf.length - 22 - 65536; i--) {
|
||
if (buf.readUInt32LE(i) === 0x06054b50) { eocd = i; break; }
|
||
}
|
||
if (eocd < 0) throw new Error("不是有效的 zip(找不到 EOCD)");
|
||
const count = buf.readUInt16LE(eocd + 10);
|
||
let ptr = buf.readUInt32LE(eocd + 16); // 中央目录起始偏移
|
||
let written = 0;
|
||
for (let n = 0; n < count; n++) {
|
||
if (buf.readUInt32LE(ptr) !== 0x02014b50) break;
|
||
const method = buf.readUInt16LE(ptr + 10);
|
||
const compSize = buf.readUInt32LE(ptr + 20);
|
||
const nameLen = buf.readUInt16LE(ptr + 28);
|
||
const extraLen = buf.readUInt16LE(ptr + 30);
|
||
const commentLen = buf.readUInt16LE(ptr + 32);
|
||
const localOff = buf.readUInt32LE(ptr + 42);
|
||
const name = buf.toString("utf8", ptr + 46, ptr + 46 + nameLen);
|
||
ptr += 46 + nameLen + extraLen + commentLen;
|
||
// 读本地头,定位数据
|
||
if (buf.readUInt32LE(localOff) !== 0x04034b50) continue;
|
||
const lNameLen = buf.readUInt16LE(localOff + 26);
|
||
const lExtraLen = buf.readUInt16LE(localOff + 28);
|
||
const dataStart = localOff + 30 + lNameLen + lExtraLen;
|
||
const comp = buf.subarray(dataStart, dataStart + compSize);
|
||
// 防目录穿越
|
||
const safe = name.replace(/\\/g, "/").replace(/^\/+/, "");
|
||
if (safe.includes("../")) continue;
|
||
const outPath = path.join(destDir, safe);
|
||
if (safe.endsWith("/")) { fs.mkdirSync(outPath, { recursive: true }); continue; }
|
||
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
||
let data;
|
||
if (method === 0) data = comp;
|
||
else if (method === 8) data = zlib.inflateRawSync(comp);
|
||
else throw new Error(`不支持的压缩方式 ${method}`);
|
||
fs.writeFileSync(outPath, data);
|
||
written++;
|
||
}
|
||
return written;
|
||
}
|
||
|
||
// ---- 零依赖 ZIP 压缩(deflate/stored),供员工团队打包等复用 ----
|
||
// CRC32 表(懒加载)
|
||
let CRC_TABLE = null;
|
||
function crc32(buf) {
|
||
if (!CRC_TABLE) {
|
||
CRC_TABLE = new Int32Array(256);
|
||
for (let n = 0; n < 256; n++) {
|
||
let c = n;
|
||
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||
CRC_TABLE[n] = c;
|
||
}
|
||
}
|
||
let crc = ~0;
|
||
for (let i = 0; i < buf.length; i++) crc = (crc >>> 8) ^ CRC_TABLE[(crc ^ buf[i]) & 0xff];
|
||
return (~crc) >>> 0;
|
||
}
|
||
|
||
// MS-DOS 时间/日期(固定用一个合法值即可,不追求精确时间戳)
|
||
function dosDateTime(d) {
|
||
const date = d || new Date();
|
||
const time = ((date.getHours() & 0x1f) << 11) | ((date.getMinutes() & 0x3f) << 5) | ((date.getSeconds() / 2) & 0x1f);
|
||
const day = (((date.getFullYear() - 1980) & 0x7f) << 9) | (((date.getMonth() + 1) & 0x0f) << 5) | (date.getDate() & 0x1f);
|
||
return { time: time & 0xffff, date: day & 0xffff };
|
||
}
|
||
|
||
// files: [{ path, content(string|Buffer), mode?(unix 权限,如 0o755) }]
|
||
// 返回一个 zip 的 Buffer。目录靠文件路径里的 "/" 隐含,无需单独目录项。
|
||
function createZip(files) {
|
||
const { time, date } = dosDateTime();
|
||
const locals = [];
|
||
const centrals = [];
|
||
let offset = 0;
|
||
for (const f of files) {
|
||
const nameBuf = Buffer.from(String(f.path).replace(/^\/+/, ""), "utf8");
|
||
const raw = Buffer.isBuffer(f.content) ? f.content : Buffer.from(String(f.content == null ? "" : f.content), "utf8");
|
||
const crc = crc32(raw);
|
||
// 尝试 deflate;若压不小就存储(method 0)
|
||
let method = 8;
|
||
let comp = zlib.deflateRawSync(raw, { level: 9 });
|
||
if (comp.length >= raw.length) { method = 0; comp = raw; }
|
||
|
||
// 本地文件头
|
||
const lh = Buffer.alloc(30);
|
||
lh.writeUInt32LE(0x04034b50, 0);
|
||
lh.writeUInt16LE(20, 4); // version needed
|
||
lh.writeUInt16LE(0, 6); // flags
|
||
lh.writeUInt16LE(method, 8);
|
||
lh.writeUInt16LE(time, 10);
|
||
lh.writeUInt16LE(date, 12);
|
||
lh.writeUInt32LE(crc, 14);
|
||
lh.writeUInt32LE(comp.length, 18);
|
||
lh.writeUInt32LE(raw.length, 22);
|
||
lh.writeUInt16LE(nameBuf.length, 26);
|
||
lh.writeUInt16LE(0, 28); // extra len
|
||
locals.push(lh, nameBuf, comp);
|
||
|
||
// 中央目录项
|
||
const ch = Buffer.alloc(46);
|
||
// unix 权限写进外部属性高 16 位(低位保留),默认 0644,可执行给 0755
|
||
const unixMode = f.mode ? f.mode : 0o644;
|
||
const externalAttr = (unixMode << 16) >>> 0;
|
||
ch.writeUInt32LE(0x02014b50, 0);
|
||
ch.writeUInt16LE(0x031e, 4); // version made by (unix)
|
||
ch.writeUInt16LE(20, 6); // version needed
|
||
ch.writeUInt16LE(0, 8); // flags
|
||
ch.writeUInt16LE(method, 10);
|
||
ch.writeUInt16LE(time, 12);
|
||
ch.writeUInt16LE(date, 14);
|
||
ch.writeUInt32LE(crc, 16);
|
||
ch.writeUInt32LE(comp.length, 20);
|
||
ch.writeUInt32LE(raw.length, 24);
|
||
ch.writeUInt16LE(nameBuf.length, 28);
|
||
ch.writeUInt16LE(0, 30); // extra len
|
||
ch.writeUInt16LE(0, 32); // comment len
|
||
ch.writeUInt16LE(0, 34); // disk number
|
||
ch.writeUInt16LE(0, 36); // internal attr
|
||
ch.writeUInt32LE(externalAttr, 38);
|
||
ch.writeUInt32LE(offset, 42);
|
||
centrals.push(ch, nameBuf);
|
||
|
||
offset += lh.length + nameBuf.length + comp.length;
|
||
}
|
||
|
||
const localPart = Buffer.concat(locals);
|
||
const centralPart = Buffer.concat(centrals);
|
||
const eocd = Buffer.alloc(22);
|
||
eocd.writeUInt32LE(0x06054b50, 0);
|
||
eocd.writeUInt16LE(0, 4);
|
||
eocd.writeUInt16LE(0, 6);
|
||
eocd.writeUInt16LE(files.length, 8);
|
||
eocd.writeUInt16LE(files.length, 10);
|
||
eocd.writeUInt32LE(centralPart.length, 12);
|
||
eocd.writeUInt32LE(localPart.length, 16); // central dir offset
|
||
eocd.writeUInt16LE(0, 20);
|
||
return Buffer.concat([localPart, centralPart, eocd]);
|
||
}
|
||
|
||
module.exports = { extractZip, createZip };
|