diff --git a/src/dotnet/ManagedMemberInspector.test.ts b/src/dotnet/ManagedMemberInspector.test.ts index c42e45d0a8..5d3ce5b902 100644 --- a/src/dotnet/ManagedMemberInspector.test.ts +++ b/src/dotnet/ManagedMemberInspector.test.ts @@ -2245,13 +2245,15 @@ it.each([ ); it.each([ - { facet: "fields", nameOffset: 2 }, - { facet: "methods", nameOffset: 8 }, - { facet: "member_refs", nameOffset: 2 }, - { facet: "types", nameOffset: 4 }, + { facet: "fields", nameOffsets: [2] }, + { facet: "methods", nameOffsets: [8] }, + { facet: "member_refs", nameOffsets: [2] }, + { facet: "types", nameOffsets: [4] }, + { facet: "types", nameOffsets: [6] }, + { facet: "types", nameOffsets: [4, 6] }, ] as const)( - "preserves independent metadata when a $facet name is unreadable", - ({ facet, nameOffset }) => { + "preserves independent metadata when $facet name columns $nameOffsets are unreadable", + ({ facet, nameOffsets }) => { const bytes = buildManagedPeFixture(); const original = inspectManagedMembersBytes( bytes, @@ -2259,7 +2261,8 @@ it.each([ ); const row = original[facet][0]?.row_offset; if (row === undefined) throw new Error(`Fixture must contain ${facet}`); - bytes.writeUInt16LE(0xffff, row + nameOffset); + for (const [index, nameOffset] of nameOffsets.entries()) + bytes.writeUInt16LE(0xffff - index, row + nameOffset); const result = inspectManagedMembersBytes( bytes, managedPeFixtureTarget(bytes), @@ -2278,13 +2281,17 @@ it.each([ expect(result[remaining]).toHaveLength(original[remaining].length); expect(result.coverage).toMatchObject({ state: "partial", - issues: [ + issues: nameOffsets.map(() => expect.objectContaining({ code: "invalid-heap-index", scope: "metadata.#Strings", + offset: expect.any(Number), }), - ], + ), }); + expect( + new Set(result.coverage.issues.map(({ offset }) => offset)).size, + ).toBe(nameOffsets.length); if (facet === "types") { expect(result.fields[0]).toMatchObject({ declaring_type_token: "0x02000001", @@ -2327,25 +2334,32 @@ describe("managed member type selection", () => { ); }); - it("does not treat a name as absent when a TypeDef name is unreadable", () => { - const corrupt = Buffer.from(bytes); - const row = everything.types[0]?.row_offset ?? 0; - // Point the TypeDef Name past the #Strings heap. - corrupt.writeUInt16LE(0xffff, row + 4); - const result = inspectManagedMembersBytes( - corrupt, - managedPeFixtureTarget(corrupt), - { typeNames: ["Fixture.Program"] }, - ); + it.each([ + { column: "name", offset: 4 }, + { column: "namespace", offset: 6 }, + ])( + "does not treat a name as absent when a TypeDef $column is unreadable", + ({ offset }) => { + const corrupt = Buffer.from(bytes); + const row = everything.types[0]?.row_offset ?? 0; + corrupt.writeUInt16LE(0xffff, row + offset); + const result = inspectManagedMembersBytes( + corrupt, + managedPeFixtureTarget(corrupt), + { typeNames: ["Fixture.Program"] }, + ); - expect(result.selection?.unmatched_type_names).toEqual(["Fixture.Program"]); - expect(result.coverage.state).toBe("partial"); - // Its methods were never inspected, so neither were their literals. - expect(result.user_strings?.state).toBe("partial"); - expect(result.limitations).toContain( - "1 TypeDef name(s) could not be read; a name in selection.unmatched_type_names may belong to one of them, so its absence is not observed.", - ); - }); + expect(result.selection?.unmatched_type_names).toEqual([ + "Fixture.Program", + ]); + expect(result.coverage.state).toBe("partial"); + // Its methods were never inspected, so neither were their literals. + expect(result.user_strings?.state).toBe("partial"); + expect(result.limitations).toContain( + "1 TypeDef name(s) could not be read; a name in selection.unmatched_type_names may belong to one of them, so its absence is not observed.", + ); + }, + ); it("omits members of unselected types without dropping member references", () => { const result = inspectManagedMembersBytes(bytes, target, { diff --git a/src/dotnet/ManagedMemberInspector.ts b/src/dotnet/ManagedMemberInspector.ts index 68f5638a31..217dc2b4fe 100644 --- a/src/dotnet/ManagedMemberInspector.ts +++ b/src/dotnet/ManagedMemberInspector.ts @@ -28,7 +28,7 @@ import { } from "./ManagedReaderFailure.js"; import { parseTypes, - typeRanges, + readTypeDefinitions, type TypeRange, } from "./ManagedMemberInspectorCore.js"; import { edgeTargets } from "./ManagedMethodSpecs.js"; @@ -190,7 +190,7 @@ export const inspectManagedMembersBytes = ( const { layout, memberLists } = readMemberLayout(bytes, pe); const identity = readManagedModuleIdentity(bytes, layout); appendManagedParseIssues(issues, identity.issues); - const ranges = typeRanges(bytes, layout, issues, memberLists); + const ranges = readTypeDefinitions(bytes, layout, issues, memberLists); const { tokens: selected, selection, diff --git a/src/dotnet/ManagedMemberInspectorCore.ts b/src/dotnet/ManagedMemberInspectorCore.ts index 8d9e45ec19..9d6e41b83e 100644 --- a/src/dotnet/ManagedMemberInspectorCore.ts +++ b/src/dotnet/ManagedMemberInspectorCore.ts @@ -60,6 +60,13 @@ export interface TypeRange { readonly methodEnd: number | null; } +/** One TypeDef row's names and coordinates, including unreadable declarations. */ +interface TypeDefinition extends TypeRange { + readonly row: number; + readonly name: string | undefined; + readonly namespace: string | undefined; +} + /** * Name and declaring type of a field, method, member reference, or method * specification, keyed by its token so edges resolve any target uniformly. @@ -109,36 +116,38 @@ const rowRange = ( export const fullName = (namespace: string, name: string): string => namespace.length === 0 ? name : `${namespace}.${name}`; -const readTypeRange = ( +const readTypeDefinition = ( bytes: Buffer, layout: ManagedMetadataLayout, row: number, issues: ManagedParseIssue[], memberLists: TypeMemberListFacts, -): TypeRange => { +): TypeDefinition => { const cursor = metadataRowCursor(bytes, layout, 2, row); cursor.readUInt32(); const nameIndex = cursor.readIndex(layout.stringIndexSize); const namespaceIndex = cursor.readIndex(layout.stringIndexSize); - const declaredName = - readManagedValue( - () => - fullName( - readMetadataString( - bytes, - layout, - namespaceIndex, - layout.strings.size, - ), - readMetadataString(bytes, layout, nameIndex, layout.strings.size), - ), - issues, - ) ?? null; + // Keep both independent name failures, but decode each heap coordinate once. + const namespace = readManagedValue( + () => + readMetadataString(bytes, layout, namespaceIndex, layout.strings.size), + issues, + ); + const name = readManagedValue( + () => readMetadataString(bytes, layout, nameIndex, layout.strings.size), + issues, + ); cursor.readIndex(layout.codedIndexSize("TypeDefOrRef")); const current = memberLists.entries[row - 1]; return { + row, + name, + namespace, token: metadataToken(2, row), - fullName: declaredName, + fullName: + namespace === undefined || name === undefined + ? null + : fullName(namespace, name), fieldStart: current?.field.start.value ?? null, fieldEnd: current?.field.end ?? null, methodStart: current?.method.start.value ?? null, @@ -146,18 +155,20 @@ const readTypeRange = ( }; }; -export const typeRanges = ( +export const readTypeDefinitions = ( bytes: Buffer, layout: ManagedMetadataLayout, issues: ManagedParseIssue[], memberLists: TypeMemberListFacts, -): readonly TypeRange[] => { +): readonly TypeDefinition[] => { issues.push(...memberLists.issues); const typeTable = layout.table(2); - const ranges: TypeRange[] = []; + const definitions: TypeDefinition[] = []; for (let row = 1; row <= (typeTable?.rowCount ?? 0); row += 1) - ranges.push(readTypeRange(bytes, layout, row, issues, memberLists)); - return ranges; + definitions.push( + readTypeDefinition(bytes, layout, row, issues, memberLists), + ); + return definitions; }; /** @@ -167,91 +178,82 @@ export const typeRanges = ( export const parseTypes = ( bytes: Buffer, layout: ManagedMetadataLayout, - ranges: readonly TypeRange[], + definitions: readonly TypeDefinition[], ): { readonly types: readonly ManagedType[]; readonly issues: readonly ManagedParseIssue[]; readonly nesting: ManagedTypeNesting; } => { - const typeTable = layout.table(2); const fields = memberListTable(layout, 4); const methods = memberListTable(layout, 6); const items: ManagedType[] = []; const issues: ManagedParseIssue[] = []; const nesting = readManagedTypeNesting(bytes, layout, issues); - for (let row = 1; row <= (typeTable?.rowCount ?? 0); row += 1) { - readManagedValue(() => { - const cursor = metadataRowCursor(bytes, layout, 2, row); - const flags = cursor.readUInt32(); - const name = readMetadataString( - bytes, - layout, - cursor.readIndex(layout.stringIndexSize), - layout.strings.size, - ); - const namespace = readMetadataString( - bytes, - layout, - cursor.readIndex(layout.stringIndexSize), - layout.strings.size, - ); - const extendsOffset = cursor.offset; - const extendsRaw = cursor.readIndex( - layout.codedIndexSize("TypeDefOrRef"), - ); - const currentRange = ranges[row - 1] ?? null; - const fieldRange = rowRange( - fields, - currentRange?.fieldStart ?? null, - currentRange?.fieldEnd ?? null, - ); - const methodRange = rowRange( - methods, - currentRange?.methodStart ?? null, - currentRange?.methodEnd ?? null, - ); - const extendsReason = metadataCodedTokenInvalidReason( + for (const definition of definitions) { + const { row, name, namespace, token } = definition; + if ( + name === undefined || + namespace === undefined || + definition.fullName === null + ) + continue; + const cursor = metadataRowCursor(bytes, layout, 2, row); + const flags = cursor.readUInt32(); + cursor.readIndex(layout.stringIndexSize); + cursor.readIndex(layout.stringIndexSize); + const extendsOffset = cursor.offset; + const extendsRaw = cursor.readIndex(layout.codedIndexSize("TypeDefOrRef")); + const fieldRange = rowRange( + fields, + definition.fieldStart, + definition.fieldEnd, + ); + const methodRange = rowRange( + methods, + definition.methodStart, + definition.methodEnd, + ); + const extendsReason = metadataCodedTokenInvalidReason( + extendsRaw, + 2, + [2, 1, 27], + layout.rowCounts, + ); + if (extendsReason !== null) + issues.push({ + code: "invalid-row", + scope: `metadata.TypeDef:${token}`, + offset: extendsOffset, + detail: `TypeDef Extends coded index 0x${extendsRaw.toString(16)} is invalid: ${extendsReason}`, + }); + const enclosingRow = nesting.enclosing.get(row); + items.push({ + token, + row_offset: cursor.start, + namespace, + name, + full_name: definition.fullName, + flags, + extends_token: metadataCodedToken( extendsRaw, 2, [2, 1, 27], layout.rowCounts, - ); - if (extendsReason !== null) - issues.push({ - code: "invalid-row", - scope: `metadata.TypeDef:${metadataToken(2, row)}`, - offset: extendsOffset, - detail: `TypeDef Extends coded index 0x${extendsRaw.toString(16)} is invalid: ${extendsReason}`, - }); - const enclosingRow = nesting.enclosing.get(row); - items.push({ - token: metadataToken(2, row), - row_offset: cursor.start, - namespace, - name, - full_name: fullName(namespace, name), - flags, - extends_token: metadataCodedToken( - extendsRaw, - 2, - [2, 1, 27], - layout.rowCounts, - ), - 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, - }, - }); - }, issues); + ), + 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, + }, + }); } return { types: items, issues, nesting }; }; diff --git a/src/dotnet/ManagedSignatureResolution.test.ts b/src/dotnet/ManagedSignatureResolution.test.ts index fe9c423e43..89a4d8b07b 100644 --- a/src/dotnet/ManagedSignatureResolution.test.ts +++ b/src/dotnet/ManagedSignatureResolution.test.ts @@ -10,7 +10,7 @@ import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js"; import { parseTypes, signature, - typeRanges, + readTypeDefinitions, } from "./ManagedMemberInspectorCore.js"; import { metadataRowOffset, @@ -169,7 +169,12 @@ describe("resolved member signature digests", () => { const parsedTypes = parseTypes( bytes, layout, - typeRanges(bytes, layout, issues, readTypeMemberListFacts(bytes, layout)), + readTypeDefinitions( + bytes, + layout, + issues, + readTypeMemberListFacts(bytes, layout), + ), ); const resolve = managedTypeNameDigests( layout,