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";