Files
reverse-engineering-anything/src/domain/managed/managedMemberComparisonItems.ts
T

997 lines
33 KiB
TypeScript

import canonicalize from "canonicalize";
import { digestCanonicalValue } from "../canonicalDigest.js";
import type { ManagedMemberInspection } from "./managedArtifact.js";
import type { ManagedMemberComparisonResult } from "./managedMemberComparison.js";
import type {
ManagedFieldMatches,
ManagedMethodMatches,
} from "./managedMemberComparisonMatch.js";
import { signatureIdentity } from "./managedMemberSignatureIdentity.js";
import { isManagedMemberReferenceInstruction } from "./managedMemberInstructionFacts.js";
import type { ManagedComparisonScope } from "./managedMemberComparisonCoverage.js";
type Method = ManagedMemberInspection["methods"][number];
type Field = ManagedMemberInspection["fields"][number];
type CallEdgesByCaller = ReadonlyMap<
string,
readonly ManagedMemberInspection["call_edges"][number][]
>;
type FieldAccessesByCaller = ReadonlyMap<
string,
readonly ManagedMemberInspection["field_accesses"][number][]
>;
type MethodItem = ManagedMemberComparisonResult["methods"][number];
type FieldItem = ManagedMemberComparisonResult["fields"][number];
type UnmatchedComparison = Extract<
MethodItem["match"],
{ readonly status: "unmatched" }
>;
type OneSided<Item> =
| { readonly left: Item; readonly right?: never }
| { readonly left?: never; readonly right: Item };
interface ComparisonItemContext {
readonly leftEvidenceId: string;
readonly rightEvidenceId: string;
readonly leftComplete: boolean;
readonly rightComplete: boolean;
readonly leftMethodsComplete: boolean;
readonly rightMethodsComplete: boolean;
readonly scope: ManagedComparisonScope;
/** Whether matching keyed signatures by resolved digests, not raw bytes. */
readonly signatureResolutionExamined: boolean;
readonly sameArtifact: boolean;
readonly leftAssembly: ManagedMemberInspection["assembly"];
readonly rightAssembly: ManagedMemberInspection["assembly"];
readonly leftCallEdgesByCaller: CallEdgesByCaller;
readonly rightCallEdgesByCaller: CallEdgesByCaller;
readonly leftFieldAccessesByCaller: FieldAccessesByCaller;
readonly rightFieldAccessesByCaller: FieldAccessesByCaller;
}
/** Whether an inventory observed every member of this member's declaring type. */
const inventoryCovers = (
complete: boolean,
observes: (member: Method | Field) => boolean,
member: Method | Field,
): boolean => complete && observes(member);
/**
* Declared type and name of each unpaired member, per side, whose signature
* was not decoded, or was decoded but not resolved while matching keyed
* resolved signatures.
*/
interface UndecodedOneSidedNames {
readonly resolved: boolean;
readonly undecoded: Readonly<Record<"left" | "right", ReadonlySet<string>>>;
readonly unresolved: Readonly<Record<"left" | "right", ReadonlySet<string>>>;
readonly unknownOwners: Readonly<
Record<"left" | "right", ReadonlySet<string>>
>;
}
const methodDimensions = (
left: Method,
right: Method,
leftCallEdgesByCaller: CallEdgesByCaller,
rightCallEdgesByCaller: CallEdgesByCaller,
leftFieldAccessesByCaller: FieldAccessesByCaller,
rightFieldAccessesByCaller: FieldAccessesByCaller,
leftAssembly: ManagedMemberInspection["assembly"],
rightAssembly: ManagedMemberInspection["assembly"],
): {
readonly dimensions: MethodItem["dimensions"];
readonly bodyUnknown: boolean;
readonly callTargetUnknown: boolean;
readonly fieldTargetUnknown: boolean;
readonly exceptionTargetUnknown: boolean;
} => {
const dimensions: MethodItem["dimensions"] = [];
const headerFields = ["max_stack", "init_locals"] as const;
const headerChanged = headerFields.some(
(field) =>
left.body[field] !== null &&
right.body[field] !== null &&
left.body[field] !== right.body[field],
);
const localsPresenceChanged =
left.body.status === "present" &&
right.body.status === "present" &&
(left.body.local_var_sig_token !== null) !==
(right.body.local_var_sig_token !== null);
if (
left.flags !== right.flags ||
left.impl_flags !== right.impl_flags ||
headerChanged ||
localsPresenceChanged
)
dimensions.push("metadata");
if (signatureIdentity(left.signature) !== signatureIdentity(right.signature))
dimensions.push("signature");
const leftBodyComplete =
left.body.status === "present" && left.body.truncated_instructions === 0;
const rightBodyComplete =
right.body.status === "present" && right.body.truncated_instructions === 0;
const bothAbsent =
left.body.status === "absent" && right.body.status === "absent";
if (
(left.body.status === "absent" && rightBodyComplete) ||
(right.body.status === "absent" && leftBodyComplete)
)
dimensions.push("availability");
if (bothAbsent)
return {
dimensions,
bodyUnknown: false,
callTargetUnknown: false,
fieldTargetUnknown: false,
exceptionTargetUnknown: false,
};
const sameCallShape =
callShape(left.body.anchors) === callShape(right.body.anchors);
const callTargets = sameCallShape
? compareCallTargets(
left,
right,
leftCallEdgesByCaller.get(left.token) ?? [],
rightCallEdgesByCaller.get(right.token) ?? [],
)
: { changed: false, unknown: false };
const sameFieldShape =
fieldShape(left.body.anchors) === fieldShape(right.body.anchors);
const fieldTargets = sameFieldShape
? compareFieldTargets(
left,
right,
leftFieldAccessesByCaller.get(left.token) ?? [],
rightFieldAccessesByCaller.get(right.token) ?? [],
)
: { changed: false, unknown: false };
const sameExceptionShape =
exceptionShape(left.body.exception_regions) ===
exceptionShape(right.body.exception_regions);
const exceptionTargets = sameExceptionShape
? compareExceptionTargets(
left.body.exception_regions,
right.body.exception_regions,
leftAssembly,
rightAssembly,
)
: { changed: false, unknown: false };
if (callTargets.changed) dimensions.push("call-target");
if (fieldTargets.changed) dimensions.push("field-target");
if (exceptionTargets.changed) dimensions.push("exception-target");
if (!leftBodyComplete || !rightBodyComplete)
return {
dimensions,
bodyUnknown: true,
callTargetUnknown: callTargets.unknown,
fieldTargetUnknown: fieldTargets.unknown,
exceptionTargetUnknown: exceptionTargets.unknown,
};
if (!sameCallShape) dimensions.push("call-shape");
// The decoded fingerprint omits switch targets and is not exact CIL identity.
if (
left.body.il_sha256 !== right.body.il_sha256 ||
left.body.normalized_il_sha256 !== right.body.normalized_il_sha256
)
dimensions.push("cil");
if (
canonicalize(left.body.opcode_counts) !==
canonicalize(right.body.opcode_counts)
)
dimensions.push("opcode-shape");
if (!sameFieldShape) dimensions.push("field-shape");
if (!sameExceptionShape) dimensions.push("exception-shape");
return {
dimensions,
callTargetUnknown: callTargets.unknown,
fieldTargetUnknown: fieldTargets.unknown,
exceptionTargetUnknown: exceptionTargets.unknown,
bodyUnknown: headerFields.some(
(field) => left.body[field] === null || right.body[field] === null,
),
};
};
const exceptionShape = (regions: Method["body"]["exception_regions"]): string =>
JSON.stringify(
regions.map(
({
flags,
try_offset,
try_length,
handler_offset,
handler_length,
filter_offset,
}) => [
flags,
try_offset,
try_length,
handler_offset,
handler_length,
filter_offset,
],
),
);
const compareExceptionTargets = (
left: Method["body"]["exception_regions"],
right: Method["body"]["exception_regions"],
leftAssembly: ManagedMemberInspection["assembly"],
rightAssembly: ManagedMemberInspection["assembly"],
): { readonly changed: boolean; readonly unknown: boolean } => {
let changed = false;
let unknown = false;
for (let index = 0; index < left.length; index += 1) {
const leftRegion = left[index];
const rightRegion = right[index];
if (leftRegion === undefined || rightRegion === undefined) {
unknown = true;
continue;
}
if (leftRegion.flags !== 0) continue;
const leftName = leftRegion.class_type_name_sha256;
const rightName = rightRegion.class_type_name_sha256;
if (leftName !== null && rightName !== null && leftName !== rightName) {
changed = true;
if (
leftRegion.class_type_identity_resolved !== true ||
rightRegion.class_type_identity_resolved !== true
)
unknown = true;
continue;
}
if (leftName === null || rightName === null) {
if (
leftRegion.class_type !== null &&
rightRegion.class_type !== null &&
leftRegion.class_type !== rightRegion.class_type
)
changed = true;
unknown = true;
continue;
}
if (
leftRegion.class_type_identity_resolved !== true ||
rightRegion.class_type_identity_resolved !== true ||
leftAssembly === null ||
rightAssembly === null
) {
unknown = true;
continue;
}
if (!sameAssemblyIdentity(leftAssembly, rightAssembly)) changed = true;
}
return { changed, unknown };
};
const sameAssemblyIdentity = (
left: NonNullable<ManagedMemberInspection["assembly"]>,
right: NonNullable<ManagedMemberInspection["assembly"]>,
): boolean =>
left.name.toLowerCase() === right.name.toLowerCase() &&
left.version === right.version &&
left.culture?.toLowerCase() === right.culture?.toLowerCase() &&
(left.flags & 0x0f00) === (right.flags & 0x0f00) &&
left.public_key.kind === right.public_key.kind &&
left.public_key.sha256 === right.public_key.sha256 &&
left.public_key.token === right.public_key.token;
interface ReferenceTarget {
readonly offset: number | null;
readonly opcode: string;
readonly token: string;
readonly identity: string | null;
}
/** Join each edge to its instruction site, then compare aligned instructions. */
const compareReferenceTargets = (
left: Method,
right: Method,
kind: "field" | "method",
leftTargets: readonly ReferenceTarget[],
rightTargets: readonly ReferenceTarget[],
): { readonly changed: boolean; readonly unknown: boolean } => {
const align = (method: Method, targets: readonly ReferenceTarget[]) => {
const anchors = method.body.anchors.filter(
(anchor) =>
isManagedMemberReferenceInstruction(anchor) &&
anchor.operand_kind === kind,
);
const byOffset = new Map<number | null, ReferenceTarget | null>();
for (const target of targets)
byOffset.set(target.offset, byOffset.has(target.offset) ? null : target);
let unknown = targets.length !== anchors.length;
const aligned = anchors.map((anchor) => {
const target = byOffset.get(anchor.il_offset);
if (
target == null ||
target.opcode !== anchor.opcode ||
target.token !== anchor.operand
) {
unknown = true;
return null;
}
return target;
});
return { aligned, unknown };
};
const a = align(left, leftTargets);
const b = align(right, rightTargets);
let changed = false;
let unknown = a.unknown || b.unknown || a.aligned.length !== b.aligned.length;
for (
let index = 0;
index < Math.max(a.aligned.length, b.aligned.length);
index += 1
) {
const leftIdentity = a.aligned[index]?.identity;
const rightIdentity = b.aligned[index]?.identity;
if (leftIdentity == null || rightIdentity == null) unknown = true;
else if (leftIdentity !== rightIdentity) changed = true;
}
return { changed, unknown };
};
const compareFieldTargets = (
left: Method,
right: Method,
leftAccesses: readonly ManagedMemberInspection["field_accesses"][number][],
rightAccesses: readonly ManagedMemberInspection["field_accesses"][number][],
) =>
compareReferenceTargets(
left,
right,
"field",
leftAccesses.map((edge) => ({
offset: edge.il_offset,
opcode: edge.opcode,
token: edge.field_token,
identity: edge.target_identity_sha256,
})),
rightAccesses.map((edge) => ({
offset: edge.il_offset,
opcode: edge.opcode,
token: edge.field_token,
identity: edge.target_identity_sha256,
})),
);
const compareCallTargets = (
left: Method,
right: Method,
leftEdges: readonly ManagedMemberInspection["call_edges"][number][],
rightEdges: readonly ManagedMemberInspection["call_edges"][number][],
) =>
compareReferenceTargets(
left,
right,
"method",
leftEdges.map((edge) => ({
offset: edge.il_offset,
opcode: edge.opcode,
token: edge.target_token,
identity: edge.target_identity_sha256,
})),
rightEdges.map((edge) => ({
offset: edge.il_offset,
opcode: edge.opcode,
token: edge.target_token,
identity: edge.target_identity_sha256,
})),
);
const callShape = (anchors: Method["body"]["anchors"]): string =>
JSON.stringify(
anchors
.filter(
(instruction) =>
isManagedMemberReferenceInstruction(instruction) &&
instruction.operand_kind === "method",
)
.map(({ opcode, operand_kind }) => [opcode, operand_kind]),
);
const fieldShape = (anchors: Method["body"]["anchors"]): string =>
JSON.stringify(
anchors
.filter(
(instruction) =>
isManagedMemberReferenceInstruction(instruction) &&
instruction.operand_kind === "field",
)
.map(({ opcode, operand_kind }) => [opcode, operand_kind]),
);
const methodIdentity = (item: Method): NonNullable<MethodItem["left"]> => ({
token: item.token,
declaring_type: item.declaring_type,
name: item.name,
signature_sha256: item.signature.raw_sha256,
normalized_il_sha256: item.body.normalized_il_sha256,
});
const fieldIdentity = (item: Field): NonNullable<FieldItem["left"]> => ({
token: item.token,
declaring_type: item.declaring_type,
name: item.name,
signature_sha256: item.signature.raw_sha256,
});
const unmatchedComparison = (): UnmatchedComparison => ({
status: "unmatched",
basis: "none",
confidence: "unknown",
candidate_left_tokens: [],
candidate_right_tokens: [],
});
const methodOnlyItem = (
member: OneSided<Method>,
identity: string,
identityUnavailable: boolean,
context: ComparisonItemContext,
undecoded: UndecodedOneSidedNames,
): MethodItem => {
if (member.left !== undefined) {
const limitations = absenceLimitations(member.left, identity, {
oppositeComplete: inventoryCovers(
context.rightMethodsComplete,
context.scope.inRight,
member.left,
),
unpaired: undecoded,
ownIdentityUnavailable: identityUnavailable,
opposite: "right",
inventory: "method",
});
return {
item_id: `mmc_method_${digestCanonicalValue({ token: member.left.token, side: "left" }, "Managed comparison")}`,
status: limitations.length === 0 ? "removed" : "unknown",
left: methodIdentity(member.left),
right: null,
match: unmatchedComparison(),
dimensions: ["availability"],
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
limitations,
};
}
const limitations = absenceLimitations(member.right, identity, {
oppositeComplete: inventoryCovers(
context.leftMethodsComplete,
context.scope.inLeft,
member.right,
),
unpaired: undecoded,
ownIdentityUnavailable: identityUnavailable,
opposite: "left",
inventory: "method",
});
return {
item_id: `mmc_method_${digestCanonicalValue({ token: member.right.token, side: "right" }, "Managed comparison")}`,
status: limitations.length === 0 ? "added" : "unknown",
left: null,
right: methodIdentity(member.right),
match: unmatchedComparison(),
dimensions: ["availability"],
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
limitations,
};
};
/**
* Why a one-sided member's absence from the other inventory was not
* observed: that inventory is incomplete, the member's own signature was not
* decoded, so no shape round could seek a changed counterpart, or the other
* inventory has an unpaired (one-sided or ambiguous) same-named member whose
* signature was not decoded, which may be this member with a changed
* signature. A decoded signature that names a type REA could not name, while
* the comparison otherwise keys resolved signatures, is compared by raw bytes
* that name types by build-local row, so it is treated the same way.
*/
const absenceLimitations = (
member: Method | Field,
identity: string,
opposite: {
readonly oppositeComplete: boolean;
readonly ownIdentityUnavailable: boolean;
readonly unpaired: UndecodedOneSidedNames;
readonly opposite: "left" | "right";
readonly inventory: "method" | "field";
},
): string[] => [
...(opposite.ownIdentityUnavailable
? [
"declaring-type-name-not-resolved: Declared type identity could not be resolved; a bare display name cannot establish a counterpart or absence.",
]
: []),
...(opposite.unpaired.unknownOwners[opposite.opposite].has(member.name)
? [
`counterpart-declaring-type-name-not-resolved: The ${opposite.opposite} ${opposite.inventory} inventory has an unpaired same-named member with unresolved enclosing-name identity, so absence was not established.`,
]
: []),
...(opposite.oppositeComplete
? []
: [
`unknown-within-incomplete-metadata: The ${opposite.opposite} ${opposite.inventory} inventory is incomplete, so absence was not observed.`,
]),
...(member.signature.parse_status === "decoded"
? []
: [
`signature-not-decoded: The member's ${member.signature.parse_status} signature was compared only by exact declared type, name, and signature bytes, so a changed or renamed counterpart could not be sought.`,
]),
...(member.signature.parse_status === "decoded" &&
opposite.unpaired.undecoded[opposite.opposite].has(identity)
? [
`counterpart-signature-not-decoded: The ${opposite.opposite} ${opposite.inventory} inventory has an unpaired member with the same declared type and name whose signature was not decoded, so it may be this member with a changed signature.`,
]
: []),
...(unresolved(member, opposite.unpaired.resolved)
? [
`signature-not-resolved: The member's signature names a type REA could not name, so it was compared by raw bytes, which name types by build-local metadata row; a counterpart whose referenced type moved rows could not be found.`,
]
: []),
...(member.signature.parse_status === "decoded" &&
!unresolved(member, opposite.unpaired.resolved) &&
opposite.unpaired.unresolved[opposite.opposite].has(identity)
? [
`counterpart-signature-not-resolved: The ${opposite.opposite} ${opposite.inventory} inventory has an unpaired member with the same declared type and name whose signature names a type REA could not name, so it may be this member.`,
]
: []),
];
/** A known signature form whose type operands still compare by raw bytes. */
const unresolved = (member: Method | Field, resolved: boolean): boolean =>
resolved &&
member.signature.kind !== "unknown" &&
member.signature.resolved_sha256 === null;
/** Exact bytes retain a pair without establishing a valid or resolved signature. */
const pairedSignatureLimitations = (
left: Method | Field,
right: Method | Field,
examined: boolean,
): string[] => {
if (
left.signature.parse_status === "malformed" ||
right.signature.parse_status === "malformed"
)
return [
"signature-malformed: At least one signature is malformed; equal signature bytes do not establish an unchanged member signature.",
];
return unresolved(left, examined) || unresolved(right, examined)
? [
"signature-not-resolved: At least one signature names a type REA could not resolve; equal build-local row bytes do not establish equivalent signature types.",
]
: [];
};
/**
* Declared names of unpaired members whose signature was not decoded: one-sided
* members and candidates of ambiguous groups, which also remain unpaired.
*/
interface Unpaired {
readonly item: Method | Field;
readonly identity: string;
readonly identityUnavailable: boolean;
}
const undecodedOneSidedNames = (
members: {
readonly leftOnly: readonly Unpaired[];
readonly rightOnly: readonly Unpaired[];
readonly ambiguous: readonly {
readonly left: readonly Unpaired[];
readonly right: readonly Unpaired[];
}[];
},
resolved: boolean,
): UndecodedOneSidedNames => {
const sides = {
left: [
...members.leftOnly,
...members.ambiguous.flatMap(({ left }) => left),
],
right: [
...members.rightOnly,
...members.ambiguous.flatMap(({ right }) => right),
],
};
const names = (
side: readonly Unpaired[],
keep: (item: Method | Field) => boolean,
) =>
new Set(
side.filter(({ item }) => keep(item)).map(({ identity }) => identity),
);
const undecoded = (item: Method | Field) =>
item.signature.parse_status !== "decoded";
const notResolved = (item: Method | Field) => unresolved(item, resolved);
return {
resolved,
unknownOwners: {
left: new Set(
sides.left.filter((x) => x.identityUnavailable).map((x) => x.item.name),
),
right: new Set(
sides.right
.filter((x) => x.identityUnavailable)
.map((x) => x.item.name),
),
},
undecoded: {
left: names(sides.left, undecoded),
right: names(sides.right, undecoded),
},
unresolved: {
left: names(sides.left, notResolved),
right: names(sides.right, notResolved),
},
};
};
const fieldOnlyItem = (
member: OneSided<Field>,
identity: string,
identityUnavailable: boolean,
context: ComparisonItemContext,
undecoded: UndecodedOneSidedNames,
): FieldItem => {
if (member.left !== undefined) {
const limitations = absenceLimitations(member.left, identity, {
oppositeComplete: inventoryCovers(
context.rightComplete,
context.scope.inRight,
member.left,
),
unpaired: undecoded,
ownIdentityUnavailable: identityUnavailable,
opposite: "right",
inventory: "field",
});
return {
item_id: `mmc_field_${digestCanonicalValue({ token: member.left.token, side: "left" }, "Managed comparison")}`,
status: limitations.length === 0 ? "removed" : "unknown",
left: fieldIdentity(member.left),
right: null,
match: unmatchedComparison(),
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
limitations,
};
}
const limitations = absenceLimitations(member.right, identity, {
oppositeComplete: inventoryCovers(
context.leftComplete,
context.scope.inLeft,
member.right,
),
unpaired: undecoded,
ownIdentityUnavailable: identityUnavailable,
opposite: "left",
inventory: "field",
});
return {
item_id: `mmc_field_${digestCanonicalValue({ token: member.right.token, side: "right" }, "Managed comparison")}`,
status: limitations.length === 0 ? "added" : "unknown",
left: null,
right: fieldIdentity(member.right),
match: unmatchedComparison(),
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
limitations,
};
};
/**
* Name only evidence shared by the whole group. Exact-signature ambiguities
* include both declared-identity collisions and unresolved signature choices.
*/
const ambiguityLimitation = (
members: "methods" | "fields",
basis: MethodItem["match"]["basis"],
identityUnavailable: boolean,
): string =>
identityUnavailable
? "declaring-type-name-not-resolved: Declared type identity remains unresolved; REA did not guess a counterpart from display names, signatures or CIL."
: basis === "exact-signature"
? `Multiple managed ${members} share the same signature; REA did not guess declared ownership or a token remap.`
: basis === "exact-il-signature"
? "Multiple managed methods share the same CIL and signature; REA did not guess a token remap."
: members === "methods"
? "Multiple managed methods share the same structural shape; REA did not guess a token remap."
: "Multiple managed fields share the same signature; REA did not guess a token remap.";
/** Local signature bytes prove identity only without metadata or declaring-context dependencies. */
const compareLocalSignatures = (
left: Method,
right: Method,
sameArtifact: boolean,
): { readonly changed: boolean; readonly unknown: boolean } => {
const leftPresent = left.body.local_var_sig_token !== null;
const rightPresent = right.body.local_var_sig_token !== null;
if (!leftPresent || !rightPresent) return { changed: false, unknown: false };
// The same method row of the same authenticated artifact names the same source bytes.
if (sameArtifact && left.token === right.token)
return { changed: false, unknown: false };
const a = left.body.local_var_signature;
const b = right.body.local_var_signature;
if (
a === null ||
b === null ||
a.has_context_dependent_types ||
b.has_context_dependent_types
)
return { changed: false, unknown: true };
return { changed: a.raw_sha256 !== b.raw_sha256, unknown: false };
};
export const buildMethodItems = (
matches: ManagedMethodMatches,
context: ComparisonItemContext,
): MethodItem[] => {
const items: MethodItem[] = [];
for (const pair of matches.pairs) {
const signatureLimitations = pairedSignatureLimitations(
pair.left.item,
pair.right.item,
context.signatureResolutionExamined,
);
const locals = compareLocalSignatures(
pair.left.item,
pair.right.item,
context.sameArtifact,
);
const {
dimensions: observedDimensions,
bodyUnknown,
callTargetUnknown,
fieldTargetUnknown,
exceptionTargetUnknown,
} = methodDimensions(
pair.left.item,
pair.right.item,
context.leftCallEdgesByCaller,
context.rightCallEdgesByCaller,
context.leftFieldAccessesByCaller,
context.rightFieldAccessesByCaller,
context.leftAssembly,
context.rightAssembly,
);
if (locals.changed && !observedDimensions.includes("metadata"))
observedDimensions.push("metadata");
const dimensions = [
...observedDimensions,
...(bodyUnknown ? (["body-coverage"] as const) : []),
...(callTargetUnknown ||
fieldTargetUnknown ||
exceptionTargetUnknown ||
locals.unknown
? (["availability"] as const)
: []),
];
const observedChange = observedDimensions.length > 0;
items.push({
item_id: `mmc_method_${digestCanonicalValue(
{
left: pair.left.item.token,
right: pair.right.item.token,
basis: pair.basis,
},
"Managed comparison",
)}`,
status: observedChange
? "changed"
: bodyUnknown ||
callTargetUnknown ||
fieldTargetUnknown ||
exceptionTargetUnknown ||
locals.unknown ||
signatureLimitations.length > 0
? "unknown"
: "unchanged",
left: methodIdentity(pair.left.item),
right: methodIdentity(pair.right.item),
match: {
status: "matched",
basis: pair.basis,
confidence: pair.confidence,
candidate_left_tokens: [],
candidate_right_tokens: [],
},
dimensions,
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
limitations: [
...signatureLimitations,
...(locals.unknown
? [
"Local variable signature identity is unknown because validated context-free signature facts are unavailable or the signature requires metadata or declaring context; build-local tokens and raw bytes do not establish equivalent local types across artifacts.",
]
: []),
...(locals.changed
? [
"The validated context-free LocalVarSig bytes changed; this records an encoding difference without asserting a semantic type change.",
]
: []),
...(bodyUnknown
? [
"Method body facets are unknown because at least one side has unavailable or partial body data.",
]
: []),
...(callTargetUnknown
? [
"One or more call targets are unavailable because the observed method edges do not cover aligned call instructions or lack a resolved nominal target identity or instruction site.",
]
: []),
...(fieldTargetUnknown
? [
"One or more field targets are unavailable because observed field accesses do not cover aligned field instructions or lack a resolved nominal target identity or instruction site.",
]
: []),
...(exceptionTargetUnknown
? [
"One or more catch type identities are unavailable because local tokens or TypeRef resolution scopes do not establish cross-build identity.",
]
: []),
],
});
}
for (const ambiguous of matches.ambiguous) {
items.push({
item_id: `mmc_method_${digestCanonicalValue(
{
left: ambiguous.left.map(({ item }) => item.token),
right: ambiguous.right.map(({ item }) => item.token),
basis: ambiguous.basis,
},
"Managed comparison",
)}`,
status: "unknown",
left: null,
right: null,
match: {
status: "ambiguous",
basis: ambiguous.basis,
confidence: "unknown",
candidate_left_tokens: ambiguous.left.map(({ item }) => item.token),
candidate_right_tokens: ambiguous.right.map(({ item }) => item.token),
},
dimensions: ["availability"],
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
limitations: [
ambiguityLimitation(
"methods",
ambiguous.basis,
ambiguous.left.some((item) => item.identityUnavailable) ||
ambiguous.right.some((item) => item.identityUnavailable),
),
],
});
}
const undecoded = undecodedOneSidedNames(
matches,
context.signatureResolutionExamined,
);
for (const item of matches.leftOnly)
items.push(
methodOnlyItem(
{ left: item.item },
item.identity,
item.identityUnavailable,
context,
undecoded,
),
);
for (const item of matches.rightOnly)
items.push(
methodOnlyItem(
{ right: item.item },
item.identity,
item.identityUnavailable,
context,
undecoded,
),
);
return items;
};
export const buildFieldItems = (
matches: ManagedFieldMatches,
context: ComparisonItemContext,
): FieldItem[] => {
const items: FieldItem[] = [];
for (const pair of matches.pairs) {
const signatureLimitations = pairedSignatureLimitations(
pair.left.item,
pair.right.item,
context.signatureResolutionExamined,
);
const changed =
signatureIdentity(pair.left.item.signature) !==
signatureIdentity(pair.right.item.signature) ||
pair.left.item.flags !== pair.right.item.flags;
items.push({
item_id: `mmc_field_${digestCanonicalValue(
{
left: pair.left.item.token,
right: pair.right.item.token,
},
"Managed comparison",
)}`,
status: changed
? "changed"
: signatureLimitations.length > 0
? "unknown"
: "unchanged",
left: fieldIdentity(pair.left.item),
right: fieldIdentity(pair.right.item),
match: {
status: "matched",
basis: pair.basis,
confidence: pair.confidence,
candidate_left_tokens: [],
candidate_right_tokens: [],
},
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
limitations: signatureLimitations,
});
}
for (const ambiguous of matches.ambiguous)
items.push({
item_id: `mmc_field_${digestCanonicalValue(
{
left: ambiguous.left.map(({ item }) => item.token),
right: ambiguous.right.map(({ item }) => item.token),
},
"Managed comparison",
)}`,
status: "unknown",
left: null,
right: null,
match: {
status: "ambiguous",
basis: ambiguous.basis,
confidence: "unknown",
candidate_left_tokens: ambiguous.left.map(({ item }) => item.token),
candidate_right_tokens: ambiguous.right.map(({ item }) => item.token),
},
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
limitations: [
ambiguityLimitation(
"fields",
ambiguous.basis,
ambiguous.left.some((item) => item.identityUnavailable) ||
ambiguous.right.some((item) => item.identityUnavailable),
),
],
});
const undecoded = undecodedOneSidedNames(
matches,
context.signatureResolutionExamined,
);
for (const item of matches.leftOnly)
items.push(
fieldOnlyItem(
{ left: item.item },
item.identity,
item.identityUnavailable,
context,
undecoded,
),
);
for (const item of matches.rightOnly)
items.push(
fieldOnlyItem(
{ right: item.item },
item.identity,
item.identityUnavailable,
context,
undecoded,
),
);
return items;
};