mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
feat(managed)!: show readable type names in member signatures (#5309)
feat(managed): show readable type names in member signatures Co-authored-by: gensokyo-dev <[email protected]>
This commit is contained in:
1 parent
afc18131f4
commit
c9bf6a9df7
12 files changed
+566
-75
No files matched your search
@@ -476,8 +476,18 @@ raw digest stands in. Missing or unreadable TypeDef and TypeRef operands are
|
||||
reported as coverage issues. Invalid NestedClass rows retain their failure
|
||||
reasons and withhold TypeDef signature identities; type names and nesting are
|
||||
parsed once by their shared owner. Each current inspection records
|
||||
`signature_resolution: "type-names-v1"`, even when none of its operands can be
|
||||
resolved. Historical Evidence defaults this attestation to `null`. When either
|
||||
`signature_resolution: "type-names-v2"`, even when none of its operands can be
|
||||
resolved. Version 2 also appends readable names to signature operands, for
|
||||
example `class:7772 (AnalysisJob)`. Coded indexes remain build-local references;
|
||||
names are display text and do not change raw or resolved signature digests.
|
||||
Nested names use `Outer/Inner`. Generic instantiations keep their decoded
|
||||
arguments, for example ``class:9 (System.Collections.Generic.List`1)<string>``;
|
||||
custom modifiers are named too. Valid but unresolved TypeSpecs, invalid scopes,
|
||||
unreadable type names, cycles and nesting beyond the cap keep their raw operand
|
||||
spelling. Invalid or missing metadata coordinates retain the existing malformed
|
||||
signature result and source issue; rendering does not bypass validation.
|
||||
Version 1 Evidence remains readable and compares using the same digest policy.
|
||||
Historical Evidence defaults this attestation to `null`. When either
|
||||
observation predates resolution, both sides compare raw digests, which pair only where type rows agree,
|
||||
and the comparison reports that limitation. The signature is exact whether or
|
||||
not REA decoded it, so undecoded signatures pair by that tuple too, but they
|
||||
|
||||
@@ -514,11 +514,11 @@ export const managedArtifactInspectionSchema = z.object({
|
||||
/** Provider-neutral, execution-free metadata/signature/IL member inspection. */
|
||||
export const managedMemberInspectionSchema = z.object({
|
||||
signature_resolution: z
|
||||
.literal("type-names-v1")
|
||||
.enum(["type-names-v1", "type-names-v2"])
|
||||
.nullable()
|
||||
.default(null)
|
||||
.describe(
|
||||
"Signature type-name resolution was examined, including unresolved operands; null means no attestation, including historical or capacity-limited Evidence.",
|
||||
"Signature type-name resolution was examined, including unresolved operands; v2 also renders readable operand names, v1 retains raw operand text; null means no attestation, including historical or capacity-limited Evidence.",
|
||||
),
|
||||
artifact: managedArtifactReferenceSchema,
|
||||
module: moduleIdentitySchema.nullable(),
|
||||
|
||||
@@ -2504,12 +2504,12 @@ it("decodes generic instantiation member signatures as complete coverage", () =>
|
||||
expect(result.methods[0]?.signature).toMatchObject({
|
||||
parse_status: "decoded",
|
||||
return_type: "void",
|
||||
parameter_types: ["class:9<string>"],
|
||||
parameter_types: ["class:9 (System.Func`1)<string>"],
|
||||
issue: null,
|
||||
});
|
||||
expect(result.fields[0]?.signature).toMatchObject({
|
||||
parse_status: "decoded",
|
||||
field_type: "valuetype:13<var:0,mvar:1>",
|
||||
field_type: "valuetype:13 (System.ValueTuple`2)<var:0,mvar:1>",
|
||||
});
|
||||
expect(result.coverage).toMatchObject({ state: "complete", issues: [] });
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
type DecodedTypeDefinition,
|
||||
} from "./ManagedMemberInspectorCore.js";
|
||||
import { edgeTargets } from "./ManagedMethodSpecs.js";
|
||||
import { managedTypeNameDigests } from "./ManagedTypeNameDigests.js";
|
||||
import { managedSignatureTypeNames } from "./ManagedSignatureTypeNames.js";
|
||||
import { typeNameLookup } from "./ManagedTypeNames.js";
|
||||
import { managedTypeReferences } from "./ManagedTypeReferences.js";
|
||||
import { parseUserStrings } from "./ManagedUserStrings.js";
|
||||
@@ -81,7 +81,7 @@ const unavailable = (
|
||||
requires_mvid: null,
|
||||
},
|
||||
types: [],
|
||||
signature_resolution: "type-names-v1",
|
||||
signature_resolution: "type-names-v2",
|
||||
nesting_read: false,
|
||||
exported_types: null,
|
||||
user_strings: null,
|
||||
@@ -211,8 +211,11 @@ export const inspectManagedMembersBytes = (
|
||||
typeNames === undefined ? 0 : ranges.length - parsedTypes.types.length;
|
||||
appendManagedParseIssues(issues, parsedTypes.issues);
|
||||
const typeReferences = managedTypeReferences(bytes, layout, issues);
|
||||
const { operand: signatureTypeName, declaration: declarationTypeName } =
|
||||
managedTypeNameDigests(layout, issues, parsedTypes, typeReferences);
|
||||
const {
|
||||
operand: signatureTypeName,
|
||||
declaration: declarationTypeName,
|
||||
displayName: displayTypeName,
|
||||
} = managedSignatureTypeNames(layout, issues, parsedTypes, typeReferences);
|
||||
const types = parsedTypes.types
|
||||
.filter(({ type }) => selected === null || selected.has(type.token))
|
||||
.map(({ row, type }) => {
|
||||
@@ -282,6 +285,7 @@ export const inspectManagedMembersBytes = (
|
||||
appendManagedParseIssues(issues, exportedTypes.issues);
|
||||
const fields = parseFields(bytes, layout, ranges, memberLists, {
|
||||
signatureTypeName,
|
||||
displayTypeName,
|
||||
declarationTypeName,
|
||||
selected,
|
||||
});
|
||||
@@ -295,6 +299,7 @@ export const inspectManagedMembersBytes = (
|
||||
const memberRefs = parseMemberRefs(bytes, layout, {
|
||||
typeName: resolveTypeName,
|
||||
signatureTypeName,
|
||||
displayTypeName,
|
||||
declarationTypeName,
|
||||
});
|
||||
appendManagedParseIssues(issues, memberRefs.issues);
|
||||
@@ -304,6 +309,7 @@ export const inspectManagedMembersBytes = (
|
||||
pe,
|
||||
ranges,
|
||||
signatureTypeName,
|
||||
displayTypeName,
|
||||
declarationTypeName,
|
||||
memberRefKinds: new Map(
|
||||
memberRefs.refs.flatMap((memberRef) =>
|
||||
@@ -394,7 +400,7 @@ export const inspectManagedMembersBytes = (
|
||||
},
|
||||
assembly: inventory.assembly,
|
||||
types,
|
||||
signature_resolution: "type-names-v1",
|
||||
signature_resolution: "type-names-v2",
|
||||
nesting_read: parsedTypes.nesting.complete,
|
||||
exported_types: exportedTypes.exportedTypes,
|
||||
fields: fields.fields,
|
||||
@@ -418,7 +424,7 @@ export const inspectManagedMembersBytes = (
|
||||
"CIL instruction anchors are decoded from file-backed method bodies only; no target assembly is loaded or executed.",
|
||||
"Exported types are reported from ExportedType rows as declared; forwarded types are not resolved in their implementing assemblies.",
|
||||
"User strings are the #US literals loaded by ldstr in the inspected method bodies, with their own coverage; instruction anchors and normalized CIL digests keep their build-local tokens.",
|
||||
"Member signatures decode ECMA-335 primitive, class, valuetype, generic instantiation, generic variable, pointer, byref, single-dimension and general array, function pointer including VARARG sentinels, custom modifier, and typed reference forms; class and valuetype operands keep their encoded TypeDefOrRefOrSpec values.",
|
||||
"Member signatures decode ECMA-335 primitive, class, valuetype, generic instantiation, generic variable, pointer, byref, single-dimension and general array, function pointer including VARARG sentinels, custom modifier, and typed reference forms; type operands keep their encoded TypeDefOrRefOrSpec values and append readable TypeDef/TypeRef names when resolved.",
|
||||
...selectionLimitations,
|
||||
],
|
||||
},
|
||||
|
||||
@@ -578,6 +578,7 @@ const readTypeSignature = (
|
||||
layout,
|
||||
operands,
|
||||
onElement,
|
||||
displayTypeName,
|
||||
}: {
|
||||
/** Validate each type operand against these tables, as specs require. */
|
||||
readonly layout?: ManagedMetadataLayout;
|
||||
@@ -585,6 +586,7 @@ const readTypeSignature = (
|
||||
readonly operands?: TypeOperand[];
|
||||
/** Observe parsed element kinds without reinterpreting signature bytes. */
|
||||
readonly onElement?: (kind: number) => void;
|
||||
readonly displayTypeName?: SignatureDisplayTypeName | undefined;
|
||||
} = {},
|
||||
): {
|
||||
readonly value: string;
|
||||
@@ -592,6 +594,15 @@ const readTypeSignature = (
|
||||
readonly nominalGenericType: NominalGenericType | null;
|
||||
} => {
|
||||
const stack: TypeContinuation[] = [];
|
||||
const namedOperand = (
|
||||
kind: string,
|
||||
operand: Parameters<SignatureTypeName>[0],
|
||||
): string => {
|
||||
// Enclosing continuations copy names during rendering; admit first.
|
||||
const name = displayTypeName?.(operand, (stack.length + 1) * 2);
|
||||
const raw = `${kind}:${String(operand.value)}`;
|
||||
return name === undefined || name === null ? raw : `${raw} (${name})`;
|
||||
};
|
||||
let cursor = offset;
|
||||
let nominalGenericType: NominalGenericType | null = null;
|
||||
let pinned = false;
|
||||
@@ -676,7 +687,7 @@ const readTypeSignature = (
|
||||
);
|
||||
const token = readTypeOperand(blob, cursor + 1, layout);
|
||||
operands?.push({ offset: cursor + 1, ...token });
|
||||
const modifier = `${kind === 0x1f ? "modreq" : "modopt"}:${String(token.value)}`;
|
||||
const modifier = namedOperand(kind === 0x1f ? "modreq" : "modopt", token);
|
||||
// A modifier frame on top was pushed by the directly preceding byte;
|
||||
// keep an adjacent run together and in source order.
|
||||
const top = stack.at(-1);
|
||||
@@ -686,7 +697,7 @@ const readTypeSignature = (
|
||||
} else if (kind === 0x11 || kind === 0x12) {
|
||||
const token = readTypeOperand(blob, cursor + 1, layout);
|
||||
operands?.push({ offset: cursor + 1, ...token });
|
||||
value = `${kind === 0x11 ? "valuetype" : "class"}:${String(token.value)}`;
|
||||
value = namedOperand(kind === 0x11 ? "valuetype" : "class", token);
|
||||
cursor = token.next;
|
||||
} else if (kind === 0x1e || kind === 0x13) {
|
||||
const variable = readCompressed(blob, cursor + 1);
|
||||
@@ -731,7 +742,10 @@ const readTypeSignature = (
|
||||
"generic instantiation has no type arguments",
|
||||
token.next,
|
||||
);
|
||||
const head = `${generic === 0x11 ? "valuetype" : "class"}:${String(token.value)}`;
|
||||
const head = namedOperand(
|
||||
generic === 0x11 ? "valuetype" : "class",
|
||||
token,
|
||||
);
|
||||
stack.push({
|
||||
kind: "list",
|
||||
remaining: argumentCount.value,
|
||||
@@ -975,6 +989,7 @@ const decodeSignature = (
|
||||
operands: TypeOperand[] = [],
|
||||
issueSink?: (issue: ManagedParseIssue) => void,
|
||||
scope = "signature",
|
||||
displayTypeName?: SignatureDisplayTypeName,
|
||||
): DecodedSignature => {
|
||||
let kind: DecodedSignature["kind"] = "unknown";
|
||||
try {
|
||||
@@ -987,6 +1002,7 @@ const decodeSignature = (
|
||||
kind = "field";
|
||||
const fieldType = readTypeSignature(blob, 1, "field", {
|
||||
operands,
|
||||
displayTypeName,
|
||||
...(layout === undefined ? {} : { layout }),
|
||||
});
|
||||
return {
|
||||
@@ -1006,6 +1022,7 @@ const decodeSignature = (
|
||||
kind = "method";
|
||||
const returnType = readTypeSignature(blob, header.next, "return", {
|
||||
operands,
|
||||
displayTypeName,
|
||||
...(layout === undefined ? {} : { layout }),
|
||||
});
|
||||
let offset = returnType.next;
|
||||
@@ -1025,6 +1042,7 @@ const decodeSignature = (
|
||||
}
|
||||
const parameter = readTypeSignature(blob, offset, "parameter", {
|
||||
operands,
|
||||
displayTypeName,
|
||||
...(layout === undefined ? {} : { layout }),
|
||||
});
|
||||
parameters.push(parameter.value);
|
||||
@@ -1110,6 +1128,12 @@ export type SignatureTypeName = (
|
||||
|
||||
export type DeclarationTypeName = (table: 1 | 2, row: number) => Buffer | null;
|
||||
|
||||
/** Display an already decoded operand, admitting its transient copies first. */
|
||||
export type SignatureDisplayTypeName = (
|
||||
operand: Parameters<SignatureTypeName>[0],
|
||||
copies: number,
|
||||
) => string | null;
|
||||
|
||||
/**
|
||||
* Each type operand is replaced by 0xff, which never starts a compressed
|
||||
* coded index, then the 32-byte name digest; a blob without type operands
|
||||
@@ -1165,13 +1189,21 @@ export const signature = (
|
||||
blob: Buffer,
|
||||
rawSha256?: string,
|
||||
typeName: SignatureTypeName = () => null,
|
||||
displayTypeName?: SignatureDisplayTypeName,
|
||||
): ManagedSignature => {
|
||||
const operands: TypeOperand[] = [];
|
||||
return projectSignature(
|
||||
blob,
|
||||
rawSha256,
|
||||
typeName,
|
||||
decodeSignature(blob, undefined, operands),
|
||||
decodeSignature(
|
||||
blob,
|
||||
undefined,
|
||||
operands,
|
||||
undefined,
|
||||
"signature",
|
||||
displayTypeName,
|
||||
),
|
||||
operands,
|
||||
);
|
||||
};
|
||||
@@ -1187,6 +1219,7 @@ export const metadataSignature = (
|
||||
scope,
|
||||
blobOffset,
|
||||
expectedKind,
|
||||
displayTypeName,
|
||||
}: {
|
||||
readonly rawSha256: string;
|
||||
readonly typeName: SignatureTypeName;
|
||||
@@ -1196,6 +1229,7 @@ export const metadataSignature = (
|
||||
/** Artifact file offset of the blob's first byte. */
|
||||
readonly blobOffset: number;
|
||||
readonly expectedKind?: "field" | "method";
|
||||
readonly displayTypeName?: SignatureDisplayTypeName | undefined;
|
||||
},
|
||||
): ManagedSignature => {
|
||||
const operands: TypeOperand[] = [];
|
||||
@@ -1205,6 +1239,7 @@ export const metadataSignature = (
|
||||
operands,
|
||||
(issue) => issues.push(artifactSignatureIssue(issue, scope, blobOffset)),
|
||||
scope,
|
||||
displayTypeName,
|
||||
);
|
||||
if (
|
||||
expectedKind !== undefined &&
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
type DecodedMethod,
|
||||
type MemberCore,
|
||||
type SignatureTypeName,
|
||||
type SignatureDisplayTypeName,
|
||||
type DeclarationTypeName,
|
||||
type TypeRange,
|
||||
} from "./ManagedMemberInspectorCore.js";
|
||||
@@ -65,10 +66,12 @@ export const parseFields = (
|
||||
memberLists: TypeMemberListFacts,
|
||||
{
|
||||
signatureTypeName,
|
||||
displayTypeName,
|
||||
declarationTypeName,
|
||||
selected = null,
|
||||
}: {
|
||||
readonly signatureTypeName: SignatureTypeName;
|
||||
readonly displayTypeName: SignatureDisplayTypeName;
|
||||
readonly declarationTypeName: DeclarationTypeName;
|
||||
readonly selected?: SelectedTypeTokens;
|
||||
},
|
||||
@@ -117,6 +120,7 @@ export const parseFields = (
|
||||
sig,
|
||||
layout,
|
||||
signatureTypeName,
|
||||
displayTypeName,
|
||||
issues,
|
||||
{ scope: `Field:${token}.signature`, kind: "field", artifact: bytes },
|
||||
);
|
||||
@@ -219,12 +223,14 @@ export const parseMemberRefs = (
|
||||
{
|
||||
typeName,
|
||||
signatureTypeName,
|
||||
displayTypeName,
|
||||
declarationTypeName,
|
||||
}: {
|
||||
readonly typeName: (
|
||||
reference: Pick<MetadataTokenFact, "table" | "row" | "token"> | null,
|
||||
) => string | null;
|
||||
readonly signatureTypeName: SignatureTypeName;
|
||||
readonly displayTypeName: SignatureDisplayTypeName;
|
||||
readonly declarationTypeName: DeclarationTypeName;
|
||||
},
|
||||
): {
|
||||
@@ -268,6 +274,7 @@ export const parseMemberRefs = (
|
||||
sig,
|
||||
layout,
|
||||
signatureTypeName,
|
||||
displayTypeName,
|
||||
issues,
|
||||
{ scope: `MemberRef:${token}.signature`, artifact: bytes },
|
||||
);
|
||||
@@ -303,6 +310,7 @@ interface ParseMethodsInput {
|
||||
readonly pe: ManagedPeLayout;
|
||||
readonly ranges: readonly TypeRange[];
|
||||
readonly signatureTypeName: SignatureTypeName;
|
||||
readonly displayTypeName: SignatureDisplayTypeName;
|
||||
readonly declarationTypeName: DeclarationTypeName;
|
||||
readonly memberRefKinds: ReadonlyMap<string, "field" | "method">;
|
||||
readonly memberLists: TypeMemberListFacts;
|
||||
@@ -317,6 +325,7 @@ export const parseMethods = ({
|
||||
pe,
|
||||
ranges,
|
||||
signatureTypeName,
|
||||
displayTypeName,
|
||||
declarationTypeName,
|
||||
memberRefKinds,
|
||||
memberLists,
|
||||
@@ -396,6 +405,7 @@ export const parseMethods = ({
|
||||
sig,
|
||||
layout,
|
||||
signatureTypeName,
|
||||
displayTypeName,
|
||||
issues,
|
||||
{
|
||||
scope: `MethodDef:${token}.signature`,
|
||||
@@ -568,6 +578,7 @@ const budgetedSignature = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
signatureTypeName: SignatureTypeName,
|
||||
displayTypeName: SignatureDisplayTypeName,
|
||||
issues: ManagedParseIssue[],
|
||||
owner: {
|
||||
readonly scope: string;
|
||||
@@ -586,6 +597,15 @@ const budgetedSignature = (
|
||||
metadataSignature(bytes, {
|
||||
rawSha256: budget.digest(bytes, "sha256"),
|
||||
typeName: signatureTypeName,
|
||||
displayTypeName: (operand, copies) => {
|
||||
const name = displayTypeName(operand, copies);
|
||||
if (name !== null)
|
||||
budget.admitTransient(
|
||||
(name.length * 6 + 2) * copies,
|
||||
layout.blob?.offset ?? layout.rootOffset,
|
||||
);
|
||||
return name;
|
||||
},
|
||||
layout,
|
||||
issues,
|
||||
scope: owner.scope,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { readMemberReferenceRowFacts } from "./ManagedMemberReferenceRows.js";
|
||||
import { readTypeDefinitionRowFacts } from "./ManagedTypeDefinitionRows.js";
|
||||
import { managedDecodeBudget } from "./ManagedDecodeBudget.js";
|
||||
import { managedTypeReferences } from "./ManagedTypeReferences.js";
|
||||
import { MAX_TYPE_NESTING_DEPTH } from "./ManagedTypeNameDigests.js";
|
||||
import { MAX_TYPE_NESTING_DEPTH } from "./ManagedSignatureTypeNames.js";
|
||||
import { readManagedTypeNesting } from "./ManagedTypeNesting.js";
|
||||
import { memberDeclaringTypeRowLookup } from "./ManagedMemberOwnership.js";
|
||||
import type { TypeMemberListFacts } from "./ManagedTypeMemberLists.js";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { fc } from "@fast-check/vitest";
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
@@ -23,12 +24,14 @@ import {
|
||||
managedPeFixtureTarget,
|
||||
} from "./ManagedPe.fixture.js";
|
||||
import { readManagedPeLayout } from "./ManagedPeReader.js";
|
||||
import { managedTypeNameDigests } from "./ManagedTypeNameDigests.js";
|
||||
import { compareManagedMembers } from "../domain/managed/managedMemberComparison.js";
|
||||
import { managedMemberInspectionSchema } from "../domain/managed/managedArtifact.js";
|
||||
import { managedSignatureTypeNames } from "./ManagedSignatureTypeNames.js";
|
||||
import { metadataCodedIndexCoordinate } from "./ManagedMetadataHeaps.js";
|
||||
import { managedTypeReferences } from "./ManagedTypeReferences.js";
|
||||
import { readTypeMemberListFacts } from "./ManagedTypeMemberLists.js";
|
||||
|
||||
// A top-level type's name digest, as ManagedTypeNameDigests forms it.
|
||||
// A top-level type's name digest, as ManagedSignatureTypeNames forms it.
|
||||
const nameDigest = (name: string): Buffer => {
|
||||
const dot = name.lastIndexOf(".");
|
||||
return createHash("sha256")
|
||||
@@ -149,7 +152,7 @@ describe("metadata type-name digest ownership", () => {
|
||||
const bytes = buildManagedPeFixture();
|
||||
const layout = metadataLayout(bytes);
|
||||
const issues: ManagedParseIssue[] = [];
|
||||
const names = managedTypeNameDigests(
|
||||
const names = managedSignatureTypeNames(
|
||||
layout,
|
||||
issues,
|
||||
{ types: [], nesting: { enclosing: new Map(), complete: true } },
|
||||
@@ -187,7 +190,7 @@ describe("metadata type-name digest ownership", () => {
|
||||
const layout = metadataLayout(bytes);
|
||||
const budget = new ManagedDecodeBudget();
|
||||
attachManagedDecodeBudget(layout, budget);
|
||||
const typeNames = managedTypeNameDigests(
|
||||
const typeNames = managedSignatureTypeNames(
|
||||
layout,
|
||||
[],
|
||||
{
|
||||
@@ -206,6 +209,7 @@ describe("metadata type-name digest ownership", () => {
|
||||
const reserve = vi.spyOn(budget, "reserve");
|
||||
expect(typeNames.operand(operand)).toEqual(nameDigest("System.Uri"));
|
||||
expect(typeNames.declaration(1, 2)).toEqual(nameDigest("System.Uri"));
|
||||
expect(typeNames.displayName(operand)).toBe("System.Uri");
|
||||
expect(reserve).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -246,7 +250,7 @@ describe("metadata type-name digest ownership", () => {
|
||||
readTypeMemberListFacts(bytes, layout),
|
||||
),
|
||||
);
|
||||
const resolve = managedTypeNameDigests(
|
||||
const resolve = managedSignatureTypeNames(
|
||||
layout,
|
||||
issues,
|
||||
parsedTypes,
|
||||
@@ -404,3 +408,344 @@ describe("type operand names", () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const inspectDisplayFixture = (
|
||||
options: Parameters<typeof buildManagedPeFixture>[0],
|
||||
) => {
|
||||
const bytes = buildManagedPeFixture({
|
||||
typeSpecSignatures: [Buffer.from([0x08])],
|
||||
...options,
|
||||
});
|
||||
return inspectManagedMembersBytes(bytes, managedPeFixtureTarget(bytes));
|
||||
};
|
||||
const displayReferences = [
|
||||
{ namespace: "N", name: "G`1" },
|
||||
{ namespace: "N", name: "Value" },
|
||||
];
|
||||
|
||||
describe("readable signature operands", () => {
|
||||
it("names fields, method returns and parameters, and member references", () => {
|
||||
const result = inspectDisplayFixture({
|
||||
fieldSignature: Buffer.from([0x06, 0x12, 0x04]),
|
||||
methodSignature: Buffer.from([0x00, 0x01, 0x12, 0x04, 0x11, 0x09]),
|
||||
memberRefSignature: Buffer.from([0x00, 0x01, 0x12, 0x09, 0x12, 0x04]),
|
||||
typeReferences: displayReferences,
|
||||
});
|
||||
expect(result.signature_resolution).toBe("type-names-v2");
|
||||
expect(result.fields[0]?.signature.field_type).toBe(
|
||||
"class:4 (Fixture.Program)",
|
||||
);
|
||||
expect(result.methods[0]?.signature).toMatchObject({
|
||||
return_type: "class:4 (Fixture.Program)",
|
||||
parameter_types: ["valuetype:9 (N.G`1)"],
|
||||
});
|
||||
expect(result.member_refs[0]?.signature).toMatchObject({
|
||||
return_type: "class:9 (N.G`1)",
|
||||
parameter_types: ["class:4 (Fixture.Program)"],
|
||||
});
|
||||
expect(result.coverage).toEqual({ state: "complete", issues: [] });
|
||||
});
|
||||
|
||||
it.each([
|
||||
["class", [0x12, 0x09], "class:9 (N.G`1)"],
|
||||
["value type", [0x11, 0x0d], "valuetype:13 (N.Value)"],
|
||||
["pointer", [0x0f, 0x12, 0x09], "class:9 (N.G`1)*"],
|
||||
["byref", [0x10, 0x12, 0x09], "class:9 (N.G`1)&"],
|
||||
["vector", [0x1d, 0x12, 0x09], "class:9 (N.G`1)[]"],
|
||||
[
|
||||
"ranked array",
|
||||
[0x14, 0x12, 0x09, 0x02, 0x00, 0x00],
|
||||
"class:9 (N.G`1)[,]",
|
||||
],
|
||||
[
|
||||
"required modifier",
|
||||
[0x1f, 0x0d, 0x12, 0x09],
|
||||
"class:9 (N.G`1) modreq:13 (N.Value)",
|
||||
],
|
||||
[
|
||||
"optional modifier",
|
||||
[0x20, 0x0d, 0x12, 0x09],
|
||||
"class:9 (N.G`1) modopt:13 (N.Value)",
|
||||
],
|
||||
[
|
||||
"generic argument",
|
||||
[0x15, 0x12, 0x09, 0x01, 0x11, 0x0d],
|
||||
"class:9 (N.G`1)<valuetype:13 (N.Value)>",
|
||||
],
|
||||
[
|
||||
"generic variable",
|
||||
[0x15, 0x12, 0x09, 0x01, 0x13, 0x00],
|
||||
"class:9 (N.G`1)<var:0>",
|
||||
],
|
||||
[
|
||||
"function pointer",
|
||||
[0x1b, 0x00, 0x01, 0x12, 0x09, 0x11, 0x0d],
|
||||
"fnptr[default](valuetype:13 (N.Value))->class:9 (N.G`1)",
|
||||
],
|
||||
[
|
||||
"mixed resolved and unresolved",
|
||||
[0x15, 0x12, 0x09, 0x01, 0x12, 0x06],
|
||||
"class:9 (N.G`1)<class:6>",
|
||||
],
|
||||
])("preserves %s grammar and signature hashes", (_label, type, expected) => {
|
||||
const blob = Buffer.from([0x06, ...(type as number[])]);
|
||||
const result = inspectDisplayFixture({
|
||||
fieldSignature: blob,
|
||||
typeReferences: displayReferences,
|
||||
});
|
||||
const decoded = result.fields[0]?.signature;
|
||||
const names = named({ 9: "N.G`1", 13: "N.Value" });
|
||||
const original = signature(blob, undefined, names);
|
||||
expect(decoded?.field_type).toBe(expected);
|
||||
expect(decoded?.parse_status).toBe(original.parse_status);
|
||||
expect(decoded?.raw_sha256).toBe(original.raw_sha256);
|
||||
expect(decoded?.resolved_sha256).toBe(original.resolved_sha256);
|
||||
});
|
||||
});
|
||||
|
||||
describe("signature display name validity", () => {
|
||||
it("resolves types outside the selected type inventory", () => {
|
||||
const result = inspectDisplayFixture({
|
||||
additionalEmptyTypes: 1,
|
||||
fieldSignature: Buffer.from([0x06, 0x12, 0x04]),
|
||||
});
|
||||
expect(result.fields[0]?.signature.field_type).toBe(
|
||||
"class:4 (Fixture.Empty0)",
|
||||
);
|
||||
const bytes = buildManagedPeFixture({
|
||||
additionalEmptyTypes: 1,
|
||||
fieldSignature: Buffer.from([0x06, 0x12, 0x04]),
|
||||
});
|
||||
const selected = inspectManagedMembersBytes(
|
||||
bytes,
|
||||
managedPeFixtureTarget(bytes),
|
||||
{ typeNames: ["Fixture.Program"] },
|
||||
);
|
||||
expect(selected.fields[0]?.signature).toEqual(result.fields[0]?.signature);
|
||||
});
|
||||
|
||||
it("uses the enclosing path for nested TypeDefs", () => {
|
||||
const result = inspectDisplayFixture({
|
||||
additionalEmptyTypes: 1,
|
||||
nestedTypes: [[2, 1]],
|
||||
fieldSignature: Buffer.from([0x06, 0x12, 0x08]),
|
||||
});
|
||||
expect(result.fields[0]?.signature.field_type).toBe(
|
||||
"class:8 (Fixture.Empty0/Fixture.Program)",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the enclosing path for nested TypeRefs", () => {
|
||||
const result = inspectDisplayFixture({
|
||||
typeReferences: [
|
||||
{ namespace: "N", name: "Outer" },
|
||||
{ namespace: "", name: "Inner", enclosingRow: 2 },
|
||||
],
|
||||
fieldSignature: Buffer.from([0x06, 0x12, 0x0d]),
|
||||
});
|
||||
expect(result.fields[0]?.signature.field_type).toBe(
|
||||
"class:13 (N.Outer/Inner)",
|
||||
);
|
||||
});
|
||||
|
||||
it.each(["", "名", "A.B", "G`1", "A (B)<C>/D", "\uFEFFName"])(
|
||||
"preserves source name %j",
|
||||
(name) => {
|
||||
const result = inspectDisplayFixture({
|
||||
typeReferences: [{ namespace: "", name }],
|
||||
fieldSignature: Buffer.from([0x06, 0x12, 0x09]),
|
||||
});
|
||||
expect(result.fields[0]?.signature.field_type).toBe(`class:9 (${name})`);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["TypeSpec", 0x06, "class:6", "decoded"],
|
||||
["missing TypeRef", 0x25, null, "malformed"],
|
||||
["missing TypeDef", 0x24, null, "malformed"],
|
||||
["invalid tag", 0x07, null, "malformed"],
|
||||
["null TypeDef", 0x00, null, "malformed"],
|
||||
] as const)(
|
||||
"preserves validity of unresolved %s operands",
|
||||
(_label, operand, display, status) => {
|
||||
const result = inspectDisplayFixture({
|
||||
fieldSignature: Buffer.from([0x06, 0x12, operand]),
|
||||
});
|
||||
expect(result.fields[0]?.signature.field_type).toBe(display);
|
||||
expect(result.fields[0]?.signature.parse_status).toBe(status);
|
||||
expect(result.fields[0]?.signature.resolved_sha256).toBeNull();
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps invalid-scope TypeRefs raw and retains their issue once", () => {
|
||||
const result = inspectDisplayFixture({
|
||||
typeRefScopeRaw: (99 << 2) | 2,
|
||||
fieldSignature: Buffer.from([0x06, 0x12, 0x05]),
|
||||
methodSignature: Buffer.from([0x00, 0x01, 0x12, 0x05, 0x12, 0x05]),
|
||||
});
|
||||
expect(result.fields[0]?.signature.field_type).toBe("class:5");
|
||||
expect(result.methods[0]?.signature.parameter_types).toEqual(["class:5"]);
|
||||
expect(result.coverage.issues).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("keeps cyclic TypeRefs raw", () => {
|
||||
const result = inspectDisplayFixture({
|
||||
typeReferences: [
|
||||
{ namespace: "", name: "A", enclosingRow: 3 },
|
||||
{ namespace: "", name: "B", enclosingRow: 2 },
|
||||
],
|
||||
fieldSignature: Buffer.from([0x06, 0x12, 0x09]),
|
||||
});
|
||||
expect(result.fields[0]?.signature.field_type).toBe("class:9");
|
||||
expect(result.coverage.state).toBe("partial");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["truncated", [0x06, 0x12], "malformed"],
|
||||
["truncated compressed index", [0x06, 0x12, 0x80], "malformed"],
|
||||
["unknown element", [0x06, 0x50], "unsupported"],
|
||||
["trailing bytes", [0x06, 0x12, 0x04, 0x08], "partial"],
|
||||
])("retains %s parse status and raw hash", (_label, bytes, status) => {
|
||||
const blob = Buffer.from(bytes as number[]);
|
||||
const result = inspectDisplayFixture({ fieldSignature: blob });
|
||||
expect(result.fields[0]?.signature.parse_status).toBe(status);
|
||||
expect(result.fields[0]?.signature.raw_sha256).toBe(
|
||||
signature(blob).raw_sha256,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("signature display compatibility and capacity", () => {
|
||||
it("reads historical v1 Evidence without relabelling its raw display", () => {
|
||||
const current = inspectDisplayFixture({
|
||||
fieldSignature: Buffer.from([0x06, 0x12, 0x04]),
|
||||
});
|
||||
const old = {
|
||||
...current,
|
||||
signature_resolution: "type-names-v1",
|
||||
fields: current.fields.map((field) => ({
|
||||
...field,
|
||||
signature: { ...field.signature, field_type: "class:4" },
|
||||
})),
|
||||
};
|
||||
const parsed = managedMemberInspectionSchema.parse(old);
|
||||
const comparison = compareManagedMembers(
|
||||
{ evidenceId: `ev_${"a".repeat(64)}`, result: parsed },
|
||||
{ evidenceId: `ev_${"b".repeat(64)}`, result: current },
|
||||
);
|
||||
expect(comparison.fields[0]?.status).toBe("unchanged");
|
||||
expect(comparison.methods[0]?.status).toBe("unchanged");
|
||||
expect(parsed.signature_resolution).toBe("type-names-v1");
|
||||
expect(parsed.fields[0]?.signature.field_type).toBe("class:4");
|
||||
expect(parsed.fields[0]?.signature.resolved_sha256).toBe(
|
||||
current.fields[0]?.signature.resolved_sha256,
|
||||
);
|
||||
});
|
||||
|
||||
it.each([64, 65])("bounds a real %i-level TypeRef display path", (depth) => {
|
||||
const typeReferences = Array.from({ length: depth }, (_, index) => ({
|
||||
namespace: "",
|
||||
name: `T${index}`,
|
||||
...(index === 0 ? {} : { enclosingRow: index + 1 }),
|
||||
}));
|
||||
const index = ((depth + 1) << 2) | 1;
|
||||
const encoded = [0x80 | (index >> 8), index & 0xff];
|
||||
const result = inspectDisplayFixture({
|
||||
typeReferences,
|
||||
fieldSignature: Buffer.from([0x06, 0x12, ...encoded]),
|
||||
});
|
||||
const expected =
|
||||
depth === 64
|
||||
? `class:${index} (${typeReferences.map((ref) => ref.name).join("/")})`
|
||||
: `class:${index}`;
|
||||
expect(result.fields[0]?.signature.field_type).toBe(expected);
|
||||
});
|
||||
|
||||
it("bounds repeated display-name expansion before allocation", () => {
|
||||
const bytes = buildManagedPeFixture({
|
||||
typeReferences: [{ namespace: "N", name: "X".repeat(200_000) }],
|
||||
methodSignature: Buffer.from([
|
||||
0x00,
|
||||
0x40,
|
||||
0x01,
|
||||
...Array.from({ length: 64 }, () => [0x12, 0x09]).flat(),
|
||||
]),
|
||||
});
|
||||
const result = inspectManagedMembersBytes(
|
||||
bytes,
|
||||
managedPeFixtureTarget(bytes),
|
||||
);
|
||||
expect(result.coverage.state).not.toBe("complete");
|
||||
expect(result.coverage.issues).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ code: "resource-limit" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("signature display property regressions", () => {
|
||||
const assertIdentity = (blob: Buffer) => {
|
||||
const digest = ({ value }: Parameters<SignatureTypeName>[0]) =>
|
||||
nameDigest(`N.Type${value}`);
|
||||
const old = signature(blob, undefined, digest);
|
||||
const current = signature(
|
||||
blob,
|
||||
undefined,
|
||||
digest,
|
||||
({ value }) => `N.Type${value}`,
|
||||
);
|
||||
const identity = ({
|
||||
field_type: _field,
|
||||
parameter_types: _parameters,
|
||||
return_type: _return,
|
||||
...rest
|
||||
}: typeof old) => rest;
|
||||
expect(identity(current)).toEqual(identity(old));
|
||||
};
|
||||
|
||||
it("keeps parse status, hashes and counts for 1000 arbitrary blobs", () => {
|
||||
fc.assert(
|
||||
fc.property(fc.uint8Array({ maxLength: 128 }), (bytes) => {
|
||||
assertIdentity(Buffer.from(bytes));
|
||||
}),
|
||||
{ seed: 5149, numRuns: 1000 },
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps identity through 1000 generated valid nested signatures", () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.array(
|
||||
fc.constantFrom(
|
||||
"pointer",
|
||||
"vector",
|
||||
"array",
|
||||
"generic",
|
||||
"modifier",
|
||||
"fnptr",
|
||||
),
|
||||
{ maxLength: 30 },
|
||||
),
|
||||
fc.constantFrom([0x12, 0x09], [0x11, 0x0d], [0x08]),
|
||||
(wrappers, leaf) => {
|
||||
let type: number[] = [...leaf];
|
||||
for (const wrapper of wrappers) {
|
||||
if (wrapper === "pointer") type = [0x0f, ...type];
|
||||
else if (wrapper === "vector") type = [0x1d, ...type];
|
||||
else if (wrapper === "array")
|
||||
type = [0x14, ...type, 0x02, 0x00, 0x00];
|
||||
else if (wrapper === "generic")
|
||||
type = [0x15, 0x12, 0x09, 0x01, ...type];
|
||||
else if (wrapper === "modifier") type = [0x1f, 0x0d, ...type];
|
||||
else type = [0x1b, 0x00, 0x00, ...type];
|
||||
}
|
||||
const blob = Buffer.from([0x06, ...type]);
|
||||
expect(signature(blob).parse_status).toBe("decoded");
|
||||
assertIdentity(blob);
|
||||
},
|
||||
),
|
||||
{ seed: 5149, numRuns: 1000 },
|
||||
);
|
||||
});
|
||||
});
|
||||
+16
-10
@@ -2,8 +2,8 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
MAX_TYPE_NESTING_DEPTH,
|
||||
nestedTypeDigests,
|
||||
} from "./ManagedTypeNameDigests.js";
|
||||
nestedTypeNames,
|
||||
} from "./ManagedSignatureTypeNames.js";
|
||||
|
||||
/** Rows 1..length, each nested in the row before it. */
|
||||
const chain = (length: number) => {
|
||||
@@ -18,14 +18,14 @@ const chain = (length: number) => {
|
||||
}
|
||||
: null;
|
||||
};
|
||||
return { calls, digest: nestedTypeDigests(link, () => undefined) };
|
||||
return { calls, digest: nestedTypeNames(link, () => undefined) };
|
||||
};
|
||||
|
||||
describe("nested signature type digests", () => {
|
||||
it("reads each row of a chain within the nesting cap once", () => {
|
||||
const { calls, digest } = chain(MAX_TYPE_NESTING_DEPTH);
|
||||
for (let row = MAX_TYPE_NESTING_DEPTH; row >= 1; row -= 1)
|
||||
expect(digest(row)).toBeInstanceOf(Buffer);
|
||||
expect(digest(row)?.digest).toBeInstanceOf(Buffer);
|
||||
|
||||
expect(calls).toHaveLength(MAX_TYPE_NESTING_DEPTH);
|
||||
});
|
||||
@@ -35,7 +35,7 @@ describe("nested signature type digests", () => {
|
||||
|
||||
expect(digest(4_000)).toBeNull();
|
||||
expect(calls.length).toBeLessThanOrEqual(MAX_TYPE_NESTING_DEPTH + 1);
|
||||
expect(digest(MAX_TYPE_NESTING_DEPTH)).toBeInstanceOf(Buffer);
|
||||
expect(digest(MAX_TYPE_NESTING_DEPTH)?.digest).toBeInstanceOf(Buffer);
|
||||
expect(digest(MAX_TYPE_NESTING_DEPTH + 1)).toBeNull();
|
||||
});
|
||||
|
||||
@@ -61,7 +61,7 @@ describe("nested signature type digests", () => {
|
||||
}
|
||||
: null;
|
||||
};
|
||||
const digest = nestedTypeDigests(link, () => undefined);
|
||||
const digest = nestedTypeNames(link, () => undefined);
|
||||
for (let row = 1_001; row <= 1_000 + children; row += 1)
|
||||
expect(digest(row)).toBeNull();
|
||||
|
||||
@@ -82,7 +82,7 @@ describe("nested signature type digests", () => {
|
||||
});
|
||||
|
||||
it("does not name a type whose nesting is cyclic", () => {
|
||||
const digest = nestedTypeDigests(
|
||||
const digest = nestedTypeNames(
|
||||
(row) => ({
|
||||
namespace: "",
|
||||
name: `T${String(row)}`,
|
||||
@@ -96,18 +96,24 @@ describe("nested signature type digests", () => {
|
||||
});
|
||||
|
||||
it("tells a nested type from a top-level type with its joined name", () => {
|
||||
const nested = nestedTypeDigests(
|
||||
const nested = nestedTypeNames(
|
||||
(row) =>
|
||||
row === 1
|
||||
? { namespace: "", name: "Outer", enclosingRow: null }
|
||||
: { namespace: "", name: "Inner", enclosingRow: 1 },
|
||||
() => undefined,
|
||||
);
|
||||
const flat = nestedTypeDigests(
|
||||
const flat = nestedTypeNames(
|
||||
() => ({ namespace: "", name: "Outer/Inner", enclosingRow: null }),
|
||||
() => undefined,
|
||||
);
|
||||
|
||||
expect(nested(2)).not.toEqual(flat(1));
|
||||
const nestedName = nested(2);
|
||||
const flatName = flat(1);
|
||||
if (nestedName === null || flatName === null)
|
||||
throw new Error("Expected both type names to resolve");
|
||||
expect(nestedName.name).toBe("Outer/Inner");
|
||||
expect(flatName.name).toBe(nestedName.name);
|
||||
expect(nestedName.digest.equals(flatName.digest)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -39,6 +39,7 @@ const NAME_SEPARATOR = Buffer.from([0]);
|
||||
|
||||
interface NamedRow {
|
||||
readonly digest: Buffer;
|
||||
readonly name: string;
|
||||
readonly depth: number;
|
||||
}
|
||||
|
||||
@@ -51,10 +52,10 @@ interface NamedRow {
|
||||
* A gap, a cycle, or nesting deeper than `MAX_TYPE_NESTING_DEPTH` has no
|
||||
* digest. `retain` is charged once per row read and once per remembered row.
|
||||
*/
|
||||
export const nestedTypeDigests = (
|
||||
export const nestedTypeNames = (
|
||||
link: (row: number) => TypeLink | null,
|
||||
retain: () => void,
|
||||
): ((row: number) => Buffer | null) => {
|
||||
retain: (bytes?: number) => void,
|
||||
): ((row: number) => NamedRow | null) => {
|
||||
const named = new Map<number, NamedRow | null>();
|
||||
const remember = (row: number, value: NamedRow | null): void => {
|
||||
retain();
|
||||
@@ -124,10 +125,21 @@ export const nestedTypeDigests = (
|
||||
.update(step.link.namespace)
|
||||
.update(NAME_SEPARATOR)
|
||||
.update(step.link.name);
|
||||
inner = { digest: hash.digest(), depth };
|
||||
// Charge each retained expanded path before allocation, including parents.
|
||||
const ownLength =
|
||||
step.link.namespace.length +
|
||||
step.link.name.length +
|
||||
Number(step.link.namespace.length > 0);
|
||||
retain(((inner?.name.length ?? -1) + 1 + ownLength) * 6 + 2);
|
||||
const ownName =
|
||||
step.link.namespace.length === 0
|
||||
? step.link.name
|
||||
: `${step.link.namespace}.${step.link.name}`;
|
||||
const name = inner === null ? ownName : `${inner.name}/${ownName}`;
|
||||
inner = { digest: hash.digest(), name, depth };
|
||||
remember(step.row, inner);
|
||||
}
|
||||
return exceeded ? null : (inner?.digest ?? null);
|
||||
return exceeded ? null : inner;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -142,7 +154,7 @@ export const nestedTypeDigests = (
|
||||
* retained in `issues`, once per row. Each coded index and each row is
|
||||
* named and digested once, so repeated operands cost a lookup.
|
||||
*/
|
||||
export const managedTypeNameDigests = (
|
||||
export const managedSignatureTypeNames = (
|
||||
layout: ManagedMetadataLayout,
|
||||
issues: ManagedParseIssue[],
|
||||
parsedTypes: Pick<ReturnType<typeof parseTypes>, "types" | "nesting">,
|
||||
@@ -150,12 +162,16 @@ export const managedTypeNameDigests = (
|
||||
): {
|
||||
readonly operand: SignatureTypeName;
|
||||
readonly declaration: DeclarationTypeName;
|
||||
readonly displayName: (
|
||||
operand: Parameters<SignatureTypeName>[0],
|
||||
) => string | null;
|
||||
} => {
|
||||
const retain = () => managedDecodeBudget(layout).reserve(128, null);
|
||||
const retain = (bytes = 128) =>
|
||||
managedDecodeBudget(layout).reserve(bytes, null);
|
||||
managedDecodeBudget(layout).reserve(parsedTypes.types.length * 128, null);
|
||||
const types = new Map<number, ManagedType>();
|
||||
for (const { row, type } of parsedTypes.types) types.set(row, type);
|
||||
const definitions = nestedTypeDigests((row) => {
|
||||
const definitions = nestedTypeNames((row) => {
|
||||
if (!parsedTypes.nesting.complete) return null;
|
||||
const type = types.get(row);
|
||||
if (
|
||||
@@ -195,33 +211,35 @@ export const managedTypeNameDigests = (
|
||||
enclosingRow,
|
||||
};
|
||||
}, retain);
|
||||
const references = nestedTypeDigests((row) => {
|
||||
const references = nestedTypeNames((row) => {
|
||||
const value = reference(row);
|
||||
return value?.scopeValid === true ? value : null;
|
||||
}, retain);
|
||||
const digests = new Map<number, Buffer | null>();
|
||||
const digestFor = (
|
||||
const names = new Map<number, NamedRow | null>();
|
||||
const nameFor = (
|
||||
key: number,
|
||||
table: number | undefined,
|
||||
row: number,
|
||||
): Buffer | null => {
|
||||
let digest = digests.get(key);
|
||||
if (digest === undefined) {
|
||||
): NamedRow | null => {
|
||||
let named = names.get(key);
|
||||
if (named === undefined) {
|
||||
managedDecodeBudget(layout).reserve(256, null);
|
||||
digest =
|
||||
named =
|
||||
table === TYPE_DEF
|
||||
? definitions(row)
|
||||
: table === TYPE_REF
|
||||
? references(row)
|
||||
: null;
|
||||
digests.set(key, digest);
|
||||
names.set(key, named);
|
||||
}
|
||||
return digest;
|
||||
return named;
|
||||
};
|
||||
return {
|
||||
operand: ({ value, coordinate }) =>
|
||||
digestFor(value, coordinate.table, coordinate.row),
|
||||
nameFor(value, coordinate.table, coordinate.row)?.digest ?? null,
|
||||
declaration: (table, row) =>
|
||||
digestFor(row * 4 + Number(table === TYPE_REF), table, row),
|
||||
nameFor(row * 4 + Number(table === TYPE_REF), table, row)?.digest ?? null,
|
||||
displayName: ({ value, coordinate }) =>
|
||||
nameFor(value, coordinate.table, coordinate.row)?.name ?? null,
|
||||
};
|
||||
};
|
||||
@@ -646,3 +646,47 @@ cliTest(
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"managed CLI renders type names and preserves unresolved operands",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-managed-signature-names-");
|
||||
const path = join(root, "names.dll");
|
||||
await writeFile(
|
||||
path,
|
||||
buildManagedPeFixture({
|
||||
fieldSignature: Buffer.from([0x06, 0x12, 0x04]),
|
||||
typeSpecSignatures: [Buffer.from([0x08])],
|
||||
methodSignature: Buffer.from([
|
||||
0x00, 0x02, 0x12, 0x04, 0x12, 0x09, 0x12, 0x06,
|
||||
]),
|
||||
typeReferences: [{ namespace: "System", name: "Uri" }],
|
||||
}),
|
||||
);
|
||||
const result = await cli.run({
|
||||
arguments: ["inspect-managed-members", path, "--json"],
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.json).toMatchObject({
|
||||
normalized_result: {
|
||||
signature_resolution: "type-names-v2",
|
||||
fields: [
|
||||
expect.objectContaining({
|
||||
signature: expect.objectContaining({
|
||||
field_type: "class:4 (Fixture.Program)",
|
||||
}),
|
||||
}),
|
||||
],
|
||||
methods: [
|
||||
expect.objectContaining({
|
||||
signature: expect.objectContaining({
|
||||
return_type: "class:4 (Fixture.Program)",
|
||||
parameter_types: ["class:9 (System.Uri)", "class:6"],
|
||||
resolved_sha256: null,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -146,6 +146,29 @@ const verifyProjection = async ({
|
||||
});
|
||||
};
|
||||
|
||||
// ldarg.0; ldfld field; call member ref; ldstr "ok"; pop; ret
|
||||
const SUMMARY_METHOD_BODY = Buffer.from([
|
||||
(18 << 2) | 2,
|
||||
0x02,
|
||||
0x7b,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0x04,
|
||||
0x28,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0x0a,
|
||||
0x72,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0x70,
|
||||
0x26,
|
||||
0x2a,
|
||||
]);
|
||||
|
||||
it("summarizes selected members and pages every original collection after the file disappears", async () => {
|
||||
const directory = await createTestTempDirectory("rea-member-summary-");
|
||||
const path = join(directory, "fixture.dll");
|
||||
@@ -153,28 +176,9 @@ it("summarizes selected members and pages every original collection after the fi
|
||||
path,
|
||||
buildManagedPeFixture({
|
||||
methodRowCount: 2,
|
||||
// ldarg.0; ldfld field; call member ref; ldstr "ok"; pop; ret
|
||||
ilBody: Buffer.from([
|
||||
(18 << 2) | 2,
|
||||
0x02,
|
||||
0x7b,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0x04,
|
||||
0x28,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0x0a,
|
||||
0x72,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0x70,
|
||||
0x26,
|
||||
0x2a,
|
||||
]),
|
||||
fieldSignature: Buffer.from([0x06, 0x12, 0x04]),
|
||||
methodSignature: Buffer.from([0x00, 0x00, 0x12, 0x04]),
|
||||
ilBody: SUMMARY_METHOD_BODY,
|
||||
userStringHeap: Buffer.from([0, 5, 0x6f, 0, 0x6b, 0, 0]),
|
||||
exportedTypes: [
|
||||
{
|
||||
@@ -225,13 +229,16 @@ it("summarizes selected members and pages every original collection after the fi
|
||||
expect(parent.parameters).toEqual({ path, type_names });
|
||||
expect(receipt.evidence_links).toEqual([parent.evidence_id]);
|
||||
const report = managedMemberInspectionSchema.parse(parent.normalized_result);
|
||||
expect(report.fields[0]?.signature.field_type).toBe(
|
||||
"class:4 (Fixture.Program)",
|
||||
);
|
||||
expect(view.summary.selection).toEqual({
|
||||
type_names,
|
||||
unmatched_type_names: ["Missing.Type"],
|
||||
});
|
||||
expect(view.summary.coverage).toEqual(report.coverage);
|
||||
expect(view.summary.method_rows).toEqual({ offset: 0, count: 2, total: 2 });
|
||||
expect(view.summary.signature_resolution).toBe("type-names-v1");
|
||||
expect(view.summary.signature_resolution).toBe("type-names-v2");
|
||||
expect(view.summary.identity_scope).toEqual(report.identity_scope);
|
||||
expect(view.summary).not.toHaveProperty("methods");
|
||||
for (const detail of [undefined, "complete"]) {
|
||||
@@ -276,7 +283,7 @@ it("summarizes selected members and pages every original collection after the fi
|
||||
if (page.kind !== "managed-members")
|
||||
throw new Error("Unexpected page kind");
|
||||
expect(page.page.collection).toBe(collection);
|
||||
expect(page.signature_resolution).toBe("type-names-v1");
|
||||
expect(page.signature_resolution).toBe("type-names-v2");
|
||||
expect(page.selection).toEqual(report.selection);
|
||||
expect(page.coverage.total).toBe(rows.length);
|
||||
expect(page.method_rows).toEqual(report.method_rows);
|
||||
|
||||
Reference in new issue
Block a user