refactor(native): consolidate provider-neutral analysis contracts

Move shared function, dossier, inventory, reference and output semantics from the legacy Hopper module to native domain owners. Migrate provider, adapter, test and verifier consumers directly.
This commit is contained in:
morluto committed 2026-10-27 06:43:53 +08:00
1 parent a6488a96a9
commit 82fd223638
94 files changed
+1050 -985

No files matched your search

@@ -1,4 +1,4 @@
import { functionDossierSchema } from "../../../dist/domain/hopperValues.js";
import { functionDossierSchema } from "../../../dist/domain/native/functionDossier.js";
/** Validated synthetic native observations shared by managed verification lanes. */
export const functionDossier = (name) =>
+1 -1
View File
@@ -1,6 +1,6 @@
import type { z } from "zod";
import type { analysisStringSchema } from "../../src/domain/hopperValues.js";
import type { analysisStringSchema } from "../../src/domain/native/nativeString.js";
type VerifierCall = (
name: string,
+1 -1
View File
@@ -14,7 +14,7 @@ import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { parseEvidence } from "../dist/domain/evidence.js";
import { functionDossierSchema } from "../dist/domain/hopperValues.js";
import { functionDossierSchema } from "../dist/domain/native/functionDossier.js";
import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs";
import { buildDosComFixture } from "../tests/conformance/ghidra/dos-com-fixture.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
+1 -1
View File
@@ -14,7 +14,7 @@ import { ArtifactResourceScope } from "../dist/artifacts/ArtifactResourceScope.j
import { readGhidraLanguageCatalog } from "../dist/ghidra/GhidraLanguageCatalog.js";
import { parseEvidence } from "../dist/domain/evidence.js";
import { parseAnalysisSnapshot } from "../dist/domain/analysisSnapshot.js";
import { functionDossierSchema } from "../dist/domain/hopperValues.js";
import { functionDossierSchema } from "../dist/domain/native/functionDossier.js";
import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs";
import { buildGameCubeDolFixture } from "../tests/conformance/ghidra/dol-fixture.mjs";
import {
+2 -4
View File
@@ -21,10 +21,8 @@ import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { parseEvidence } from "../dist/domain/evidence.js";
import {
functionBodySchema,
functionDossierSchema,
} from "../dist/domain/hopperValues.js";
import { functionDossierSchema } from "../dist/domain/native/functionDossier.js";
import { functionBodySchema } from "../dist/domain/native/functionBody.js";
import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs";
import { buildDosMzFixture } from "../tests/conformance/ghidra/dos-mz-fixture.mjs";
import {
@@ -12,7 +12,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
import { parseEvidence } from "../dist/domain/evidence.js";
import { parseAnalysisSnapshot } from "../dist/domain/analysisSnapshot.js";
import { functionDossierSchema } from "../dist/domain/hopperValues.js";
import { functionDossierSchema } from "../dist/domain/native/functionDossier.js";
import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs";
import { buildMipsRelocatableFixture } from "../tests/conformance/ghidra/mips-fixture.mjs";
import {
+1 -1
View File
@@ -21,7 +21,7 @@ import {
} from "../tests/conformance/ghidra/mips-checks.mjs";
import { parseEvidence } from "../dist/domain/evidence.js";
import { parseAnalysisSnapshot } from "../dist/domain/analysisSnapshot.js";
import { functionDossierSchema } from "../dist/domain/hopperValues.js";
import { functionDossierSchema } from "../dist/domain/native/functionDossier.js";
import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs";
import { buildMipsFixture } from "../tests/conformance/ghidra/mips-fixture.mjs";
import {
+1 -1
View File
@@ -18,7 +18,7 @@ import {
} from "../tests/conformance/ghidra/ppc-checks.mjs";
import { parseEvidence } from "../dist/domain/evidence.js";
import { parseAnalysisSnapshot } from "../dist/domain/analysisSnapshot.js";
import { functionDossierSchema } from "../dist/domain/hopperValues.js";
import { functionDossierSchema } from "../dist/domain/native/functionDossier.js";
import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs";
import { buildPowerPcFixture } from "../tests/conformance/ghidra/ppc-fixture.mjs";
import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs";
+1 -1
View File
@@ -21,7 +21,7 @@ import addFormats from "ajv-formats";
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
import { parseAnalysisSnapshot } from "../dist/domain/analysisSnapshot.js";
import { parseEvidence } from "../dist/domain/evidence.js";
import { functionDossierSchema } from "../dist/domain/hopperValues.js";
import { functionDossierSchema } from "../dist/domain/native/functionDossier.js";
import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs";
import { buildRiscvFixture } from "../tests/conformance/ghidra/riscv-fixture.mjs";
import {
+1 -1
View File
@@ -60,7 +60,7 @@ const [
] = await Promise.all([
importBuilt("contracts/toolContracts.js"),
importBuilt("domain/evidence.js"),
importBuilt("domain/hopperValues.js"),
importBuilt("domain/native/functionDossier.js"),
importBuilt("ida/IdaConfiguration.js"),
importBuilt("ida/IdaMcpConnection.js"),
importBuilt("ida/IdaProtocolValues.js"),
+2 -4
View File
@@ -6,10 +6,8 @@ import type {
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { AnalysisOutputError } from "../domain/analysisErrorCore.js";
import type { JsonValue } from "../domain/jsonValue.js";
import {
analysisBookmarkSchema,
parseContainingProcedureResolution,
} from "../domain/hopperValues.js";
import { analysisBookmarkSchema } from "../domain/native/nativeInventory.js";
import { parseContainingProcedureResolution } from "../domain/native/nativeObservation.js";
import { resolveAnalysisDocument } from "./AnalysisDocument.js";
import { err, ok, type Result } from "../domain/result.js";
+1 -1
View File
@@ -9,7 +9,7 @@ import {
AnalysisOutputError,
} from "../domain/analysisErrorCore.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { parseDocuments } from "../domain/hopperValues.js";
import { parseDocuments } from "../domain/native/nativeDocument.js";
import { err, ok, type Result } from "../domain/result.js";
const executionOptions = (signal: AbortSignal | undefined) =>
+1 -1
View File
@@ -8,7 +8,7 @@ import type {
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { parseRelatedAddresses } from "../domain/hopperValues.js";
import { parseRelatedAddresses } from "../domain/native/nativeObservation.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { err, ok, type Result } from "../domain/result.js";
import type { EnhancedResult } from "./EnhancedToolTypes.js";
+1 -1
View File
@@ -10,7 +10,7 @@ import type { JsonValue } from "../domain/jsonValue.js";
import {
parseContainingProcedureResolution,
type ContainingProcedureResolution,
} from "../domain/hopperValues.js";
} from "../domain/native/nativeObservation.js";
import { err, ok, type Result } from "../domain/result.js";
import type { EnhancedResult, TraceReference } from "./EnhancedToolTypes.js";
+1 -1
View File
@@ -3,7 +3,7 @@ import type { z } from "zod";
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { JsonValue } from "../domain/jsonValue.js";
import type { ContainingProcedureResolution } from "../domain/hopperValues.js";
import type { ContainingProcedureResolution } from "../domain/native/nativeObservation.js";
import { err, type Result } from "../domain/result.js";
import {
enhancedInputSchemas,
+6 -4
View File
@@ -21,13 +21,16 @@ import {
} from "../domain/analysisErrorCore.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { parseFunctionDossier } from "../domain/native/functionDossier.js";
import {
parseFunctionDossier,
parseListCount,
parseRelatedAddresses,
addressedValueSchema,
} from "../domain/native/nativeInventory.js";
import { parseRelatedAddresses } from "../domain/native/nativeObservation.js";
import {
parseSegments,
projectBinaryOverview,
} from "../domain/hopperValues.js";
} from "../domain/native/nativeSegments.js";
import { err, ok, type Result } from "../domain/result.js";
import {
categorizeSwiftTypes,
@@ -40,7 +43,6 @@ import { traceNativeValues } from "./native/NativeValueTrace.js";
import { traceNativeUiAction } from "./native/NativeUiActionTrace.js";
import { decompileAll } from "./decompileDirectory/decompileAll.js";
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
import { addressedValueSchema } from "../domain/native/nativeInventory.js";
import {
nativeDispatchMetadataResultSchema,
MAX_DISPATCH_METADATA_RECORDS,
+1 -1
View File
@@ -6,7 +6,7 @@ import type {
import { nativeProcedureInputSchema } from "../domain/native/nativeProcedureInput.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import { createAnalysisExecutionEvidence } from "../domain/analysisObservation.js";
import { parseFunctionDossier } from "../domain/hopperValues.js";
import { parseFunctionDossier } from "../domain/native/functionDossier.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { completeProviderEvidence } from "./ProviderOperationExecution.js";
import type { Evidence } from "../domain/evidence.js";
+1 -1
View File
@@ -16,7 +16,7 @@ import {
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { nativeApiBoundarySchema } from "../domain/native/nativeApiBoundary.js";
import { projectNativeApiInspection } from "./native/NativeApiInspection.js";
import { functionDossierSchema } from "../domain/hopperValues.js";
import { functionDossierSchema } from "../domain/native/functionDossier.js";
import { ghidraFunctionDossier } from "../domain/ghidraValues.fixture.js";
import { ANALYSIS_SNAPSHOT_PROFILE } from "../domain/analysisSnapshot.fixture.js";
import { createEvidence } from "../domain/evidence.js";
@@ -20,7 +20,7 @@ import {
rebindEvidenceToTargetPath,
} from "../../domain/evidence.js";
import { inspectAnalysisViewInputSchema } from "../../domain/analysisView/analysisView.js";
import { functionDossierSchema } from "../../domain/hopperValues.js";
import { functionDossierSchema } from "../../domain/native/functionDossier.js";
import { ghidraFunctionDossier } from "../../domain/ghidraValues.fixture.js";
import {
inspectAnalysisView,
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { functionDossierSchema } from "../../domain/hopperValues.js";
import { functionDossierSchema } from "../../domain/native/functionDossier.js";
import { ghidraFunctionDossier } from "../../domain/ghidraValues.fixture.js";
import { nativeApiBoundarySchema } from "../../domain/native/nativeApiBoundary.js";
import { projectNativeApiInspection } from "./NativeApiInspection.js";
@@ -1,4 +1,4 @@
import type { FunctionDossier } from "../../domain/hopperValues.js";
import type { FunctionDossier } from "../../domain/native/functionDossier.js";
import {
nativeApiInspectionResultSchema,
type NativeApiBoundary,
+2 -4
View File
@@ -4,10 +4,8 @@ import type {
ExecutionOptions,
ProviderIdentity,
} from "../AnalysisProvider.js";
import {
parseRelatedAddresses,
referenceKindSchema,
} from "../../domain/hopperValues.js";
import { parseRelatedAddresses } from "../../domain/native/nativeObservation.js";
import { referenceKindSchema } from "../../domain/native/nativeReference.js";
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { err, ok, type Result } from "../../domain/result.js";
@@ -5,7 +5,7 @@ import { nativeFunctionMatchingSchema } from "../../domain/native/functionMatchi
import { createEvidence, immutableEvidence } from "../../domain/evidence.js";
import { createAnalysisProfile } from "../../domain/analysisProfile.js";
import { AnalysisResourceConstraintError } from "../../domain/analysisErrorCore.js";
import { functionDossierSchema } from "../../domain/hopperValues.js";
import { functionDossierSchema } from "../../domain/native/functionDossier.js";
import { procedureInfoSchema } from "../../domain/native/procedureInfo.js";
import {
nativeMatchingNamedInventory as nativeMatchingInventory,
@@ -5,7 +5,7 @@ import {
type AnalysisOperationPort,
} from "../AnalysisProvider.js";
import { ghidraFunctionDossier } from "../../domain/ghidraValues.fixture.js";
import { functionDossierSchema } from "../../domain/hopperValues.js";
import { functionDossierSchema } from "../../domain/native/functionDossier.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
import { nativeValueTraceSchema } from "../../domain/native/nativeValueTrace.js";
import {
+1 -1
View File
@@ -1,4 +1,4 @@
import { functionDossierSchema } from "../../domain/hopperValues.js";
import { functionDossierSchema } from "../../domain/native/functionDossier.js";
import { nativeCallTargetsSchema } from "../../domain/native/nativeInstruction.js";
import {
nativeValueTraceInputSchema,
+6 -4
View File
@@ -1,6 +1,7 @@
import {
nativeAddressListSchema,
nativeObservationSourceSchemas,
containingProcedureResolutionSchema,
} from "../domain/native/nativeObservation.js";
import {
nativeObservationSummaryViewResultSchemas,
@@ -22,13 +23,14 @@ import { nativeByteReadSchema } from "../domain/native/nativeByteRead.js";
import { analysisDetailOutputSchemaOf } from "./analysisDetail.js";
import {
functionDossierSchema,
analysisBookmarkSchema,
functionInstructionWindowSchema,
} from "../domain/native/functionDossier.js";
import { analysisBookmarkSchema } from "../domain/native/nativeInventory.js";
import {
referenceEdgeSchema,
unresolvedCallSchema,
procedureIdentitySchema,
containingProcedureResolutionSchema,
} from "../domain/hopperValues.js";
} from "../domain/native/nativeReference.js";
import { procedureIdentitySchema } from "../domain/native/procedureInfo.js";
import { nativeInventoryRowsSchemas } from "../domain/native/nativeInventory.js";
import { z } from "zod";
import { jsonRecordSchema } from "../domain/jsonRecord.js";
+1 -1
View File
@@ -2,7 +2,7 @@ import { procedureInfoSchema } from "../domain/native/procedureInfo.js";
import { z } from "zod";
import { utcDateTimeSchema } from "../domain/offsetDateTime.js";
import { functionDossierSchema } from "../domain/hopperValues.js";
import { functionDossierSchema } from "../domain/native/functionDossier.js";
import { analysisProfileSchema } from "../domain/analysisProfile.js";
import {
evidenceSchema,
+1 -1
View File
@@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest";
import { z } from "zod";
import { packetFieldsSchema } from "../domain/captures/packetCapture.js";
import { swiftStringLiteralSchema } from "../domain/hopperValues.js";
import { swiftStringLiteralSchema } from "../domain/native/swiftStringLiteral.js";
import { applicationVersionComparisonResultSchema } from "../domain/javascript/javascriptApplicationVersionComparisonSchemas.js";
import { kernelModuleSchema } from "../domain/native/kernelModule.js";
import { pinvokeVerificationSchema } from "../domain/managed/managedNativeVerificationSchemas.js";
+1 -1
View File
@@ -5,7 +5,7 @@ import {
} from "./analysisViewCoverage.js";
import { expect, it } from "vitest";
import { functionDossierSchema } from "../hopperValues.js";
import { functionDossierSchema } from "../native/functionDossier.js";
import { ghidraFunctionDossier } from "../ghidraValues.fixture.js";
import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "../../contracts/javascript/javascriptRuntimeReconciliationExample.js";
import { ARTIFACT_COMPARISON_EXAMPLE } from "../../contracts/artifactComparisonExample.js";
+1 -1
View File
@@ -195,7 +195,7 @@ import { bundleResourceRootSchema } from "../apple/bundleResourceRoot.js";
import {
functionDossierSchema,
functionFacetAvailabilitySchema,
} from "../hopperValues.js";
} from "../native/functionDossier.js";
import {
artifactGraphManifestSchema,
artifactInventoryCoverageSchema,
@@ -2,7 +2,7 @@ import { analysisViewInputError } from "./analysisViewErrors.js";
import { analysisTextPage } from "./analysisViewText.js";
import type { AnalysisError } from "../analysisErrorBase.js";
import { uniqueSorted } from "../canonicalOrdering.js";
import type { FunctionDossier } from "../hopperValues.js";
import type { FunctionDossier } from "../native/functionDossier.js";
import { jsonValueSchema } from "../jsonValue.js";
import { err, ok, type Result } from "../result.js";
import type {
@@ -1,5 +1,5 @@
import { z } from "zod";
import { analysisStringSchema } from "../hopperValues.js";
import { analysisStringSchema } from "../native/nativeString.js";
import { jsonValueSchema } from "../jsonValue.js";
import {
addressedValueSchema,
@@ -1,8 +1,10 @@
import { NATIVE_OBSERVATION_OPERATIONS } from "../native/nativeObservation.js";
import {
NATIVE_OBSERVATION_OPERATIONS,
containingProcedureResolutionSchema,
} from "../native/nativeObservation.js";
import { z } from "zod";
import { digestSchema } from "../digests.js";
import { evidenceSchema } from "../evidence.js";
import { containingProcedureResolutionSchema } from "../hopperValues.js";
import {
completeRelationshipCoverageSchema,
partialRelationshipCoverageSchema,
+1 -1
View File
@@ -2,7 +2,7 @@ import {
relationshipCoverageLimitations,
type RelationshipCoverage,
} from "./relationshipCoverage.js";
import type { FunctionDossier } from "./hopperValues.js";
import type { FunctionDossier } from "./native/functionDossier.js";
import { uniqueSorted } from "./canonicalOrdering.js";
const callerCoveragePrefix = "Callers: ";
+1 -1
View File
@@ -7,7 +7,7 @@ import { functionComparisonResultSchema } from "./functionComparisonSchemas.js";
import { createFunctionCoordinateContext } from "./native/functionCoordinateContext.js";
import { createAnalysisProfile } from "./analysisProfile.js";
import { createEvidence, type Evidence } from "./evidence.js";
import { functionDossierSchema } from "./hopperValues.js";
import { functionDossierSchema } from "./native/functionDossier.js";
import { canonicalJson } from "./comparisonSemantics.js";
import { digestCanonicalValue } from "./canonicalDigest.js";
import { compareUnicodeCodePoints } from "./unicodeCodePointOrder.js";
+4 -1
View File
@@ -5,7 +5,10 @@ import { readEvidence, readEvidencePayload } from "./evidenceValidation.js";
import type { JsonValue } from "./jsonValue.js";
import { err, ok, type Result } from "./result.js";
import { compareUnicodeCodePoints } from "./unicodeCodePointOrder.js";
import { functionDossierSchema, type FunctionDossier } from "./hopperValues.js";
import {
functionDossierSchema,
type FunctionDossier,
} from "./native/functionDossier.js";
import { assessFunctionCallCoverage } from "./functionCallCoverage.js";
/** Complete function observation parsed from one analyze_function Evidence record. */
-264
View File
@@ -1,264 +0,0 @@
import { describe, expect, it } from "vitest";
import {
functionBodySchema,
addressDistance,
procedureIdentitySchema,
parseDocuments,
parseListCount,
parseRelatedAddresses,
parseSegments,
} from "./hopperValues.js";
describe("Hopper boundary values", () => {
it("distinguishes measured zero, malformed, incomparable, reversed, and oversized extents", () => {
expect(addressDistance("not-an-address", "0x2000")).toBeNull();
expect(addressDistance("ram:0x1000", "rom:0x2000")).toBeNull();
expect(addressDistance("0x2000", "0x1000")).toBeNull();
expect(addressDistance("0x1000", "0x1000")).toBe(0);
expect(addressDistance("0x1000", "0x2000")).toBe(4096);
expect(addressDistance("0x0", "0x20000000000000")).toBeNull();
});
it("counts Hopper's address-keyed string maps", () => {
expect(
parseListCount({ "0x1000": "hello", "0x2000": "world" }, "strings"),
).toEqual({ ok: true, value: 2 });
});
it("parses direct and wrapped protocol shapes", () => {
expect(parseRelatedAddresses({ callers: ["0x3"] }, "callers")).toEqual({
ok: true,
value: ["0x3"],
});
expect(
parseSegments([
{
name: "__TEXT",
start: "0x1",
end: "0x2",
readable: true,
writable: false,
executable: null,
},
]),
).toEqual({
ok: true,
value: [
{
name: "__TEXT",
start: "0x1",
end: "0x2",
readable: true,
writable: false,
executable: null,
},
],
});
expect(parseDocuments(["fixture"])).toEqual({
ok: true,
value: ["fixture"],
});
});
it.each([
["relations", () => parseRelatedAddresses([1], "callees")],
["segments", () => parseSegments([{ name: 1 }])],
["documents", () => parseDocuments([1])],
])(
"rejects malformed %s instead of manufacturing empty output",
(_label, parse) => {
const result = parse();
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error._tag).toBe("AnalysisOutputError");
},
);
it("uses provider-neutral wording for malformed shared values", () => {
const result = parseSegments([{ name: 1 }]);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error._tag).toBe("AnalysisOutputError");
expect(result.error.operation).toBe("list_segments");
expect(result.error.reason).toBe(
"Provider returned an invalid segment list",
);
}
});
});
describe("provider-neutral inclusive function body evidence", () => {
const observed = () => ({
available: true,
provenance: "test-address-set",
ranges: [
{ start: "0x1000", end: "0x1002" },
{ start: "0x1010", end: "0x1011" },
],
total_bytes: 5,
span_bytes: 18,
non_contiguous: true,
contains_entry: true,
});
it("rejects missing provider body metadata", () => {
expect(
procedureIdentitySchema.safeParse({ address: "0x1000", name: "main" })
.success,
).toBe(false);
});
it("rejects segments when required coordinates or unknown permissions are omitted", () => {
expect(
parseSegments([
{
name: "__TEXT",
start: "0x1",
end: "0x2",
readable: null,
writable: null,
},
]).ok,
).toBe(false);
expect(
parseSegments([{ name: "__TEXT", start: "0x1", end: "0x2" }]).ok,
).toBe(false);
});
it("counts inclusive one-byte ranges without assuming an enclosing continuous body", () => {
expect(
functionBodySchema.safeParse({
...observed(),
ranges: [{ start: "0x1000", end: "0x1000" }],
total_bytes: 1,
span_bytes: 1,
non_contiguous: false,
}).success,
).toBe(true);
expect(functionBodySchema.parse(observed())).toMatchObject({
total_bytes: 5,
span_bytes: 18,
});
});
it("preserves address-space identity and leaves a multi-space span unknown", () => {
expect(
functionBodySchema.safeParse({
...observed(),
ranges: [
{ start: "a:0x1000", end: "a:0x1000" },
{ start: "b:0x1000", end: "b:0x1001" },
],
total_bytes: 3,
span_bytes: null,
}).success,
).toBe(true);
expect(
functionBodySchema.safeParse({
...observed(),
ranges: [{ start: "a:0x1000", end: "b:0x1000" }],
}).success,
).toBe(false);
});
it("allows an observed empty set without manufacturing entry membership", () => {
expect(
procedureIdentitySchema.safeParse({
address: "0x1000",
name: "external",
classification: null,
body: {
...observed(),
ranges: [],
total_bytes: 0,
span_bytes: 0,
non_contiguous: false,
contains_entry: false,
},
}).success,
).toBe(true);
});
it("checks entry membership using address space and inclusive endpoints", () => {
expect(
procedureIdentitySchema.safeParse({
address: "0x1002",
name: "last",
classification: null,
body: observed(),
}).success,
).toBe(true);
expect(
procedureIdentitySchema.safeParse({
address: "0x1003",
name: "gap",
classification: null,
body: observed(),
}).success,
).toBe(false);
expect(
procedureIdentitySchema.safeParse({
address: "other:0x1000",
name: "other",
classification: null,
body: observed(),
}).success,
).toBe(false);
});
it.each([
{ total_bytes: 18 },
{ span_bytes: 5 },
{ non_contiguous: false },
{
ranges: [
{ start: "0x1000", end: "0x1002" },
{ start: "0x1003", end: "0x1004" },
],
},
{
ranges: [
{ start: "0x1000", end: "0x1002" },
{ start: "0x1002", end: "0x1004" },
],
},
{
ranges: [
{ start: "0x1010", end: "0x1011" },
{ start: "0x1000", end: "0x1002" },
],
},
{
ranges: [{ start: "0x0", end: "0x20000000000000" }],
total_bytes: Number.MAX_SAFE_INTEGER,
span_bytes: Number.MAX_SAFE_INTEGER,
non_contiguous: false,
},
])("rejects contradictory or unsafe range facts %j", (change) => {
expect(
functionBodySchema.safeParse({ ...observed(), ...change }).success,
).toBe(false);
});
});
it.each(["procedures", "strings"])(
"counts every own entry of direct and wrapped %s maps",
(property) => {
for (const text of [
'{"__proto__":"observed"}',
'{"__proto__":"observed","0x1000":"one"}',
]) {
const source = JSON.parse(text);
const count = Object.keys(source).length;
expect(parseListCount(source, property)).toEqual({
ok: true,
value: count,
});
expect(parseListCount({ [property]: source }, property)).toEqual({
ok: true,
value: count,
});
expect(JSON.stringify(source)).toBe(text);
}
expect(parseListCount({ [property]: [] }, property)).toEqual({
ok: true,
value: 0,
});
expect(parseListCount({}, property)).toEqual({ ok: true, value: 0 });
for (const value of [null, "not a list", 3, false])
expect(parseListCount(value, property).ok).toBe(false);
},
);
-616
View File
@@ -1,616 +0,0 @@
import { nativeSymbolMetadataSchema } from "./native/nativeSymbol.js";
import { z } from "zod";
import { fixedTupleSchema } from "./fixedTupleSchema.js";
import { addressCoordinate } from "./native/addressCoordinate.js";
import { jsonObjectSchema, type JsonValue } from "./jsonValue.js";
import { err, ok, type Result } from "./result.js";
import { nativeApiBoundarySchema } from "./native/nativeApiBoundary.js";
import { nativeValueFlowSchema } from "./native/nativeValueFlow.js";
import { nativeCallClassificationSchema } from "./native/nativeCallReference.js";
import { AnalysisOutputError } from "./analysisErrorCore.js";
export interface AddressedName {
readonly address: string;
readonly name: string;
readonly objc_class_label?: "hopper_legacy_class_object";
}
export interface SegmentSummary {
readonly name: string;
readonly start: string;
readonly end: string;
readonly readable: boolean | null;
readonly writable: boolean | null;
readonly executable: boolean | null;
readonly image_base?: string;
readonly address_space?: string;
}
/** Return a bounded byte distance between canonical addresses in one space. */
export const addressDistance = (start: string, end: string): number | null => {
const left = addressCoordinate(start);
const right = addressCoordinate(end);
if (left === null || right === null || left.space !== right.space)
return null;
const distance = right.offset - left.offset;
if (distance < 0n || distance > BigInt(Number.MAX_SAFE_INTEGER)) return null;
return Number(distance);
};
const segmentSchema = z.object({
name: z.string(),
start: z.string(),
end: z.string(),
readable: z.boolean().nullable(),
writable: z.boolean().nullable(),
executable: z.boolean().nullable(),
image_base: z.string().min(1).exactOptional(),
address_space: z.string().min(1).exactOptional(),
});
const unavailableAnalysisFactSchema = z
.object({ available: z.literal(false), reason: z.string() })
.strict();
/** Typed string evidence shared by inventories and referenced function literals. */
export const analysisStringSchema = z.object({
address: z.string(),
value: z.string(),
provider_value: z
.string()
.exactOptional()
.describe(
"Original provider display text, which may shorten the typed object.",
),
decoding: unavailableAnalysisFactSchema
.exactOptional()
.describe(
"When present, value retains native display text because typed bytes could not be decoded.",
),
string: z
.object({
encoding: z.string().min(1),
encoding_status: z
.enum(["observed", "inferred"])
.exactOptional()
.describe(
"Whether the provider declares the encoding or REA infers it from the typed bytes and native display. This does not establish the original source's intended encoding.",
),
termination: z.enum(["missing", "present_or_not_required"]),
byte_length: z.int().min(0),
})
.exactOptional(),
});
/** An analyzed bookmark retains a missing provider label as null. */
export const analysisBookmarkSchema = z.strictObject({
address: z.string(),
name: z.string().nullable(),
});
/** Complete observed function-body ranges; every range endpoint is inclusive. */
export const availableFunctionBodySchema = z
.strictObject({
available: z.literal(true),
provenance: z.string().min(1),
ranges: z.array(
z.strictObject({
start: z.string(),
end: z
.string()
.describe("Inclusive last address of this observed body range."),
}),
),
total_bytes: z.int().min(0).max(Number.MAX_SAFE_INTEGER),
span_bytes: z.int().min(0).max(Number.MAX_SAFE_INTEGER).nullable(),
non_contiguous: z.boolean(),
contains_entry: z.boolean(),
})
.superRefine((body, context) => {
let total = 0n;
const bounds = new Map<string | null, { start: bigint; end: bigint }>();
const closedSpaces = new Set<string | null>();
let previousSpace: string | null | undefined;
for (const range of body.ranges) {
const start = addressCoordinate(range.start);
const end = addressCoordinate(range.end);
if (
start === null ||
end === null ||
start.space !== end.space ||
end.offset < start.offset
) {
context.addIssue({
code: "custom",
message: "Function body range has invalid inclusive endpoints",
});
return;
}
if (previousSpace !== undefined && previousSpace !== start.space)
closedSpaces.add(previousSpace);
const previous = bounds.get(start.space);
if (
closedSpaces.has(start.space) ||
(previous !== undefined && start.offset <= previous.end + 1n)
) {
context.addIssue({
code: "custom",
message:
"Function body ranges must be ordered, disjoint and maximal within each address space",
});
return;
}
bounds.set(start.space, {
start: previous?.start ?? start.offset,
end: end.offset,
});
previousSpace = start.space;
total += end.offset - start.offset + 1n;
}
const onlyBounds = bounds.size === 1 ? [...bounds.values()][0] : undefined;
const span =
bounds.size === 0
? 0n
: onlyBounds === undefined
? null
: onlyBounds.end - onlyBounds.start + 1n;
const reportedSpan =
body.span_bytes === null ? null : BigInt(body.span_bytes);
if (
total !== BigInt(body.total_bytes) ||
span !== reportedSpan ||
body.non_contiguous !== body.ranges.length > 1
)
context.addIssue({
code: "custom",
message:
"Function body counts, enclosing span or contiguity disagree with inclusive ranges",
});
});
export const functionBodySchema = z.discriminatedUnion("available", [
unavailableAnalysisFactSchema,
availableFunctionBodySchema,
]);
/** Check the declared entry membership against complete, inclusive body ranges. */
export const functionBodyEntryAgrees = (
body: z.infer<typeof functionBodySchema>,
address: string,
): boolean => {
if (!body.available) return true;
const entry = addressCoordinate(address);
if (entry === null) return false;
const contains = body.ranges.some((range) => {
const start = addressCoordinate(range.start);
const end = addressCoordinate(range.end);
return (
start !== null &&
end !== null &&
start.space === entry.space &&
entry.offset >= start.offset &&
entry.offset <= end.offset
);
});
return contains === body.contains_entry;
};
export const procedureClassificationSchema = z
.object({
external: z.boolean(),
thunk: z.boolean(),
thunk_target: z.string().nullable(),
provenance: z.string().min(1),
})
.strict();
export const procedureIdentitySchema = z
.object({
address: z.string(),
name: z.string(),
symbol: nativeSymbolMetadataSchema.nullable().exactOptional(),
classification: procedureClassificationSchema.nullable(),
body: functionBodySchema,
})
.strict()
.superRefine((identity, context) => {
if (!functionBodyEntryAgrees(identity.body, identity.address))
context.addIssue({
code: "custom",
message:
"Function body entry membership disagrees with procedure identity",
});
});
/** Exact containing-procedure outcome; valid absence is distinct from unreadable output. */
export const containingProcedureResolutionSchema = z.discriminatedUnion(
"found",
[
z.object({
query_address: z.string(),
found: z.literal(true),
procedure: procedureIdentitySchema,
}),
z.object({
query_address: z.string(),
found: z.literal(false),
procedure: z.null(),
reason: z.enum(["outside_segments", "not_in_procedure"]),
}),
],
);
export type ContainingProcedureResolution = z.infer<
typeof containingProcedureResolutionSchema
>;
/** Validate provider procedure facts before a workflow interprets absence. */
export const parseContainingProcedureResolution = (
value: JsonValue,
): Result<ContainingProcedureResolution, AnalysisOutputError> => {
const parsed = containingProcedureResolutionSchema.safeParse(value);
return parsed.success
? ok(parsed.data)
: err(
new AnalysisOutputError(
"resolve_containing_procedure",
"expected a containing-procedure resolution",
{ cause: parsed.error },
),
);
};
export const localVariableSchema = z
.object({
description: z.string(),
provenance: z.string().min(1),
name: z.string().nullable().exactOptional(),
stack_displacement: z
.string()
.regex(/^-?\d+$/u)
.nullable()
.exactOptional()
.describe(
"Observed signed stack byte displacement, encoded as decimal to preserve precision.",
),
})
.strict();
/** Provider-neutral complete raw-instruction list for one analyzed function. */
export const functionInstructionWindowSchema = z
.object({
procedure: procedureIdentitySchema,
instructions: z.array(z.string()),
limitations: z.array(z.string()),
})
.strict();
export const availableReferenceKindSchema = z
.object({
available: z.literal(true),
provenance: z.string().min(1),
type: z.string(),
flow: z.boolean(),
call: z.boolean(),
jump: z.boolean(),
data: z.boolean(),
read: z.boolean(),
write: z.boolean(),
indirect: z.boolean(),
computed: z.boolean(),
conditional: z.boolean(),
terminal: z.boolean(),
primary: z.boolean(),
operand_index: z.int(),
external: z.boolean(),
})
.strict();
export const referenceKindSchema = z.discriminatedUnion("available", [
unavailableAnalysisFactSchema,
availableReferenceKindSchema,
]);
/** Reported call sites without resolved targets; unreported dispatch stays unknown. */
export const unresolvedCallSchema = z.strictObject({
address: z.string(),
reason: z.string(),
});
const swiftStringLiteralFields = {
source_address: z
.string()
.describe(
"Instruction that consumes both words: a call, tail call, return, or paired store.",
),
value: z
.string()
.describe("Decoded UTF-8 text, including the canonical empty string."),
registers: fixedTupleSchema([z.string(), z.string()]),
words: fixedTupleSchema([z.string(), z.string()]).describe(
"Observed constant register values, in register order.",
),
};
/** A Swift String literal decoded from the two words of its ABI value, not a data reference. */
export const swiftSmallStringLiteralSchema = z.strictObject({
...swiftStringLiteralFields,
representation: z
.literal("small")
.describe("The UTF-8 bytes are inside the two words."),
address: z.null().describe("A small literal has no storage of its own."),
});
export const swiftLargeStringLiteralSchema = z.strictObject({
...swiftStringLiteralFields,
representation: z
.literal("large")
.describe(
"An immortal literal whose words hold its count and a pointer to its bytes.",
),
address: z.string().describe("Address of a large literal's bytes."),
});
export const swiftStringLiteralSchema = z.discriminatedUnion("representation", [
swiftSmallStringLiteralSchema,
swiftLargeStringLiteralSchema,
]);
const swiftStringLiteralOmissionFields = {
source_address: swiftStringLiteralFields.source_address,
byte_length: z.int().min(0).max(Number.MAX_SAFE_INTEGER),
reason: z
.enum(["capacity", "unresolved_jump"])
.describe(
"Why this candidate is omitted: capacity stopped decoding, or an unresolved jump left its use unknown.",
),
registers: swiftStringLiteralFields.registers,
words: swiftStringLiteralFields.words,
};
/** A Swift literal candidate omitted because decoding capacity or control flow left it unknown. */
export const swiftSmallStringLiteralOmissionSchema = z.strictObject({
...swiftStringLiteralOmissionFields,
representation: z.literal("small"),
address: z.null(),
});
export const swiftLargeStringLiteralOmissionSchema = z.strictObject({
...swiftStringLiteralOmissionFields,
representation: z.literal("large"),
address: z.string().describe("Address of the literal's bytes."),
});
export const swiftStringLiteralOmissionSchema = z.discriminatedUnion(
"representation",
[
swiftSmallStringLiteralOmissionSchema,
swiftLargeStringLiteralOmissionSchema,
],
);
/** Observed endpoints with complete or partial reference classification evidence. */
export const referenceEdgeSchema = z
.object({
source_address: z.string(),
target_address: z.string(),
source_procedure: procedureIdentitySchema.nullable(),
target_procedure: procedureIdentitySchema.nullable(),
kind: referenceKindSchema,
call: nativeCallClassificationSchema.exactOptional(),
})
.strict();
const facetAvailabilitySchema = z.discriminatedUnion("available", [
z.object({ available: z.literal(true) }).strict(),
z.object({ available: z.literal(false), reason: z.string().min(1) }).strict(),
]);
/** Provider-declared availability per dossier facet; an omitted facet declares nothing. */
export const functionFacetAvailabilitySchema = z
.object({
pseudocode: facetAvailabilitySchema.exactOptional(),
assembly: facetAvailabilitySchema.exactOptional(),
comments: facetAvailabilitySchema.exactOptional(),
callers: facetAvailabilitySchema.exactOptional(),
callees: facetAvailabilitySchema.exactOptional(),
references: facetAvailabilitySchema.exactOptional(),
strings_names: facetAvailabilitySchema.exactOptional(),
cfg: facetAvailabilitySchema.exactOptional(),
})
.strict();
export const functionDossierSchema = z
.object({
procedure: procedureIdentitySchema.safeExtend({
signature: z.string().nullable(),
locals: z.array(localVariableSchema),
}),
pseudocode: z.string(),
assembly: z.array(z.string()),
comments: z.array(
z
.object({
address: z.string(),
kind: z.enum(["comment", "inline"]),
text: z.string(),
})
.strict(),
),
callers: z.array(procedureIdentitySchema),
callees: z.array(procedureIdentitySchema),
incoming_references: z.array(referenceEdgeSchema),
outgoing_references: z.array(referenceEdgeSchema),
unresolved_calls: z.array(unresolvedCallSchema).exactOptional(),
referenced_strings: z.array(
analysisStringSchema.extend({ source_address: z.string() }),
),
referenced_names: z.array(
z
.object({
address: z.string(),
value: z.string(),
source_address: z.string(),
})
.strict(),
),
swift_string_literals: z
.array(swiftStringLiteralSchema)
.exactOptional()
.describe(
"Swift String literals decoded from constant register values. Absent when the provider does not decode them for this target.",
),
swift_string_literal_omissions: z
.array(swiftStringLiteralOmissionSchema)
.exactOptional()
.describe(
"Literal candidates omitted for capacity or unresolved control flow. Present exactly when swift_string_literals is present; empty when all examined use sites were classified.",
),
basic_blocks: z.array(
z
.object({
start: z.string(),
end: z.string(),
successors: z.array(z.string()),
})
.strict(),
),
native_api: nativeApiBoundarySchema.nullable(),
native_value_flow: nativeValueFlowSchema.nullable(),
facet_availability: functionFacetAvailabilitySchema.exactOptional(),
limitations: z.array(z.string()),
})
.strict();
/** Strict analyzed-function dossier shared by provider and comparison boundaries. */
export type FunctionDossier = z.infer<typeof functionDossierSchema>;
/** Strictly parse a complete provider-neutral function dossier. */
export const parseFunctionDossier = (
value: JsonValue,
): Result<FunctionDossier, AnalysisOutputError> => {
const parsed = functionDossierSchema.safeParse(value);
return parsed.success
? ok(parsed.data)
: err(
new AnalysisOutputError(
"analyze_function",
"provider output did not match the dossier contract",
{ cause: parsed.error },
),
);
};
/** Parse callee/caller strings from direct or wrapped provider results. */
export const parseRelatedAddresses = (
value: JsonValue,
relation: "callees" | "callers",
operation = relation === "callees"
? "procedure_callees"
: "procedure_callers",
): Result<readonly string[], AnalysisOutputError> => {
const parsed = z.array(z.string()).safeParse(unwrapProperty(value, relation));
return parsed.success
? ok(parsed.data)
: invalid(operation, `${relation} list`, parsed.error);
};
/** Preserve reported address context in direct or wrapped provider segments. */
export const parseSegments = (
value: JsonValue,
operation = "list_segments",
): Result<readonly SegmentSummary[], AnalysisOutputError> => {
const parsed = z
.array(segmentSchema)
.safeParse(unwrapProperty(value, "segments"));
return parsed.success
? ok(parsed.data)
: invalid(operation, "segment list", parsed.error);
};
/** Return a reported overview base only when every parsed segment agrees. */
export const parseSegmentImageBase = (
segments: readonly SegmentSummary[],
): string | undefined => {
const base = segments[0]?.image_base;
return base !== undefined &&
segments.every((segment) => segment.image_base === base)
? base
: undefined;
};
/** Project the same overview from complete lists or provider-owned aggregates. */
export const projectBinaryOverview = (input: {
readonly document: string;
readonly procedure_count: number;
readonly string_count: number;
readonly segments?: readonly SegmentSummary[];
}): JsonValue => {
const { document, procedure_count, string_count, segments } = input;
const imageBase =
segments === undefined ? undefined : parseSegmentImageBase(segments);
return {
document,
...(imageBase === undefined ? {} : { image_base: imageBase }),
...(segments === undefined
? {}
: {
segments: segments.map(
({ name, start, end, image_base, address_space }) => ({
name,
start,
end,
length: addressDistance(start, end),
...(image_base === undefined ? {} : { image_base }),
...(address_space === undefined ? {} : { address_space }),
}),
),
segment_count: segments.length,
}),
procedure_count,
string_count,
};
};
/** Parse direct or wrapped provider document names. */
export const parseDocuments = (
value: JsonValue,
operation = "list_documents",
): Result<readonly string[], AnalysisOutputError> => {
const parsed = z
.array(z.string())
.safeParse(unwrapProperty(value, "documents"));
return parsed.success
? ok(parsed.data)
: invalid(operation, "document list", parsed.error);
};
/** Parse a direct or wrapped list when only its cardinality is required. */
export const parseListCount = (
value: JsonValue,
property: string,
operation = property === "procedures" ? "list_procedures" : "list_strings",
): Result<number, AnalysisOutputError> => {
const unwrapped = unwrapProperty(value, property);
const list = z.array(z.unknown()).safeParse(unwrapped);
if (list.success) return ok(list.data.length);
const map = jsonObjectSchema.safeParse(unwrapped);
return map.success
? ok(Object.keys(map.data).length)
: invalid(operation, `${property} list`, map.error);
};
const unwrapProperty = (value: JsonValue, property: string): JsonValue => {
if (
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
property in value
) {
return value[property] ?? null;
}
return value;
};
const invalid = <T>(
operation: string,
expected: string,
cause: z.ZodError,
): Result<T, AnalysisOutputError> =>
err(
new AnalysisOutputError(
operation,
`Provider returned an invalid ${expected}`,
{
cause,
},
),
);
@@ -7,7 +7,7 @@ import { EvidenceIntegrityError } from "../evidenceErrors.js";
import { analysisInputErrorFromIssues } from "../inputIssueProjection.js";
import { readEvidence, readEvidencePayload } from "../evidenceValidation.js";
import { err, ok, type Result } from "../result.js";
import { functionDossierSchema } from "../hopperValues.js";
import { functionDossierSchema } from "../native/functionDossier.js";
import { inspectMachoSchema } from "../native/nativeInspection.js";
import {
managedNativeBoundaryInspectionSchema,
+11
View File
@@ -9,3 +9,14 @@ export const addressCoordinate = (
if (!/^0x[0-9a-f]+$/u.test(offset)) return null;
return { space, offset: BigInt(offset) };
};
/** Return a bounded byte distance between canonical addresses in one space. */
export const addressDistance = (start: string, end: string): number | null => {
const left = addressCoordinate(start);
const right = addressCoordinate(end);
if (left === null || right === null || left.space !== right.space)
return null;
const distance = right.offset - left.offset;
if (distance < 0n || distance > BigInt(Number.MAX_SAFE_INTEGER)) return null;
return Number(distance);
};
+134
View File
@@ -0,0 +1,134 @@
import { describe, expect, it } from "vitest";
import { functionBodySchema } from "./functionBody.js";
import { procedureIdentitySchema } from "./procedureInfo.js";
describe("provider-neutral inclusive function body evidence", () => {
const observed = () => ({
available: true,
provenance: "test-address-set",
ranges: [
{ start: "0x1000", end: "0x1002" },
{ start: "0x1010", end: "0x1011" },
],
total_bytes: 5,
span_bytes: 18,
non_contiguous: true,
contains_entry: true,
});
it("rejects missing provider body metadata", () => {
expect(
procedureIdentitySchema.safeParse({ address: "0x1000", name: "main" })
.success,
).toBe(false);
});
it("counts inclusive one-byte ranges without assuming an enclosing continuous body", () => {
expect(
functionBodySchema.safeParse({
...observed(),
ranges: [{ start: "0x1000", end: "0x1000" }],
total_bytes: 1,
span_bytes: 1,
non_contiguous: false,
}).success,
).toBe(true);
expect(functionBodySchema.parse(observed())).toMatchObject({
total_bytes: 5,
span_bytes: 18,
});
});
it("preserves address-space identity and leaves a multi-space span unknown", () => {
expect(
functionBodySchema.safeParse({
...observed(),
ranges: [
{ start: "a:0x1000", end: "a:0x1000" },
{ start: "b:0x1000", end: "b:0x1001" },
],
total_bytes: 3,
span_bytes: null,
}).success,
).toBe(true);
expect(
functionBodySchema.safeParse({
...observed(),
ranges: [{ start: "a:0x1000", end: "b:0x1000" }],
}).success,
).toBe(false);
});
it("allows an observed empty set without manufacturing entry membership", () => {
expect(
procedureIdentitySchema.safeParse({
address: "0x1000",
name: "external",
classification: null,
body: {
...observed(),
ranges: [],
total_bytes: 0,
span_bytes: 0,
non_contiguous: false,
contains_entry: false,
},
}).success,
).toBe(true);
});
it("checks entry membership using address space and inclusive endpoints", () => {
expect(
procedureIdentitySchema.safeParse({
address: "0x1002",
name: "last",
classification: null,
body: observed(),
}).success,
).toBe(true);
expect(
procedureIdentitySchema.safeParse({
address: "0x1003",
name: "gap",
classification: null,
body: observed(),
}).success,
).toBe(false);
expect(
procedureIdentitySchema.safeParse({
address: "other:0x1000",
name: "other",
classification: null,
body: observed(),
}).success,
).toBe(false);
});
it.each([
{ total_bytes: 18 },
{ span_bytes: 5 },
{ non_contiguous: false },
{
ranges: [
{ start: "0x1000", end: "0x1002" },
{ start: "0x1003", end: "0x1004" },
],
},
{
ranges: [
{ start: "0x1000", end: "0x1002" },
{ start: "0x1002", end: "0x1004" },
],
},
{
ranges: [
{ start: "0x1010", end: "0x1011" },
{ start: "0x1000", end: "0x1002" },
],
},
{
ranges: [{ start: "0x0", end: "0x20000000000000" }],
total_bytes: Number.MAX_SAFE_INTEGER,
span_bytes: Number.MAX_SAFE_INTEGER,
non_contiguous: false,
},
])("rejects contradictory or unsafe range facts %j", (change) => {
expect(
functionBodySchema.safeParse({ ...observed(), ...change }).success,
).toBe(false);
});
});
+110
View File
@@ -0,0 +1,110 @@
import { z } from "zod";
import { addressCoordinate } from "./addressCoordinate.js";
import { unavailableNativeFactSchema } from "./nativeFactAvailability.js";
/** Complete observed function-body ranges; every range endpoint is inclusive. */
export const availableFunctionBodySchema = z
.strictObject({
available: z.literal(true),
provenance: z.string().min(1),
ranges: z.array(
z.strictObject({
start: z.string(),
end: z
.string()
.describe("Inclusive last address of this observed body range."),
}),
),
total_bytes: z.int().min(0).max(Number.MAX_SAFE_INTEGER),
span_bytes: z.int().min(0).max(Number.MAX_SAFE_INTEGER).nullable(),
non_contiguous: z.boolean(),
contains_entry: z.boolean(),
})
.superRefine((body, context) => {
let total = 0n;
const bounds = new Map<string | null, { start: bigint; end: bigint }>();
const closedSpaces = new Set<string | null>();
let previousSpace: string | null | undefined;
for (const range of body.ranges) {
const start = addressCoordinate(range.start);
const end = addressCoordinate(range.end);
if (
start === null ||
end === null ||
start.space !== end.space ||
end.offset < start.offset
) {
context.addIssue({
code: "custom",
message: "Function body range has invalid inclusive endpoints",
});
return;
}
if (previousSpace !== undefined && previousSpace !== start.space)
closedSpaces.add(previousSpace);
const previous = bounds.get(start.space);
if (
closedSpaces.has(start.space) ||
(previous !== undefined && start.offset <= previous.end + 1n)
) {
context.addIssue({
code: "custom",
message:
"Function body ranges must be ordered, disjoint and maximal within each address space",
});
return;
}
bounds.set(start.space, {
start: previous?.start ?? start.offset,
end: end.offset,
});
previousSpace = start.space;
total += end.offset - start.offset + 1n;
}
const onlyBounds = bounds.size === 1 ? [...bounds.values()][0] : undefined;
const span =
bounds.size === 0
? 0n
: onlyBounds === undefined
? null
: onlyBounds.end - onlyBounds.start + 1n;
const reportedSpan =
body.span_bytes === null ? null : BigInt(body.span_bytes);
if (
total !== BigInt(body.total_bytes) ||
span !== reportedSpan ||
body.non_contiguous !== body.ranges.length > 1
)
context.addIssue({
code: "custom",
message:
"Function body counts, enclosing span or contiguity disagree with inclusive ranges",
});
});
export const functionBodySchema = z.discriminatedUnion("available", [
unavailableNativeFactSchema,
availableFunctionBodySchema,
]);
/** Check the declared entry membership against complete, inclusive body ranges. */
export const functionBodyEntryAgrees = (
body: z.infer<typeof functionBodySchema>,
address: string,
): boolean => {
if (!body.available) return true;
const entry = addressCoordinate(address);
if (entry === null) return false;
const contains = body.ranges.some((range) => {
const start = addressCoordinate(range.start);
const end = addressCoordinate(range.end);
return (
start !== null &&
end !== null &&
start.space === entry.space &&
entry.offset >= start.offset &&
entry.offset <= end.offset
);
});
return contains === body.contains_entry;
};
+1 -1
View File
@@ -1,6 +1,6 @@
import { createHash } from "node:crypto";
import type { z } from "zod";
import type { functionBodySchema } from "../hopperValues.js";
import type { functionBodySchema } from "../native/functionBody.js";
import type { nativeByteReadSchema } from "./nativeByteRead.js";
import { addressCoordinate } from "./addressCoordinate.js";
+128
View File
@@ -0,0 +1,128 @@
import { z } from "zod";
import { err, ok, type Result } from "../result.js";
import { AnalysisOutputError } from "../analysisErrorCore.js";
import type { JsonValue } from "../jsonValue.js";
import {
procedureIdentitySchema,
localVariableSchema,
} from "./procedureInfo.js";
import { analysisStringSchema } from "./nativeString.js";
import {
referenceEdgeSchema,
unresolvedCallSchema,
} from "./nativeReference.js";
import {
swiftStringLiteralSchema,
swiftStringLiteralOmissionSchema,
} from "./swiftStringLiteral.js";
import { nativeApiBoundarySchema } from "./nativeApiBoundary.js";
import { nativeValueFlowSchema } from "./nativeValueFlow.js";
/** Provider-neutral complete raw-instruction list for one analyzed function. */
export const functionInstructionWindowSchema = z
.object({
procedure: procedureIdentitySchema,
instructions: z.array(z.string()),
limitations: z.array(z.string()),
})
.strict();
const facetAvailabilitySchema = z.discriminatedUnion("available", [
z.object({ available: z.literal(true) }).strict(),
z.object({ available: z.literal(false), reason: z.string().min(1) }).strict(),
]);
/** Provider-declared availability per dossier facet; an omitted facet declares nothing. */
export const functionFacetAvailabilitySchema = z
.object({
pseudocode: facetAvailabilitySchema.exactOptional(),
assembly: facetAvailabilitySchema.exactOptional(),
comments: facetAvailabilitySchema.exactOptional(),
callers: facetAvailabilitySchema.exactOptional(),
callees: facetAvailabilitySchema.exactOptional(),
references: facetAvailabilitySchema.exactOptional(),
strings_names: facetAvailabilitySchema.exactOptional(),
cfg: facetAvailabilitySchema.exactOptional(),
})
.strict();
export const functionDossierSchema = z
.object({
procedure: procedureIdentitySchema.safeExtend({
signature: z.string().nullable(),
locals: z.array(localVariableSchema),
}),
pseudocode: z.string(),
assembly: z.array(z.string()),
comments: z.array(
z
.object({
address: z.string(),
kind: z.enum(["comment", "inline"]),
text: z.string(),
})
.strict(),
),
callers: z.array(procedureIdentitySchema),
callees: z.array(procedureIdentitySchema),
incoming_references: z.array(referenceEdgeSchema),
outgoing_references: z.array(referenceEdgeSchema),
unresolved_calls: z.array(unresolvedCallSchema).exactOptional(),
referenced_strings: z.array(
analysisStringSchema.extend({ source_address: z.string() }),
),
referenced_names: z.array(
z
.object({
address: z.string(),
value: z.string(),
source_address: z.string(),
})
.strict(),
),
swift_string_literals: z
.array(swiftStringLiteralSchema)
.exactOptional()
.describe(
"Swift String literals decoded from constant register values. Absent when the provider does not decode them for this target.",
),
swift_string_literal_omissions: z
.array(swiftStringLiteralOmissionSchema)
.exactOptional()
.describe(
"Literal candidates omitted for capacity or unresolved control flow. Present exactly when swift_string_literals is present; empty when all examined use sites were classified.",
),
basic_blocks: z.array(
z
.object({
start: z.string(),
end: z.string(),
successors: z.array(z.string()),
})
.strict(),
),
native_api: nativeApiBoundarySchema.nullable(),
native_value_flow: nativeValueFlowSchema.nullable(),
facet_availability: functionFacetAvailabilitySchema.exactOptional(),
limitations: z.array(z.string()),
})
.strict();
/** Strict analyzed-function dossier shared by provider and comparison boundaries. */
export type FunctionDossier = z.infer<typeof functionDossierSchema>;
/** Strictly parse a complete provider-neutral function dossier. */
export const parseFunctionDossier = (
value: JsonValue,
): Result<FunctionDossier, AnalysisOutputError> => {
const parsed = functionDossierSchema.safeParse(value);
return parsed.success
? ok(parsed.data)
: err(
new AnalysisOutputError(
"analyze_function",
"provider output did not match the dossier contract",
{ cause: parsed.error },
),
);
};
+2 -2
View File
@@ -4,11 +4,11 @@ import type { Evidence } from "../evidence.js";
import { EvidenceIntegrityError } from "../evidenceErrors.js";
import { readEvidencePayload } from "../evidenceValidation.js";
import { digestCanonicalValue } from "../canonicalDigest.js";
import { functionDossierSchema } from "../native/functionDossier.js";
import {
functionBodySchema,
functionBodyEntryAgrees,
functionDossierSchema,
} from "../hopperValues.js";
} from "../native/functionBody.js";
import { nativeSymbolOrigin } from "./nativeSymbol.js";
import {
addressedValueSchema,
@@ -1,5 +1,5 @@
import type { Evidence } from "../evidence.js";
import type { FunctionDossier } from "../hopperValues.js";
import type { FunctionDossier } from "../native/functionDossier.js";
import { digestCanonicalValue } from "../canonicalDigest.js";
import { normalizeCfg } from "../functionComparisonNormalization.js";
import { hasStableNativeSymbol } from "./nativeSymbol.js";
+1 -1
View File
@@ -1,5 +1,5 @@
import { z } from "zod";
import { analysisStringSchema } from "../hopperValues.js";
import { analysisStringSchema } from "../native/nativeString.js";
import { addressedValueSchema } from "./nativeInventory.js";
/** Tracing reuses the search row's qualifications without reinterpreting them. */
+21
View File
@@ -0,0 +1,21 @@
import { z } from "zod";
import { ok, type Result } from "../result.js";
import { AnalysisOutputError } from "../analysisErrorCore.js";
import type { JsonValue } from "../jsonValue.js";
import {
unwrapNativeProperty,
invalidNativeOutput,
} from "./nativeProviderOutput.js";
/** Parse direct or wrapped provider document names. */
export const parseDocuments = (
value: JsonValue,
operation = "list_documents",
): Result<readonly string[], AnalysisOutputError> => {
const parsed = z
.array(z.string())
.safeParse(unwrapNativeProperty(value, "documents"));
return parsed.success
? ok(parsed.data)
: invalidNativeOutput(operation, "document list", parsed.error);
};
@@ -0,0 +1,6 @@
import { z } from "zod";
/** An explicitly unavailable provider fact preserves its reported reason. */
export const unavailableNativeFactSchema = z
.object({ available: z.literal(false), reason: z.string() })
.strict();
@@ -1,5 +1,5 @@
import { z } from "zod";
import { functionDossierSchema } from "../hopperValues.js";
import { functionDossierSchema } from "../native/functionDossier.js";
/** One explicit function's analyst-authored metadata changes. */
export const nativeFunctionAnnotationsInputSchema = z
+35 -1
View File
@@ -1,6 +1,14 @@
import { z } from "zod";
import { nativeSymbolMetadataSchema } from "./nativeSymbol.js";
import { analysisStringSchema } from "../hopperValues.js";
import { analysisStringSchema } from "./nativeString.js";
import { ok, type Result } from "../result.js";
import { AnalysisOutputError } from "../analysisErrorCore.js";
import type { JsonValue } from "../jsonValue.js";
import { jsonObjectSchema } from "../jsonValue.js";
import {
unwrapNativeProperty,
invalidNativeOutput,
} from "./nativeProviderOutput.js";
/** Canonical named native observations; same-address alternatives stay separate. */
export const addressedValueSchema = z.object({
@@ -49,3 +57,29 @@ export const isNativeInventoryOperation = (
operation: string,
): operation is NativeInventoryOperation =>
NATIVE_INVENTORY_OPERATIONS.some((supported) => supported === operation);
export interface AddressedName {
readonly address: string;
readonly name: string;
readonly objc_class_label?: "hopper_legacy_class_object";
}
/** An analyzed bookmark retains a missing provider label as null. */
export const analysisBookmarkSchema = z.strictObject({
address: z.string(),
name: z.string().nullable(),
});
/** Parse a direct or wrapped list when only its cardinality is required. */
export const parseListCount = (
value: JsonValue,
property: string,
operation = property === "procedures" ? "list_procedures" : "list_strings",
): Result<number, AnalysisOutputError> => {
const unwrapped = unwrapNativeProperty(value, property);
const list = z.array(z.unknown()).safeParse(unwrapped);
if (list.success) return ok(list.data.length);
const map = jsonObjectSchema.safeParse(unwrapped);
return map.success
? ok(Object.keys(map.data).length)
: invalidNativeOutput(operation, `${property} list`, map.error);
};
+61 -1
View File
@@ -1,7 +1,51 @@
import {
unwrapNativeProperty,
invalidNativeOutput,
} from "./nativeProviderOutput.js";
import { z } from "zod";
import { containingProcedureResolutionSchema } from "../hopperValues.js";
import { procedureIdentitySchema } from "./procedureInfo.js";
import { AnalysisOutputError } from "../analysisErrorCore.js";
import type { JsonValue } from "../jsonValue.js";
import { err, ok, type Result } from "../result.js";
import { nativeCallGraphSchema } from "./nativeCallGraph.js";
/** Exact containing-procedure outcome; valid absence is distinct from unreadable output. */
export const containingProcedureResolutionSchema = z.discriminatedUnion(
"found",
[
z.object({
query_address: z.string(),
found: z.literal(true),
procedure: procedureIdentitySchema,
}),
z.object({
query_address: z.string(),
found: z.literal(false),
procedure: z.null(),
reason: z.enum(["outside_segments", "not_in_procedure"]),
}),
],
);
export type ContainingProcedureResolution = z.infer<
typeof containingProcedureResolutionSchema
>;
/** Validate provider procedure facts before a workflow interprets absence. */
export const parseContainingProcedureResolution = (
value: JsonValue,
): Result<ContainingProcedureResolution, AnalysisOutputError> => {
const parsed = containingProcedureResolutionSchema.safeParse(value);
return parsed.success
? ok(parsed.data)
: err(
new AnalysisOutputError(
"resolve_containing_procedure",
"expected a containing-procedure resolution",
{ cause: parsed.error },
),
);
};
export const NATIVE_PRIMITIVE_OBSERVATION_OPERATIONS = [
"procedure_address",
"procedure_callees",
@@ -34,3 +78,19 @@ export const isNativeObservationOperation = (
operation: string,
): operation is NativeObservationOperation =>
NATIVE_OBSERVATION_OPERATIONS.some((supported) => supported === operation);
/** Parse callee/caller strings from direct or wrapped provider results. */
export const parseRelatedAddresses = (
value: JsonValue,
relation: "callees" | "callers",
operation = relation === "callees"
? "procedure_callees"
: "procedure_callers",
): Result<readonly string[], AnalysisOutputError> => {
const parsed = z
.array(z.string())
.safeParse(unwrapNativeProperty(value, relation));
return parsed.success
? ok(parsed.data)
: invalidNativeOutput(operation, `${relation} list`, parsed.error);
};
@@ -0,0 +1,129 @@
import { describe, expect, it } from "vitest";
import { addressDistance } from "./addressCoordinate.js";
import { parseDocuments } from "./nativeDocument.js";
import { parseListCount } from "./nativeInventory.js";
import { parseRelatedAddresses } from "./nativeObservation.js";
import { parseSegments } from "./nativeSegments.js";
describe("native provider boundary values", () => {
it("distinguishes measured zero, malformed, incomparable, reversed, and oversized extents", () => {
expect(addressDistance("not-an-address", "0x2000")).toBeNull();
expect(addressDistance("ram:0x1000", "rom:0x2000")).toBeNull();
expect(addressDistance("0x2000", "0x1000")).toBeNull();
expect(addressDistance("0x1000", "0x1000")).toBe(0);
expect(addressDistance("0x1000", "0x2000")).toBe(4096);
expect(addressDistance("0x0", "0x20000000000000")).toBeNull();
});
it("rejects segments when required coordinates or unknown permissions are omitted", () => {
expect(
parseSegments([
{
name: "__TEXT",
start: "0x1",
end: "0x2",
readable: null,
writable: null,
},
]).ok,
).toBe(false);
expect(
parseSegments([{ name: "__TEXT", start: "0x1", end: "0x2" }]).ok,
).toBe(false);
});
it("counts Hopper's address-keyed string maps", () => {
expect(
parseListCount({ "0x1000": "hello", "0x2000": "world" }, "strings"),
).toEqual({ ok: true, value: 2 });
});
it("parses direct and wrapped protocol shapes", () => {
expect(parseRelatedAddresses({ callers: ["0x3"] }, "callers")).toEqual({
ok: true,
value: ["0x3"],
});
expect(
parseSegments([
{
name: "__TEXT",
start: "0x1",
end: "0x2",
readable: true,
writable: false,
executable: null,
},
]),
).toEqual({
ok: true,
value: [
{
name: "__TEXT",
start: "0x1",
end: "0x2",
readable: true,
writable: false,
executable: null,
},
],
});
expect(parseDocuments(["fixture"])).toEqual({
ok: true,
value: ["fixture"],
});
});
it.each([
["relations", () => parseRelatedAddresses([1], "callees")],
["segments", () => parseSegments([{ name: 1 }])],
["documents", () => parseDocuments([1])],
])(
"rejects malformed %s instead of manufacturing empty output",
(_label, parse) => {
const result = parse();
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error._tag).toBe("AnalysisOutputError");
},
);
it("uses provider-neutral wording for malformed shared values", () => {
const result = parseSegments([{ name: 1 }]);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error._tag).toBe("AnalysisOutputError");
expect(result.error.operation).toBe("list_segments");
expect(result.error.reason).toBe(
"Provider returned an invalid segment list",
);
}
});
});
it.each(["procedures", "strings"])(
"counts every own entry of direct and wrapped %s maps",
(property) => {
for (const text of [
'{"__proto__":"observed"}',
'{"__proto__":"observed","0x1000":"one"}',
]) {
const source = JSON.parse(text);
const count = Object.keys(source).length;
expect(parseListCount(source, property)).toEqual({
ok: true,
value: count,
});
expect(parseListCount({ [property]: source }, property)).toEqual({
ok: true,
value: count,
});
expect(JSON.stringify(source)).toBe(text);
}
expect(parseListCount({ [property]: [] }, property)).toEqual({
ok: true,
value: 0,
});
expect(parseListCount({}, property)).toEqual({ ok: true, value: 0 });
for (const value of [null, "not a list", 3, false])
expect(parseListCount(value, property).ok).toBe(false);
},
);
+34
View File
@@ -0,0 +1,34 @@
import { z } from "zod";
import { err, type Result } from "../result.js";
import { AnalysisOutputError } from "../analysisErrorCore.js";
import type { JsonValue } from "../jsonValue.js";
export const unwrapNativeProperty = (
value: JsonValue,
property: string,
): JsonValue => {
if (
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
property in value
) {
return value[property] ?? null;
}
return value;
};
export const invalidNativeOutput = <T>(
operation: string,
expected: string,
cause: z.ZodError,
): Result<T, AnalysisOutputError> =>
err(
new AnalysisOutputError(
operation,
`Provider returned an invalid ${expected}`,
{
cause,
},
),
);
+46
View File
@@ -0,0 +1,46 @@
import { z } from "zod";
import { unavailableNativeFactSchema } from "./nativeFactAvailability.js";
import { procedureIdentitySchema } from "./procedureInfo.js";
import { nativeCallClassificationSchema } from "./nativeCallReference.js";
export const availableReferenceKindSchema = z
.object({
available: z.literal(true),
provenance: z.string().min(1),
type: z.string(),
flow: z.boolean(),
call: z.boolean(),
jump: z.boolean(),
data: z.boolean(),
read: z.boolean(),
write: z.boolean(),
indirect: z.boolean(),
computed: z.boolean(),
conditional: z.boolean(),
terminal: z.boolean(),
primary: z.boolean(),
operand_index: z.int(),
external: z.boolean(),
})
.strict();
export const referenceKindSchema = z.discriminatedUnion("available", [
unavailableNativeFactSchema,
availableReferenceKindSchema,
]);
/** Reported call sites without resolved targets; unreported dispatch stays unknown. */
export const unresolvedCallSchema = z.strictObject({
address: z.string(),
reason: z.string(),
});
/** Observed endpoints with complete or partial reference classification evidence. */
export const referenceEdgeSchema = z
.object({
source_address: z.string(),
target_address: z.string(),
source_procedure: procedureIdentitySchema.nullable(),
target_procedure: procedureIdentitySchema.nullable(),
kind: referenceKindSchema,
call: nativeCallClassificationSchema.exactOptional(),
})
.strict();
+87
View File
@@ -0,0 +1,87 @@
import { z } from "zod";
import { ok, type Result } from "../result.js";
import { AnalysisOutputError } from "../analysisErrorCore.js";
import type { JsonValue } from "../jsonValue.js";
import {
unwrapNativeProperty,
invalidNativeOutput,
} from "./nativeProviderOutput.js";
import { addressDistance } from "./addressCoordinate.js";
export interface SegmentSummary {
readonly name: string;
readonly start: string;
readonly end: string;
readonly readable: boolean | null;
readonly writable: boolean | null;
readonly executable: boolean | null;
readonly image_base?: string;
readonly address_space?: string;
}
const segmentSchema = z.object({
name: z.string(),
start: z.string(),
end: z.string(),
readable: z.boolean().nullable(),
writable: z.boolean().nullable(),
executable: z.boolean().nullable(),
image_base: z.string().min(1).exactOptional(),
address_space: z.string().min(1).exactOptional(),
});
/** Preserve reported address context in direct or wrapped provider segments. */
export const parseSegments = (
value: JsonValue,
operation = "list_segments",
): Result<readonly SegmentSummary[], AnalysisOutputError> => {
const parsed = z
.array(segmentSchema)
.safeParse(unwrapNativeProperty(value, "segments"));
return parsed.success
? ok(parsed.data)
: invalidNativeOutput(operation, "segment list", parsed.error);
};
/** Return a reported overview base only when every parsed segment agrees. */
export const parseSegmentImageBase = (
segments: readonly SegmentSummary[],
): string | undefined => {
const base = segments[0]?.image_base;
return base !== undefined &&
segments.every((segment) => segment.image_base === base)
? base
: undefined;
};
/** Project the same overview from complete lists or provider-owned aggregates. */
export const projectBinaryOverview = (input: {
readonly document: string;
readonly procedure_count: number;
readonly string_count: number;
readonly segments?: readonly SegmentSummary[];
}): JsonValue => {
const { document, procedure_count, string_count, segments } = input;
const imageBase =
segments === undefined ? undefined : parseSegmentImageBase(segments);
return {
document,
...(imageBase === undefined ? {} : { image_base: imageBase }),
...(segments === undefined
? {}
: {
segments: segments.map(
({ name, start, end, image_base, address_space }) => ({
name,
start,
end,
length: addressDistance(start, end),
...(image_base === undefined ? {} : { image_base }),
...(address_space === undefined ? {} : { address_space }),
}),
),
segment_count: segments.length,
}),
procedure_count,
string_count,
};
};
+32
View File
@@ -0,0 +1,32 @@
import { z } from "zod";
import { unavailableNativeFactSchema } from "./nativeFactAvailability.js";
/** Typed string evidence shared by inventories and referenced function literals. */
export const analysisStringSchema = z.object({
address: z.string(),
value: z.string(),
provider_value: z
.string()
.exactOptional()
.describe(
"Original provider display text, which may shorten the typed object.",
),
decoding: unavailableNativeFactSchema
.exactOptional()
.describe(
"When present, value retains native display text because typed bytes could not be decoded.",
),
string: z
.object({
encoding: z.string().min(1),
encoding_status: z
.enum(["observed", "inferred"])
.exactOptional()
.describe(
"Whether the provider declares the encoding or REA infers it from the typed bytes and native display. This does not establish the original source's intended encoding.",
),
termination: z.enum(["missing", "present_or_not_required"]),
byte_length: z.int().min(0),
})
.exactOptional(),
});
+42 -6
View File
@@ -1,11 +1,47 @@
import { nativeSymbolMetadataSchema } from "./nativeSymbol.js";
import { z } from "zod";
import {
functionBodySchema,
localVariableSchema,
procedureClassificationSchema,
} from "../hopperValues.js";
import { nativeSymbolMetadataSchema } from "./nativeSymbol.js";
import { functionBodySchema, functionBodyEntryAgrees } from "./functionBody.js";
export const procedureClassificationSchema = z
.object({
external: z.boolean(),
thunk: z.boolean(),
thunk_target: z.string().nullable(),
provenance: z.string().min(1),
})
.strict();
export const procedureIdentitySchema = z
.object({
address: z.string(),
name: z.string(),
symbol: nativeSymbolMetadataSchema.nullable().exactOptional(),
classification: procedureClassificationSchema.nullable(),
body: functionBodySchema,
})
.strict()
.superRefine((identity, context) => {
if (!functionBodyEntryAgrees(identity.body, identity.address))
context.addIssue({
code: "custom",
message:
"Function body entry membership disagrees with procedure identity",
});
});
export const localVariableSchema = z
.object({
description: z.string(),
provenance: z.string().min(1),
name: z.string().nullable().exactOptional(),
stack_displacement: z
.string()
.regex(/^-?\d+$/u)
.nullable()
.exactOptional()
.describe(
"Observed signed stack byte displacement, encoded as decimal to preserve precision.",
),
})
.strict();
/** Provider-neutral procedure metadata, including exact body ranges. */
export const procedureInfoSchema = z.object({
name: z.string(),
+70
View File
@@ -0,0 +1,70 @@
import { z } from "zod";
import { fixedTupleSchema } from "../fixedTupleSchema.js";
const swiftStringLiteralFields = {
source_address: z
.string()
.describe(
"Instruction that consumes both words: a call, tail call, return, or paired store.",
),
value: z
.string()
.describe("Decoded UTF-8 text, including the canonical empty string."),
registers: fixedTupleSchema([z.string(), z.string()]),
words: fixedTupleSchema([z.string(), z.string()]).describe(
"Observed constant register values, in register order.",
),
};
/** A Swift String literal decoded from the two words of its ABI value, not a data reference. */
export const swiftSmallStringLiteralSchema = z.strictObject({
...swiftStringLiteralFields,
representation: z
.literal("small")
.describe("The UTF-8 bytes are inside the two words."),
address: z.null().describe("A small literal has no storage of its own."),
});
export const swiftLargeStringLiteralSchema = z.strictObject({
...swiftStringLiteralFields,
representation: z
.literal("large")
.describe(
"An immortal literal whose words hold its count and a pointer to its bytes.",
),
address: z.string().describe("Address of a large literal's bytes."),
});
export const swiftStringLiteralSchema = z.discriminatedUnion("representation", [
swiftSmallStringLiteralSchema,
swiftLargeStringLiteralSchema,
]);
const swiftStringLiteralOmissionFields = {
source_address: swiftStringLiteralFields.source_address,
byte_length: z.int().min(0).max(Number.MAX_SAFE_INTEGER),
reason: z
.enum(["capacity", "unresolved_jump"])
.describe(
"Why this candidate is omitted: capacity stopped decoding, or an unresolved jump left its use unknown.",
),
registers: swiftStringLiteralFields.registers,
words: swiftStringLiteralFields.words,
};
/** A Swift literal candidate omitted because decoding capacity or control flow left it unknown. */
export const swiftSmallStringLiteralOmissionSchema = z.strictObject({
...swiftStringLiteralOmissionFields,
representation: z.literal("small"),
address: z.null(),
});
export const swiftLargeStringLiteralOmissionSchema = z.strictObject({
...swiftStringLiteralOmissionFields,
representation: z.literal("large"),
address: z.string().describe("Address of the literal's bytes."),
});
export const swiftStringLiteralOmissionSchema = z.discriminatedUnion(
"representation",
[
swiftSmallStringLiteralOmissionSchema,
swiftLargeStringLiteralOmissionSchema,
],
);
@@ -11,7 +11,7 @@ import {
type EvidenceSubjectTarget,
} from "./evidence.js";
import { createEvidenceBundle, parseEvidenceBundle } from "./evidenceBundle.js";
import { functionDossierSchema } from "./hopperValues.js";
import { functionDossierSchema } from "./native/functionDossier.js";
import {
jsonObjectSchema,
jsonValueSchema,
+1 -1
View File
@@ -13,7 +13,7 @@ import { AnalysisInputError } from "./analysisErrorCore.js";
import { EvidenceIntegrityError } from "./evidenceErrors.js";
import { analysisInputErrorFromIssues } from "./inputIssueProjection.js";
import { readEvidencePayload } from "./evidenceValidation.js";
import { functionDossierSchema } from "./hopperValues.js";
import { functionDossierSchema } from "./native/functionDossier.js";
import { assessFunctionCallCoverage } from "./functionCallCoverage.js";
import { processCaptureSchema } from "./process/processCapture.js";
import { digestCanonicalValue } from "./canonicalDigest.js";
+1 -1
View File
@@ -11,7 +11,7 @@ import { createEvidence, type Evidence } from "./evidence.js";
import { functionComparisonResultSchema } from "./functionComparisonSchemas.js";
import { jsonValueSchema, type JsonValue } from "./jsonValue.js";
import { FUNCTION_COMPARISON_EXAMPLE } from "../contracts/functionComparisonExample.js";
import { functionDossierSchema } from "./hopperValues.js";
import { functionDossierSchema } from "./native/functionDossier.js";
import { compareFunctions } from "./functionComparison.js";
import { functionSnapshotFixture } from "./functionDossierEvidence.fixture.js";
import {
+1 -1
View File
@@ -1,6 +1,6 @@
import { objcClassSymbol } from "./objcClassSymbol.js";
import type { JsonValue } from "./jsonValue.js";
import type { AddressedName } from "./hopperValues.js";
import type { AddressedName } from "./native/nativeInventory.js";
import type {
MachoImportBinding,
ObjcClassImportBindingCoverage,
+1 -1
View File
@@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest";
import type { JsonValue } from "../domain/jsonValue.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { functionDossierSchema } from "../domain/hopperValues.js";
import { functionDossierSchema } from "../domain/native/functionDossier.js";
import {
compactReference,
compactInstruction,
+6 -2
View File
@@ -20,16 +20,20 @@ import {
AnalysisOutputError,
} from "../domain/analysisErrorCore.js";
import {
functionBodyEntryAgrees,
functionInstructionWindowSchema,
functionDossierSchema,
} from "../domain/native/functionDossier.js";
import {
availableReferenceKindSchema,
unresolvedCallSchema,
} from "../domain/native/nativeReference.js";
import {
swiftSmallStringLiteralSchema,
swiftLargeStringLiteralSchema,
swiftSmallStringLiteralOmissionSchema,
swiftLargeStringLiteralOmissionSchema,
} from "../domain/hopperValues.js";
} from "../domain/native/swiftStringLiteral.js";
import { functionBodyEntryAgrees } from "../domain/native/functionBody.js";
import {
jsonObjectSchema,
jsonValueSchema,
+3 -3
View File
@@ -19,12 +19,12 @@ import {
type AnalysisInputError,
AnalysisOutputError,
} from "../domain/analysisErrorCore.js";
import { projectBinaryOverview } from "../domain/native/nativeSegments.js";
import {
availableFunctionBodySchema,
procedureIdentitySchema,
functionBodyEntryAgrees,
projectBinaryOverview,
} from "../domain/hopperValues.js";
} from "../domain/native/functionBody.js";
import { procedureIdentitySchema } from "../domain/native/procedureInfo.js";
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
import { err, ok, type Result } from "../domain/result.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
+1 -1
View File
@@ -6,7 +6,7 @@ import { createTestBinarySession } from "../../tests/fixtures/binarySession.js";
import { parseConfig } from "../config/parseConfig.js";
import type { AnalysisOperation } from "../application/AnalysisProvider.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import { functionDossierSchema } from "../domain/hopperValues.js";
import { functionDossierSchema } from "../domain/native/functionDossier.js";
import { createAnalysisProfile } from "../domain/analysisProfile.js";
import { ghidraFunctionDossier } from "../domain/ghidraValues.fixture.js";
import { silentLogger } from "../logger.js";
+1 -1
View File
@@ -6,7 +6,7 @@ import {
AnalysisCancelledError,
AnalysisOutputError,
} from "../domain/analysisErrorCore.js";
import { analysisStringSchema } from "../domain/hopperValues.js";
import { analysisStringSchema } from "../domain/native/nativeString.js";
import { HopperRemoteError } from "../domain/hopperErrors.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { err, ok, type Result } from "../domain/result.js";
+1 -1
View File
@@ -25,7 +25,7 @@ import {
import {
functionDossierSchema,
functionInstructionWindowSchema,
} from "../domain/hopperValues.js";
} from "../domain/native/functionDossier.js";
import type { JsonValue } from "../domain/jsonValue.js";
import type { RelationshipCoverage } from "../domain/relationshipCoverage.js";
import { combineFunctionRelationshipCoverage } from "../domain/functionCallCoverage.js";
+1 -1
View File
@@ -12,7 +12,7 @@ import {
DEFAULT_REGEX_MATCHING_DEADLINE,
REGEX_MATCHING_DEADLINE_SETTING,
} from "../config/regexMatchingDeadline.js";
import { functionDossierSchema } from "../domain/hopperValues.js";
import { functionDossierSchema } from "../domain/native/functionDossier.js";
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
import {
AnalysisProtocolError,
+1 -1
View File
@@ -4,7 +4,7 @@ import {
AnalysisResourceConstraintError,
} from "../domain/analysisErrorCore.js";
import type { JsonValue } from "../domain/jsonValue.js";
import type { FunctionDossier } from "../domain/hopperValues.js";
import type { FunctionDossier } from "../domain/native/functionDossier.js";
import type { RelationshipCoverage } from "../domain/relationshipCoverage.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { err, ok, type Result } from "../domain/result.js";
+1 -1
View File
@@ -34,7 +34,7 @@ import {
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import type { EvidenceLocation } from "../domain/evidence.js";
import { combineFunctionRelationshipCoverage } from "../domain/functionCallCoverage.js";
import { functionDossierSchema } from "../domain/hopperValues.js";
import { functionDossierSchema } from "../domain/native/functionDossier.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
+1 -1
View File
@@ -1,7 +1,7 @@
import { z } from "zod";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { analysisStringSchema } from "../domain/hopperValues.js";
import type { analysisStringSchema } from "../domain/native/nativeString.js";
import { err, ok, type Result } from "../domain/result.js";
import { compareKunaAddresses } from "./KunaAddress.js";
import { KunaJsonDocumentReader, parseKunaRow } from "./KunaJsonStream.js";
@@ -9,7 +9,7 @@ import { registerCoreBinaryCommands } from "../../../src/cli/coreBinaryCommands.
import type { CliInstance } from "../../../src/cli/types.js";
import { CLI_COMMANDS } from "../../../src/cliCommandNames.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { functionDossierSchema } from "../../../src/domain/hopperValues.js";
import { functionDossierSchema } from "../../../src/domain/native/functionDossier.js";
import { ghidraFunctionDossier } from "../../../src/domain/ghidraValues.fixture.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
+1 -1
View File
@@ -26,7 +26,7 @@ import {
jsonObjectSchema,
type JsonValue,
} from "../../../src/domain/jsonValue.js";
import { functionDossierSchema } from "../../../src/domain/hopperValues.js";
import { functionDossierSchema } from "../../../src/domain/native/functionDossier.js";
import { inspectMachoSchema } from "../../../src/domain/native/nativeInspection.js";
import { ghidraFunctionDossier } from "../../../src/domain/ghidraValues.fixture.js";
import { ok } from "../../../src/domain/result.js";
@@ -16,7 +16,7 @@ import { join } from "node:path";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import type { JsonValue } from "../../../src/domain/jsonValue.js";
import { ok } from "../../../src/domain/result.js";
import { functionDossierSchema } from "../../../src/domain/hopperValues.js";
import { functionDossierSchema } from "../../../src/domain/native/functionDossier.js";
import { ghidraFunctionDossier } from "../../../src/domain/ghidraValues.fixture.js";
import { parseGhidraFunctionInput } from "../../../src/ghidra/GhidraFunctionValues.js";
import { EvidenceLedger } from "../../../src/application/investigation/EvidenceLedger.js";
@@ -4,7 +4,7 @@ import { expect, it } from "vitest";
import { FUNCTION_COMPARISON_EXAMPLE } from "../../../src/contracts/functionComparisonExample.js";
import { callPathResultSchema } from "../../../src/domain/callPathSchemas.js";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { functionDossierSchema } from "../../../src/domain/hopperValues.js";
import { functionDossierSchema } from "../../../src/domain/native/functionDossier.js";
import { createServer } from "../../../src/server/createServer.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
@@ -1,7 +1,7 @@
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { afterEach, describe, expect, it } from "vitest";
import { functionDossierSchema } from "../../../src/domain/hopperValues.js";
import { functionDossierSchema } from "../../../src/domain/native/functionDossier.js";
import { ghidraFunctionDossier } from "../../../src/domain/ghidraValues.fixture.js";
import { observed } from "../../fixtures/analysisExecution.js";
import { closeEnhancedToolResources, connect } from "./enhancedToolsHarness.js";
@@ -4,7 +4,7 @@ import { expect, it } from "vitest";
import { FUNCTION_COMPARISON_EXAMPLE } from "../../../src/contracts/functionComparisonExample.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { functionDossierSchema } from "../../../src/domain/hopperValues.js";
import { functionDossierSchema } from "../../../src/domain/native/functionDossier.js";
import type { RelationshipCoverage } from "../../../src/domain/relationshipCoverage.js";
import { createServer } from "../../../src/server/createServer.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
@@ -3,7 +3,7 @@ import { expect, it } from "vitest";
import { FUNCTION_COMPARISON_EXAMPLE } from "../../../src/contracts/functionComparisonExample.js";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { functionDossierSchema } from "../../../src/domain/hopperValues.js";
import { functionDossierSchema } from "../../../src/domain/native/functionDossier.js";
import { jsonValueSchema } from "../../../src/domain/jsonValue.js";
import { createServer } from "../../../src/server/createServer.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
@@ -16,7 +16,7 @@ import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { createServer } from "../../../src/server/createServer.js";
import { observed } from "../../fixtures/analysisExecution.js";
import { reconstructionVerificationResultSchema } from "../../../src/domain/reconstructionVerificationSchemas.js";
import { functionDossierSchema } from "../../../src/domain/hopperValues.js";
import { functionDossierSchema } from "../../../src/domain/native/functionDossier.js";
it.each([
{ leftSpace: "ram", rightSpace: "ram" },
@@ -10,7 +10,7 @@ import {
} from "../../../src/domain/analysisProfile.js";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { functionComparisonResultSchema } from "../../../src/domain/functionComparisonSchemas.js";
import { functionDossierSchema } from "../../../src/domain/hopperValues.js";
import { functionDossierSchema } from "../../../src/domain/native/functionDossier.js";
import { reconstructionVerificationResultSchema } from "../../../src/domain/reconstructionVerificationSchemas.js";
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
@@ -3,7 +3,7 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { FUNCTION_COMPARISON_EXAMPLE } from "../../../src/contracts/functionComparisonExample.js";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { functionDossierSchema } from "../../../src/domain/hopperValues.js";
import { functionDossierSchema } from "../../../src/domain/native/functionDossier.js";
import {
functionComparisonResultSchema,
type FunctionComparisonResult as Comparison,
+1 -1
View File
@@ -2,7 +2,7 @@ import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { describe, expect, it } from "vitest";
import { EnhancedTools } from "../../../src/application/EnhancedTools.js";
import { connectGhidraMcp, sessionEvidence } from "./ghidraMcpHarness.js";
import { functionDossierSchema } from "../../../src/domain/hopperValues.js";
import { functionDossierSchema } from "../../../src/domain/native/functionDossier.js";
import { functionComparisonResultSchema } from "../../../src/domain/functionComparisonSchemas.js";
import {
ghidraFunctionDossier,
@@ -10,7 +10,7 @@ import { join } from "node:path";
import { jsonValueSchema } from "../../../src/domain/jsonValue.js";
import { nativeLoadImageSchema } from "../../../src/domain/native/nativeLoadImage.js";
import { nativeApiInspectionResultSchema } from "../../../src/domain/native/nativeApiBoundary.js";
import { functionDossierSchema } from "../../../src/domain/hopperValues.js";
import { functionDossierSchema } from "../../../src/domain/native/functionDossier.js";
import { ok } from "../../../src/domain/result.js";
import { ghidraFunctionDossier } from "../../../src/domain/ghidraValues.fixture.js";
import { toolContract } from "../../../src/contracts/toolContracts.js";
@@ -5,7 +5,7 @@ import { join, resolve } from "node:path";
import { afterAll, beforeAll, expect } from "vitest";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { functionDossierSchema } from "../../../src/domain/hopperValues.js";
import { functionDossierSchema } from "../../../src/domain/native/functionDossier.js";
import {
MANAGED_APPLICATION_GRAPH_EXAMPLE,
MANAGED_NATIVE_VERIFICATION_EXAMPLE,
@@ -9,7 +9,7 @@ import {
import { observed as ok } from "../../fixtures/analysisExecution.js";
import { err } from "../../../src/domain/result.js";
import { AnalysisCapabilityUnavailableError } from "../../../src/domain/analysisErrorCore.js";
import type { SegmentSummary } from "../../../src/domain/hopperValues.js";
import type { SegmentSummary } from "../../../src/domain/native/nativeSegments.js";
afterEach(closeEnhancedToolResources);
+1 -1
View File
@@ -4,7 +4,7 @@ import { describe, expect, it, onTestFinished } from "vitest";
import type { AnalysisOperationPort } from "../../../src/application/AnalysisProvider.js";
import { runDirectAnalysis } from "../../../src/application/DirectAnalysis.js";
import { functionDossierSchema } from "../../../src/domain/hopperValues.js";
import { functionDossierSchema } from "../../../src/domain/native/functionDossier.js";
import { ghidraFunctionDossier } from "../../../src/domain/ghidraValues.fixture.js";
import {
jsonValueSchema,
@@ -3,7 +3,7 @@ import { expect, it } from "vitest";
import { FUNCTION_COMPARISON_EXAMPLE } from "../../../src/contracts/functionComparisonExample.js";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { functionDossierSchema } from "../../../src/domain/hopperValues.js";
import { functionDossierSchema } from "../../../src/domain/native/functionDossier.js";
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
@@ -6,7 +6,7 @@ import { describe, expect, it } from "vitest";
import { z } from "zod";
import { OFFICIAL_TOOL_CONTRACTS } from "../../../../src/contracts/officialToolContracts.js";
import { nativeSymbolMetadataSchema } from "../../../../src/domain/native/nativeSymbol.js";
import { functionDossierSchema } from "../../../../src/domain/hopperValues.js";
import { functionDossierSchema } from "../../../../src/domain/native/functionDossier.js";
const execute = promisify(execFile);
const bridgePath = new URL(
@@ -1,7 +1,7 @@
import { rm } from "node:fs/promises";
import { afterEach, describe, expect, it } from "vitest";
import { z } from "zod";
import { functionDossierSchema } from "../../../../src/domain/hopperValues.js";
import { functionDossierSchema } from "../../../../src/domain/native/functionDossier.js";
import {
jsonValueSchema,
type JsonValue,
@@ -19,7 +19,7 @@ import {
} from "../../../../src/domain/managed/managedNativeVerificationSchemas.js";
import { managedNativeBoundaryInspectionSchema } from "../../../../src/domain/managed/managedArtifact.js";
import { inspectMachoSchema } from "../../../../src/domain/native/nativeInspection.js";
import { functionDossierSchema } from "../../../../src/domain/hopperValues.js";
import { functionDossierSchema } from "../../../../src/domain/native/functionDossier.js";
import { verifyManagedNativeVerificationSource } from "../../../../src/domain/managed/managedNativeVerification.js";
import { readManagedNativeVerificationSource } from "../../../../src/domain/managed/managedNativeVerificationSource.js";
+1 -1
View File
@@ -21,7 +21,7 @@ import { createJavaScriptSemanticGraph } from "../../src/domain/javascript/javas
import type { BinaryLayout } from "../../src/domain/native/binaryLayout.js";
import { binaryLayoutSchema } from "../../src/domain/native/binaryLayout.js";
import { ghidraFunctionDossier } from "../../src/domain/ghidraValues.fixture.js";
import { functionDossierSchema } from "../../src/domain/hopperValues.js";
import { functionDossierSchema } from "../../src/domain/native/functionDossier.js";
import type { RelationshipCoverage } from "../../src/domain/relationshipCoverage.js";
/** Canonical portable Evidence; no native provider is started by this fixture. */
+1 -1
View File
@@ -2,7 +2,7 @@ import { createEvidence, type Evidence } from "../../src/domain/evidence.js";
import { FUNCTION_COMPARISON_EXAMPLE } from "../../src/contracts/functionComparisonExample.js";
import type { NativeSymbolMetadata } from "../../src/domain/native/nativeSymbol.js";
import { functionDossierSchema } from "../../src/domain/hopperValues.js";
import { functionDossierSchema } from "../../src/domain/native/functionDossier.js";
/** Explicit synthetic producer facts; they do not qualify any real provider. */
export const nativeMatchingProvider = {