Files
rea/src/application/javascript/JavaScriptArtifactReconstruction.ts
T

169 lines
6.7 KiB
TypeScript

import { lstat, realpath } from "node:fs/promises";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import { createJavaScriptArtifactReader as createReader } from "../../artifacts/javascript/JavaScriptArtifactReader.js";
import type { JavaScriptApplicationGraph } from "../../domain/javascript/javascriptApplicationGraph.js";
import type { JavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticGraph.js";
import type { ElectronBoundarySummary } from "../../domain/javascript/javascriptApplicationAnalysis.js";
import { analyzeAndProjectJavaScriptArtifactFiles } from "./JavaScriptArtifactAnalysis.js";
import { readJavaScriptArtifactFiles } from "../../artifacts/javascript/JavaScriptArtifactFiles.js";
import { buildJavaScriptArtifactGraph } from "./JavaScriptArtifactGraphBuilder.js";
import {
javascriptArtifactReconstructionInputSchema,
type JavaScriptArtifactReconstructionInput,
} from "./JavaScriptArtifactReconstructionInput.js";
import { scanCanonicalArtifactInventory } from "../../artifacts/inventory/ArtifactInventory.js";
import { summarizeElectronBoundaries } from "./ElectronBoundaryAnalysis.js";
import { createJavaScriptSemanticGraphProjection } from "./JavaScriptSemanticGraphBuilder.js";
import type { ProgressReporter } from "../ProgressReporter.js";
import { checkpointJavaScriptAnalysis } from "./JavaScriptAnalysisControl.js";
/** Application-layer result retaining local diagnostics outside the canonical graph. */
export interface JavaScriptArtifactReconstructionResult {
readonly input_path: string;
readonly format: "asar" | "directory";
readonly root_artifact_sha256: string;
readonly inventory_manifest_id: string;
readonly inventory_graph_sha256: string;
readonly graph: JavaScriptApplicationGraph;
readonly semantic_graph: JavaScriptSemanticGraph;
readonly electron_summary: ElectronBoundarySummary;
readonly statistics: {
readonly relevant_files: number;
readonly nested_asar_containers: number;
readonly text_bytes_read: number;
readonly invalid_utf8_files: number;
readonly parsed_javascript_files: number;
readonly visited_ast_nodes: number;
readonly findings: number;
readonly modules: number;
readonly parse_failures: number;
readonly truncated_scopes: number;
};
readonly limitations: readonly string[];
}
/** Reconstruct one local ASAR or extracted directory without executing code. */
export const reconstructJavaScriptArtifact = async (
rawInput: unknown,
signal?: AbortSignal,
progress?: ProgressReporter,
): Promise<JavaScriptArtifactReconstructionResult> => {
const reportPhase = async (phase: string, message: string): Promise<void> => {
await progress?.report({ phase, completed: 0, total: 1, message });
await checkpointJavaScriptAnalysis(signal);
};
const input = javascriptArtifactReconstructionInputSchema.parse(rawInput);
abortIfNeeded(signal);
const path = await realpath(input.input_path);
const format = await resolveFormat(path, input);
const snapshot = await scanCanonicalArtifactInventory(path, { signal });
if (snapshot.manifest.root_format !== format)
throw new ArtifactReaderFailure(
"format",
`Artifact inventory classified ${path} as ${snapshot.manifest.root_format}, not ${format}`,
);
const reader = createReader(path, format);
try {
await reportPhase(
"read_javascript_artifacts",
"Reading inventoried JavaScript application sources",
);
const files = await readJavaScriptArtifactFiles(reader, snapshot, signal);
await reportPhase(
"parse_javascript_sources",
`Parsing and projecting ${String(files.files.length)} application source files`,
);
const semanticProjection = createJavaScriptSemanticGraphProjection();
const analysis = await analyzeAndProjectJavaScriptArtifactFiles(
files,
semanticProjection.projectFile,
async (file, completed, total) => {
abortIfNeeded(signal);
await progress?.report({
phase: "parse_javascript_source",
completed: 0,
total: 1,
message: `Parsing and projecting ${file.path} (${String(completed + 1)}/${String(total)})`,
});
abortIfNeeded(signal);
},
);
await checkpointJavaScriptAnalysis(signal);
abortIfNeeded(signal);
await reportPhase(
"build_javascript_application_graph",
"Constructing application and Electron boundary relationships",
);
const graph = buildJavaScriptArtifactGraph(snapshot, files, analysis);
await reportPhase(
"build_javascript_semantic_graph",
"Binding and validating static semantic relationships",
);
const semanticGraph = semanticProjection.finish(
snapshot.manifest.root_sha256,
graph,
analysis,
);
await checkpointJavaScriptAnalysis(signal);
return {
input_path: path,
format,
root_artifact_sha256: snapshot.manifest.root_sha256,
inventory_manifest_id: snapshot.manifest.manifest_id,
inventory_graph_sha256: snapshot.manifest.graph_sha256,
graph,
semantic_graph: semanticGraph,
electron_summary: summarizeElectronBoundaries(analysis),
statistics: {
relevant_files: files.files.length,
nested_asar_containers: files.containers.length,
text_bytes_read: files.text_bytes_read,
invalid_utf8_files: files.invalid_utf8_files,
parsed_javascript_files: analysis.files.filter(
({ javascript }) => javascript !== null,
).length,
visited_ast_nodes: analysis.visited_ast_nodes,
findings: analysis.findings,
modules: analysis.modules,
parse_failures: analysis.parse_failures,
truncated_scopes: analysis.truncated_scopes,
},
limitations: analysis.limitations,
};
} finally {
await reader.close();
}
};
const resolveFormat = async (
path: string,
input: JavaScriptArtifactReconstructionInput,
): Promise<"asar" | "directory"> => {
const metadata = await lstat(path);
const observed = metadata.isDirectory()
? "directory"
: metadata.isFile() && path.toLowerCase().endsWith(".asar")
? "asar"
: undefined;
if (observed === undefined)
throw new ArtifactReaderFailure(
"format",
`JavaScript artifact reconstruction accepts only a directory or .asar file: ${path}`,
);
if (input.format !== "auto" && input.format !== observed)
throw new ArtifactReaderFailure(
"format",
`Requested ${input.format} input but observed ${observed}: ${path}`,
);
return observed;
};
const abortIfNeeded = (signal?: AbortSignal): void => {
if (signal?.aborted === true)
throw new ArtifactReaderFailure(
"cancelled",
"JavaScript artifact reconstruction cancelled",
);
};