diff --git a/src/dotnet/ManagedMemberInspector.ts b/src/dotnet/ManagedMemberInspector.ts index c47f5f8bd2..f90065be3d 100644 --- a/src/dotnet/ManagedMemberInspector.ts +++ b/src/dotnet/ManagedMemberInspector.ts @@ -209,16 +209,12 @@ export const inspectManagedMembersBytes = ( const { operand: signatureTypeName, declaration: declarationTypeName } = managedTypeNameDigests(layout, issues, parsedTypes, typeReferences); const types = parsedTypes.types - .filter((type) => selected === null || selected.has(type.token)) - .map((type) => { + .filter(({ type }) => selected === null || selected.has(type.token)) + .map(({ row, type }) => { managedDecodeBudget(layout).reserve(512, type.row_offset); return { ...type, - name_sha256: - declarationTypeName( - 2, - Number.parseInt(type.token.slice(4), 16), - )?.toString("hex") ?? null, + name_sha256: declarationTypeName(2, row)?.toString("hex") ?? null, }; }); const envelope = managedPeInspectionEnvelope( diff --git a/src/dotnet/ManagedMemberInspectorCore.ts b/src/dotnet/ManagedMemberInspectorCore.ts index 1811045556..cd7524fe3c 100644 --- a/src/dotnet/ManagedMemberInspectorCore.ts +++ b/src/dotnet/ManagedMemberInspectorCore.ts @@ -36,6 +36,12 @@ import { memberListTable } from "./ManagedMemberPointers.js"; import type { TypeMemberListFacts } from "./ManagedTypeMemberLists.js"; export type ManagedType = ManagedMemberInspection["types"][number]; +/** A renderable TypeDef projection paired with its original source row. */ +export interface DecodedTypeDefinition { + readonly row: number; + readonly type: ManagedType; +} + export type ManagedExportedType = NonNullable< ManagedMemberInspection["exported_types"] >[number]; @@ -225,13 +231,13 @@ export const parseTypes = ( layout: ManagedMetadataLayout, definitions: readonly TypeDefinition[], ): { - readonly types: readonly ManagedType[]; + readonly types: readonly DecodedTypeDefinition[]; readonly issues: readonly ManagedParseIssue[]; readonly nesting: ManagedTypeNesting; } => { const fields = memberListTable(layout, 4); const methods = memberListTable(layout, 6); - const items: ManagedType[] = []; + const items: DecodedTypeDefinition[] = []; const issues: ManagedParseIssue[] = []; const nesting = readManagedTypeNesting(bytes, layout, issues); for (const definition of definitions) { @@ -272,26 +278,31 @@ export const parseTypes = ( detail: `TypeDef Extends coded index 0x${extendsRaw.toString(16)} is invalid: ${extendsIndex.reason}`, }); const enclosingRow = nesting.enclosing.get(row); + managedDecodeBudget(layout).reserve(128, cursor.start); items.push({ - token, - row_offset: cursor.start, - namespace, - name, - full_name: definition.fullName, - flags, - extends_token: extendsIndex.kind === "valid" ? extendsIndex.token : null, - enclosing_type_token: - enclosingRow === undefined ? null : metadataToken(2, enclosingRow), - name_sha256: null, - field_list: { - first_row: fieldRange.first, - last_row: fieldRange.last, - count: fieldRange.count, - }, - method_list: { - first_row: methodRange.first, - last_row: methodRange.last, - count: methodRange.count, + row, + type: { + token, + row_offset: cursor.start, + namespace, + name, + full_name: definition.fullName, + flags, + extends_token: + extendsIndex.kind === "valid" ? extendsIndex.token : null, + enclosing_type_token: + enclosingRow === undefined ? null : metadataToken(2, enclosingRow), + name_sha256: null, + field_list: { + first_row: fieldRange.first, + last_row: fieldRange.last, + count: fieldRange.count, + }, + method_list: { + first_row: methodRange.first, + last_row: methodRange.last, + count: methodRange.count, + }, }, }); } diff --git a/src/dotnet/ManagedMemberTypeInventory.test.ts b/src/dotnet/ManagedMemberTypeInventory.test.ts index f4eae02293..4fd7278c73 100644 --- a/src/dotnet/ManagedMemberTypeInventory.test.ts +++ b/src/dotnet/ManagedMemberTypeInventory.test.ts @@ -7,6 +7,17 @@ import { } from "./ManagedPe.fixture.js"; import { buildManagedMemberCapacityFixture } from "../../tests/fixtures/managedMemberCapacity.js"; import { readManagedPeLayout } from "./ManagedPeReader.js"; +import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js"; +import { + parseTypes, + readTypeDefinitions, +} from "./ManagedMemberInspectorCore.js"; +import { + readManagedMetadataInventory, + readManagedResourceDirectory, +} from "./ManagedMetadataInventory.js"; +import { managedMetadataTypeNames } from "./ManagedMetadataInventoryRows.js"; +import { readTypeMemberListFacts } from "./ManagedTypeMemberLists.js"; import { metadataRowOffset, readManagedMetadataLayout, @@ -206,3 +217,81 @@ it("does not turn an unreadable requested type name into observed absence during ]); expect(result.limitations.join(" ")).toContain("absence is not observed"); }); + +it("keeps later type identity, member ownership and constructor names at their original row", () => { + const bytes = buildManagedPeFixture({ + additionalEmptyTypes: 1, + customAttributeTypeRaw: (1 << 3) | 2, + typeName: "LookupAttribute", + extendsRaw: 0, + methodName: ".ctor", + methodFlags: 0x1886, + methodSignature: Buffer.from([0x20, 0x01, 0x01, 0x0e]), + }); + const healthy = inspectManagedMembersBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + const owner = healthy.types.find(({ token }) => token === "0x02000002"); + expect(owner?.full_name).toBe("Fixture.LookupAttribute"); + expect(owner?.name_sha256).toMatch(/^[0-9a-f]{64}$/u); + expect(healthy.fields).toHaveLength(1); + expect(healthy.methods).toHaveLength(1); + const pe = readManagedPeLayout(bytes); + if (pe.cli === null) throw new Error("Expected CLI fixture"); + const layout = readManagedMetadataLayout( + bytes, + pe.rvaToOffset(pe.cli.metadata.rva, pe.cli.metadata.size, "cli.metadata"), + pe.cli.metadata.size, + ); + expect(layout.stringIndexSize).toBe(2); + // Only the preceding empty type's name is unreadable; row 2 still owns + // FieldDef 1 and MethodDef 1, including the local attribute constructor. + bytes.writeUInt16LE(0xffff, metadataRowOffset(layout, 2, 1) + 4); + const target = managedPeFixtureTarget(bytes); + const result = inspectManagedMembersBytes(bytes, target, { + typeNames: ["Fixture.LookupAttribute", "Fixture.Empty0"], + }); + + expect(result.types).toEqual([owner]); + expect(result.types[0]).toMatchObject({ + token: "0x02000002", + row_offset: metadataRowOffset(layout, 2, 2), + name_sha256: owner?.name_sha256, + field_list: { first_row: 1, last_row: 1, count: 1 }, + method_list: { first_row: 1, last_row: 1, count: 1 }, + }); + expect(result.fields).toEqual(healthy.fields); + expect(result.methods).toEqual(healthy.methods); + for (const member of [...result.fields, ...result.methods]) + expect(member).toMatchObject({ + declaring_type_token: "0x02000002", + declaring_type: "Fixture.LookupAttribute", + }); + expect(result.artifact.sha256).toBe(target.sha256); + expect(result.coverage.state).toBe("partial"); + expect(result.selection?.unmatched_type_names).toEqual(["Fixture.Empty0"]); + expect(result.limitations.join(" ")).toContain("absence is not observed"); + + // Member inspection uses the parsed-type name owner for its metadata + // inventory, whose attribute names are not included in the member report. + const issues: ManagedParseIssue[] = []; + const memberLists = readTypeMemberListFacts(bytes, layout); + const definitions = readTypeDefinitions(bytes, layout, issues, memberLists); + const parsedTypes = parseTypes(bytes, layout, definitions); + const names = managedMetadataTypeNames(bytes, layout, issues, parsedTypes); + const inventory = readManagedMetadataInventory( + bytes, + layout, + readManagedResourceDirectory(pe, issues), + memberLists, + undefined, + names, + ); + expect(inventory.attributes).toMatchObject([ + { + constructor_token: "0x06000001", + type_name: "Fixture.LookupAttribute", + }, + ]); +}); diff --git a/src/dotnet/ManagedMetadataInventoryRows.ts b/src/dotnet/ManagedMetadataInventoryRows.ts index 0fba855438..3638aa89c2 100644 --- a/src/dotnet/ManagedMetadataInventoryRows.ts +++ b/src/dotnet/ManagedMetadataInventoryRows.ts @@ -2,7 +2,6 @@ import { managedDecodeBudget } from "./ManagedDecodeBudget.js"; import { managedTypeReferences } from "./ManagedTypeReferences.js"; import { MAX_TYPE_NESTING_DEPTH } from "./ManagedTypeNameDigests.js"; import { readManagedTypeNesting } from "./ManagedTypeNesting.js"; -import type { ManagedTypeNesting } from "./ManagedTypeNesting.js"; import { memberDeclaringTypeRowLookup } from "./ManagedMemberOwnership.js"; import type { TypeMemberListFacts } from "./ManagedTypeMemberLists.js"; @@ -27,7 +26,7 @@ import { import { managedFailure, readManagedValue } from "./ManagedReaderFailure.js"; import type { ManagedTypeReferenceLookup } from "./ManagedTypeReferences.js"; import type { ManagedResourceDirectory } from "./ManagedMetadataInventory.js"; -import type { ManagedType } from "./ManagedMemberInspectorCore.js"; +import type { ManagedType, parseTypes } from "./ManagedMemberInspectorCore.js"; type ModuleIdentity = NonNullable; type AssemblyIdentity = NonNullable; @@ -246,10 +245,7 @@ export const managedMetadataTypeNames = ( bytes: Buffer, layout: ManagedMetadataLayout, issues: ManagedParseIssue[], - parsedTypes?: { - readonly types: readonly ManagedType[]; - readonly nesting: ManagedTypeNesting; - }, + parsedTypes?: Pick, "types" | "nesting">, parsedReferences?: ManagedTypeReferenceLookup, ): ManagedMetadataTypeNameLookup => { const nesting = @@ -259,8 +255,7 @@ export const managedMetadataTypeNames = ( const definitions = new Map(); if (parsedTypes !== undefined) { managedDecodeBudget(layout).reserve(parsedTypes.types.length * 128, null); - for (const type of parsedTypes.types) - definitions.set(Number.parseInt(type.token.slice(4), 16), type); + for (const { row, type } of parsedTypes.types) definitions.set(row, type); } const cache = new Map< string, diff --git a/src/dotnet/ManagedTypeNameDigests.ts b/src/dotnet/ManagedTypeNameDigests.ts index c470a5a86f..9c07a82eb1 100644 --- a/src/dotnet/ManagedTypeNameDigests.ts +++ b/src/dotnet/ManagedTypeNameDigests.ts @@ -4,10 +4,10 @@ import { managedDecodeBudget } from "./ManagedDecodeBudget.js"; import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js"; import type { parseTypes, + ManagedType, SignatureTypeName, DeclarationTypeName, } from "./ManagedMemberInspectorCore.js"; -import { metadataToken } from "./ManagedMetadataHeaps.js"; import type { ManagedTypeReferenceLookup } from "./ManagedTypeReferences.js"; import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js"; import { managedFailure, readManagedValue } from "./ManagedReaderFailure.js"; @@ -153,11 +153,11 @@ export const managedTypeNameDigests = ( } => { const retain = () => managedDecodeBudget(layout).reserve(128, null); managedDecodeBudget(layout).reserve(parsedTypes.types.length * 128, null); - const types = new Map(); - for (const type of parsedTypes.types) types.set(type.token, type); + const types = new Map(); + for (const { row, type } of parsedTypes.types) types.set(row, type); const definitions = nestedTypeDigests((row) => { if (!parsedTypes.nesting.complete) return null; - const type = types.get(metadataToken(TYPE_DEF, row)); + const type = types.get(row); if ( type === undefined && (row < 1 || row > (layout.table(TYPE_DEF)?.rowCount ?? 0))