* feat(native): inspect a universal Mach-O slice in Go and Rust metadata readers inspect_go_binary and inspect_rust_binary refused universal Mach-O files and offered no slice selection, so hosts without lipo could not inspect them. Both now accept architecture (x86, x86_64, arm, arm64). One selector reads REA's own FAT table, validates the slice range, alignment and header CPU, and returns the thin image with its macho-selected-image-v1 identity. The universal file stays the artifact; results add selected_slice and keep every source range a file offset. Missing, absent, duplicate or misplaced selections fail on architecture and list the slices present. Subject format mach-o-universal now has one owner shared by the services and the retained Go/Rust views. Fixes #4102 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(native): reject damaged universal files and self-contradicting slice reports A FAT table whose slice could not be read was treated as a non-universal file, so callers were told to omit architecture. Its reason now fails on path. Go and Rust reports with a selected slice must be Mach-O and name the slice's CPU family, through one shared agreement check. The CLI architecture flag is described in help. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
121 lines
4.1 KiB
TypeScript
121 lines
4.1 KiB
TypeScript
import { metadataReportSubjectFormat } from "../../domain/apple/machoTargetSelection.js";
|
|
import type { ExecutionOptions } from "../AnalysisProvider.js";
|
|
import type { RustBinaryPort } from "./RustBinaryPort.js";
|
|
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
|
import {
|
|
AnalysisCancelledError,
|
|
AnalysisOutputError,
|
|
} from "../../domain/analysisErrorCore.js";
|
|
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
|
import {
|
|
rustArchitectureForSubject,
|
|
rustBinarySchema,
|
|
type InspectRustBinaryInput,
|
|
} from "../../domain/rust/rustBinary.js";
|
|
import { inspectRustBinaryRequestSchema } from "../../domain/rust/rustBinaryQuery.js";
|
|
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
|
|
import { err, ok, type Result } from "../../domain/result.js";
|
|
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
|
|
import {
|
|
requireAnalysisViewRetention,
|
|
projectRetainedAnalysis,
|
|
type AnalysisViewRetention,
|
|
} from "../analysisView/AnalysisViewService.js";
|
|
|
|
const OPERATION = "inspect_rust_binary";
|
|
|
|
/** Shared exact input validation and artifact-bound Evidence for CLI and MCP. */
|
|
export class RustBinaryService {
|
|
constructor(readonly provider: RustBinaryPort) {}
|
|
|
|
close(): Promise<void> {
|
|
return this.provider.close?.() ?? Promise.resolve();
|
|
}
|
|
|
|
/** Inspect one explicitly selected file without acquiring a binary session. */
|
|
async inspect(
|
|
rawInput: unknown,
|
|
options?: ExecutionOptions,
|
|
retention: AnalysisViewRetention = {
|
|
recordEvidence: undefined,
|
|
evidenceById: undefined,
|
|
},
|
|
): Promise<Result<Evidence, AnalysisError>> {
|
|
if (options?.signal?.aborted)
|
|
return err(
|
|
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
|
|
);
|
|
const input = inspectRustBinaryRequestSchema.safeParse(rawInput);
|
|
if (!input.success)
|
|
return err(
|
|
analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, {
|
|
cause: input.error,
|
|
}),
|
|
);
|
|
const { detail, view, ...parameters } = input.data;
|
|
const compact = detail === "summary" || view !== undefined;
|
|
if (compact) {
|
|
const ready = requireAnalysisViewRetention(OPERATION, retention);
|
|
if (!ready.ok) return ready;
|
|
}
|
|
const inspected = await this.provider.inspect(parameters, options);
|
|
if (!inspected.ok) return inspected;
|
|
const report = rustBinarySchema.safeParse(inspected.value);
|
|
if (!report.success || report.data.artifact.path !== input.data.path)
|
|
return err(
|
|
new AnalysisOutputError(
|
|
OPERATION,
|
|
"Rust dependency reader returned malformed metadata or changed the selected artifact identity.",
|
|
),
|
|
);
|
|
const completed = completeProviderEvidence(
|
|
evidenceFor(this.provider.identity, parameters, report.data),
|
|
options?.signal,
|
|
);
|
|
return compact && completed.ok
|
|
? projectRetainedAnalysis(
|
|
completed.value,
|
|
retention,
|
|
view === undefined ? { kind: "summary" } : { kind: "rust", ...view },
|
|
)
|
|
: completed;
|
|
}
|
|
}
|
|
|
|
const evidenceFor = (
|
|
provider: RustBinaryPort["identity"],
|
|
parameters: InspectRustBinaryInput,
|
|
value: ReturnType<typeof rustBinarySchema.parse>,
|
|
): Evidence => {
|
|
const architecture = rustArchitectureForSubject(value.architecture);
|
|
const section = value.auditable_metadata;
|
|
return createEvidence(
|
|
{
|
|
path: value.artifact.path,
|
|
sha256: value.artifact.sha256,
|
|
format: metadataReportSubjectFormat(value),
|
|
...(architecture === undefined ? {} : { architecture }),
|
|
},
|
|
provider,
|
|
{
|
|
operation: OPERATION,
|
|
parameters,
|
|
result: value,
|
|
confidence: "observed",
|
|
limitations: value.limitations,
|
|
locations: [
|
|
{ kind: "artifact-path", path: value.artifact.path },
|
|
...(section === null
|
|
? []
|
|
: [
|
|
{
|
|
kind: "file-offset-range" as const,
|
|
start: section.file_offset,
|
|
end: section.file_offset + section.section_bytes,
|
|
},
|
|
]),
|
|
],
|
|
},
|
|
);
|
|
};
|