import type { BinaryTarget } from "../../domain/binaryTargetTypes.js"; import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js"; import type { JsonValue } from "../../domain/jsonValue.js"; import type { EvidenceBundle } from "../../domain/evidenceBundle.js"; import { EvidenceIntegrityError, AnalysisSnapshotMismatchError, } from "../../domain/evidenceErrors.js"; import { NoBinaryOpenError } from "../../domain/configurationErrors.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import type { AnalysisInputError } from "../../domain/analysisErrorCore.js"; import { rebindEvidenceSubjectTargetPath } from "../../domain/evidence.js"; import { err, ok, type Result } from "../../domain/result.js"; import { analysisQueryId, evidenceMatchesEntry, evidenceMatchesWorkflowEntry, createAnalysisSnapshotEntry, createAnalysisSnapshotWorkflowEntry, snapshotBinding, snapshotMatchesProfile, snapshotMatchesTarget, snapshotTarget, parseAnalysisSnapshot, immutableAnalysisSnapshot, type AnalysisSnapshot, type AnalysisSnapshotEntry, type AnalysisSnapshotWorkflowEntry, type AnalysisSnapshotBinding, type AnalysisSnapshotTarget, } from "../../domain/analysisSnapshot.js"; import type { AnalysisExecution, AnalysisOperation, CapabilityDescriptor, } from "../AnalysisProvider.js"; import { OFFICIAL_TOOL_CONTRACTS } from "../../contracts/officialToolContracts.js"; import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js"; import type { EvidenceDisplayReplacement } from "../investigation/EvidenceLedger.js"; const RELATIONSHIP_OPERATIONS: ReadonlySet = new Set([ "procedure_callers", "procedure_callees", ]); const STATEFUL_OPERATIONS: ReadonlySet = new Set([ "health", "current_address", "current_procedure", "current_document", "goto_address", "list_documents", ]); const CURSOR_DEFAULT_OPERATIONS: ReadonlySet = new Set([ "address_name", "comment", "inline_comment", "next_address", "prev_address", "xrefs", ]); const DOCUMENT_SCOPED_OPERATIONS: ReadonlySet = new Set( OFFICIAL_TOOL_CONTRACTS.map(({ name }) => name).filter( (name) => name !== "current_document" && name !== "list_documents", ), ); /** Maximum duplicated result bindings retained across direct and workflow queries. */ export const SNAPSHOT_CACHE_ENTRY_CEILING = 10_000; /** Whether an operation is immutable and independent of provider UI state. */ export const isSnapshotCacheable = ( operation: AnalysisOperation, descriptor: CapabilityDescriptor | undefined, parameters: Readonly>, ): descriptor is CapabilityDescriptor => !STATEFUL_OPERATIONS.has(operation) && (!DOCUMENT_SCOPED_OPERATIONS.has(operation) || typeof parameters.document === "string") && (!CURSOR_DEFAULT_OPERATIONS.has(operation) || typeof parameters.address === "string") && descriptor?.effects.mutatesArtifact === false && descriptor.cachePolicy !== "live" && (descriptor.cachePolicy === "snapshot" || descriptor.effects.mayWriteFilesystem === false) && descriptor.effects.changesPermissions === false; /** Windows Ghidra must admit the selected source through its native snapshot owner. */ export const requiresNativeSourceAdmissionBeforeReplay = ( providerId: string | undefined, ): boolean => process.platform === "win32" && providerId === "ghidra"; /** Bounded in-memory cache for one immutable binary identity. */ export class AnalysisSnapshotCache { readonly #entries = new Map(); readonly #workflowEntries = new Map(); #target: AnalysisSnapshotTarget | undefined; #binding: AnalysisSnapshotBinding | undefined; /** Whether staged entries belong to the supplied target and profile. */ matches( target: BinaryTarget, profile: AnalysisProfileCommitment | undefined, ): boolean { return ( (this.#target === undefined && this.#binding === undefined) || (this.#target !== undefined && this.#binding !== undefined && profile !== undefined && snapshotMatchesTarget(this.#target, target) && snapshotMatchesProfile(this.#binding, profile)) ); } /** Replace cache state when the target or selected profile changes. */ select(target: BinaryTarget, profile: AnalysisProfileCommitment): void { if (!this.matches(target, profile)) this.#clearEntries(); this.#target = snapshotTarget(target); this.#binding = snapshotBinding(profile); } /** Merge already-validated entries and return the new-entry count. */ stage(snapshot: AnalysisSnapshot): number { snapshot = immutableAnalysisSnapshot(snapshot); if ( (this.#target !== undefined && JSON.stringify(this.#target) !== JSON.stringify(snapshot.target)) || (this.#binding !== undefined && !snapshotMatchesProfile( this.#binding, snapshot.binding.analysis_profile, )) ) this.#clearEntries(); this.#target = structuredClone(snapshot.target); this.#binding = structuredClone(snapshot.binding); let imported = 0; for (const entry of snapshot.entries) { if (!this.#entries.has(entry.query_id) && !this.#hasCapacity()) continue; if (!this.#entries.has(entry.query_id)) imported += 1; this.#entries.set(entry.query_id, entry); } for (const entry of snapshot.workflow_entries) { if (!this.#workflowEntries.has(entry.query_id) && !this.#hasCapacity()) continue; if (!this.#workflowEntries.has(entry.query_id)) imported += 1; this.#workflowEntries.set(entry.query_id, entry); } return imported; } /** Refresh only display bindings backed by the replaced authenticated records. */ refreshEvidenceDisplay( replacements: readonly EvidenceDisplayReplacement[], ): void { if (this.#target === undefined || this.#binding === undefined) return; const header = { target: this.#target, binding: this.#binding }; for (const { previous, current } of replacements) { if ( current.subject === null || previous.subject?.local_path === current.subject.local_path ) continue; const queryId = analysisQueryId( header.target, header.binding, previous.operation, previous.parameters, ); const entry = this.#entries.get(queryId); if ( entry !== undefined && entry.execution.subject !== null && evidenceMatchesEntry(previous, entry, header) ) this.#entries.set(queryId, { ...entry, execution: { ...entry.execution, subject: { ...entry.execution.subject, path: current.subject.local_path, }, }, }); const workflow = this.#workflowEntries.get(queryId); if ( workflow !== undefined && workflow.execution.subject !== null && evidenceMatchesWorkflowEntry(previous, workflow, header) ) this.#workflowEntries.set(queryId, { ...workflow, execution: { ...workflow.execution, subject: { ...workflow.execution.subject, path: current.subject.local_path, }, }, }); } } /** Build a complete snapshot for an active target. */ export( target: BinaryTarget | undefined, profile: AnalysisProfileCommitment | undefined, evidenceBundle: EvidenceBundle, ): Result { if (target === undefined || profile === undefined) return err(new NoBinaryOpenError()); try { const snapshotTargetIdentity = snapshotTarget(target); const binding = snapshotBinding(profile); const entries = [...this.#entries.values()].sort((left, right) => compareUnicodeCodePoints(left.query_id, right.query_id), ); const workflows = [...this.#workflowEntries.values()].sort( (left, right) => compareUnicodeCodePoints(left.query_id, right.query_id), ); // Parsing owns the returned JSON and metadata; pre-cloning the same // payloads here only adds another full materialization. return ok( parseAnalysisSnapshot({ target: snapshotTargetIdentity, binding, entries, workflow_entries: workflows, evidence_bundle: evidenceBundle, }), ); } catch (cause: unknown) { return err( new EvidenceIntegrityError("Analysis snapshot validation failed", { cause, }), ); } } /** Validate target identity, merge evidence atomically, then stage entries. */ import( snapshot: AnalysisSnapshot, active: | { readonly target: BinaryTarget; readonly profile: AnalysisProfileCommitment; } | undefined, mergeEvidence: ( bundle: EvidenceBundle, ) => Result, ): Result { let validated: AnalysisSnapshot; try { validated = immutableAnalysisSnapshot(snapshot); } catch (cause: unknown) { return err( new EvidenceIntegrityError("Analysis snapshot validation failed", { cause, }), ); } if ( active !== undefined && (!snapshotMatchesTarget(validated.target, active.target) || !snapshotMatchesProfile(validated.binding, active.profile)) ) return err( new AnalysisSnapshotMismatchError( "Analysis snapshot profile_mismatch: target, provider, or analysis profile does not match the active binary", ), ); const importedEvidence = mergeEvidence(validated.evidence_bundle); return importedEvidence.ok ? ok(this.stage(validated)) : importedEvidence; } /** Return canonical entries for persistence. */ entries(): AnalysisSnapshotEntry[] { return [...this.#entries.values()] .sort((left, right) => compareUnicodeCodePoints(left.query_id, right.query_id), ) .map((entry) => structuredClone(entry)); } /** Return canonical composed-workflow entries for persistence. */ workflowEntries(): AnalysisSnapshotWorkflowEntry[] { return [...this.#workflowEntries.values()] .sort((left, right) => compareUnicodeCodePoints(left.query_id, right.query_id), ) .map((entry) => structuredClone(entry)); } /** Record one exact derived workflow result unless the shared cache is full. */ recordWorkflow(input: { readonly target: BinaryTarget; readonly profile: AnalysisProfileCommitment; readonly operation: string; readonly parameters: Readonly>; readonly execution: { readonly result: JsonValue; readonly rawResult: JsonValue | null; readonly provider: AnalysisExecution["provider"]; readonly analysisProfile: AnalysisProfileCommitment; readonly limitations: readonly string[]; readonly relationshipCoverage?: AnalysisExecution["relationshipCoverage"]; readonly locations: AnalysisExecution["locations"]; readonly subject: NonNullable | null; }; }): void { this.select(input.target, input.profile); const queryId = analysisQueryId( snapshotTarget(input.target), snapshotBinding(input.profile), input.operation, input.parameters, ); if (!this.#workflowEntries.has(queryId) && !this.#hasCapacity()) return; const entry = createAnalysisSnapshotWorkflowEntry({ target: snapshotTarget(input.target), binding: snapshotBinding(input.profile), operation: input.operation, parameters: input.parameters, execution: input.execution, }); this.#workflowEntries.set(entry.query_id, entry); } /** Replay an exact provider-specific query, marking its cached provenance. */ lookup( target: BinaryTarget, profile: AnalysisProfileCommitment, operation: AnalysisOperation, parameters: Readonly>, ): AnalysisExecution | undefined { const queryId = analysisQueryId( snapshotTarget(target), snapshotBinding(profile), operation, parameters, ); const cached = this.#entries.get(queryId); if (cached === undefined) return undefined; const cachedSubject = cached.execution.subject; const subject = cachedSubject == null ? null : rebindEvidenceSubjectTargetPath( { path: cachedSubject.path, sha256: cachedSubject.sha256, format: cachedSubject.format, ...(cachedSubject.architecture === null ? {} : { architecture: cachedSubject.architecture }), }, target, ); return structuredClone({ result: cached.execution.result, rawResult: cached.execution.raw_result, provider: cached.execution.provider, analysisProfile: structuredClone(profile), limitations: [ ...cached.execution.limitations, "Loaded from a local REA analysis snapshot; this call did not re-run the provider.", ], ...(cached.execution.relationship_coverage === null && RELATIONSHIP_OPERATIONS.has(operation) ? { relationshipCoverage: { status: "unknown", reason: "Snapshot entry does not report relationship coverage.", }, } : cached.execution.relationship_coverage === null ? {} : { relationshipCoverage: cached.execution.relationship_coverage, }), locations: cached.execution.locations, subject: subject ?? null, }); } /** Record one successful immutable call unless the cache is full. */ record(input: { readonly target: BinaryTarget; readonly profile: AnalysisProfileCommitment; readonly operation: AnalysisOperation; readonly parameters: Readonly>; readonly execution: AnalysisExecution; }): void { const { target, profile, operation, parameters, execution } = input; this.select(target, profile); // The snapshot format does not carry reference truncation; a replay would // present a capped read as complete, so it is not recorded. if (execution.referencesTruncated === true) return; const queryId = analysisQueryId( snapshotTarget(target), snapshotBinding(profile), operation, parameters, ); if (!this.#entries.has(queryId) && !this.#hasCapacity()) return; const entry = createAnalysisSnapshotEntry({ target: snapshotTarget(target), binding: snapshotBinding(profile), operation, parameters, execution: { ...execution, subject: execution.subject === undefined ? { path: target.path, sha256: target.sha256, format: target.format, ...(target.architecture === undefined ? {} : { architecture: target.architecture }), } : execution.subject, }, }); this.#entries.set(entry.query_id, entry); } /** Forget all target-bound entries. */ clear(): void { this.#clearEntries(); this.#target = undefined; this.#binding = undefined; } #clearEntries(): void { this.#entries.clear(); this.#workflowEntries.clear(); } #hasCapacity(): boolean { return ( this.#entries.size + this.#workflowEntries.size < SNAPSHOT_CACHE_ENTRY_CEILING ); } }