mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
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:
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
};
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user