diff --git a/src/dotnet/ManagedMemberInspectorCore.ts b/src/dotnet/ManagedMemberInspectorCore.ts index 1027f793b3..3df701b7f2 100644 --- a/src/dotnet/ManagedMemberInspectorCore.ts +++ b/src/dotnet/ManagedMemberInspectorCore.ts @@ -23,7 +23,6 @@ import type { MetadataTableLayout, } from "./ManagedMetadataLayout.js"; import { - metadataRowCursor, readManagedCompressedUnsigned, decodeMetadataCodedIndex, metadataCodedIndexCoordinate, @@ -34,7 +33,11 @@ import { sha256Bytes, } from "./ManagedMetadataHeaps.js"; import { memberListTable } from "./ManagedMemberPointers.js"; -import type { TypeMemberListFacts } from "./ManagedTypeMemberLists.js"; +import type { + TypeMemberListEntry, + TypeMemberListFacts, +} from "./ManagedTypeMemberLists.js"; +import type { TypeDefinitionRowFacts } from "./ManagedTypeDefinitionRows.js"; export type ManagedType = ManagedMemberInspection["types"][number]; /** A renderable TypeDef projection paired with its original source row. */ @@ -72,6 +75,7 @@ export interface TypeRange { /** One TypeDef row's names and coordinates, including unreadable declarations. */ interface TypeDefinition extends TypeRange { + readonly source: TypeDefinitionRowFacts; readonly row: number; readonly name: string | undefined; readonly namespace: string | undefined; @@ -173,12 +177,10 @@ const readTypeDefinition = ( layout: ManagedMetadataLayout, row: number, issues: ManagedParseIssue[], - memberLists: TypeMemberListFacts, + current: TypeMemberListEntry, ): TypeDefinition => { - const cursor = metadataRowCursor(bytes, layout, 2, row); - cursor.readUInt32(); - const nameIndex = cursor.readIndex(layout.stringIndexSize); - const namespaceIndex = cursor.readIndex(layout.stringIndexSize); + const { source } = current; + const { nameIndex, namespaceIndex } = source; // Keep both independent name failures, but decode each heap coordinate once. const namespace = readManagedValue( () => @@ -189,9 +191,8 @@ const readTypeDefinition = ( () => readMetadataString(bytes, layout, nameIndex, layout.strings.size), issues, ); - cursor.readIndex(layout.codedIndexSize("TypeDefOrRef")); - const current = memberLists.entries[row - 1]; return { + source, row, name, namespace, @@ -200,10 +201,10 @@ const readTypeDefinition = ( 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, - methodEnd: current?.method.end ?? null, + fieldStart: current.field.start.value, + fieldEnd: current.field.end, + methodStart: current.method.start.value, + methodEnd: current.method.end, }; }; @@ -214,11 +215,10 @@ export const readTypeDefinitions = ( memberLists: TypeMemberListFacts, ): readonly TypeDefinition[] => { appendManagedParseIssues(issues, memberLists.issues); - const typeTable = layout.table(2); const definitions: TypeDefinition[] = []; - for (let row = 1; row <= (typeTable?.rowCount ?? 0); row += 1) + for (const [index, entry] of memberLists.entries.entries()) definitions.push( - readTypeDefinition(bytes, layout, row, issues, memberLists), + readTypeDefinition(bytes, layout, index + 1, issues, entry), ); return definitions; }; @@ -249,12 +249,7 @@ export const parseTypes = ( 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 { flags, extendsRaw, extendsOffset, rowOffset } = definition.source; const fieldRange = rowRange( fields, definition.fieldStart, @@ -279,12 +274,12 @@ 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); + managedDecodeBudget(layout).reserve(128, rowOffset); items.push({ row, type: { token, - row_offset: cursor.start, + row_offset: rowOffset, namespace, name, full_name: definition.fullName, diff --git a/src/dotnet/ManagedMemberTypeInventory.test.ts b/src/dotnet/ManagedMemberTypeInventory.test.ts index 4fd7278c73..6d12a77f00 100644 --- a/src/dotnet/ManagedMemberTypeInventory.test.ts +++ b/src/dotnet/ManagedMemberTypeInventory.test.ts @@ -295,3 +295,84 @@ it("keeps later type identity, member ownership and constructor names at their o }, ]); }); + +it("keeps independent name failures ahead of conditional Extends diagnostics", () => { + const bytes = buildManagedPeFixture({ extendsRaw: 12 }); + const readable = inspectManagedMembersBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + const row = readable.types[0]?.row_offset; + if (row === undefined) throw new Error("Expected TypeDef fixture"); + expect(readable.coverage.issues).toContainEqual( + expect.objectContaining({ + code: "invalid-row", + scope: "metadata.TypeDef:0x02000001", + offset: row + 8, + detail: expect.stringContaining("Extends"), + }), + ); + bytes.writeUInt16LE(0xffff, row + 4); + bytes.writeUInt16LE(0xfffe, row + 6); + const partial = inspectManagedMembersBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + expect(partial.types).toEqual([]); + expect(partial.fields).toHaveLength(1); + expect(partial.methods).toHaveLength(1); + for (const member of [...partial.fields, ...partial.methods]) + expect(member).toMatchObject({ + declaring_type_token: "0x02000001", + declaring_type: null, + }); + expect(partial.module).toEqual(readable.module); + expect(partial.coverage).toMatchObject({ + state: "partial", + issues: [ + expect.objectContaining({ + code: "invalid-heap-index", + scope: "metadata.#Strings", + }), + expect.objectContaining({ + code: "invalid-heap-index", + scope: "metadata.#Strings", + }), + ], + }); + expect( + new Set(partial.coverage.issues.map(({ offset }) => offset)).size, + ).toBe(2); +}); + +it("reads inventory names on demand without admitting TypeDef member lists or Extends", () => { + const bytes = buildManagedPeFixture({ + additionalEmptyTypes: 1, + typeName: "Lookup", + }); + 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); + const earlier = metadataRowOffset(layout, 2, 1); + bytes.writeUInt16LE(0xffff, earlier + 4); + bytes.writeUInt16LE(0xfffe, earlier + 6); + const selected = metadataRowOffset(layout, 2, 2); + bytes.writeUInt16LE(0xffff, selected + 8); + bytes.writeUInt16LE(0, selected + 10); + bytes.writeUInt16LE(0, selected + 12); + const issues: ManagedParseIssue[] = []; + const names = managedMetadataTypeNames(bytes, layout, issues); + expect(names(2, 2)).toBe("Fixture.Lookup"); + expect(issues).toEqual([]); + expect(readTypeMemberListFacts(bytes, layout).issues).toEqual( + expect.arrayContaining([ + expect.objectContaining({ code: "invalid-row", offset: selected + 10 }), + expect.objectContaining({ code: "invalid-row", offset: selected + 12 }), + ]), + ); +}); diff --git a/src/dotnet/ManagedMetadataInventoryRows.ts b/src/dotnet/ManagedMetadataInventoryRows.ts index 3638aa89c2..9c17b9f2ae 100644 --- a/src/dotnet/ManagedMetadataInventoryRows.ts +++ b/src/dotnet/ManagedMetadataInventoryRows.ts @@ -1,3 +1,4 @@ +import { readTypeDefinitionRowFacts } from "./ManagedTypeDefinitionRows.js"; import { managedDecodeBudget } from "./ManagedDecodeBudget.js"; import { managedTypeReferences } from "./ManagedTypeReferences.js"; import { MAX_TYPE_NESTING_DEPTH } from "./ManagedTypeNameDigests.js"; @@ -315,18 +316,18 @@ export const managedMetadataTypeNames = ( namespace = parsed.namespace; flags = parsed.flags; } else { - const cursor = metadataRowCursor(bytes, layout, table, row); - flags = cursor.readUInt32(); + const source = readTypeDefinitionRowFacts(bytes, layout, row); + flags = source.flags; name = readMetadataString( bytes, layout, - cursor.readIndex(layout.stringIndexSize), + source.nameIndex, layout.strings.size, ); namespace = readMetadataString( bytes, layout, - cursor.readIndex(layout.stringIndexSize), + source.namespaceIndex, layout.strings.size, ); } diff --git a/src/dotnet/ManagedTypeDefinitionRows.ts b/src/dotnet/ManagedTypeDefinitionRows.ts new file mode 100644 index 0000000000..33dc9aff17 --- /dev/null +++ b/src/dotnet/ManagedTypeDefinitionRows.ts @@ -0,0 +1,46 @@ +import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js"; +import { metadataRowCursor } from "./ManagedMetadataHeaps.js"; +import { memberListIndexSize } from "./ManagedMemberPointers.js"; + +/** Scalar TypeDef source facts; consumers own heap and coded-index admission. */ +export interface TypeDefinitionRowFacts { + readonly rowOffset: number; + readonly flags: number; + readonly nameIndex: number; + readonly namespaceIndex: number; + readonly extendsRaw: number; + readonly extendsOffset: number; + readonly fieldListRaw: number; + readonly fieldListOffset: number; + readonly methodListRaw: number; + readonly methodListOffset: number; +} + +export const readTypeDefinitionRowFacts = ( + bytes: Buffer, + layout: ManagedMetadataLayout, + row: number, +): TypeDefinitionRowFacts => { + const cursor = metadataRowCursor(bytes, layout, 2, row); + const flags = cursor.readUInt32(); + const nameIndex = cursor.readIndex(layout.stringIndexSize); + const namespaceIndex = cursor.readIndex(layout.stringIndexSize); + const extendsOffset = cursor.offset; + const extendsRaw = cursor.readIndex(layout.codedIndexSize("TypeDefOrRef")); + const fieldListOffset = cursor.offset; + const fieldListRaw = cursor.readIndex(memberListIndexSize(layout, 4)); + const methodListOffset = cursor.offset; + const methodListRaw = cursor.readIndex(memberListIndexSize(layout, 6)); + return { + rowOffset: cursor.start, + flags, + nameIndex, + namespaceIndex, + extendsRaw, + extendsOffset, + fieldListRaw, + fieldListOffset, + methodListRaw, + methodListOffset, + }; +}; diff --git a/src/dotnet/ManagedTypeMemberLists.ts b/src/dotnet/ManagedTypeMemberLists.ts index 1db5674c64..e66ea37a2f 100644 --- a/src/dotnet/ManagedTypeMemberLists.ts +++ b/src/dotnet/ManagedTypeMemberLists.ts @@ -1,13 +1,17 @@ import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js"; import { managedDecodeBudget } from "./ManagedDecodeBudget.js"; import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js"; -import { metadataRowCursor, metadataToken } from "./ManagedMetadataHeaps.js"; +import { metadataToken } from "./ManagedMetadataHeaps.js"; import { - memberListIndexSize, memberListTable, validatedMemberListStart, } from "./ManagedMemberPointers.js"; +import { + readTypeDefinitionRowFacts, + type TypeDefinitionRowFacts, +} from "./ManagedTypeDefinitionRows.js"; + export type MemberDefinitionTable = 4 | 6; export interface TypeMemberListStart { @@ -23,6 +27,7 @@ export interface TypeMemberListRange { } export interface TypeMemberListEntry { + readonly source: TypeDefinitionRowFacts; readonly field: TypeMemberListRange; readonly method: TypeMemberListRange; } @@ -39,33 +44,33 @@ export const readTypeMemberListFacts = ( ): TypeMemberListFacts => { const fields = memberListTable(layout, 4); const methods = memberListTable(layout, 6); - const rows: { field: TypeMemberListStart; method: TypeMemberListStart }[] = - []; + const rows: { + source: TypeDefinitionRowFacts; + field: TypeMemberListStart; + method: TypeMemberListStart; + }[] = []; const issues: ManagedParseIssue[] = []; const types = layout.table(2); const typeCount = types?.rowCount ?? 0; - // Peak retained shape includes both the parsed-start and range arrays, their - // nested records, plus each eventual JSON object/key projection. Malformed + // Peak retained shape includes shared scalar source facts, the parsed-start + // and range arrays, plus each eventual JSON object/key projection. Malformed // row diagnostics are admitted separately before they are allocated. managedDecodeBudget(layout).reserve( - typeCount * 512 + 256, + typeCount * 640 + 256, types?.offset ?? null, ); for (let row = 1; row <= typeCount; row += 1) { - const cursor = metadataRowCursor(bytes, layout, 2, row); - cursor.readUInt32(); - cursor.readIndex(layout.stringIndexSize); - cursor.readIndex(layout.stringIndexSize); - cursor.readIndex(layout.codedIndexSize("TypeDefOrRef")); + const source = readTypeDefinitionRowFacts(bytes, layout, row); const field = { - offset: cursor.offset, - raw: cursor.readIndex(memberListIndexSize(layout, 4)), + offset: source.fieldListOffset, + raw: source.fieldListRaw, }; const method = { - offset: cursor.offset, - raw: cursor.readIndex(memberListIndexSize(layout, 6)), + offset: source.methodListOffset, + raw: source.methodListRaw, }; const entry = { + source, field: { ...field, value: validatedMemberListStart(field.raw, fields) }, method: { ...method, @@ -116,6 +121,7 @@ export const readTypeMemberListFacts = ( const entries: TypeMemberListEntry[] = rows.map((entry, index) => { const next = rows[index + 1]; return { + source: entry.source, field: { start: { ...entry.field,