mirror of
https://github.com/morluto/rea.git
synced 2026-11-03 21:36:13 +00:00
* 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>
301 lines
9.7 KiB
TypeScript
301 lines
9.7 KiB
TypeScript
import { metadataReportSubjectFormat } from "../apple/machoTargetSelection.js";
|
|
import { z } from "zod";
|
|
import type { AnalysisError } from "../analysisErrorBase.js";
|
|
import {
|
|
goArchitectureForSubject,
|
|
goBinarySchema,
|
|
goBuildInfoSchema,
|
|
goModuleMetadataSchema,
|
|
goModuleSchema,
|
|
type GoBinary,
|
|
} from "../go/goBinary.js";
|
|
import { isImmutableJsonSnapshot } from "../immutableJson.js";
|
|
import { err, ok, type Result } from "../result.js";
|
|
import type {
|
|
AnalysisViewParent,
|
|
AnalysisViewRequest,
|
|
UnsignedAnalysisView,
|
|
} from "./analysisView.js";
|
|
import {
|
|
boundedViewMessages,
|
|
completeWithinViewCoverage,
|
|
pageViewCoverage,
|
|
} from "./analysisViewCoverage.js";
|
|
import { analysisViewInputError } from "./analysisViewErrors.js";
|
|
import { parseAnalysisViewResult } from "./analysisViewParent.js";
|
|
import { selectViewRows } from "./selectViewRows.js";
|
|
|
|
const indexSchema = z.number().int().nonnegative();
|
|
const count = z.number().int().nonnegative();
|
|
const settingSchema = goModuleMetadataSchema.shape.settings.element;
|
|
const page = {
|
|
kind: z.literal("go"),
|
|
indices: z.array(indexSchema).optional(),
|
|
offset: indexSchema.default(0),
|
|
limit: z.number().int().positive().default(64),
|
|
};
|
|
|
|
/** Exact filters intersect; empty arrays select none. Offsets count matching rows. */
|
|
export const goBinaryViewRequestSchema = z.discriminatedUnion("collection", [
|
|
z.strictObject({
|
|
...page,
|
|
collection: z.literal("dependencies"),
|
|
paths: z.array(z.string()).optional(),
|
|
fields: z.array(goModuleSchema.keyof()).optional(),
|
|
}),
|
|
z.strictObject({
|
|
...page,
|
|
collection: z.literal("settings"),
|
|
keys: z.array(z.string()).optional(),
|
|
fields: z.array(settingSchema.keyof()).optional(),
|
|
}),
|
|
]);
|
|
|
|
export const goBinaryViewPageSchema = z.discriminatedUnion("collection", [
|
|
z.strictObject({
|
|
collection: z.literal("dependencies"),
|
|
rows: z.array(goModuleSchema.partial().extend({ index: indexSchema })),
|
|
}),
|
|
z.strictObject({
|
|
collection: z.literal("settings"),
|
|
rows: z.array(settingSchema.partial().extend({ index: indexSchema })),
|
|
}),
|
|
]);
|
|
|
|
export const goBinaryViewContextSchema = z.strictObject({
|
|
source_coverage: z.strictObject({
|
|
build_info_available: z.boolean(),
|
|
module_parse_complete: z.boolean().nullable(),
|
|
runtime_dependencies: z.literal("unknown"),
|
|
}),
|
|
});
|
|
|
|
export const goBinarySummarySchema = z.strictObject({
|
|
bytes: count,
|
|
format: goBinarySchema.shape.format,
|
|
architecture: goBinarySchema.shape.architecture,
|
|
bits: goBinarySchema.shape.bits,
|
|
byte_order: goBinarySchema.shape.byte_order,
|
|
selected_slice: goBinarySchema.shape.selected_slice,
|
|
build_info: goBuildInfoSchema
|
|
.omit({
|
|
go_version_bytes_base64: true,
|
|
module_text: true,
|
|
module_bytes_base64: true,
|
|
module: true,
|
|
})
|
|
.extend({
|
|
module: goModuleMetadataSchema
|
|
.omit({
|
|
dependencies: true,
|
|
settings: true,
|
|
unparsed_lines: true,
|
|
unparsed_line_bytes_base64: true,
|
|
})
|
|
.extend({
|
|
available_counts: z.strictObject({
|
|
dependencies: count,
|
|
settings: count,
|
|
unparsed_lines: count,
|
|
unparsed_line_bytes_base64: count,
|
|
}),
|
|
}),
|
|
})
|
|
.nullable(),
|
|
});
|
|
|
|
// Reuse only authenticated immutable report facts, for their Evidence lifetime.
|
|
// Inline mutable reports are validated each time; this never caches live files.
|
|
const checkedReports = new WeakMap<object, GoBinary>();
|
|
const parseReport = (
|
|
parent: AnalysisViewParent,
|
|
): Result<GoBinary, AnalysisError> => {
|
|
const key = parent.normalizedResult;
|
|
const reusable =
|
|
parent.sealedResult === true &&
|
|
typeof key === "object" &&
|
|
key !== null &&
|
|
isImmutableJsonSnapshot(key);
|
|
const existing = reusable ? checkedReports.get(key) : undefined;
|
|
if (existing !== undefined) return ok(existing);
|
|
const parsed = parseAnalysisViewResult(goBinarySchema, parent);
|
|
if (!parsed.ok) return parsed;
|
|
// No schema transforms/coercions: keep the validated owner's frozen value.
|
|
const report = reusable ? (key as GoBinary) : parsed.value;
|
|
if (reusable) checkedReports.set(key, report);
|
|
return ok(report);
|
|
};
|
|
|
|
/** Query the existing metadata owner's facts; never reopen or reinterpret a file. */
|
|
export const projectGoBinaryView = (
|
|
parent: AnalysisViewParent,
|
|
view: AnalysisViewRequest,
|
|
): Result<UnsignedAnalysisView, AnalysisError> => {
|
|
if (view.kind !== "summary" && view.kind !== "go")
|
|
return err(
|
|
analysisViewInputError([
|
|
{
|
|
path: ["view"],
|
|
reason: "invalid_value",
|
|
message: "Go build Evidence supports summary and go views.",
|
|
},
|
|
]),
|
|
);
|
|
const parsed = parseReport(parent);
|
|
if (!parsed.ok) return parsed;
|
|
const report = parsed.value;
|
|
if (
|
|
parent.subject?.local_path !== report.artifact.path ||
|
|
parent.subject.digest.sha256 !== report.artifact.sha256 ||
|
|
parent.subject.format !== metadataReportSubjectFormat(report) ||
|
|
parent.subject.architecture !==
|
|
(goArchitectureForSubject(report.architecture) ?? null)
|
|
)
|
|
return err(
|
|
analysisViewInputError([
|
|
{
|
|
path: ["source", "subject"],
|
|
reason: "invalid_value",
|
|
message:
|
|
"Go parent subject must identify the report's exact artifact.",
|
|
},
|
|
]),
|
|
);
|
|
const info = report.build_info;
|
|
const runtimeUnknown =
|
|
"Embedded build records do not establish complete runtime dependencies or target behavior.";
|
|
const unknowns = [
|
|
runtimeUnknown,
|
|
...(info === null
|
|
? [
|
|
"Build information is unavailable; language and dependencies remain unknown.",
|
|
]
|
|
: [
|
|
...(info.module.complete
|
|
? []
|
|
: [
|
|
"Some module records were not parsed; consult the complete parent Evidence for original lines and bytes.",
|
|
]),
|
|
...(info.module.main === null
|
|
? [
|
|
"No main module was decoded; embedded dependency coverage remains unknown.",
|
|
]
|
|
: []),
|
|
]),
|
|
];
|
|
const shared = {
|
|
parent_evidence_id: parent.evidenceId,
|
|
parent_operation: "inspect_go_binary" as const,
|
|
parent_digest: parent.evidenceId.slice(3),
|
|
artifact: { path: report.artifact.path, sha256: report.artifact.sha256 },
|
|
source_coverage: {
|
|
build_info_available: info !== null,
|
|
module_parse_complete: info?.module.complete ?? null,
|
|
runtime_dependencies: "unknown" as const,
|
|
},
|
|
limitations: boundedViewMessages(
|
|
[...parent.limitations, ...report.limitations],
|
|
parent.evidenceId,
|
|
"limitations",
|
|
),
|
|
unknowns: boundedViewMessages(unknowns, parent.evidenceId, "unknowns", [
|
|
runtimeUnknown,
|
|
]),
|
|
};
|
|
if (view.kind === "summary")
|
|
return ok({
|
|
kind: "summary",
|
|
view,
|
|
...shared,
|
|
summary: {
|
|
bytes: report.artifact.bytes,
|
|
format: report.format,
|
|
architecture: report.architecture,
|
|
bits: report.bits,
|
|
byte_order: report.byte_order,
|
|
...(report.selected_slice === undefined
|
|
? {}
|
|
: { selected_slice: report.selected_slice }),
|
|
build_info:
|
|
info === null
|
|
? null
|
|
: {
|
|
header_offset: info.header_offset,
|
|
encoding: info.encoding,
|
|
go_version: info.go_version,
|
|
version_location: info.version_location,
|
|
module_location: info.module_location,
|
|
module: {
|
|
path: info.module.path,
|
|
main: info.module.main,
|
|
complete: info.module.complete,
|
|
available_counts: {
|
|
dependencies: info.module.dependencies.length,
|
|
settings: info.module.settings.length,
|
|
unparsed_lines: info.module.unparsed_lines.length,
|
|
unparsed_line_bytes_base64:
|
|
info.module.unparsed_line_bytes_base64.length,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
coverage: completeWithinViewCoverage(1, 1),
|
|
});
|
|
const indices =
|
|
view.indices === undefined ? undefined : new Set(view.indices);
|
|
const fields =
|
|
view.fields === undefined ? undefined : new Set<string>(view.fields);
|
|
if (view.collection === "dependencies") {
|
|
const paths = view.paths === undefined ? undefined : new Set(view.paths);
|
|
const selected = selectViewRows(
|
|
info?.module.dependencies ?? [],
|
|
view,
|
|
(row, index) =>
|
|
(indices?.has(index) ?? true) && (paths?.has(row.path) ?? true),
|
|
(row, index) => ({
|
|
index,
|
|
...((fields?.has("path") ?? true) && { path: row.path }),
|
|
...((fields?.has("version") ?? true) && { version: row.version }),
|
|
...((fields?.has("sum") ?? true) && { sum: row.sum }),
|
|
...((fields?.has("replacement") ?? true) && {
|
|
replacement: row.replacement,
|
|
}),
|
|
}),
|
|
);
|
|
return ok({
|
|
kind: "go",
|
|
view,
|
|
...shared,
|
|
page: { collection: "dependencies", rows: selected.rows },
|
|
coverage: pageViewCoverage(
|
|
view.offset,
|
|
selected.rows.length,
|
|
selected.total,
|
|
),
|
|
});
|
|
}
|
|
const keys = view.keys === undefined ? undefined : new Set(view.keys);
|
|
const selected = selectViewRows(
|
|
info?.module.settings ?? [],
|
|
view,
|
|
(row, index) =>
|
|
(indices?.has(index) ?? true) && (keys?.has(row.key) ?? true),
|
|
(row, index) => ({
|
|
index,
|
|
...((fields?.has("key") ?? true) && { key: row.key }),
|
|
...((fields?.has("value") ?? true) && { value: row.value }),
|
|
}),
|
|
);
|
|
return ok({
|
|
kind: "go",
|
|
view,
|
|
...shared,
|
|
page: { collection: "settings", rows: selected.rows },
|
|
coverage: pageViewCoverage(
|
|
view.offset,
|
|
selected.rows.length,
|
|
selected.total,
|
|
),
|
|
});
|
|
};
|