自动同步: 2026-07-01 02:10
This commit is contained in:
+127
@@ -0,0 +1,127 @@
|
||||
import { gunzipSync } from "fflate";
|
||||
const TAR_BLOCK_SIZE = 512;
|
||||
function isRecord(value) {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
function textFromBytes(bytes) {
|
||||
return new TextDecoder().decode(bytes);
|
||||
}
|
||||
function readTarString(block, offset, length) {
|
||||
const slice = block.subarray(offset, offset + length);
|
||||
const end = slice.indexOf(0);
|
||||
return textFromBytes(end === -1 ? slice : slice.subarray(0, end)).trim();
|
||||
}
|
||||
function readTarSize(block) {
|
||||
const raw = readTarString(block, 124, 12).split("\0").join("").trim();
|
||||
if (!raw)
|
||||
return 0;
|
||||
const size = Number.parseInt(raw, 8);
|
||||
if (!Number.isFinite(size) || size < 0)
|
||||
throw new Error("Invalid tar entry size");
|
||||
return size;
|
||||
}
|
||||
function normalizeTarPath(path) {
|
||||
const normalized = path.replaceAll("\\", "/").replace(/^\.\/+/, "");
|
||||
if (!normalized || normalized.startsWith("/") || normalized.includes("\0"))
|
||||
return null;
|
||||
const segments = normalized.split("/").filter(Boolean);
|
||||
if (segments.length === 0 || segments.some((segment) => segment === "." || segment === "..")) {
|
||||
return null;
|
||||
}
|
||||
return segments.join("/");
|
||||
}
|
||||
function isZeroBlock(block) {
|
||||
return block.every((byte) => byte === 0);
|
||||
}
|
||||
function nextTarOffset(offset, size) {
|
||||
return offset + Math.ceil(size / TAR_BLOCK_SIZE) * TAR_BLOCK_SIZE;
|
||||
}
|
||||
function parseTarEntries(bytes) {
|
||||
const entries = [];
|
||||
let offset = 0;
|
||||
while (offset + TAR_BLOCK_SIZE <= bytes.byteLength) {
|
||||
const header = bytes.subarray(offset, offset + TAR_BLOCK_SIZE);
|
||||
if (isZeroBlock(header))
|
||||
break;
|
||||
const name = readTarString(header, 0, 100);
|
||||
const prefix = readTarString(header, 345, 155);
|
||||
const path = normalizeTarPath(prefix ? `${prefix}/${name}` : name);
|
||||
if (!path)
|
||||
throw new Error("ClawPack contains an unsafe tar path");
|
||||
const size = readTarSize(header);
|
||||
const payloadOffset = offset + TAR_BLOCK_SIZE;
|
||||
const payloadEnd = payloadOffset + size;
|
||||
if (payloadEnd > bytes.byteLength)
|
||||
throw new Error("ClawPack tar entry is truncated");
|
||||
const typeflag = String.fromCharCode(header[156] ?? 0).replace("\0", "");
|
||||
if (typeflag === "" || typeflag === "0") {
|
||||
if (!path.startsWith("package/")) {
|
||||
throw new Error("ClawPack entries must be rooted under package/");
|
||||
}
|
||||
const relPath = path.slice("package/".length);
|
||||
if (relPath) {
|
||||
entries.push({
|
||||
path: relPath,
|
||||
bytes: Uint8Array.from(bytes.subarray(payloadOffset, payloadEnd)),
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (typeflag !== "5") {
|
||||
throw new Error("ClawPack may only contain regular files and directories");
|
||||
}
|
||||
offset = nextTarOffset(payloadOffset, size);
|
||||
}
|
||||
if (entries.length === 0)
|
||||
throw new Error("ClawPack contains no files");
|
||||
return entries;
|
||||
}
|
||||
export function parseClawPack(bytes) {
|
||||
let tarBytes;
|
||||
try {
|
||||
tarBytes = gunzipSync(bytes);
|
||||
}
|
||||
catch {
|
||||
throw new Error("ClawPack must be a gzip-compressed npm pack tarball");
|
||||
}
|
||||
const entries = parseTarEntries(tarBytes);
|
||||
const packageJsonEntry = entries.find((entry) => entry.path === "package.json");
|
||||
if (!packageJsonEntry)
|
||||
throw new Error("ClawPack must contain package/package.json");
|
||||
const pluginManifestEntry = entries.find((entry) => entry.path === "openclaw.plugin.json");
|
||||
if (!pluginManifestEntry) {
|
||||
throw new Error("ClawPack must contain package/openclaw.plugin.json");
|
||||
}
|
||||
let packageJson;
|
||||
try {
|
||||
packageJson = JSON.parse(textFromBytes(packageJsonEntry.bytes));
|
||||
}
|
||||
catch {
|
||||
throw new Error("ClawPack package.json is invalid JSON");
|
||||
}
|
||||
if (!isRecord(packageJson))
|
||||
throw new Error("ClawPack package.json must be an object");
|
||||
const packageName = typeof packageJson.name === "string" ? packageJson.name.trim() : "";
|
||||
const packageVersion = typeof packageJson.version === "string" ? packageJson.version.trim() : "";
|
||||
if (!packageName)
|
||||
throw new Error("ClawPack package.json must declare a name");
|
||||
if (!packageVersion)
|
||||
throw new Error("ClawPack package.json must declare a version");
|
||||
let pluginManifest;
|
||||
try {
|
||||
pluginManifest = JSON.parse(textFromBytes(pluginManifestEntry.bytes));
|
||||
}
|
||||
catch {
|
||||
throw new Error("ClawPack openclaw.plugin.json is invalid JSON");
|
||||
}
|
||||
if (!isRecord(pluginManifest)) {
|
||||
throw new Error("ClawPack openclaw.plugin.json must be an object");
|
||||
}
|
||||
return {
|
||||
packageName,
|
||||
packageVersion,
|
||||
entries,
|
||||
packageJson,
|
||||
pluginManifest,
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=clawpack.js.map
|
||||
Reference in New Issue
Block a user