Files
dsh-purge/lib/zip-extract.js

162 lines
5.5 KiB
JavaScript

import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import zlib from "node:zlib";
const HIDDEN = {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
windowsHide: true,
timeout: 60000,
};
/** 各平台解压顺序。node 不调用外部命令,macOS / Linux / Windows 都能用。 */
export function unzipTools(platform = process.platform) {
if (platform === "win32") return ["tar", "powershell", "node"];
return ["tar", "unzip", "python3", "bsdtar", "node"];
}
function safeZipName(name) {
const rel = String(name || "").replace(/\\/g, "/").replace(/^\.?\//, "").replace(/\/+$/, "");
if (!rel) return "";
if (rel.startsWith("/") || /^[A-Za-z]:/.test(rel)) throw new Error("zip 路径不合法");
const parts = rel.split("/");
if (parts.some((part) => part === ".." || part === "")) throw new Error("zip 路径不合法");
return parts.join("/");
}
function findEocd(buf) {
const min = Math.max(0, buf.length - 22 - 65535);
for (let i = buf.length - 22; i >= min; i -= 1) {
if (buf.readUInt32LE(i) === 0x06054b50) return i;
}
throw new Error("不是 zip");
}
function readZipEntries(buf) {
if (!Buffer.isBuffer(buf) || buf.length < 22) throw new Error("不是 zip");
const eocd = findEocd(buf);
const total = buf.readUInt16LE(eocd + 10);
let cursor = buf.readUInt32LE(eocd + 16);
const entries = [];
for (let n = 0; n < total; n += 1) {
if (cursor + 46 > buf.length || buf.readUInt32LE(cursor) !== 0x02014b50) {
throw new Error("zip 目录损坏");
}
const method = buf.readUInt16LE(cursor + 10);
const compSize = buf.readUInt32LE(cursor + 20);
const nameLen = buf.readUInt16LE(cursor + 28);
const extraLen = buf.readUInt16LE(cursor + 30);
const commentLen = buf.readUInt16LE(cursor + 32);
const localOffset = buf.readUInt32LE(cursor + 42);
const name = buf.toString("utf8", cursor + 46, cursor + 46 + nameLen);
entries.push({ name, method, compSize, localOffset });
cursor += 46 + nameLen + extraLen + commentLen;
}
return entries;
}
export function listZipEntries(buf) {
return readZipEntries(buf).map((entry) => entry.name.replace(/\\/g, "/"));
}
export function listZipFile(zipPath) {
return listZipEntries(fs.readFileSync(zipPath));
}
function localDataOffset(buf, localOffset) {
if (localOffset + 30 > buf.length || buf.readUInt32LE(localOffset) !== 0x04034b50) {
throw new Error("zip 本地头损坏");
}
const nameLen = buf.readUInt16LE(localOffset + 26);
const extraLen = buf.readUInt16LE(localOffset + 28);
return localOffset + 30 + nameLen + extraLen;
}
export function extractZipBuffer(buf, dest) {
fs.mkdirSync(dest, { recursive: true });
const root = path.resolve(dest);
for (const entry of readZipEntries(buf)) {
const rel = safeZipName(entry.name);
if (!rel) continue;
if (entry.compSize === 0xffffffff || entry.localOffset === 0xffffffff) {
throw new Error("不支持 zip64");
}
if (entry.method !== 0 && entry.method !== 8) {
throw new Error(`不支持的 zip 压缩方式 ${entry.method}`);
}
const start = localDataOffset(buf, entry.localOffset);
const end = start + entry.compSize;
if (end > buf.length) throw new Error("zip 数据被截断");
const compressed = buf.subarray(start, end);
const body = entry.method === 0 ? compressed : zlib.inflateRawSync(compressed);
const out = path.resolve(root, rel);
const prefix = root.endsWith(path.sep) ? root : root + path.sep;
if (out !== root && !out.startsWith(prefix)) throw new Error("zip 路径不合法");
fs.mkdirSync(path.dirname(out), { recursive: true });
fs.writeFileSync(out, body);
}
}
function attempt(file, args, extra = {}) {
try {
execFileSync(file, args, {
...HIDDEN,
stdio: ["ignore", "pipe", "pipe"],
...extra,
});
return "";
} catch (error) {
const detail = String((error && (error.stderr || error.stdout || error.message)) || "")
.trim()
.split(/\r?\n/)
.filter(Boolean)[0];
return detail ? `${file}: ${detail}` : file;
}
}
/** 按平台试外部工具,最后用 Node 解 zip。成功返回用到的方式。 */
export function extractZipArchive(zipPath, dest, tools = unzipTools()) {
fs.mkdirSync(dest, { recursive: true });
const notes = [];
for (const tool of tools) {
if (tool === "node") {
try {
extractZipBuffer(fs.readFileSync(zipPath), dest);
return "node";
} catch (error) {
notes.push(`node: ${error && error.message ? error.message : error}`);
break;
}
}
if (tool === "tar" || tool === "bsdtar") {
const note = attempt(tool, ["-xf", zipPath, "-C", dest]);
if (!note) return tool;
notes.push(note);
continue;
}
if (tool === "unzip") {
const note = attempt("unzip", ["-o", "-q", zipPath, "-d", dest]);
if (!note) return "unzip";
notes.push(note);
continue;
}
if (tool === "python3") {
const note = attempt("python3", ["-m", "zipfile", "-e", zipPath, dest]);
if (!note) return "python3";
notes.push(note);
continue;
}
if (tool === "powershell") {
const note = attempt(
"powershell",
["-NoProfile", "-Command", "Expand-Archive -LiteralPath $env:DSH_UPD_ZIP -DestinationPath $env:DSH_UPD_DIR -Force"],
{ env: { ...process.env, DSH_UPD_ZIP: zipPath, DSH_UPD_DIR: dest } },
);
if (!note) return "powershell";
notes.push(note);
}
}
throw new Error(`解压失败:${notes.join(" | ") || "没有可用的解压工具"}`);
}