diff --git a/src/application/Setup.ts b/src/application/Setup.ts index c36f22f38..47e9d1587 100644 --- a/src/application/Setup.ts +++ b/src/application/Setup.ts @@ -308,7 +308,7 @@ const objectValue = (value: unknown): Record => { return parsed.success ? parsed.data : {}; }; const parseObject = (text: string): Record => - objectValue(JSON.parse(text)); + objectSchema.parse(JSON.parse(text)); const isMissing = (cause: unknown): boolean => cause instanceof Error && "code" in cause && cause.code === "ENOENT"; const restoreConfig = async ( diff --git a/src/domain/binaryTarget.ts b/src/domain/binaryTarget.ts index 00839e15f..d8352a9a1 100644 --- a/src/domain/binaryTarget.ts +++ b/src/domain/binaryTarget.ts @@ -1,5 +1,5 @@ import { constants } from "node:fs"; -import { access, open, realpath } from "node:fs/promises"; +import { access, open, realpath, stat } from "node:fs/promises"; import { isAbsolute, resolve } from "node:path"; import { BinaryTargetError } from "./errors.js"; @@ -36,6 +36,8 @@ export const parseBinaryTarget = async ( try { await access(candidate, constants.R_OK); const path = await realpath(candidate); + if (!(await stat(path)).isFile()) + return err(new BinaryTargetError(path, "target is not a regular file")); if ( targetKind === "database" || (targetKind === undefined && path.toLowerCase().endsWith(".hop")) diff --git a/tests/binaryTarget.test.ts b/tests/binaryTarget.test.ts index a9c2d7819..d2cb496ee 100644 --- a/tests/binaryTarget.test.ts +++ b/tests/binaryTarget.test.ts @@ -67,7 +67,7 @@ describe("executable header parsing", () => { describe("binary target I/O", () => { it("resolves relative Hopper databases and rejects unknown or unreadable paths", async () => { - directory = await mkdtemp(join(tmpdir(), "bb-target-")); + directory = await mkdtemp(join(tmpdir(), "rea-target-")); await writeFile(join(directory, "sample.hop"), "database"); await writeFile(join(directory, "text"), "hello"); expect((await parseBinaryTarget("sample.hop", directory)).ok).toBe(true); @@ -75,6 +75,11 @@ describe("binary target I/O", () => { expect((await parseBinaryTarget("missing", directory)).ok).toBe(false); }); + it("rejects non-regular targets before reading them", async () => { + directory = await mkdtemp(join(tmpdir(), "rea-target-")); + expect((await parseBinaryTarget(directory)).ok).toBe(false); + }); + it("honors an explicit database kind without relying on the file suffix", async () => { directory = await mkdtemp(join(tmpdir(), "rea-target-")); await writeFile(join(directory, "saved-analysis"), "database"); diff --git a/tests/setupConfig.test.ts b/tests/setupConfig.test.ts index f62384683..4836ee2ef 100644 --- a/tests/setupConfig.test.ts +++ b/tests/setupConfig.test.ts @@ -59,4 +59,20 @@ describe("JSON client configuration transaction", () => { }); expect(await readFile(configPath, "utf8")).toBe("not-json"); }); + + it.each(["null", "[]", '"value"'])( + "refuses a non-object JSON root without overwriting %s", + async (original) => { + directory = await mkdtemp(join(tmpdir(), "rea-setup-")); + const configPath = join(directory, "mcp.json"); + await writeFile(configPath, original); + expect(await configureJsonClient({ name: "cursor", configPath })).toEqual( + { status: "failed", reason: "readback" }, + ); + expect(await readFile(configPath, "utf8")).toBe(original); + await expect( + readFile(`${configPath}.rea.backup`, "utf8"), + ).rejects.toThrow(); + }, + ); });