From c09fa01b379b5571e97d69b0ccde5fc214a73a43 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 27 Oct 2026 01:29:36 +0800 Subject: [PATCH] refactor(managed): centralize MemberRef row facts --- src/dotnet/ManagedArtifactInspector.test.ts | 29 ++++++ src/dotnet/ManagedMemberInspector.test.ts | 99 +++++++++++++++++++++ src/dotnet/ManagedMemberReferenceRows.ts | 33 +++++++ src/dotnet/ManagedMemberRows.ts | 22 ++--- src/dotnet/ManagedMetadataInventoryRows.ts | 9 +- src/dotnet/ManagedMetadataLayout.ts | 3 + src/dotnet/ManagedMethodSpecs.ts | 10 ++- 7 files changed, 179 insertions(+), 26 deletions(-) create mode 100644 src/dotnet/ManagedMemberReferenceRows.ts diff --git a/src/dotnet/ManagedArtifactInspector.test.ts b/src/dotnet/ManagedArtifactInspector.test.ts index d009bf64a5..249fc507af 100644 --- a/src/dotnet/ManagedArtifactInspector.test.ts +++ b/src/dotnet/ManagedArtifactInspector.test.ts @@ -583,6 +583,35 @@ describe("managed artifact coded indexes", () => { }, 30_000); }); +it.each([false, true])( + "resolves an attribute type without reading malformed constructor heaps (bad name %s)", + (badName) => { + const bytes = buildManagedPeFixture(); + const layout = metadataLayoutFor(bytes); + const row = layout.table(10)!; + const nameColumn = row.offset + layout.codedIndexSize("MemberRefParent"); + const signatureColumn = nameColumn + layout.stringIndexSize; + if (badName) { + if (layout.stringIndexSize === 2) bytes.writeUInt16LE(0xffff, nameColumn); + else bytes.writeUInt32LE(0xffffffff, nameColumn); + } + if (layout.blobIndexSize === 2) + bytes.writeUInt16LE(0xffff, signatureColumn); + else bytes.writeUInt32LE(0xffffffff, signatureColumn); + + const result = inspectManagedArtifactBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + expect(result.attributes[0]).toMatchObject({ + constructor_token: "0x0a000001", + type_name: "System.Runtime.Versioning.TargetFrameworkAttribute", + decoded_fixed_string: ".NETCoreApp,Version=v8.0", + }); + expect(result.coverage).toEqual({ state: "complete", issues: [] }); + }, +); + describe("managed artifact reference inventory", () => { it("qualifies local custom-attribute names with their enclosing types", () => { const bytes = buildManagedPeFixture({ diff --git a/src/dotnet/ManagedMemberInspector.test.ts b/src/dotnet/ManagedMemberInspector.test.ts index e6ba63e414..5262170d55 100644 --- a/src/dotnet/ManagedMemberInspector.test.ts +++ b/src/dotnet/ManagedMemberInspector.test.ts @@ -571,6 +571,56 @@ const genericRef = Buffer.from([0x30, 0x01, 0x00, 0x01]); // GENERIC, one generic parameter, no parameters, void. const genericDef = Buffer.from([0x10, 0x01, 0x00, 0x01]); +it.each([ + { badName: true, parent: 0, expectedHeap: "#Strings" }, + { badName: false, parent: 0, expectedHeap: "#Blob" }, + { badName: true, parent: 17, expectedHeap: "#Strings" }, + { badName: false, parent: 17, expectedHeap: "#Blob" }, +])( + "reports $expectedHeap before invalid MemberRef parent $parent (bad name $badName)", + ({ badName, parent, expectedHeap }) => { + const bytes = buildManagedPeFixture({ memberRefParentRaw: parent }); + const pe = readManagedPeLayout(bytes); + if (pe.cli === null) throw new Error("fixture has no CLI header"); + const { rva, size } = pe.cli.metadata; + const layout = readManagedMetadataLayout( + bytes, + pe.rvaToOffset(rva, size, "metadata"), + size, + ); + const nameColumn = + metadataRowOffset(layout, 10, 1) + + layout.codedIndexSize("MemberRefParent"); + if (badName) { + if (layout.stringIndexSize === 2) bytes.writeUInt16LE(0xffff, nameColumn); + else bytes.writeUInt32LE(0xffffffff, nameColumn); + } + const signatureColumn = nameColumn + layout.stringIndexSize; + if (layout.blobIndexSize === 2) + bytes.writeUInt16LE(0xffff, signatureColumn); + else bytes.writeUInt32LE(0xffffffff, signatureColumn); + const result = inspectManagedMembersBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + expect(result.member_refs).toEqual([]); + expect(result.coverage.state).toBe("partial"); + expect( + result.coverage.issues.filter( + ({ scope }) => + scope === "metadata.#Strings" || + scope === "metadata.#Blob" || + scope === "metadata.MemberRef:0x0a000001", + ), + ).toEqual([ + expect.objectContaining({ + code: "invalid-heap-index", + scope: `metadata.${expectedHeap}`, + }), + ]); + }, +); + describe("managed TypeSpec declaring names", () => { it("names a generic TypeSpec parent by its generic type and leaves other TypeSpecs unknown", () => { // GENERICINST CLASS TypeRef(1) @@ -956,6 +1006,55 @@ describe("managed MethodSpec call targets", () => { }); }); +it("reads MethodSpec arity from an unnamed MemberRef independently of parent admission", () => { + const bytes = buildManagedPeFixture({ + memberRefSignature: genericRef, + memberRefParentRaw: 17, + methodSpecMethods: [(1 << 1) | 1], + methodSpecInstantiation: Buffer.from([0x0a, 0x01, 0x0e]), + ilBody: tokenBody([[call, 0x2b00_0001]]), + }); + const pe = readManagedPeLayout(bytes); + if (pe.cli === null) throw new Error("fixture has no CLI header"); + const { rva, size } = pe.cli.metadata; + const layout = readManagedMetadataLayout( + bytes, + pe.rvaToOffset(rva, size, "metadata"), + size, + ); + const nameColumn = + metadataRowOffset(layout, 10, 1) + layout.codedIndexSize("MemberRefParent"); + if (layout.stringIndexSize === 2) bytes.writeUInt16LE(0xffff, nameColumn); + else bytes.writeUInt32LE(0xffffffff, nameColumn); + const result = inspectManagedMembersBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + expect(result.member_refs).toEqual([]); + expect(result.call_edges).toMatchObject([ + { + target_token: "0x2b000001", + target_kind: "method-spec", + target_name: null, + target_declaring_type: null, + }, + ]); + expect(result.coverage).toMatchObject({ + state: "partial", + issues: [ + expect.objectContaining({ + code: "invalid-heap-index", + scope: "metadata.#Strings", + }), + ], + }); + expect( + result.coverage.issues.some(({ scope }) => + scope.startsWith("metadata.MethodSpec:"), + ), + ).toBe(false); +}); + describe("managed MethodDef heap admission", () => { it("keeps names from unpaged methods without reading their signatures or bodies", () => { const bytes = buildMultiMethodManagedPeFixture({ diff --git a/src/dotnet/ManagedMemberReferenceRows.ts b/src/dotnet/ManagedMemberReferenceRows.ts new file mode 100644 index 0000000000..129c190c7e --- /dev/null +++ b/src/dotnet/ManagedMemberReferenceRows.ts @@ -0,0 +1,33 @@ +import { + MEMBER_REF_PARENT_CODED_INDEX, + type ManagedMetadataLayout, +} from "./ManagedMetadataLayout.js"; +import { + decodeMetadataCodedIndex, + metadataRowCursor, + metadataToken, +} from "./ManagedMetadataHeaps.js"; + +/** Scalar MemberRef facts; consumers own heap admission and parent diagnostics. */ +export const readMemberReferenceRowFacts = ( + bytes: Buffer, + layout: ManagedMetadataLayout, + row: number, +) => { + // Layout admission bounds the complete row independently of its heap values. + const cursor = metadataRowCursor(bytes, layout, 10, row); + const parentRaw = cursor.readIndex(layout.codedIndexSize("MemberRefParent")); + return { + token: metadataToken(10, row), + rowOffset: cursor.start, + parentRaw, + parent: decodeMetadataCodedIndex( + parentRaw, + MEMBER_REF_PARENT_CODED_INDEX.bits, + MEMBER_REF_PARENT_CODED_INDEX.tables, + layout.rowCounts, + ), + nameIndex: cursor.readIndex(layout.stringIndexSize), + signatureIndex: cursor.readIndex(layout.blobIndexSize), + }; +}; diff --git a/src/dotnet/ManagedMemberRows.ts b/src/dotnet/ManagedMemberRows.ts index 6bf00eac54..55881b94fc 100644 --- a/src/dotnet/ManagedMemberRows.ts +++ b/src/dotnet/ManagedMemberRows.ts @@ -1,3 +1,4 @@ +import { readMemberReferenceRowFacts } from "./ManagedMemberReferenceRows.js"; import { readMethodDefinitionRow } from "./ManagedMethodDefinitionRows.js"; import { jsonStringByteLength } from "../domain/jsonStringByteLength.js"; import { isManagedMemberReferenceInstruction } from "../domain/managed/managedMemberInstructionFacts.js"; @@ -234,34 +235,25 @@ export const parseMemberRefs = ( const table = layout.table(10); for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) { readManagedValue(() => { - const cursor = metadataRowCursor(bytes, layout, 10, row); - const parentRaw = cursor.readIndex( - layout.codedIndexSize("MemberRefParent"), - ); + const { token, rowOffset, parentRaw, parent, nameIndex, signatureIndex } = + readMemberReferenceRowFacts(bytes, layout, row); const name = readMetadataString( bytes, layout, - cursor.readIndex(layout.stringIndexSize), + nameIndex, layout.strings.size, ); const sig = readMetadataBlob( bytes, layout, - cursor.readIndex(layout.blobIndexSize), + signatureIndex, layout.blob?.size ?? 0, ); - const token = metadataToken(10, row); - const parent = decodeMetadataCodedIndex( - parentRaw, - 3, - [2, 1, 26, 6, 27], - layout.rowCounts, - ); if (parent.kind !== "valid") issues.push({ code: "invalid-row", scope: `metadata.MemberRef:${token}`, - offset: cursor.start, + offset: rowOffset, detail: parent.kind === "null" ? "MemberRef parent coded index 0x0 is null, but the Class column must reference a row" @@ -278,7 +270,7 @@ export const parseMemberRefs = ( ); refs.push({ token, - row_offset: cursor.start, + row_offset: rowOffset, parent_token: parentToken, parent_name: parentName, name, diff --git a/src/dotnet/ManagedMetadataInventoryRows.ts b/src/dotnet/ManagedMetadataInventoryRows.ts index 9c17b9f2ae..bc2e13370a 100644 --- a/src/dotnet/ManagedMetadataInventoryRows.ts +++ b/src/dotnet/ManagedMetadataInventoryRows.ts @@ -1,3 +1,4 @@ +import { readMemberReferenceRowFacts } from "./ManagedMemberReferenceRows.js"; import { readTypeDefinitionRowFacts } from "./ManagedTypeDefinitionRows.js"; import { managedDecodeBudget } from "./ManagedDecodeBudget.js"; import { managedTypeReferences } from "./ManagedTypeReferences.js"; @@ -408,13 +409,7 @@ const attributeTypeName = ( const { table, row } = constructor; if (table === 6) return declaringTypeForMethod(bytes, layout, row, memberLists, typeName); - const cursor = metadataRowCursor(bytes, layout, 10, row); - const parent = decodeMetadataCodedIndex( - cursor.readIndex(layout.codedIndexSize("MemberRefParent")), - 3, - [2, 1, 26, 6, 27], - layout.rowCounts, - ); + const { parent } = readMemberReferenceRowFacts(bytes, layout, row); if (parent.kind === "null") return null; if (parent.table === 2 || parent.table === 1) return typeName(parent.table, parent.row); diff --git a/src/dotnet/ManagedMetadataLayout.ts b/src/dotnet/ManagedMetadataLayout.ts index 8b4ab09f39..148d4f89e9 100644 --- a/src/dotnet/ManagedMetadataLayout.ts +++ b/src/dotnet/ManagedMetadataLayout.ts @@ -118,6 +118,9 @@ const CODED_INDEXES = { TypeOrMethodDef: { bits: 1, tables: [2, 6] }, } as const; +/** Shared MemberRef parent tag mapping, also used to select its column width. */ +export const MEMBER_REF_PARENT_CODED_INDEX = CODED_INDEXES.MemberRefParent; + const SUPPORTED_TABLE_MASK = (1n << BigInt(METADATA_TABLE_NAMES.length)) - 1n; /** Check a range within `bytes`, which start at artifact file offset `base`. */ diff --git a/src/dotnet/ManagedMethodSpecs.ts b/src/dotnet/ManagedMethodSpecs.ts index 6a2f897dd5..ab71be8e33 100644 --- a/src/dotnet/ManagedMethodSpecs.ts +++ b/src/dotnet/ManagedMethodSpecs.ts @@ -1,3 +1,4 @@ +import { readMemberReferenceRowFacts } from "./ManagedMemberReferenceRows.js"; import { readMethodDefinitionRow } from "./ManagedMethodDefinitionRows.js"; import { createHash } from "node:crypto"; import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js"; @@ -163,10 +164,11 @@ const methodSignatureBlob = ( if (table === 6) { signatureIndex = readMethodDefinitionRow(bytes, layout, row).signatureIndex; } else { - const cursor = metadataRowCursor(bytes, layout, table, row); - cursor.readIndex(layout.codedIndexSize("MemberRefParent")); - cursor.readIndex(layout.stringIndexSize); - signatureIndex = cursor.readIndex(layout.blobIndexSize); + signatureIndex = readMemberReferenceRowFacts( + bytes, + layout, + row, + ).signatureIndex; } return readMetadataBlob( bytes,