134 lines
5.9 KiB
JavaScript
134 lines
5.9 KiB
JavaScript
import { fnv1a64 } from "../worker/replay.mjs";
|
|
import { normalizeWorkspacePath } from "./runtime.mjs";
|
|
|
|
export const INI_SCHEMA = "ini-resource-graph.v1";
|
|
export const INI_ERRORS = Object.freeze({
|
|
missing: "ini_source_missing",
|
|
cycle: "ini_include_cycle",
|
|
depth: "ini_include_depth",
|
|
section: "ini_section_invalid",
|
|
key: "ini_key_invalid",
|
|
assignment: "ini_assignment_invalid",
|
|
variable: "ini_variable_missing",
|
|
path: "ini_path_invalid",
|
|
});
|
|
|
|
function clone(value) { return structuredClone(value); }
|
|
function error(code, path, line, message = code, extra = {}) {
|
|
return Object.assign(new Error(message), { code, source: path, line, column: 1, ...extra });
|
|
}
|
|
function stripComment(line) {
|
|
let quote = null;
|
|
for (let index = 0; index < line.length; index += 1) {
|
|
const char = line[index];
|
|
if ((char === "\"" || char === "'") && (!quote || quote === char)) quote = quote ? null : char;
|
|
if (!quote && (char === ";" || char === "#")) return line.slice(0, index);
|
|
}
|
|
return line;
|
|
}
|
|
function resolveWorkspaceRelative(basePath, value) {
|
|
const parts = basePath.split("/");
|
|
parts.pop();
|
|
for (const part of value.replaceAll("\\", "/").split("/")) {
|
|
if (!part || part === ".") continue;
|
|
if (part === "..") {
|
|
if (!parts.length) throw error(INI_ERRORS.path, basePath, 0, "ini_include_path_escape");
|
|
parts.pop();
|
|
continue;
|
|
}
|
|
parts.push(part);
|
|
}
|
|
return normalizeWorkspacePath(`/workspace/${parts.join("/")}`);
|
|
}
|
|
function resolveInclude(basePath, includePath) {
|
|
if (includePath.startsWith("/") || includePath.includes("\\")) throw error(INI_ERRORS.path, basePath, 0, "ini_include_path_invalid");
|
|
if (includePath.startsWith("linuxcnc/")) return normalizeWorkspacePath(`/workspace/${includePath}`);
|
|
return resolveWorkspaceRelative(basePath, includePath);
|
|
}
|
|
function expand(value, variables, path, line) {
|
|
return value.replace(/\$\{([^}]+)\}|\$\(([^)]+)\)|\$([A-Za-z_][A-Za-z0-9_]*)/g, (match, brace, paren, plain) => {
|
|
const name = brace ?? paren ?? plain;
|
|
if (!(name in variables)) throw error(INI_ERRORS.variable, path, line, "ini_variable_missing", { variable: name });
|
|
return String(variables[name]);
|
|
});
|
|
}
|
|
|
|
export function parseIni({ workspace, entrypoint, variables = {}, maxDepth = 32 } = {}) {
|
|
if (!workspace || typeof workspace.read !== "function") throw new Error("ini_workspace_required");
|
|
const root = normalizeWorkspacePath(entrypoint);
|
|
const sections = {};
|
|
const entries = [];
|
|
const includes = [];
|
|
const diagnostics = [];
|
|
const graph = [];
|
|
const active = [];
|
|
const vars = { ...variables };
|
|
|
|
function parseFile(path, depth) {
|
|
if (depth > maxDepth) throw error(INI_ERRORS.depth, path, 0, "ini_include_depth", { maxDepth });
|
|
if (active.includes(path)) throw error(INI_ERRORS.cycle, path, 0, "ini_include_cycle", { cycle: [...active, path] });
|
|
active.push(path);
|
|
let source;
|
|
try { source = workspace.read(`/workspace/${path}`); } catch (cause) {
|
|
active.pop();
|
|
throw error(INI_ERRORS.missing, path, 0, "ini_source_missing", { cause: String(cause?.message ?? cause) });
|
|
}
|
|
graph.push(path);
|
|
let section = "";
|
|
const lines = source.split(/\r?\n/);
|
|
lines.forEach((rawLine, offset) => {
|
|
const lineNumber = offset + 1;
|
|
const hashInclude = rawLine.trim().match(/^#INCLUDE\s+(.+)$/i);
|
|
if (hashInclude) {
|
|
const rawInclude = hashInclude[1].trim().replace(/^['"]|['"]$/g, "");
|
|
let child;
|
|
try { child = resolveInclude(path, expand(rawInclude, vars, path, lineNumber)); } catch (cause) { throw cause.code ? cause : error(INI_ERRORS.path, path, lineNumber, "ini_include_path_invalid"); }
|
|
includes.push({ source: path, line: lineNumber, path: child });
|
|
parseFile(child, depth + 1);
|
|
return;
|
|
}
|
|
const clean = stripComment(rawLine).trim();
|
|
if (!clean) return;
|
|
const includeMatch = clean.match(/^(?:include|INCLUDE)\s*(?:=\s*)?(.+)$/);
|
|
if (includeMatch) {
|
|
const rawInclude = includeMatch[1].trim().replace(/^['"]|['"]$/g, "");
|
|
let child;
|
|
try { child = resolveInclude(path, expand(rawInclude, vars, path, lineNumber)); } catch (cause) { throw cause.code ? cause : error(INI_ERRORS.path, path, lineNumber, "ini_include_path_invalid"); }
|
|
includes.push({ source: path, line: lineNumber, path: child });
|
|
parseFile(child, depth + 1);
|
|
return;
|
|
}
|
|
const sectionMatch = clean.match(/^\[([^\]]+)\]$/);
|
|
if (sectionMatch) {
|
|
section = sectionMatch[1].trim();
|
|
if (!section || /[\0\r\n]/.test(section)) throw error(INI_ERRORS.section, path, lineNumber, "ini_section_invalid");
|
|
if (!sections[section]) sections[section] = {};
|
|
return;
|
|
}
|
|
const assignment = clean.match(/^([^=:#]+?)\s*(?:=|:)\s*(.*)$/);
|
|
if (!assignment || !section) throw error(!section ? INI_ERRORS.section : INI_ERRORS.assignment, path, lineNumber, !section ? "ini_section_invalid" : "ini_assignment_invalid");
|
|
const key = assignment[1].trim();
|
|
if (!/^[A-Za-z_][A-Za-z0-9_.-]*$/.test(key)) throw error(INI_ERRORS.key, path, lineNumber, "ini_key_invalid");
|
|
const value = expand(assignment[2].trim(), vars, path, lineNumber);
|
|
sections[section][key] = value;
|
|
if (section.toUpperCase() === "VAR" || section.toUpperCase() === "VARIABLES") vars[key] = value;
|
|
entries.push({ source: path, line: lineNumber, section, key, value });
|
|
});
|
|
active.pop();
|
|
}
|
|
|
|
parseFile(root, 0);
|
|
const normalizedSections = Object.fromEntries(Object.keys(sections).sort().map((name) => [name, Object.fromEntries(Object.keys(sections[name]).sort().map((key) => [key, sections[name][key]]))]));
|
|
const result = {
|
|
schema: INI_SCHEMA,
|
|
entrypoint: root,
|
|
sections: normalizedSections,
|
|
entries,
|
|
includes,
|
|
graph: [...new Set(graph)],
|
|
variables: Object.fromEntries(Object.keys(vars).sort().map((key) => [key, vars[key]])),
|
|
diagnostics,
|
|
};
|
|
return { ...result, state_hash: fnv1a64(result) };
|
|
}
|