Files
linuxcncWebWASM/web/workspace/configuration.mjs

156 lines
6.9 KiB
JavaScript

const CONFIGURATION_SCHEMA = "configuration-startup-closure.v1";
export const CONFIGURATION_ERRORS = Object.freeze({
invalid_reference: "configuration_reference_invalid",
empty_reference: "configuration_reference_empty",
host_path: "configuration_host_path",
outside_repo: "configuration_outside_repo",
path_escape: "configuration_path_escape",
missing: "configuration_resource_missing",
ambiguous: "configuration_resource_ambiguous",
unavailable: "configuration_resource_unavailable",
});
export const LIBRARY_ROOTS = Object.freeze([
"linuxcnc/lib/hallib",
"linuxcnc/lib",
]);
function asPathSet(paths) {
if (paths instanceof Set) return paths;
if (Array.isArray(paths)) return new Set(paths);
if (paths && typeof paths === "object") return new Set(Object.keys(paths));
return new Set();
}
function normalizePath(value) {
if (typeof value !== "string" || value.length === 0) {
return { status: "blocked", code: CONFIGURATION_ERRORS.invalid_reference, path: null };
}
if (value.includes("\0")) {
return { status: "blocked", code: CONFIGURATION_ERRORS.invalid_reference, path: null };
}
const parts = [];
for (const part of value.replaceAll("\\", "/").split("/")) {
if (!part || part === ".") continue;
if (part === "..") {
if (parts.length === 0) return { status: "blocked", code: CONFIGURATION_ERRORS.path_escape, path: null };
parts.pop();
continue;
}
parts.push(part);
}
return { status: "accepted", code: null, path: parts.join("/") };
}
function dirname(path) {
const index = path.lastIndexOf("/");
return index < 0 ? "" : path.slice(0, index);
}
function joinPath(base, value) {
return normalizePath(base ? `${base}/${value}` : value);
}
export function splitReference(rawValue) {
if (typeof rawValue !== "string") {
return { status: "blocked", code: CONFIGURATION_ERRORS.invalid_reference, value: null, args: [] };
}
const raw = rawValue.trim().replace(/^['"]|['"]$/g, "");
if (!raw) return { status: "blocked", code: CONFIGURATION_ERRORS.empty_reference, value: null, args: [] };
const tokens = raw.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];
if (tokens.length === 0) return { status: "blocked", code: CONFIGURATION_ERRORS.empty_reference, value: null, args: [] };
const value = tokens[0].replace(/^['"]|['"]$/g, "");
const args = tokens.slice(1).map((token) => token.replace(/^['"]|['"]$/g, ""));
return { status: "accepted", code: null, raw, value, args };
}
export function resolveReference({ rawValue, entrypoint, availablePaths = [], libraryRoots = LIBRARY_ROOTS } = {}) {
const parsed = splitReference(rawValue);
if (parsed.status !== "accepted") return { ...parsed, path: null, attempted_paths: [] };
if (typeof entrypoint !== "string" || entrypoint.length === 0) {
return { status: "blocked", code: CONFIGURATION_ERRORS.invalid_reference, value: parsed.value, args: parsed.args, path: null, attempted_paths: [] };
}
if (parsed.value.startsWith("~") || parsed.value.startsWith("$") || parsed.value.startsWith("${")) {
return { status: "blocked", code: CONFIGURATION_ERRORS.host_path, value: parsed.value, args: parsed.args, path: null, attempted_paths: [] };
}
const available = asPathSet(availablePaths);
const entry = normalizePath(entrypoint);
if (entry.status !== "accepted") return { ...entry, value: parsed.value, args: parsed.args, attempted_paths: [] };
let candidates;
let resolution = "relative";
if (parsed.value.startsWith("LIB:")) {
resolution = "library";
const libraryName = parsed.value.slice("LIB:".length);
if (!libraryName || libraryName.startsWith("/") || libraryName.includes("..")) {
return { status: "blocked", code: CONFIGURATION_ERRORS.invalid_reference, value: parsed.value, args: parsed.args, path: null, attempted_paths: [] };
}
candidates = libraryRoots.map((root) => normalizePath(`${root}/${libraryName}`)).filter((item) => item.status === "accepted").map((item) => item.path);
} else if (parsed.value.startsWith("/")) {
return { status: "blocked", code: CONFIGURATION_ERRORS.outside_repo, value: parsed.value, args: parsed.args, path: null, attempted_paths: [] };
} else if (parsed.value.startsWith("linuxcnc/")) {
resolution = "repo_relative";
const normalized = normalizePath(parsed.value);
if (normalized.status !== "accepted") return { ...normalized, value: parsed.value, args: parsed.args, attempted_paths: [] };
candidates = [normalized.path];
} else {
const normalized = joinPath(dirname(entry.path), parsed.value);
if (normalized.status !== "accepted") return { ...normalized, value: parsed.value, args: parsed.args, attempted_paths: [] };
if (normalized.path !== "linuxcnc" && !normalized.path.startsWith("linuxcnc/")) {
return { status: "blocked", code: CONFIGURATION_ERRORS.outside_repo, value: parsed.value, args: parsed.args, path: null, attempted_paths: [normalized.path], resolution };
}
candidates = [normalized.path];
}
const existing = candidates.filter((candidate) => available.has(candidate));
if (existing.length > 1) {
return { status: "blocked", code: CONFIGURATION_ERRORS.ambiguous, value: parsed.value, args: parsed.args, path: null, attempted_paths: candidates, resolution };
}
if (existing.length === 1) {
return { status: "resolved", code: null, value: parsed.value, args: parsed.args, path: existing[0], attempted_paths: candidates, resolution };
}
return { status: "blocked", code: CONFIGURATION_ERRORS.missing, value: parsed.value, args: parsed.args, path: null, attempted_paths: candidates, resolution };
}
export function resolveResourceRow(row, availablePaths, options = {}) {
if (!row || typeof row !== "object") return { status: "blocked", code: CONFIGURATION_ERRORS.invalid_reference, path: null };
const result = resolveReference({
rawValue: row.raw_value ?? row.path,
entrypoint: row.entrypoint,
availablePaths,
...options,
});
return {
resource_source_id: row.resource_source_id ?? null,
entrypoint: row.entrypoint ?? null,
original_path: row.path ?? null,
original_status: row.parse_status ?? null,
...result,
};
}
export function preflightConfiguration({ entrypoint, rows = [], availablePaths = [], libraryRoots = LIBRARY_ROOTS } = {}) {
const resolved = rows.map((row) => resolveResourceRow(row, availablePaths, { libraryRoots }));
const blocked = resolved.filter((row) => row.status !== "resolved");
return {
schema: CONFIGURATION_SCHEMA,
entrypoint,
status: blocked.length === 0 ? "ready" : "blocked",
resource_count: resolved.length,
resolved_count: resolved.length - blocked.length,
blocked_count: blocked.length,
resources: resolved,
};
}
export { CONFIGURATION_SCHEMA };
export {
CONFIGURATION_STARTUP_SCHEMA,
STARTUP_SCHEMA,
STARTUP_STATES,
STARTUP_ERRORS,
ConfigurationStartup,
ConfigurationStartupStateMachine,
startConfiguration,
createConfigurationStartup,
} from "./configuration-startup.mjs";