Files
rea/src/artifacts/ArtifactProvider.ts
T

332 lines
11 KiB
TypeScript

import {
createAnalysisExecution,
type AnalysisClient,
type AnalysisOperation,
type AnalysisProvider,
type CapabilityDescriptor,
type ProviderIdentity,
type ExecutionOptions,
} from "../application/AnalysisProvider.js";
import { inspectBundleKeyedArchive } from "./KeyedArchiveReader.js";
import { basename, dirname } from "node:path";
import { inventoryArtifact } from "../application/ArtifactInventory.js";
import { extractArtifact } from "../application/ArtifactExtraction.js";
import { analyzeInterfaceBuilderBundle } from "../application/InterfaceBuilderAnalysis.js";
import { analyzeAppleAssetCatalogs } from "../application/AppleAssetCatalogAnalysis.js";
import {
ARTIFACT_ANALYSIS_OPERATIONS,
artifactInventoryInputSchema,
artifactExtractionExecutionSchema,
type ArtifactAnalysisOperation,
} from "../contracts/artifactToolContracts.js";
import type { BinaryTarget } from "../domain/binaryTarget.js";
import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js";
import { ArtifactOperationError } from "../domain/artifactOperationError.js";
import { type AnalysisError } from "../domain/analysisErrorBase.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { interfaceBuilderLimitsSchema } from "../domain/interfaceBuilderGraph.js";
import { err, ok } from "../domain/result.js";
import { ArtifactReaderFailure } from "./ArtifactReader.js";
import {
ARTIFACT_PROVIDER_IDENTITY as IDENTITY,
artifactCapabilities,
} from "./ArtifactProviderMetadata.js";
import { createEvidence } from "../domain/evidence.js";
import { createArtifactInspection } from "../domain/artifactInspection.js";
import { resolveArtifactIntegrityPolicy } from "../application/ArtifactInventory/policy.js";
/** Read-only inventory and exclusively owned extraction provider. */
export class ArtifactProvider implements AnalysisProvider {
readonly #capabilities: readonly CapabilityDescriptor[];
constructor(platform: NodeJS.Platform = process.platform) {
this.#capabilities = artifactCapabilities(platform);
}
identity(): ProviderIdentity {
return IDENTITY;
}
capabilities(): readonly CapabilityDescriptor[] {
return this.#capabilities;
}
createClient(target: BinaryTarget): AnalysisClient {
return new ArtifactClient(target);
}
}
class ArtifactClient implements AnalysisClient {
constructor(private readonly target: BinaryTarget) {}
async execute(
operation: AnalysisOperation,
parameters: Readonly<Record<string, JsonValue>>,
options?: ExecutionOptions,
) {
if (operation === "health")
return ok(createAnalysisExecution(null, IDENTITY));
if (!isArtifactOperation(operation))
return err(
new AnalysisCapabilityUnavailableError(
IDENTITY.id,
operation,
"Operation is not implemented by artifact graph provider.",
),
);
try {
if (operation === "inspect_artifact") {
const inspected = await this.inspectArtifact(parameters, options);
return inspected;
}
if (operation === "decode_interface_builder") {
if (
this.target.kind !== "executable" ||
this.target.sourcePath === undefined ||
!this.target.sourcePath.toLowerCase().endsWith(".app")
)
throw new ArtifactReaderFailure(
"unavailable",
"decode_interface_builder requires an active .app bundle target",
);
const limits = interfaceBuilderLimitsSchema.parse(parameters);
const result = await analyzeInterfaceBuilderBundle({
bundlePath: this.target.sourcePath,
targetSha256: this.target.sha256,
limits,
...(options?.signal === undefined ? {} : { signal: options.signal }),
});
return ok(
createAnalysisExecution(result, IDENTITY, {
limitations: result.limitations,
locations: result.documents.map(({ relative_path: path }) => ({
kind: "artifact-path" as const,
path,
})),
}),
);
}
if (operation === "inspect_keyed_archive") {
const standalone =
this.target.kind === "artifact" && this.target.format === "plist";
if (
standalone &&
parameters.path !== undefined &&
parameters.path !== "." &&
parameters.path !== basename(this.target.path)
)
throw new ArtifactReaderFailure(
"path",
"For an active plist, path must select that archive (omit path or use its basename)",
);
const bundlePath = standalone
? dirname(this.target.path)
: this.target.sourcePath;
if (
bundlePath === undefined ||
(!standalone && !bundlePath.toLowerCase().endsWith(".app"))
)
throw new ArtifactReaderFailure(
"unavailable",
"inspect_keyed_archive requires an active plist or .app bundle",
);
const result = await inspectBundleKeyedArchive({
bundlePath,
targetSha256: this.target.sha256,
parameters: standalone
? { ...parameters, path: basename(this.target.path) }
: parameters,
...(options?.signal === undefined ? {} : { signal: options.signal }),
});
if (standalone && result.archive_sha256 !== this.target.sha256)
throw new ArtifactReaderFailure(
"integrity",
`Active archive digest changed: expected ${this.target.sha256}, observed ${result.archive_sha256}`,
);
return ok(
createAnalysisExecution(result, IDENTITY, {
limitations: result.limitations,
locations: [{ kind: "artifact-path", path: result.archive_path }],
}),
);
}
if (operation === "inspect_asset_catalog") {
return await this.inspectAssetCatalog(parameters, options);
}
if (operation === "extract_artifact") {
const parsed = artifactExtractionExecutionSchema.parse(parameters);
const result = await extractArtifact(
{
inputPath: this.target.sourcePath ?? this.target.path,
inputFormat: this.target.format,
outputRoot: parsed.output_root,
},
options?.signal,
);
return ok(
createAnalysisExecution(result, IDENTITY, {
rawResult: null,
limitations: result.limitations,
subject: subjectFor(
this.target.sourcePath ?? this.target.path,
result.manifest,
),
locations: result.artifacts.map(({ relative_path: path }) => ({
kind: "artifact-path" as const,
path,
})),
}),
);
}
const parsed = artifactInventoryInputSchema.parse(parameters);
const result = await this.inventory(parsed, options);
return ok(
createAnalysisExecution(result, IDENTITY, {
rawResult: null,
limitations: result.limitations,
subject: subjectFor(
this.target.sourcePath ?? this.target.path,
result.manifest,
),
locations: result.occurrences.map(({ logical_path: path }) => ({
kind: "artifact-path" as const,
path,
})),
}),
);
} catch (cause: unknown) {
return err(translateFailure(operation, cause));
}
}
close(): Promise<void> {
return Promise.resolve();
}
private async inspectAssetCatalog(
parameters: Readonly<Record<string, JsonValue>>,
options?: ExecutionOptions,
) {
const bundlePath = this.target.sourcePath;
if (
this.target.kind !== "executable" ||
bundlePath === undefined ||
!bundlePath.toLowerCase().endsWith(".app")
)
throw new ArtifactReaderFailure(
"unavailable",
"inspect_asset_catalog requires an active .app bundle target",
);
const result = await analyzeAppleAssetCatalogs({
bundlePath,
targetSha256: this.target.sha256,
page: parameters,
...(options?.signal === undefined ? {} : { signal: options.signal }),
});
return ok(
createAnalysisExecution(result, IDENTITY, {
limitations: result.limitations,
locations: result.catalogs.map(({ path }) => ({
kind: "artifact-path" as const,
path,
})),
}),
);
}
private async inspectArtifact(
parameters: Readonly<Record<string, JsonValue>>,
options?: ExecutionOptions,
) {
const parsed = artifactInventoryInputSchema.parse(parameters);
const inventoryParameters = {
integrity_policy: parsed.integrity_policy,
};
await options?.progress?.report({
phase: "inspect_artifact.inventory",
completed: 0,
total: 1,
message: "inventory substep started",
});
const inventory = await this.inventory(inventoryParameters, options);
const subject = subjectFor(
this.target.sourcePath ?? this.target.path,
inventory.manifest,
);
const locations = inventory.occurrences.map(({ logical_path: path }) => ({
kind: "artifact-path" as const,
path,
}));
const inventoryEvidence = createEvidence(subject, IDENTITY, {
operation: "inventory_artifact",
parameters: inventoryParameters,
result: inventory,
rawResult: null,
limitations: inventory.limitations,
locations,
});
const result = createArtifactInspection(inventoryEvidence);
await options?.progress?.report({
phase: "inspect_artifact.inventory",
completed: 1,
total: 1,
message: "inventory substep completed",
});
return ok(
createAnalysisExecution(result, IDENTITY, {
rawResult: null,
limitations: result.limitations,
subject,
locations,
}),
);
}
private inventory(
parsed: {
readonly integrity_policy: "fail" | "record-and-continue";
},
options?: ExecutionOptions,
) {
return inventoryArtifact(this.target.sourcePath ?? this.target.path, {
...(options?.signal === undefined ? {} : { signal: options.signal }),
integrity: resolveArtifactIntegrityPolicy({
mode: parsed.integrity_policy,
}),
});
}
}
const isArtifactOperation = (
operation: AnalysisOperation,
): operation is ArtifactAnalysisOperation =>
ARTIFACT_ANALYSIS_OPERATIONS.includes(
operation as (typeof ARTIFACT_ANALYSIS_OPERATIONS)[number],
);
const translateFailure = (
operation: ArtifactAnalysisOperation,
cause: unknown,
): AnalysisError => {
if (cause instanceof ArtifactReaderFailure)
return new ArtifactOperationError(
operation,
cause.reason,
cause.details,
cause.message,
);
return new ArtifactOperationError(operation, "io");
};
const subjectFor = (
path: string,
manifest: {
readonly root_sha256: string;
readonly root_format: import("../domain/artifactGraph.js").ArtifactNode["format"];
},
) => ({
path,
sha256: manifest.root_sha256,
format: manifest.root_format,
});