mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-19 05:50:47 +00:00
197 lines
6.6 KiB
TypeScript
197 lines
6.6 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
assertCanonicalPathWithinBase,
|
|
resolveSafeInstallDir,
|
|
safeDirName,
|
|
safePathSegmentHashed,
|
|
unscopedPackageName,
|
|
} from "./install-safe-path.js";
|
|
|
|
describe("unscopedPackageName", () => {
|
|
it.each([
|
|
{ value: "@openclaw/matrix", expected: "matrix" },
|
|
{ value: " matrix ", expected: "matrix" },
|
|
{ value: "", expected: "" },
|
|
])("normalizes package names for %j", ({ value, expected }) => {
|
|
expect(unscopedPackageName(value)).toBe(expected);
|
|
});
|
|
});
|
|
|
|
describe("safeDirName", () => {
|
|
it.each([
|
|
{ value: " matrix ", expected: "matrix" },
|
|
{ value: "../matrix/plugin", expected: "..__matrix__plugin" },
|
|
{ value: "dir\\plugin", expected: "dir__plugin" },
|
|
])("normalizes install dir names for %j", ({ value, expected }) => {
|
|
expect(safeDirName(value)).toBe(expected);
|
|
});
|
|
});
|
|
|
|
describe("safePathSegmentHashed", () => {
|
|
it("keeps safe names unchanged", () => {
|
|
expect(safePathSegmentHashed("demo-skill")).toBe("demo-skill");
|
|
});
|
|
|
|
it("falls back to a hashed skill name for empty or dot-like segments", () => {
|
|
expect(safePathSegmentHashed(" ")).toMatch(/^skill-[a-f0-9]{10}$/);
|
|
expect(safePathSegmentHashed(".")).toMatch(/^skill-[a-f0-9]{10}$/);
|
|
expect(safePathSegmentHashed("..")).toMatch(/^skill-[a-f0-9]{10}$/);
|
|
});
|
|
|
|
it("normalizes separators and adds hash suffix", () => {
|
|
const result = safePathSegmentHashed("../../demo/skill");
|
|
expect(result.includes("/")).toBe(false);
|
|
expect(result.includes("\\")).toBe(false);
|
|
expect(result).toMatch(/-[a-f0-9]{10}$/);
|
|
});
|
|
|
|
it("hashes long names while staying bounded", () => {
|
|
const long = "a".repeat(100);
|
|
const result = safePathSegmentHashed(long);
|
|
expect(result.length).toBeLessThanOrEqual(61);
|
|
expect(result).toMatch(/-[a-f0-9]{10}$/);
|
|
});
|
|
});
|
|
|
|
describe("resolveSafeInstallDir", () => {
|
|
it("resolves install dirs under the base directory", () => {
|
|
expect(
|
|
resolveSafeInstallDir({
|
|
baseDir: "/tmp/plugins",
|
|
id: "@openclaw/matrix",
|
|
invalidNameMessage: "invalid plugin name",
|
|
}),
|
|
).toEqual({
|
|
ok: true,
|
|
path: path.join("/tmp/plugins", "@openclaw__matrix"),
|
|
});
|
|
});
|
|
|
|
it("rejects ids that resolve to the base directory itself", () => {
|
|
expect(
|
|
resolveSafeInstallDir({
|
|
baseDir: "/tmp/plugins",
|
|
id: " ",
|
|
invalidNameMessage: "invalid plugin name",
|
|
}),
|
|
).toEqual({
|
|
ok: false,
|
|
error: "invalid plugin name",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("assertCanonicalPathWithinBase", () => {
|
|
it("accepts in-base directories", async () => {
|
|
const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-install-safe-"));
|
|
try {
|
|
const candidate = path.join(baseDir, "tools");
|
|
await fs.mkdir(candidate, { recursive: true });
|
|
await expect(
|
|
assertCanonicalPathWithinBase({
|
|
baseDir,
|
|
candidatePath: candidate,
|
|
boundaryLabel: "install directory",
|
|
}),
|
|
).resolves.toBeUndefined();
|
|
} finally {
|
|
await fs.rm(baseDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("accepts missing candidate paths when their parent stays in base", async () => {
|
|
const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-install-safe-"));
|
|
try {
|
|
const candidate = path.join(baseDir, "tools", "plugin");
|
|
await fs.mkdir(path.dirname(candidate), { recursive: true });
|
|
await expect(
|
|
assertCanonicalPathWithinBase({
|
|
baseDir,
|
|
candidatePath: candidate,
|
|
boundaryLabel: "install directory",
|
|
}),
|
|
).resolves.toBeUndefined();
|
|
} finally {
|
|
await fs.rm(baseDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("rejects non-directory base paths", async () => {
|
|
const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-install-safe-"));
|
|
const baseFile = path.join(baseDir, "not-a-dir");
|
|
await fs.writeFile(baseFile, "nope", "utf-8");
|
|
try {
|
|
await expect(
|
|
assertCanonicalPathWithinBase({
|
|
baseDir: baseFile,
|
|
candidatePath: path.join(baseFile, "child"),
|
|
boundaryLabel: "install directory",
|
|
}),
|
|
).rejects.toThrow(/base directory must be a real directory/i);
|
|
} finally {
|
|
await fs.rm(baseDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("rejects non-directory candidate paths inside the base", async () => {
|
|
const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-install-safe-"));
|
|
const candidate = path.join(baseDir, "file.txt");
|
|
await fs.writeFile(candidate, "nope", "utf-8");
|
|
try {
|
|
await expect(
|
|
assertCanonicalPathWithinBase({
|
|
baseDir,
|
|
candidatePath: candidate,
|
|
boundaryLabel: "install directory",
|
|
}),
|
|
).rejects.toThrow(/must stay within install directory/i);
|
|
} finally {
|
|
await fs.rm(baseDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it.runIf(process.platform !== "win32")(
|
|
"rejects symlinked candidate directories that escape the base",
|
|
async () => {
|
|
const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-install-safe-"));
|
|
const outsideDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-install-safe-outside-"));
|
|
try {
|
|
const linkDir = path.join(baseDir, "alias");
|
|
await fs.symlink(outsideDir, linkDir);
|
|
await expect(
|
|
assertCanonicalPathWithinBase({
|
|
baseDir,
|
|
candidatePath: linkDir,
|
|
boundaryLabel: "install directory",
|
|
}),
|
|
).rejects.toThrow(/must stay within install directory/i);
|
|
} finally {
|
|
await fs.rm(baseDir, { recursive: true, force: true });
|
|
await fs.rm(outsideDir, { recursive: true, force: true });
|
|
}
|
|
},
|
|
);
|
|
|
|
it.runIf(process.platform !== "win32")("rejects symlinked base directories", async () => {
|
|
const parentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-install-safe-"));
|
|
const realBaseDir = path.join(parentDir, "real-base");
|
|
const symlinkBaseDir = path.join(parentDir, "base-link");
|
|
await fs.mkdir(realBaseDir, { recursive: true });
|
|
await fs.symlink(realBaseDir, symlinkBaseDir);
|
|
try {
|
|
await expect(
|
|
assertCanonicalPathWithinBase({
|
|
baseDir: symlinkBaseDir,
|
|
candidatePath: path.join(symlinkBaseDir, "tool"),
|
|
boundaryLabel: "install directory",
|
|
}),
|
|
).rejects.toThrow(/base directory must be a real directory/i);
|
|
} finally {
|
|
await fs.rm(parentDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|