// 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 };