New packages/runs reads run records, result.json, the release pointer
and the activation log without writing, pruning or following a link out
of the data root. mosaic-task.mjs list and show use it, so a malformed
result.json or a run that is a regular file no longer crashes them.
Deliberate deltas are listed in the README.
Rocko built it. Round 1 (ed3c5392) was approved with notes by Darkwing
(27115) and Filbert (27116); round 2 (2727198f, tests and wording only)
was approved by Darkwing (27123) and Filbert (27124). Sage's gate on
c9a25a47 plus the candidate: runs 41/0, queue 148/0, webui 22/0,
conversation 182/0 (181/1 in the full run on the K12 cgroup timing
test under load, 182/0 alone), control-board 124/0, every
scripts/test-*.sh 0 failed (task 98/0 with Docker, 26/0 without).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
50 lines
1.7 KiB
JavaScript
50 lines
1.7 KiB
JavaScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { RunsError } from "./errors.mjs";
|
|
|
|
export const RUNS_DIRNAME = "runs";
|
|
export const STATE_DIRNAME = "state";
|
|
|
|
export function isMissing(error) {
|
|
return error?.code === "ENOENT" || error?.code === "ENOTDIR";
|
|
}
|
|
|
|
function isInside(root, target) {
|
|
const relative = path.relative(root, target);
|
|
return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
}
|
|
|
|
// Resolves <dataRoot>/<parts...> through any symbolic links and returns the
|
|
// real path, or null when it doesn't exist. The data root is trusted as
|
|
// configured; a path below it that resolves outside it refuses, so no reader
|
|
// here follows a link out of the data root.
|
|
export function resolveInside(dataRoot, ...parts) {
|
|
const target = path.join(dataRoot, ...parts);
|
|
let root;
|
|
let real;
|
|
try {
|
|
root = fs.realpathSync(dataRoot);
|
|
real = fs.realpathSync(target);
|
|
} catch (error) {
|
|
if (isMissing(error)) return null;
|
|
throw new RunsError(`cannot resolve ${target}: ${error.code ?? error.message}`);
|
|
}
|
|
if (!isInside(root, real)) {
|
|
throw new RunsError(`${target} resolves outside the data root (${root})`);
|
|
}
|
|
return real;
|
|
}
|
|
|
|
// A JSON object read from <dataRoot>/<parts...>, or null when the file is
|
|
// missing, unreadable, not JSON, not an object or outside the data root.
|
|
export function readJsonObject(dataRoot, ...parts) {
|
|
try {
|
|
const file = resolveInside(dataRoot, ...parts);
|
|
if (file === null) return null;
|
|
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|