feat(managed): name the declaring type of call and field targets (#2163)

* feat(managed): name the declaring type of call and field targets

Call edges named external targets by member name alone (Join, ToJson,
WriteAllText), and method-spec edges had no name at all, so an analyst could
not tell which library a method calls or find every use of a type without a
decompiler. Field accesses through MemberRefs had no name either.

Name each TypeDef, TypeRef, and generic-instantiation TypeSpec once, in the
namespace-qualified form TypeDef full_name uses, and add member_refs
parent_name, call_edges target_declaring_type, and field_accesses
field_declaring_type. A method-spec target reports the generic method it
instantiates. Fields, member references, methods, and method specs now
resolve through one token-keyed MemberCore lookup instead of three maps.
Evidence recorded before these fields reads them as null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(managed): validate spec blobs and member kinds before naming targets

Address review findings on declaring-type naming:

- A generic instantiation must name a TypeDef or TypeRef, so a TypeSpec
  base is reported as malformed instead of followed. Naming no longer
  recurses.
- TypeSpec and MethodSpec instantiation blobs are decoded with the
  member-signature grammar. Malformed blobs and out-of-range TypeDef rows
  are reported against their owning row and make coverage partial.
- A MethodSpec reads both columns. An invalid Method coded index is
  reported; a readable Method keeps its name beside a malformed
  instantiation.
- MemberCore carries its signature kind, so a field operand is named only
  by a field or field-signature MemberRef. This replaces the token-table
  guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(managed): validate MethodSpec arity and report unsupported spec blobs

- A MethodSpec now checks that its method signature is generic and that
  the instantiation supplies as many type arguments as it declares. A
  non-generic target, a field reference, or a count mismatch is reported
  as invalid-row; the readable method keeps its name.
- Unsupported TypeSpec and MethodSpec forms raise an owner-scoped
  unsupported-signature issue instead of reading as an intentional
  unknown.
- MethodSpec reading moves to ManagedMethodSpecs.ts, and its tests get
  their own describe block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(managed): reject non-method signature headers and invalid TypeRef scopes

The method signature header reader now owns the calling-convention rule
that only FNPTR applied: FIELD, LOCAL_SIG, PROPERTY, and GENERICINST
headers, the reserved bit, and EXPLICITTHIS without HASTHIS are malformed.
A MethodSpec whose method has a PROPERTY header is no longer accepted as a
generic method, and member signatures with such headers no longer decode
as methods.

The type-name lookup validates a TypeRef's ResolutionScope coded index and
reports an out-of-range scope as invalid-row, keeping the readable name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(managed): hold member rows to MethodDefSig and FieldSig headers

Method signature headers are now checked in context. MethodDef and
MemberRef rows accept only the MethodDefSig/MethodRefSig headers: DEFAULT,
VARARG, or GENERIC with DEFAULT. FNPTR keeps the unmanaged conventions
compilers emit for function pointers. A field signature must be exactly
FIELD, so a flagged FIELD header is malformed and never names a field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(managed): accept MethodSpec arity only from a fully decoded signature

The arity check read only the target's signature header, so a truncated
generic method signature passed. Under type selection a MethodDef target
outside the selection is never decoded elsewhere, so coverage stayed
complete. The arity now comes from a transient full decode, and only a
decoded method signature supplies it. signature() keeps its raw length
and digest around the shared decodeSignature.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(managed): decode each MethodSpec target signature once

edgeTargets caches the generic arity, including a failed decode, by
target method token, so many instantiations of one generic method share
a single signature decode. Unity.InputSystem has 472 distinct MethodSpecs
over about 133 target methods.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(managed): validate spec type operands and transitive reference scopes

* fix(managed): retain readable names when a scope row is unreadable

* test(managed): pin owner-scoped spec operand and scope-chain checks

e59217c83 validates spec type operands inside specSignature, so a
malformed generic TypeSpec base is now invalid-row under its owning
TypeSpec row; the three stale expectations follow that. New cases cover
a TypeSpec argument and a MethodSpec argument that name a missing TypeRef,
and a TypeRef whose ResolutionScope names itself. The scope cases move to
their own describe block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(managed): keep field targets from naming MethodSpecs

A MethodSpec over a field-signature MemberRef inherited kind "field", so
an ldsfld on that MethodSpec published a field name and declaring type
for malformed IL. A MethodSpec now borrows its target's name only when
the target is not a field; the arity check still reports it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(managed): reuse owner-decoded signatures for MethodSpec arity

MemberRef signatures, and MethodDef signatures in the type selection, are
already fully decoded, but the MethodSpec arity check decoded them again
and admitted a second transient allocation, which could exhaust the
budget. MemberCore now carries the owner's genericArity, and only a target
nothing decoded, such as an unselected MethodDef, falls back to the raw
signature. signatureGenericArity is the one rule for both paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
Nelglor
2026-10-13 09:12:15 -04:00
committed by GitHub
parent df04f99df0
commit 91830f1e0e
9 changed files with 1060 additions and 108 deletions
+22
View File
@@ -147,6 +147,28 @@ opposite observation's selection unknown, and application graphs report their
selected scope as partial. Neither makes an absence claim about the whole
assembly. Older member Evidence without `selection` remains readable.
### Edge target declaring types
`member_refs[].parent_name`, `call_edges[].target_declaring_type`, and
`field_accesses[].field_declaring_type` name the type that declares each
target. Referenced spec signatures validate encoded type coordinates against
the metadata tables, including nested type arguments and custom modifiers.
TypeRef resolution scopes are checked transitively with cycle detection;
readable names survive malformed scopes as partial evidence.
TypeDef and TypeRef names use the namespace-qualified `full_name`
form, and nested types keep bare names. A generic-instantiation TypeSpec
parent names its generic type, such as ``System.Collections.Generic.List`1``.
Other well-formed TypeSpec forms and ModuleRef or MethodDef parents stay
`null`. A `method-spec` call edge reports the name and declaring type of the
generic method it instantiates. A field access is named only by a field or a
field-signature MemberRef. TypeSpec and MethodSpec blobs are decoded with the
member-signature grammar, and a MethodSpec must instantiate a generic method
with as many type arguments as it declares. Malformed or unsupported rows and
blobs, and arity mismatches, make coverage partial. A readable MethodSpec
method column that names a method, or a TypeRef whose resolution scope is
invalid, keeps its name.
Older member Evidence without these fields reads them as `null`: unknown.
## Analysis objective
Start by identifying the artifact with `inspect_managed_artifact`, then inspect
+18 -1
View File
@@ -203,10 +203,18 @@ const managedMethodSchema = z.object({
body: managedMethodBodySchema,
});
// Declaring types are namespace-qualified like TypeDef `full_name`, nested
// types keep bare names, and a generic instantiation names its generic type.
// Evidence recorded before declaring types were named reads them as null.
const declaringTypeNameSchema = z.string().nullable().default(null);
const managedMemberReferenceSchema = z.object({
token: tokenSchema,
row_offset: offsetSchema,
parent_token: tokenSchema.nullable(),
parent_name: declaringTypeNameSchema.describe(
"Name of a TypeDef, TypeRef, or generic-instantiation TypeSpec parent; null for other parents or when unknown",
),
name: z.string(),
signature: managedSignatureSchema,
});
@@ -281,7 +289,13 @@ const managedCallEdgeSchema = z.object({
opcode: z.string(),
target_token: tokenSchema,
target_kind: z.enum(["method-def", "member-ref", "method-spec", "unknown"]),
target_name: z.string().nullable(),
target_name: z
.string()
.nullable()
.describe("Method name; a method-spec target reports its generic method"),
target_declaring_type: declaringTypeNameSchema.describe(
"Declaring type of the called method; null when unknown",
),
});
const managedFieldAccessSchema = z.object({
@@ -290,6 +304,9 @@ const managedFieldAccessSchema = z.object({
opcode: z.string(),
field_token: tokenSchema,
field_name: z.string().nullable(),
field_declaring_type: declaringTypeNameSchema.describe(
"Declaring type of the accessed field; null when unknown",
),
});
const managedParseIssueSchema = z.object({
+376 -18
View File
@@ -110,6 +110,8 @@ describe("managed member inspection", () => {
expect(result.member_refs).toEqual([
expect.objectContaining({
token: "0x0a000001",
parent_token: "0x01000001",
parent_name: "System.Runtime.Versioning.TargetFrameworkAttribute",
name: ".ctor",
signature: expect.objectContaining({
kind: "method",
@@ -126,6 +128,8 @@ describe("managed member inspection", () => {
target_token: "0x0a000001",
target_kind: "member-ref",
target_name: ".ctor",
target_declaring_type:
"System.Runtime.Versioning.TargetFrameworkAttribute",
},
]);
expect(result.field_accesses).toEqual([
@@ -135,12 +139,366 @@ describe("managed member inspection", () => {
opcode: "ldfld",
field_token: "0x04000001",
field_name: "counter",
field_declaring_type: "Fixture.Program",
},
]);
expect(result.coverage).toMatchObject({ state: "complete", issues: [] });
});
});
/** A tiny-header body of token-operand instructions followed by `ret`. */
const tokenBody = (
instructions: readonly (readonly [opcode: number, token: number])[],
): Buffer => {
const il = Buffer.concat([
...instructions.map(([opcode, token]) => {
const instruction = Buffer.alloc(5);
instruction.writeUInt8(opcode, 0);
instruction.writeUInt32LE(token, 1);
return instruction;
}),
Buffer.from([0x2a]),
]);
return Buffer.concat([Buffer.from([(il.length << 2) | 2]), il]);
};
const attribute = "System.Runtime.Versioning.TargetFrameworkAttribute";
const inspect = (options: Parameters<typeof buildManagedPeFixture>[0]) => {
const bytes = buildManagedPeFixture(options);
return inspectManagedMembersBytes(bytes, managedPeFixtureTarget(bytes));
};
const call = 0x28;
const ldsfld = 0x7e;
const memberRefOnTypeSpec = (signature: Buffer) =>
inspect({
typeSpecSignatures: [signature],
memberRefParentRaw: (1 << 3) | 4,
});
// HASTHIS | GENERIC, one generic parameter, no parameters, void.
const genericRef = Buffer.from([0x30, 0x01, 0x00, 0x01]);
// GENERIC, one generic parameter, no parameters, void.
const genericDef = Buffer.from([0x10, 0x01, 0x00, 0x01]);
describe("managed edge target declaring types", () => {
it("names a generic TypeSpec parent by its generic type and leaves other TypeSpecs unknown", () => {
// GENERICINST CLASS TypeRef(1) <string>
const generic = memberRefOnTypeSpec(
Buffer.from([0x15, 0x12, (1 << 2) | 1, 0x01, 0x0e]),
);
expect(generic.member_refs[0]?.parent_name).toBe(attribute);
expect(generic.call_edges[0]?.target_declaring_type).toBe(attribute);
// SZARRAY i4
const array = memberRefOnTypeSpec(Buffer.from([0x1d, 0x08]));
expect(array.member_refs[0]?.parent_name).toBeNull();
expect(array.coverage).toEqual({ state: "complete", issues: [] });
});
it.each([
[
"names a TypeSpec as its generic type",
[0x15, 0x12, (1 << 2) | 2, 0x01, 0x0e],
"invalid-row",
"metadata.TypeSpec:0x1b000001",
],
[
"has no type arguments",
[0x15, 0x12, (1 << 2) | 1],
"invalid-blob",
"metadata.TypeSpec:0x1b000001",
],
[
"names a missing TypeRef",
[0x15, 0x12, (9 << 2) | 1, 0x01, 0x0e],
"invalid-row",
"metadata.TypeSpec:0x1b000001",
],
[
"names a missing TypeDef",
[0x15, 0x12, (9 << 2) | 0, 0x01, 0x0e],
"invalid-row",
"metadata.TypeSpec:0x1b000001",
],
[
"has a type argument naming a missing TypeRef",
[0x15, 0x12, (1 << 2) | 1, 0x01, 0x12, (9 << 2) | 1],
"invalid-row",
"metadata.TypeSpec:0x1b000001",
],
])(
"reports a generic TypeSpec parent that %s and leaves it unknown",
(_case, signature, code, scope) => {
const result = memberRefOnTypeSpec(Buffer.from(signature));
expect(result.member_refs[0]?.parent_name).toBeNull();
expect(result.coverage).toMatchObject({
state: "partial",
issues: [{ code, scope }],
});
},
);
it("reports unsupported TypeSpec and MethodSpec forms instead of treating them as unknown", () => {
const typeSpec = memberRefOnTypeSpec(Buffer.from([0xff]));
const methodSpec = inspect({
memberRefSignature: genericRef,
methodSpecMethods: [(1 << 1) | 1],
methodSpecInstantiation: Buffer.from([0x0a, 0x01, 0xff]),
ilBody: tokenBody([[call, 0x2b00_0001]]),
});
expect(typeSpec.member_refs[0]?.parent_name).toBeNull();
expect(typeSpec.coverage).toMatchObject({
state: "partial",
issues: [
{
code: "unsupported-signature",
scope: "metadata.TypeSpec:0x1b000001",
},
],
});
expect(methodSpec.coverage).toMatchObject({
state: "partial",
issues: [
{
code: "unsupported-signature",
scope: "metadata.MethodSpec:0x2b000001",
},
],
});
});
it("names field MemberRefs as fields and leaves mismatched member kinds unnamed", () => {
const result = inspect({
memberRefName: "Empty",
// FIELD string
memberRefSignature: Buffer.from([0x06, 0x0e]),
ilBody: tokenBody([
[ldsfld, 0x0a00_0001],
[call, 0x0400_0001],
]),
});
const methodRef = inspect({ ilBody: tokenBody([[ldsfld, 0x0a00_0001]]) });
const flaggedField = inspect({
memberRefName: "Empty",
// GENERIC | FIELD string: FieldSig allows no flags.
memberRefSignature: Buffer.from([0x16, 0x0e]),
ilBody: tokenBody([[ldsfld, 0x0a00_0001]]),
});
expect(result.field_accesses).toMatchObject([
{ field_name: "Empty", field_declaring_type: attribute },
]);
expect(methodRef.field_accesses).toMatchObject([
{ field_name: null, field_declaring_type: null },
]);
expect(flaggedField.field_accesses).toMatchObject([
{ field_name: null, field_declaring_type: null },
]);
expect(flaggedField.coverage.state).toBe("partial");
expect(result.call_edges).toMatchObject([
{
target_kind: "unknown",
target_name: null,
target_declaring_type: null,
},
]);
});
it("reads Evidence recorded before declaring types were named as unknown", () => {
const current = inspect({});
const parsed = managedMemberInspectionSchema.parse({
...current,
member_refs: current.member_refs.map(
({ parent_name: _omitted, ...legacy }) => legacy,
),
call_edges: current.call_edges.map(
({ target_declaring_type: _omitted, ...legacy }) => legacy,
),
field_accesses: current.field_accesses.map(
({ field_declaring_type: _omitted, ...legacy }) => legacy,
),
});
expect(parsed.member_refs[0]?.parent_name).toBeNull();
expect(parsed.call_edges[0]?.target_declaring_type).toBeNull();
expect(parsed.field_accesses[0]?.field_declaring_type).toBeNull();
});
});
describe("managed TypeRef resolution scopes", () => {
it.each([
// AssemblyRef 9 does not exist.
["an invalid", (9 << 2) | 2, "is invalid"],
// The TypeRef is its own enclosing type.
["a cyclic", (1 << 2) | 3, "contains a cycle"],
])(
"keeps a TypeRef name beside %s resolution scope",
(_case, typeRefScopeRaw, detail) => {
const result = inspect({ typeRefScopeRaw });
expect(result.member_refs[0]?.parent_name).toBe(attribute);
expect(result.call_edges[0]?.target_declaring_type).toBe(attribute);
expect(result.coverage).toMatchObject({
state: "partial",
issues: [
{
code: "invalid-row",
scope: "metadata.TypeRef:0x01000001",
detail: expect.stringContaining(detail),
},
],
});
},
);
});
describe("managed MethodSpec call targets", () => {
it("names generic method calls after the method they instantiate", () => {
const result = inspect({
memberRefSignature: genericRef,
methodSignature: genericDef,
methodSpecMethods: [(1 << 1) | 1, (1 << 1) | 0],
ilBody: tokenBody([
[call, 0x2b00_0001],
[call, 0x2b00_0002],
]),
});
expect(
result.call_edges.map((edge) => [
edge.target_kind,
edge.target_name,
edge.target_declaring_type,
]),
).toEqual([
["method-spec", ".ctor", attribute],
["method-spec", "Main", "Fixture.Program"],
]);
expect(result.coverage).toEqual({ state: "complete", issues: [] });
});
it.each([
[
"a non-generic method",
{},
Buffer.from([0x0a, 0x01, 0x0e]),
"but its method declares 0 generic parameter(s)",
],
[
"the wrong number of type arguments",
{ memberRefSignature: genericRef },
Buffer.from([0x0a, 0x02, 0x0e, 0x08]),
"supplies 2 type argument(s), but its method declares 1",
],
[
"a field reference",
{ memberRefSignature: Buffer.from([0x06, 0x0e]) },
Buffer.from([0x0a, 0x01, 0x0e]),
"does not have a decodable method signature",
],
[
"a property signature",
// PROPERTY | GENERIC, one generic parameter, no parameters, void.
{ memberRefSignature: Buffer.from([0x18, 0x01, 0x00, 0x01]) },
Buffer.from([0x0a, 0x01, 0x0e]),
"does not have a decodable method signature",
],
[
"a truncated signature",
// HASTHIS | GENERIC, one generic parameter, no parameters, no return type.
{ memberRefSignature: Buffer.from([0x30, 0x01, 0x00]) },
Buffer.from([0x0a, 0x01, 0x0e]),
"does not have a decodable method signature",
],
[
"a type argument naming a missing TypeRef",
{ memberRefSignature: genericRef },
Buffer.from([0x0a, 0x01, 0x12, (9 << 2) | 1]),
"TypeDefOrRefOrSpec operand 0x25 is invalid",
],
[
"an unmanaged-convention signature",
// C | GENERIC: a function pointer convention, not a MethodRefSig one.
{ memberRefSignature: Buffer.from([0x11, 0x01, 0x00, 0x01]) },
Buffer.from([0x0a, 0x01, 0x0e]),
"does not have a decodable method signature",
],
])(
"reports a MethodSpec that instantiates %s",
(_case, options, methodSpecInstantiation, detail) => {
const result = inspect({
...options,
methodSpecMethods: [(1 << 1) | 1],
methodSpecInstantiation,
ilBody: tokenBody([[call, 0x2b00_0001]]),
});
expect(result.coverage).toMatchObject({
state: "partial",
issues: [
{
code: "invalid-row",
scope: "metadata.MethodSpec:0x2b000001",
detail: expect.stringContaining(detail),
},
],
});
},
);
it("never names a field through a MethodSpec", () => {
const result = inspect({
memberRefName: "Empty",
// FIELD string: a MethodSpec on it is malformed.
memberRefSignature: Buffer.from([0x06, 0x0e]),
methodSpecMethods: [(1 << 1) | 1],
ilBody: tokenBody([
[ldsfld, 0x2b00_0001],
[call, 0x2b00_0001],
]),
});
expect(result.field_accesses).toMatchObject([
{ field_name: null, field_declaring_type: null },
]);
expect(result.call_edges).toMatchObject([
{ target_name: null, target_declaring_type: null },
]);
expect(result.coverage.state).toBe("partial");
});
it("keeps a readable MethodSpec method beside a malformed instantiation", () => {
const result = inspect({
methodSpecMethods: [(1 << 1) | 1, (9 << 1) | 1],
methodSpecInstantiation: Buffer.from([0x0a, 0x00]),
ilBody: tokenBody([
[call, 0x2b00_0001],
[call, 0x2b00_0002],
]),
});
expect(
result.call_edges.map(({ target_name, target_declaring_type }) => [
target_name,
target_declaring_type,
]),
).toEqual([
[".ctor", attribute],
[null, null],
]);
expect(result.coverage.state).toBe("partial");
expect(
result.coverage.issues.map(({ code, scope }) => [code, scope]),
).toEqual([
["invalid-blob", "metadata.MethodSpec:0x2b000001"],
["invalid-row", "metadata.MethodSpec:0x2b000002"],
["invalid-blob", "metadata.MethodSpec:0x2b000002"],
]);
});
});
describe("managed exported types", () => {
it("reports type forwarders and nested exported types with their implementation scope", () => {
const bytes = buildManagedPeFixture({
@@ -373,15 +731,9 @@ describe("managed derived edges", () => {
body: { anchors: Array.from({ length: 40_000 }, () => anchor) },
} as unknown as ManagedMethod;
expect(() =>
edges(
[method],
new Map(),
new Map(),
new Map(),
new ManagedDecodeBudget(),
),
).toThrow(ManagedReaderFailure);
expect(() => edges([method], new Map(), new ManagedDecodeBudget())).toThrow(
ManagedReaderFailure,
);
});
});
@@ -413,15 +765,9 @@ describe("managed derived caller names", () => {
},
} as unknown as ManagedMethod;
expect(() =>
edges(
[method],
new Map(),
new Map(),
new Map(),
new ManagedDecodeBudget(),
),
).toThrow(ManagedReaderFailure);
expect(() => edges([method], new Map(), new ManagedDecodeBudget())).toThrow(
ManagedReaderFailure,
);
expect(built).toBe(false);
});
});
@@ -671,6 +1017,18 @@ it.each([
});
});
it.each([
["a property header", [0x08, 0x00, 0x08]],
["an explicit-this header without has-this", [0x40, 0x00, 0x01]],
["a C calling convention header", [0x01, 0x00, 0x01]],
["a generic vararg header", [0x15, 0x01, 0x00, 0x01]],
])("reports %s as a malformed member signature", (_name, blob) => {
expect(signature(Buffer.from(blob))).toMatchObject({
kind: "unknown",
parse_status: "malformed",
});
});
it("rejects array ranks beyond the CLR limit before rendering dimensions", () => {
expect(
signature(
+14 -4
View File
@@ -27,6 +27,8 @@ import {
typeRanges,
type TypeRange,
} from "./ManagedMemberInspectorCore.js";
import { edgeTargets } from "./ManagedMethodSpecs.js";
import { typeNameLookup } from "./ManagedTypeNames.js";
import {
edges,
parseExportedTypes,
@@ -171,7 +173,11 @@ export const inspectManagedMembersBytes = (
const exportedTypes = parseExportedTypes(bytes, layout);
issues.push(...exportedTypes.issues);
const fields = parseFields(bytes, layout, ranges, selected);
const memberRefs = parseMemberRefs(bytes, layout);
const memberRefs = parseMemberRefs(
bytes,
layout,
typeNameLookup(bytes, layout, ranges, issues),
);
issues.push(...memberRefs.issues);
const methods = parseMethods({
bytes,
@@ -181,6 +187,12 @@ export const inspectManagedMembersBytes = (
selected,
});
issues.push(...fields.issues, ...methods.issues);
const targets = edgeTargets(bytes, layout, methods.methods, [
fields.core,
memberRefs.core,
methods.core,
]);
issues.push(...targets.issues);
const coverageIssues = uniqueManagedParseIssues(issues);
const incompleteMembers =
[...fields.fields, ...memberRefs.refs, ...methods.methods].some(
@@ -193,9 +205,7 @@ export const inspectManagedMembersBytes = (
);
const related = edges(
methods.methods,
methods.core,
fields.core,
memberRefs.core,
targets.members,
managedDecodeBudget(layout),
);
return managedMemberInspectionSchema.parse(
+186 -44
View File
@@ -7,6 +7,7 @@ import type {
ManagedMemberInspection,
ManagedParseIssue,
} from "../domain/managed/managedArtifact.js";
import { managedDecodeBudget } from "./ManagedDecodeBudget.js";
import type {
ManagedMetadataLayout,
MetadataTableLayout,
@@ -49,20 +50,22 @@ export interface TypeRange {
readonly methodEnd: number;
}
export interface MethodCore {
/**
* Name and declaring type of a field, method, member reference, or method
* specification, keyed by its token so edges resolve any target uniformly.
*/
export interface MemberCore {
readonly token: string;
readonly name: string;
readonly declaringType: string | null;
}
export interface FieldCore {
readonly token: string;
readonly name: string;
}
export interface MemberRefCore {
readonly token: string;
readonly name: string;
/** Fields and methods by table; member references by decoded signature. */
readonly kind: ManagedSignature["kind"];
/**
* Generic arity from the owner's decoded signature (see
* `signatureGenericArity`); absent when no owner decoded it, as for a
* MethodDef outside the type selection.
*/
readonly genericArity?: number | null;
}
export interface ParsedInstruction {
@@ -88,7 +91,7 @@ const rowRange = (
return { first: start, last: end - 1, count: end - start };
};
const fullName = (namespace: string, name: string): string =>
export const fullName = (namespace: string, name: string): string =>
namespace.length === 0 ? name : `${namespace}.${name}`;
const readTypeRange = (
@@ -242,7 +245,7 @@ export const parseTypes = (
return { types: items, issues };
};
const readCompressed = (
export const readCompressed = (
bytes: Buffer,
offset: number,
): { readonly value: number; readonly next: number } => {
@@ -341,13 +344,38 @@ interface MethodSignatureHeader {
readonly next: number;
}
// MethodDefSig and MethodRefSig headers (ECMA-335 II.23.2.1-2) without
// HASTHIS and EXPLICITTHIS: DEFAULT, VARARG, or GENERIC, which implies DEFAULT.
const MEMBER_METHOD_HEADERS = new Set([0x00, 0x05, 0x10]);
// FNPTR also carries the unmanaged C, STDCALL, THISCALL, FASTCALL, and
// UNMANAGED conventions that compilers emit for unmanaged function pointers.
const FUNCTION_POINTER_CONVENTIONS = new Set([0, 1, 2, 3, 4, 5, 9]);
/**
* Read a method signature's calling convention and counts. A header that is
* not a method signature in its context (a MethodDef or MemberRef row, or an
* FNPTR type), sets the reserved bit, or sets EXPLICITTHIS without HASTHIS is
* malformed, so no caller consumes its counts. FIELD, LOCAL_SIG, PROPERTY,
* and GENERICINST headers are never method signatures.
*/
const readMethodSignatureHeader = (
blob: Buffer,
offset: number,
context: "member" | "function-pointer",
): MethodSignatureHeader => {
const first = blob[offset];
if (first === undefined)
return malformedSignature("truncated method signature", offset);
const allowed =
context === "member"
? MEMBER_METHOD_HEADERS.has(first & 0x9f)
: (first & 0x80) === 0 && FUNCTION_POINTER_CONVENTIONS.has(first & 0x0f);
if (!allowed || ((first & 0x40) !== 0 && (first & 0x20) === 0))
return malformedSignature(
`calling convention 0x${first.toString(16)} does not head a ${context} method signature`,
offset,
);
let next = offset + 1;
let genericParameterCount: number | null = null;
if ((first & 0x10) !== 0) {
@@ -366,12 +394,6 @@ const readMethodSignatureHeader = (
const MAX_ARRAY_RANK = 32;
// FNPTR carries a method signature: managed DEFAULT/VARARG, or the unmanaged
// C, STDCALL, THISCALL, FASTCALL, and UNMANAGED conventions that compilers
// emit for unmanaged function pointers. FIELD, LOCAL_SIG, PROPERTY, and
// GENERICINST headers are not method signatures.
const FUNCTION_POINTER_CONVENTIONS = new Set([0, 1, 2, 3, 4, 5, 9]);
const arrayShape = (
blob: Buffer,
offset: number,
@@ -449,10 +471,40 @@ const POSITION_ELEMENTS: ReadonlyMap<number, readonly SignaturePosition[]> =
[0x10, ["return", "parameter", "field"]],
]);
/** Decode a type operand once; spec callers also validate its metadata coordinate. */
const readTypeOperand = (
blob: Buffer,
offset: number,
layout?: ManagedMetadataLayout,
tables: readonly number[] = [2, 1, 27],
): ReturnType<typeof readCompressed> => {
const token = readCompressed(blob, offset);
if (layout !== undefined) {
const reason =
token.value === 0
? "type operand is null"
: metadataCodedTokenInvalidReason(
token.value,
2,
tables,
layout.rowCounts,
);
if (reason !== null)
throw managedFailure(
"invalid-row",
"signature",
`TypeDefOrRefOrSpec operand 0x${token.value.toString(16)} is invalid: ${reason}`,
offset,
);
}
return token;
};
const readTypeSignature = (
blob: Buffer,
offset: number,
base: SignaturePosition,
layout?: ManagedMetadataLayout,
): { readonly value: string; readonly next: number } => {
const stack: TypeContinuation[] = [];
let cursor = offset;
@@ -479,7 +531,7 @@ const readTypeSignature = (
stack.push({ kind: "suffix", suffix });
cursor += 1;
} else if (kind === 0x1f || kind === 0x20) {
const token = readCompressed(blob, cursor + 1);
const token = readTypeOperand(blob, cursor + 1, layout);
const modifier = `${kind === 0x1f ? "modreq" : "modopt"}:${String(token.value)}`;
// A modifier frame on top was pushed by the directly preceding byte;
// keep an adjacent run together and in source order.
@@ -488,7 +540,7 @@ const readTypeSignature = (
else stack.push({ kind: "modifiers", modifiers: [modifier] });
cursor = token.next;
} else if (kind === 0x11 || kind === 0x12) {
const token = readCompressed(blob, cursor + 1);
const token = readTypeOperand(blob, cursor + 1, layout);
value = `${kind === 0x11 ? "valuetype" : "class"}:${String(token.value)}`;
cursor = token.next;
} else if (kind === 0x1e || kind === 0x13) {
@@ -508,7 +560,7 @@ const readTypeSignature = (
"generic instantiation does not name a class or value type",
cursor + 1,
);
const token = readCompressed(blob, cursor + 2);
const token = readTypeOperand(blob, cursor + 2, layout, [2, 1]);
const argumentCount = readCompressed(blob, token.next);
if (argumentCount.value === 0)
return malformedSignature(
@@ -525,16 +577,11 @@ const readTypeSignature = (
});
cursor = argumentCount.next;
} else if (kind === 0x1b) {
const header = readMethodSignatureHeader(blob, cursor + 1);
if (
!FUNCTION_POINTER_CONVENTIONS.has(header.first & 0x0f) ||
(header.first & 0x80) !== 0 ||
((header.first & 0x40) !== 0 && (header.first & 0x20) === 0)
)
return malformedSignature(
`function pointer calling convention 0x${header.first.toString(16)} does not head a method signature`,
cursor + 1,
);
const header = readMethodSignatureHeader(
blob,
cursor + 1,
"function-pointer",
);
const convention = callingConvention(header.first);
const head =
header.genericParameterCount === null
@@ -602,22 +649,81 @@ const callingConvention = (value: number): string => {
return flags.length === 0 ? name : `${name} ${flags.join(" ")}`;
};
export const signature = (
/**
* Decode a whole TypeSpec blob, or a MethodSpec instantiation (GENERICINST,
* argument count, Types), with the member-signature grammar and return the
* rendered types. A malformed or unsupported blob throws invalid-blob or
* unsupported-signature under the owning row, so coverage records it.
*/
export const specSignature = (
layout: ManagedMetadataLayout,
blob: Buffer,
rawSha256?: string,
): ManagedSignature => {
const raw = {
raw_length: blob.length,
raw_sha256: rawSha256 ?? sha256Bytes(blob),
};
owner: {
readonly form: "type" | "instantiation";
readonly scope: string;
readonly offset: number;
},
): readonly string[] => {
const budget = managedDecodeBudget(layout);
// Rendering is transient, so charge it as member signatures are charged.
budget.admitTransient(blob.length * 256, owner.offset);
try {
let cursor = 0;
let count = 1;
if (owner.form === "instantiation") {
if (blob[0] !== 0x0a)
malformedSignature("MethodSpec instantiation is not GENERICINST", 0);
const header = readCompressed(blob, 1);
if (header.value === 0)
malformedSignature("MethodSpec instantiation has no type arguments", 1);
count = header.value;
cursor = header.next;
}
const types: string[] = [];
// Each Type consumes at least one byte, so a false count fails on truncation.
for (let index = 0; index < count; index += 1) {
const type = readTypeSignature(blob, cursor, "type", layout);
types.push(type.value);
cursor = type.next;
}
if (cursor !== blob.length)
malformedSignature("Trailing signature data", cursor);
return types;
} catch (cause: unknown) {
if (
!(cause instanceof ManagedReaderFailure) ||
(cause.issue.code !== "invalid-blob" &&
cause.issue.code !== "invalid-row" &&
cause.issue.code !== "unsupported-signature")
)
throw cause;
throw managedFailure(
cause.issue.code,
owner.scope,
cause.issue.detail,
owner.offset,
);
} finally {
budget.releaseTransient();
}
};
type DecodedSignature = Omit<ManagedSignature, "raw_length" | "raw_sha256">;
/**
* Decode a field or member method signature. A malformed or unsupported blob
* decodes as kind `unknown` with its reason.
*/
const decodeSignature = (blob: Buffer): DecodedSignature => {
try {
if (blob.length === 0)
throw managedFailure("invalid-blob", "signature", "empty signature", 0);
const first = blob[0] ?? 0;
if ((first & 0x0f) === 6) {
// FieldSig (II.23.2.4) is exactly FIELD; a flagged FIELD header is not a
// method header either, so it is malformed below.
if (first === 0x06) {
const fieldType = readTypeSignature(blob, 1, "field");
return {
...raw,
kind: "field",
parse_status: fieldType.next === blob.length ? "decoded" : "partial",
calling_convention: callingConvention(first),
@@ -630,7 +736,7 @@ export const signature = (
fieldType.next === blob.length ? null : "Trailing signature data",
};
}
const header = readMethodSignatureHeader(blob, 0);
const header = readMethodSignatureHeader(blob, 0, "member");
const returnType = readTypeSignature(blob, header.next, "return");
let offset = returnType.next;
const parameters: string[] = [];
@@ -640,7 +746,6 @@ export const signature = (
offset = parameter.next;
}
return {
...raw,
kind: "method",
parse_status: offset === blob.length ? "decoded" : "partial",
calling_convention: callingConvention(first),
@@ -659,7 +764,6 @@ export const signature = (
)
throw cause;
return {
...raw,
kind: "unknown",
parse_status:
cause.issue.code === "unsupported-signature"
@@ -675,3 +779,41 @@ export const signature = (
};
}
};
/**
* Generic parameter count of a method signature that decodes completely, 0
* when it is not generic, or null when the blob is not a decodable method
* signature. The rendered decode is transient.
*/
export const methodGenericArity = (
layout: ManagedMetadataLayout,
blob: Buffer,
): number | null => {
const budget = managedDecodeBudget(layout);
budget.admitTransient(blob.length * 256, layout.blob.offset);
try {
return signatureGenericArity(decodeSignature(blob));
} finally {
budget.releaseTransient();
}
};
/**
* Generic parameter count of a completely decoded method signature, 0 when it
* is not generic, or null for any other signature.
*/
export const signatureGenericArity = (
decoded: DecodedSignature,
): number | null =>
decoded.kind === "method" && decoded.parse_status === "decoded"
? (decoded.generic_parameter_count ?? 0)
: null;
export const signature = (
blob: Buffer,
rawSha256?: string,
): ManagedSignature => ({
raw_length: blob.length,
raw_sha256: rawSha256 ?? sha256Bytes(blob),
...decodeSignature(blob),
});
+78 -35
View File
@@ -9,15 +9,14 @@ import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js";
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
import {
signature,
type FieldCore,
signatureGenericArity,
type ManagedCallEdge,
type ManagedExportedType,
type ManagedField,
type ManagedFieldAccess,
type ManagedMemberRef,
type ManagedMethod,
type MemberRefCore,
type MethodCore,
type MemberCore,
type TypeRange,
} from "./ManagedMemberInspectorCore.js";
import {
@@ -51,11 +50,11 @@ export const parseFields = (
selected: SelectedTypeTokens = null,
): {
readonly fields: readonly ManagedField[];
readonly core: ReadonlyMap<string, FieldCore>;
readonly core: ReadonlyMap<string, MemberCore>;
readonly issues: readonly ManagedParseIssue[];
} => {
const fields: ManagedField[] = [];
const core = new Map<string, FieldCore>();
const core = new Map<string, MemberCore>();
const issues: ManagedParseIssue[] = [];
const table = layout.table(4);
const ownerRow = readManagedValue(
@@ -78,7 +77,12 @@ export const parseFields = (
: (readManagedValue(() => ownerRow(row), issues) ?? 0);
const declared = ranges[owner - 1];
const token = metadataToken(4, row);
core.set(token, { token, name });
core.set(token, {
token,
name,
declaringType: declared?.fullName ?? null,
kind: "field",
});
if (!isSelected(selected, declared)) return;
const sig = readMetadataBlob(
bytes,
@@ -174,13 +178,14 @@ export const parseExportedTypes = (
export const parseMemberRefs = (
bytes: Buffer,
layout: ManagedMetadataLayout,
typeName: (token: string | null) => string | null,
): {
readonly refs: readonly ManagedMemberRef[];
readonly core: ReadonlyMap<string, MemberRefCore>;
readonly core: ReadonlyMap<string, MemberCore>;
readonly issues: readonly ManagedParseIssue[];
} => {
const refs: ManagedMemberRef[] = [];
const core = new Map<string, MemberRefCore>();
const core = new Map<string, MemberCore>();
const issues: ManagedParseIssue[] = [];
const table = layout.table(10);
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
@@ -218,19 +223,29 @@ export const parseMemberRefs = (
? "MemberRef parent coded index 0x0 is null, but the Class column must reference a row"
: `MemberRef parent coded index 0x${parentRaw.toString(16)} is invalid: ${parentReason}`,
});
const parentToken = metadataCodedToken(
parentRaw,
3,
[2, 1, 26, 6, 27],
layout.rowCounts,
);
const parentName = typeName(parentToken);
const decoded = budgetedSignature(sig, layout);
refs.push({
token,
row_offset: cursor.start,
parent_token: metadataCodedToken(
parentRaw,
3,
[2, 1, 26, 6, 27],
layout.rowCounts,
),
parent_token: parentToken,
parent_name: parentName,
name,
signature: budgetedSignature(sig, layout),
signature: decoded,
});
core.set(token, {
token,
name,
declaringType: parentName,
kind: decoded.kind,
genericArity: signatureGenericArity(decoded),
});
core.set(token, { token, name });
}, issues);
}
return { refs, core, issues };
@@ -252,11 +267,11 @@ export const parseMethods = ({
selected = null,
}: ParseMethodsInput): {
readonly methods: readonly ManagedMethod[];
readonly core: ReadonlyMap<string, MethodCore>;
readonly core: ReadonlyMap<string, MemberCore>;
readonly issues: readonly ManagedParseIssue[];
} => {
const methods: ManagedMethod[] = [];
const core = new Map<string, MethodCore>();
const core = new Map<string, MemberCore>();
const issues: ManagedParseIssue[] = [];
const table = layout.table(6);
const ownerRow = readManagedValue(
@@ -281,11 +296,13 @@ export const parseMethods = ({
: (readManagedValue(() => ownerRow(row), issues) ?? 0);
const declared = ranges[owner - 1];
const token = metadataToken(6, row);
core.set(token, {
const member = {
token,
name,
declaringType: declared?.fullName ?? null,
});
kind: "method",
} as const;
core.set(token, member);
if (!isSelected(selected, declared)) return;
const sig = readMetadataBlob(
bytes,
@@ -294,6 +311,11 @@ export const parseMethods = ({
layout.blob.size,
);
cursor.readIndex(layout.tableIndexSize(8));
const decoded = budgetedSignature(sig, layout);
core.set(token, {
...member,
genericArity: signatureGenericArity(decoded),
});
methods.push({
token,
row_offset: cursor.start,
@@ -303,7 +325,7 @@ export const parseMethods = ({
rva,
impl_flags: implFlags,
flags,
signature: budgetedSignature(sig, layout),
signature: decoded,
body: methodBody(
bytes,
pe,
@@ -317,6 +339,15 @@ export const parseMethods = ({
return { methods, core, issues };
};
/**
* Member kinds each operand kind may name. A MemberRef whose signature does
* not decode keeps its name on calls, as before, but is never named a field.
*/
const NAMEABLE_KINDS = new Map<string, ReadonlySet<MemberCore["kind"]>>([
["method", new Set(["method", "unknown"])],
["field", new Set(["field"])],
]);
const targetKind = (token: string): ManagedCallEdge["target_kind"] =>
token.startsWith("0x06")
? "method-def"
@@ -326,11 +357,22 @@ const targetKind = (token: string): ManagedCallEdge["target_kind"] =>
? "method-spec"
: "unknown";
/** The member an operand names, if the operand kind can name that member's kind. */
const namedTarget = (
members: ReadonlyMap<string, MemberCore>,
operandKind: string,
operand: string,
): MemberCore | undefined => {
const member = members.get(operand);
return member !== undefined &&
NAMEABLE_KINDS.get(operandKind)?.has(member.kind) === true
? member
: undefined;
};
export const edges = (
methods: readonly ManagedMethod[],
methodCore: ReadonlyMap<string, MethodCore>,
fieldCore: ReadonlyMap<string, FieldCore>,
refCore: ReadonlyMap<string, MemberRefCore>,
members: ReadonlyMap<string, MemberCore>,
budget: ManagedDecodeBudget,
): {
readonly callEdges: readonly ManagedCallEdge[];
@@ -350,11 +392,10 @@ export const edges = (
const caller = derives ? callerName(method) : method.name;
for (const anchor of method.body.anchors) {
if (anchor.opcode === "ldtoken") continue;
if (anchor.operand_kind === "method" && anchor.operand !== null) {
const named =
methodCore.get(anchor.operand)?.name ??
refCore.get(anchor.operand)?.name ??
null;
const { operand } = anchor;
if (operand === null) continue;
const target = namedTarget(members, anchor.operand_kind, operand);
if (anchor.operand_kind === "method") {
// Each derived record is admitted before the graph grows further.
callEdges.push(
admitManagedProjection(
@@ -362,23 +403,25 @@ export const edges = (
caller_token: method.token,
caller,
opcode: anchor.opcode,
target_token: anchor.operand,
target_kind: targetKind(anchor.operand),
target_name: named,
target_token: operand,
target_kind: targetKind(operand),
target_name: target?.name ?? null,
target_declaring_type: target?.declaringType ?? null,
},
budget,
),
);
}
if (anchor.operand_kind === "field" && anchor.operand !== null) {
if (anchor.operand_kind === "field") {
fieldAccesses.push(
admitManagedProjection(
{
method_token: method.token,
method: caller,
opcode: anchor.opcode,
field_token: anchor.operand,
field_name: fieldCore.get(anchor.operand)?.name ?? null,
field_token: operand,
field_name: target?.name ?? null,
field_declaring_type: target?.declaringType ?? null,
},
budget,
),
+154
View File
@@ -0,0 +1,154 @@
import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js";
import {
methodGenericArity,
specSignature,
type ManagedMethod,
type MemberCore,
} from "./ManagedMemberInspectorCore.js";
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
import {
metadataCodedToken,
metadataCodedTokenInvalidReason,
metadataRowCursor,
readMetadataBlob,
} from "./ManagedMetadataHeaps.js";
import { managedFailure, readManagedValue } from "./ManagedReaderFailure.js";
/**
* Index fields, member references, and methods by token, then name each
* MethodSpec an inspected body calls after the generic method it instantiates,
* so every edge target resolves through one lookup.
*/
export const edgeTargets = (
bytes: Buffer,
layout: ManagedMetadataLayout,
methods: readonly ManagedMethod[],
cores: readonly ReadonlyMap<string, MemberCore>[],
): {
readonly members: ReadonlyMap<string, MemberCore>;
readonly issues: readonly ManagedParseIssue[];
} => {
const members = new Map(cores.flatMap((core) => [...core]));
const issues: ManagedParseIssue[] = [];
const seen = new Set<string>();
// Generic arity by method token, so instantiations of one method decode it once.
const arities = new Map<string, number | null>();
for (const method of methods)
for (const { operand } of method.body.anchors) {
if (operand?.startsWith("0x2b") !== true || seen.has(operand)) continue;
seen.add(operand);
readManagedValue(
() =>
readMethodSpec({ bytes, layout, members, issues, arities }, operand),
issues,
);
}
return { members, issues };
};
/**
* Map one MethodSpec to its generic method, then check its instantiation
* decodes and matches that method's generic arity. A readable Method column
* keeps its name when the rest is malformed; the failure makes coverage partial.
*/
const readMethodSpec = (
{
bytes,
layout,
members,
issues,
arities,
}: {
readonly bytes: Buffer;
readonly layout: ManagedMetadataLayout;
readonly members: Map<string, MemberCore>;
readonly issues: ManagedParseIssue[];
readonly arities: Map<string, number | null>;
},
token: string,
): void => {
const scope = `metadata.MethodSpec:${token}`;
const cursor = metadataRowCursor(
bytes,
layout,
0x2b,
Number.parseInt(token.slice(4), 16),
);
const methodRaw = cursor.readIndex(layout.codedIndexSize("MethodDefOrRef"));
const instantiation = cursor.readIndex(layout.blobIndexSize);
const methodReason = metadataCodedTokenInvalidReason(
methodRaw,
1,
[6, 10],
layout.rowCounts,
);
if (methodReason !== null || methodRaw === 0)
issues.push({
code: "invalid-row",
scope,
offset: cursor.start,
detail:
methodReason === null
? "MethodSpec method coded index 0x0 is null, but the Method column must reference a row"
: `MethodSpec method coded index 0x${methodRaw.toString(16)} is invalid: ${methodReason}`,
});
const generic = metadataCodedToken(methodRaw, 1, [6, 10], layout.rowCounts);
const target = generic === null ? undefined : members.get(generic);
// A MethodSpec instantiates a method, so a field target never lends it a
// name, even to a field operand.
if (target !== undefined && target.kind !== "field")
members.set(token, { ...target, token });
const types = specSignature(
layout,
readMetadataBlob(bytes, layout, instantiation, layout.blob.size),
{ form: "instantiation", scope, offset: cursor.start },
);
if (generic === null) return;
// Prefer the arity the target's owner decoded; read the raw signature only
// for a target nothing decoded, such as a MethodDef outside the selection.
if (target?.genericArity === undefined && !arities.has(generic))
arities.set(
generic,
methodGenericArity(layout, methodSignatureBlob(bytes, layout, generic)),
);
const arity =
target?.genericArity === undefined
? (arities.get(generic) ?? null)
: target.genericArity;
if (arity !== types.length)
throw managedFailure(
"invalid-row",
scope,
arity === null
? "MethodSpec method does not have a decodable method signature"
: `MethodSpec supplies ${String(types.length)} type argument(s), but its method declares ${String(arity)} generic parameter(s)`,
cursor.start,
);
};
/** The Signature column of a MethodDef or MemberRef row. */
const methodSignatureBlob = (
bytes: Buffer,
layout: ManagedMetadataLayout,
token: string,
): Buffer => {
const table = Number.parseInt(token.slice(2, 4), 16);
const cursor = metadataRowCursor(
bytes,
layout,
table,
Number.parseInt(token.slice(4), 16),
);
if (table === 6) {
cursor.readUInt32();
cursor.readUInt16();
cursor.readUInt16();
} else cursor.readIndex(layout.codedIndexSize("MemberRefParent"));
cursor.readIndex(layout.stringIndexSize);
return readMetadataBlob(
bytes,
layout,
cursor.readIndex(layout.blobIndexSize),
layout.blob.size,
);
};
+75 -6
View File
@@ -29,6 +29,16 @@ interface ManagedPeFixtureOptions {
readonly moduleRowCount?: number;
readonly assemblyRowCount?: number;
readonly memberRefParentRaw?: number;
readonly memberRefName?: string;
readonly memberRefSignature?: Buffer;
/** Raw MethodDefOrRef coded values, one MethodSpec row each. */
readonly methodSpecMethods?: readonly number[];
/** Instantiation blob shared by every MethodSpec row. */
readonly methodSpecInstantiation?: Buffer;
/** TypeSpec signature blobs, one TypeSpec row each. */
readonly typeSpecSignatures?: readonly Buffer[];
/** Raw ResolutionScope coded value of the TypeRef row; null by default. */
readonly typeRefScopeRaw?: number;
readonly mvid?: Buffer;
readonly pinvoke?: {
readonly importName?: string;
@@ -174,10 +184,10 @@ const typeRefRow = (
name: number,
namespace: number,
stringSize: MetadataIndexSize,
resolutionScopeSize: MetadataIndexSize,
resolutionScope: Buffer,
): Buffer =>
Buffer.concat([
metadataIndex(0, resolutionScopeSize),
resolutionScope,
metadataIndex(name, stringSize),
metadataIndex(namespace, stringSize),
]);
@@ -524,6 +534,8 @@ interface FixtureMetadataRowContext {
readonly methodSignature: number;
readonly constructorSignature: number;
readonly attributeBlob: number;
readonly typeSpecSignatures: readonly number[];
readonly methodSpecInstantiation: number;
};
readonly heapSizes: {
readonly strings: MetadataIndexSize;
@@ -595,6 +607,37 @@ const exportedTypeTable = (
],
];
const specTables = (
options: ManagedPeFixtureOptions,
indexes: FixtureMetadataRowContext["indexes"],
heapSizes: FixtureMetadataRowContext["heapSizes"],
widths: FixtureIndexWidths,
): readonly (readonly [number, readonly Buffer[]])[] => [
...(indexes.typeSpecSignatures.length === 0
? []
: ([
[
27,
indexes.typeSpecSignatures.map((signature) =>
metadataIndex(signature, heapSizes.blobs),
),
],
] as const)),
...(options.methodSpecMethods === undefined
? []
: ([
[
43,
options.methodSpecMethods.map((method) =>
Buffer.concat([
metadataIndex(method, widths.coded([6, 10], 1)),
metadataIndex(indexes.methodSpecInstantiation, heapSizes.blobs),
]),
),
],
] as const)),
];
const fixtureIndexWidths = ({
options,
referenceStringIndexes,
@@ -616,10 +659,12 @@ const fixtureIndexWidths = ({
],
...(indexes.pinvokeModuleName === null ? [] : ([[26, 1]] as const)),
...(indexes.pinvokeImportName === null ? [] : ([[28, 1]] as const)),
[27, indexes.typeSpecSignatures.length],
[32, options.assemblyRowCount ?? 1],
[35, referenceStringIndexes.length],
[39, indexes.exportedTypes.length],
[40, 1],
[43, options.methodSpecMethods?.length ?? 0],
]);
const count = (table: number): number => rowCounts.get(table) ?? 0;
const table = (index: number): MetadataIndexSize => indexSize(count(index));
@@ -673,7 +718,7 @@ const fixtureMetadataRows = ({
indexes.attributeName,
indexes.attributeNamespace,
heapSizes.strings,
widths.resolutionScope,
metadataIndex(options.typeRefScopeRaw ?? 0, widths.resolutionScope),
),
],
],
@@ -780,6 +825,7 @@ const fixtureMetadataRows = ({
),
],
...exportedTypeTable(options, indexes, heapSizes, widths),
...specTables(options, indexes, heapSizes, widths),
[
40,
[
@@ -795,6 +841,29 @@ const fixtureMetadataRows = ({
return rows;
};
/** Blob indexes for the MemberRef and optional spec rows; defaults leave the heap unchanged. */
const referenceBlobIndexes = (
blobs: BlobHeap,
options: ManagedPeFixtureOptions,
): Pick<
FixtureMetadataRowContext["indexes"],
"constructorSignature" | "typeSpecSignatures" | "methodSpecInstantiation"
> => ({
constructorSignature: blobs.add(
options.memberRefSignature ?? Buffer.from([0x20, 0x01, 0x01, 0x0e]),
),
typeSpecSignatures: (options.typeSpecSignatures ?? []).map((signature) =>
blobs.add(signature),
),
// Defaults to GENERICINST with one string type argument.
methodSpecInstantiation:
options.methodSpecMethods === undefined
? 0
: blobs.add(
options.methodSpecInstantiation ?? Buffer.from([0x0a, 0x01, 0x0e]),
),
});
const buildManagedFixtureMetadata = (
options: ManagedPeFixtureOptions,
methodRva: number,
@@ -805,7 +874,7 @@ const buildManagedFixtureMetadata = (
const assemblyName = strings.add("Fixture.Managed");
const attributeName = strings.add("TargetFrameworkAttribute");
const attributeNamespace = strings.add("System.Runtime.Versioning");
const constructorName = strings.add(".ctor");
const constructorName = strings.add(options.memberRefName ?? ".ctor");
const typeName = strings.add(options.typeName ?? "Program");
const additionalTypeNames = Array.from(
{ length: options.additionalEmptyTypes ?? 0 },
@@ -840,7 +909,7 @@ const buildManagedFixtureMetadata = (
const methodSignature = blobs.add(
options.methodSignature ?? Buffer.from([0x00, 0x00, 0x01]),
);
const constructorSignature = blobs.add(Buffer.from([0x20, 0x01, 0x01, 0x0e]));
const referenceBlobs = referenceBlobIndexes(blobs, options);
const attributeBlob = blobs.add(
fixedStringAttributeBlob(
options.targetFramework === null
@@ -873,8 +942,8 @@ const buildManagedFixtureMetadata = (
tokenBlob,
fieldSignature,
methodSignature,
constructorSignature,
attributeBlob,
...referenceBlobs,
},
heapSizes: { strings: stringSize, blobs: blobSize, guids: guidSize },
});
+137
View File
@@ -0,0 +1,137 @@
import { managedDecodeBudget } from "./ManagedDecodeBudget.js";
import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js";
import {
fullName,
readCompressed,
specSignature,
type TypeRange,
} from "./ManagedMemberInspectorCore.js";
import {
metadataRowOffset,
type ManagedMetadataLayout,
} from "./ManagedMetadataLayout.js";
import {
metadataCodedTokenInvalidReason,
metadataRowCursor,
metadataToken,
readMetadataBlob,
readMetadataString,
} from "./ManagedMetadataHeaps.js";
import { managedFailure, readManagedValue } from "./ManagedReaderFailure.js";
/**
* Name the type a TypeDef, TypeRef, or generic-instantiation TypeSpec token
* denotes, in the namespace-qualified form TypeDef `full_name` uses, once per
* token. Other decoded TypeSpecs stay null: the type is unknown. Malformed or
* unsupported rows and blobs are reported to `issues` and also stay null,
* except that a TypeRef with an invalid ResolutionScope keeps its readable name.
*/
export const typeNameLookup = (
bytes: Buffer,
layout: ManagedMetadataLayout,
ranges: readonly TypeRange[],
issues: ManagedParseIssue[],
): ((token: string | null) => string | null) => {
const names = new Map<string, string | null>();
const checkedScopes = new Set<number>();
const validateScopeChain = (row: number): void => {
const path = new Set<number>();
let current = row;
while (!checkedScopes.has(current)) {
const cursor = metadataRowCursor(bytes, layout, 1, current);
if (path.has(current)) {
issues.push({
code: "invalid-row",
scope: `metadata.TypeRef:${metadataToken(1, current)}`,
offset: cursor.start,
detail: "TypeRef ResolutionScope chain contains a cycle",
});
break;
}
// Admit the chain's visited entry and retained validation-cache entry.
managedDecodeBudget(layout).reserve(128, cursor.start);
path.add(current);
const raw = cursor.readIndex(layout.codedIndexSize("ResolutionScope"));
const reason = metadataCodedTokenInvalidReason(
raw,
2,
[0, 26, 35, 1],
layout.rowCounts,
);
if (reason !== null) {
issues.push({
code: "invalid-row",
scope: `metadata.TypeRef:${metadataToken(1, current)}`,
offset: cursor.start,
detail: `TypeRef ResolutionScope coded index 0x${raw.toString(16)} is invalid: ${reason}`,
});
break;
}
// Null scopes are legal; non-TypeRef scopes terminate local validation.
if (raw === 0 || (raw & 3) !== 3) break;
current = Math.floor(raw / 4);
}
for (const visited of path) checkedScopes.add(visited);
};
const definitionName = (table: 1 | 2, row: number): string | null => {
if (table === 2) {
metadataRowOffset(layout, 2, row);
return ranges[row - 1]?.fullName ?? null;
}
const cursor = metadataRowCursor(bytes, layout, 1, row);
cursor.readIndex(layout.codedIndexSize("ResolutionScope"));
readManagedValue(() => validateScopeChain(row), issues);
const name = cursor.readIndex(layout.stringIndexSize);
const namespace = cursor.readIndex(layout.stringIndexSize);
return fullName(
readMetadataString(bytes, layout, namespace, layout.strings.size),
readMetadataString(bytes, layout, name, layout.strings.size),
);
};
const specName = (row: number): string | null => {
const cursor = metadataRowCursor(bytes, layout, 27, row);
const spec = readMetadataBlob(
bytes,
layout,
cursor.readIndex(layout.blobIndexSize),
layout.blob.size,
);
const owner = {
form: "type",
scope: `metadata.TypeSpec:${metadataToken(27, row)}`,
offset: cursor.start,
} as const;
specSignature(layout, spec, owner);
if (spec[0] !== 0x15) return null;
// GENERICINST (CLASS | VALUETYPE) TypeDefOrRefEncoded: never a TypeSpec,
// so naming never recurses.
const encoded = readCompressed(spec, 2).value;
const table = ([2, 1] as const)[encoded & 3];
if (table === undefined)
throw managedFailure(
"invalid-blob",
owner.scope,
"Generic instantiation does not name a TypeDef or TypeRef",
owner.offset,
);
return definitionName(table, Math.floor(encoded / 4));
};
return (token) => {
if (token === null) return null;
if (names.has(token)) return names.get(token) ?? null;
const table = Number.parseInt(token.slice(2, 4), 16);
const row = Number.parseInt(token.slice(4), 16);
const name =
readManagedValue(
() =>
table === 27
? specName(row)
: table === 1 || table === 2
? definitionName(table, row)
: null,
issues,
) ?? null;
names.set(token, name);
return name;
};
};