127 lines
4.7 KiB
JavaScript
127 lines
4.7 KiB
JavaScript
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
|