Files

148 lines
5.9 KiB
JavaScript
Raw Permalink Normal View History

2026-07-15 18:01:56 +08:00
// 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 };