Merge pull request #1074 from morluto/fix/evidence-file-missing

fix(evidence): report missing evidence files instead of permission advice
This commit is contained in:
Kaoru0822
2026-10-08 07:22:31 -07:00
committed by GitHub
6 changed files with 92 additions and 16 deletions
+44 -9
View File
@@ -10,10 +10,14 @@ import { err, ok, type Result } from "../domain/result.js";
export const readJsonFile = async (
path: string,
): Promise<Result<unknown, EvidenceFileError>> => {
const requestedPath = resolve(path);
try {
const canonicalPath = await realpath(resolve(path));
const canonicalPath = await realpath(requestedPath);
const stats = await lstat(canonicalPath);
if (!stats.isFile()) return err(new EvidenceFileError("read", "not-file"));
if (!stats.isFile())
return err(
new EvidenceFileError("read", "not-file", { path: requestedPath }),
);
const encoded = await readFile(canonicalPath);
let decoded: unknown;
try {
@@ -23,11 +27,21 @@ export const readJsonFile = async (
),
);
} catch (cause: unknown) {
return err(new EvidenceFileError("read", "invalid-json", { cause }));
return err(
new EvidenceFileError("read", "invalid-json", {
cause,
path: requestedPath,
}),
);
}
return ok(decoded);
} catch (cause: unknown) {
return err(new EvidenceFileError("read", "io", { cause }));
return err(
new EvidenceFileError("read", missingOrIo(cause), {
cause,
path: requestedPath,
}),
);
}
};
@@ -40,8 +54,8 @@ export const writeTextFile = async (
Result<{ readonly path: string; readonly bytes: number }, EvidenceFileError>
> => {
const bytes = Buffer.byteLength(encoded, "utf8");
const requestedPath = resolve(path);
try {
const requestedPath = resolve(path);
const canonicalParent = await realpath(dirname(requestedPath));
const destination = resolve(canonicalParent, basename(requestedPath));
const existing = await lstat(destination).catch((cause: unknown) => {
@@ -49,9 +63,14 @@ export const writeTextFile = async (
throw cause;
});
if (existing !== undefined) {
if (!overwrite) return err(new EvidenceFileError("write", "exists"));
if (!overwrite)
return err(
new EvidenceFileError("write", "exists", { path: requestedPath }),
);
if (!existing.isFile() || existing.isSymbolicLink())
return err(new EvidenceFileError("write", "not-file"));
return err(
new EvidenceFileError("write", "not-file", { path: requestedPath }),
);
}
if (overwrite)
await writeFileAtomic(destination, encoded, {
@@ -63,11 +82,27 @@ export const writeTextFile = async (
return ok({ path: requestedPath, bytes });
} catch (cause: unknown) {
if (!overwrite && fileErrorCode(cause) === "EEXIST")
return err(new EvidenceFileError("write", "exists", { cause }));
return err(new EvidenceFileError("write", "io", { cause }));
return err(
new EvidenceFileError("write", "exists", {
cause,
path: requestedPath,
}),
);
return err(
new EvidenceFileError("write", missingOrIo(cause), {
cause,
path: requestedPath,
}),
);
}
};
/** A missing path or parent is a selection error, not a permission failure. */
const missingOrIo = (cause: unknown): "missing" | "io" => {
const code = fileErrorCode(cause);
return code === "ENOENT" || code === "ENOTDIR" ? "missing" : "io";
};
const publishNewFile = async (
destination: string,
encoded: string,