mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
1a1b67c888
Use POSIX path semantics for recorded POSIX roots when reconciling runtime Evidence on Windows. Preserve Windows mapping behavior and cover implicit and explicit file roots through the reconciliation service. Assisted-by: OpenAI GPT-6 <noreply@openai.com> Co-authored-by: Aurantium-47 <280805072+Aurantium-47@users.noreply.github.com>
322 lines
10 KiB
TypeScript
322 lines
10 KiB
TypeScript
import { posix, win32 } from "node:path";
|
|
|
|
import type { ApplicationNode } from "./javascriptApplicationGraphSchemas.js";
|
|
import type { JavaScriptApplicationGraph } from "./javascriptApplicationGraph.js";
|
|
import {
|
|
logicalPathEscapesRoot,
|
|
normalizeJoinedLogicalPath,
|
|
} from "../artifactIdentity.js";
|
|
import { uniqueSorted } from "../canonicalOrdering.js";
|
|
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
|
|
import type { ParsedStaticLayer } from "./javascriptRuntimeReconciliationParsing.js";
|
|
import type { RuntimeReconciliationEntity } from "./javascriptRuntimeReconciliationRuntime.js";
|
|
|
|
export interface StaticRuntimeCandidate {
|
|
readonly layer: ParsedStaticLayer;
|
|
readonly node: ApplicationNode;
|
|
readonly category: "renderer" | "javascript" | "worker" | "service-worker";
|
|
readonly paths: readonly string[];
|
|
readonly digests: readonly {
|
|
readonly sha256: string;
|
|
readonly basis: "content-sha256" | "module-source-sha256";
|
|
}[];
|
|
}
|
|
|
|
export interface MappedRuntimePath {
|
|
readonly path: string;
|
|
readonly basis:
|
|
| "artifact-path"
|
|
| "operator-file-mapping"
|
|
| "operator-url-mapping";
|
|
}
|
|
|
|
/** Precomputed mapping scope and graph paths for static load-state decisions. */
|
|
export interface StaticRuntimeScope {
|
|
readonly artifactPrefixes: readonly string[];
|
|
readonly pathsByNodeId: ReadonlyMap<string, readonly string[]>;
|
|
}
|
|
|
|
/** Index statically reconstructed entities that can receive runtime matches. */
|
|
export const collectStaticRuntimeCandidates = (
|
|
layers: readonly ParsedStaticLayer[],
|
|
): StaticRuntimeCandidate[] =>
|
|
layers
|
|
.flatMap((layer) => candidatesForLayer(layer))
|
|
.sort(
|
|
(left, right) =>
|
|
compareUnicodeCodePoints(left.layer.layerId, right.layer.layerId) ||
|
|
compareUnicodeCodePoints(left.node.node_id, right.node.node_id),
|
|
);
|
|
|
|
/** Translate one already-authorized runtime location into artifact paths. */
|
|
export const mappedRuntimePaths = (
|
|
layer: ParsedStaticLayer,
|
|
entity: RuntimeReconciliationEntity,
|
|
): MappedRuntimePath[] => {
|
|
const paths: MappedRuntimePath[] = [];
|
|
if (entity.location.kind === "file" && layer.result.format === "directory") {
|
|
const path = pathBelowRoot(layer.result.input_path, entity.location.value);
|
|
if (path !== null) paths.push({ path, basis: "artifact-path" });
|
|
}
|
|
for (const mapping of layer.runtimeMappings) {
|
|
const suffix =
|
|
mapping.kind === "file-root" && entity.location.kind === "file"
|
|
? pathBelowRoot(mapping.root, entity.location.value)
|
|
: mapping.kind === "url-prefix" && entity.location.kind === "url"
|
|
? pathBelowUrlPrefix(mapping.prefix, entity.location.value)
|
|
: null;
|
|
if (suffix === null) continue;
|
|
const path = safeArtifactPath(mapping.artifact_prefix, suffix);
|
|
if (path === null) continue;
|
|
paths.push({
|
|
path,
|
|
basis:
|
|
mapping.kind === "file-root"
|
|
? "operator-file-mapping"
|
|
: "operator-url-mapping",
|
|
});
|
|
}
|
|
return uniqueMappedPaths(paths);
|
|
};
|
|
|
|
/** Precompute the artifact prefixes exercised by a bounded runtime set. */
|
|
export const createStaticRuntimeScope = (
|
|
layer: ParsedStaticLayer,
|
|
entities: readonly RuntimeReconciliationEntity[],
|
|
): StaticRuntimeScope => ({
|
|
artifactPrefixes: activeArtifactPrefixes(layer, entities),
|
|
pathsByNodeId: indexStaticPaths(layer.graph),
|
|
});
|
|
|
|
/** Whether one static node falls under a mapping exercised by this runtime set. */
|
|
export const staticNodeWithinRuntimeScope = (
|
|
node: ApplicationNode,
|
|
scope: StaticRuntimeScope,
|
|
): boolean =>
|
|
(scope.pathsByNodeId.get(node.node_id) ?? []).some((path) =>
|
|
scope.artifactPrefixes.some((prefix) => pathWithinPrefix(path, prefix)),
|
|
);
|
|
|
|
const candidatesForLayer = (
|
|
layer: ParsedStaticLayer,
|
|
): StaticRuntimeCandidate[] => {
|
|
const graph = layer.graph;
|
|
const pathsByNodeId = indexStaticPaths(graph);
|
|
return graph.nodes.flatMap((node) => {
|
|
const category = candidateCategory(node);
|
|
if (category === null) return [];
|
|
return [
|
|
{
|
|
layer,
|
|
node,
|
|
category,
|
|
paths: pathsByNodeId.get(node.node_id) ?? [],
|
|
digests: candidateDigests(node),
|
|
},
|
|
];
|
|
});
|
|
};
|
|
|
|
const candidateCategory = (
|
|
node: ApplicationNode,
|
|
): StaticRuntimeCandidate["category"] | null => {
|
|
if (node.kind === "electron-renderer") return "renderer";
|
|
if (
|
|
node.kind === "javascript-asset" ||
|
|
(node.kind === "javascript-module" && !isStaticSourceModule(node))
|
|
)
|
|
return "javascript";
|
|
if (node.kind === "worker") return "worker";
|
|
if (node.kind === "service-worker") return "service-worker";
|
|
return null;
|
|
};
|
|
|
|
const isStaticSourceModule = (node: ApplicationNode): boolean =>
|
|
node.observations.some(
|
|
({ properties }) => properties.semantic_role === "source-module",
|
|
);
|
|
|
|
const indexStaticPaths = (
|
|
graph: JavaScriptApplicationGraph,
|
|
): ReadonlyMap<string, readonly string[]> => {
|
|
const nodes = new Map(
|
|
graph.nodes.map((candidate) => [candidate.node_id, candidate]),
|
|
);
|
|
const relatedBySource = new Map<string, ApplicationNode[]>();
|
|
for (const edge of graph.edges) {
|
|
if (edge.relation !== "maps_to") continue;
|
|
const target = nodes.get(edge.target_node_id);
|
|
if (target === undefined) continue;
|
|
const related = relatedBySource.get(edge.source_node_id);
|
|
if (related === undefined)
|
|
relatedBySource.set(edge.source_node_id, [target]);
|
|
else related.push(target);
|
|
}
|
|
return new Map(
|
|
graph.nodes.map((node) => [
|
|
node.node_id,
|
|
uniqueSorted([
|
|
...nodePaths(node),
|
|
...(relatedBySource.get(node.node_id) ?? []).flatMap(nodePaths),
|
|
]),
|
|
]),
|
|
);
|
|
};
|
|
|
|
const nodePaths = (node: ApplicationNode): string[] => {
|
|
const paths: string[] = [];
|
|
if (node.identity.strategy === "canonical-path")
|
|
paths.push(node.identity.path);
|
|
for (const observation of node.observations) {
|
|
for (const property of ["path", "resolved_path", "declared_path"])
|
|
if (typeof observation.properties[property] === "string")
|
|
paths.push(observation.properties[property]);
|
|
if (
|
|
observation.evidence.location.available &&
|
|
observation.evidence.location.value.kind === "artifact-path"
|
|
)
|
|
paths.push(observation.evidence.location.value.path);
|
|
}
|
|
return paths
|
|
.map(relativeLogicalPath)
|
|
.filter(
|
|
(path): path is string => path !== null && path !== "artifact-root",
|
|
);
|
|
};
|
|
|
|
const candidateDigests = (
|
|
node: ApplicationNode,
|
|
): StaticRuntimeCandidate["digests"] => {
|
|
const values: StaticRuntimeCandidate["digests"][number][] = [];
|
|
if (
|
|
node.kind === "javascript-asset" &&
|
|
node.identity.strategy === "content-digest"
|
|
)
|
|
values.push({
|
|
sha256: node.identity.sha256,
|
|
basis: "content-sha256",
|
|
});
|
|
if (node.kind === "javascript-module")
|
|
for (const observation of node.observations) {
|
|
const digest = observation.properties.source_sha256;
|
|
if (typeof digest === "string" && /^[a-f0-9]{64}$/u.test(digest))
|
|
values.push({ sha256: digest, basis: "module-source-sha256" });
|
|
}
|
|
return [
|
|
...new Map(
|
|
values.map((value) => [`${value.basis}:${value.sha256}`, value]),
|
|
).values(),
|
|
];
|
|
};
|
|
|
|
const pathBelowRoot = (root: string, value: string): string | null => {
|
|
const rootStyle = absoluteFilePathStyle(root);
|
|
if (rootStyle === null || rootStyle !== absoluteFilePathStyle(value))
|
|
return null;
|
|
const remainder =
|
|
rootStyle === "windows"
|
|
? win32.relative(root, value)
|
|
: posix.relative(root, value);
|
|
const portableRemainder = remainder.replaceAll("\\", "/");
|
|
if (
|
|
remainder === "" ||
|
|
(rootStyle === "windows" && win32.isAbsolute(remainder)) ||
|
|
portableRemainder === ".." ||
|
|
portableRemainder.startsWith("../")
|
|
)
|
|
return null;
|
|
return relativeLogicalPath(portableRemainder);
|
|
};
|
|
|
|
const absoluteFilePathStyle = (path: string): "posix" | "windows" | null => {
|
|
if (
|
|
/^[a-z]:[\\/]/iu.test(path) ||
|
|
/^(?:\\\\|\/\/)[^\\/]+[\\/][^\\/]+(?:[\\/]|$)/u.test(path)
|
|
)
|
|
return "windows";
|
|
return path.startsWith("/") ? "posix" : null;
|
|
};
|
|
|
|
const pathBelowUrlPrefix = (prefix: string, value: string): string | null => {
|
|
let base: URL;
|
|
let candidate: URL;
|
|
try {
|
|
base = new URL(prefix);
|
|
candidate = new URL(value);
|
|
} catch (cause: unknown) {
|
|
// Invalid input is represented by the null return.
|
|
void cause;
|
|
return null;
|
|
}
|
|
if (
|
|
base.origin !== candidate.origin ||
|
|
!candidate.pathname.startsWith(base.pathname)
|
|
)
|
|
return null;
|
|
try {
|
|
return relativeLogicalPath(
|
|
decodeURIComponent(candidate.pathname.slice(base.pathname.length)),
|
|
);
|
|
} catch (cause: unknown) {
|
|
// Undecodable path segments are represented by the null return.
|
|
void cause;
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const activeArtifactPrefixes = (
|
|
layer: ParsedStaticLayer,
|
|
entities: readonly RuntimeReconciliationEntity[],
|
|
): string[] => {
|
|
const prefixes = new Set<string>();
|
|
if (
|
|
layer.result.format === "directory" &&
|
|
entities.some(
|
|
({ location }) =>
|
|
location.kind === "file" &&
|
|
pathBelowRoot(layer.result.input_path, location.value) !== null,
|
|
)
|
|
)
|
|
prefixes.add("");
|
|
for (const mapping of layer.runtimeMappings)
|
|
if (
|
|
entities.some(({ location }) =>
|
|
mapping.kind === "file-root" && location.kind === "file"
|
|
? pathBelowRoot(mapping.root, location.value) !== null
|
|
: mapping.kind === "url-prefix" && location.kind === "url"
|
|
? pathBelowUrlPrefix(mapping.prefix, location.value) !== null
|
|
: false,
|
|
)
|
|
)
|
|
prefixes.add(mapping.artifact_prefix);
|
|
return [...prefixes].sort(compareUnicodeCodePoints);
|
|
};
|
|
|
|
const pathWithinPrefix = (path: string, prefix: string): boolean =>
|
|
prefix === "" || path === prefix || path.startsWith(`${prefix}/`);
|
|
|
|
const safeArtifactPath = (prefix: string, suffix: string): string | null =>
|
|
relativeLogicalPath(prefix === "" ? suffix : posix.join(prefix, suffix));
|
|
|
|
const relativeLogicalPath = (value: string): string | null => {
|
|
const normalized = normalizeJoinedLogicalPath(value);
|
|
return normalized !== "" &&
|
|
normalized !== "." &&
|
|
!logicalPathEscapesRoot(normalized)
|
|
? normalized
|
|
: null;
|
|
};
|
|
|
|
const uniqueMappedPaths = (
|
|
values: readonly MappedRuntimePath[],
|
|
): MappedRuntimePath[] =>
|
|
[
|
|
...new Map(
|
|
values.map((value) => [`${value.basis}:${value.path}`, value]),
|
|
).values(),
|
|
].sort(
|
|
(left, right) =>
|
|
compareUnicodeCodePoints(left.path, right.path) ||
|
|
compareUnicodeCodePoints(left.basis, right.basis),
|
|
);
|