diff --git a/src/dotnet/ManagedMemberInspector.test.ts b/src/dotnet/ManagedMemberInspector.test.ts index a79df5c88b..d3a1ce6f27 100644 --- a/src/dotnet/ManagedMemberInspector.test.ts +++ b/src/dotnet/ManagedMemberInspector.test.ts @@ -17,6 +17,8 @@ import { edges } from "./ManagedMemberRows.js"; import { parseUserStrings } from "./ManagedUserStrings.js"; import { signature, type ManagedMethod } from "./ManagedMemberInspectorCore.js"; import { ManagedReaderFailure } from "./ManagedReaderFailure.js"; +import { managedTypeReferences } from "./ManagedTypeReferences.js"; +import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js"; import { managedMemberInspectionSchema } from "../domain/managed/managedArtifact.js"; import { comparisonLimitations } from "../domain/managed/managedMemberComparisonCoverage.js"; import { buildManagedPeFixture as buildMultiMethodManagedPeFixture } from "../../scripts/fixtures/managed/pe.mjs"; @@ -637,6 +639,38 @@ describe("managed edge target declaring types", () => { }); describe("managed TypeRef resolution scopes", () => { + it.each([0, 9])( + "retains invalid enclosing TypeRef row %s without granting scope validity", + (enclosingRow) => { + const bytes = buildManagedPeFixture({ + typeRefScopeRaw: enclosingRow * 4 + 3, + }); + 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 issues: ManagedParseIssue[] = []; + const reference = managedTypeReferences(bytes, layout, issues)(1); + + expect(reference).toMatchObject({ enclosingRow, scopeValid: false }); + expect(issues).toMatchObject([ + { + code: "invalid-row", + scope: "metadata.TypeRef:0x01000001", + detail: expect.stringContaining( + enclosingRow === 0 + ? "coded index selects row zero in table 1" + : "coded index selects row 9 in table 1, which has 1 rows", + ), + }, + ]); + }, + ); + it.each([ // AssemblyRef 9 does not exist. ["an invalid", (9 << 2) | 2, "is invalid"], diff --git a/src/dotnet/ManagedMemberInspectorCore.ts b/src/dotnet/ManagedMemberInspectorCore.ts index 831b6849cd..b30596a91f 100644 --- a/src/dotnet/ManagedMemberInspectorCore.ts +++ b/src/dotnet/ManagedMemberInspectorCore.ts @@ -23,8 +23,7 @@ import type { } from "./ManagedMetadataLayout.js"; import { metadataRowCursor, - metadataCodedToken, - metadataCodedTokenInvalidReason, + decodeMetadataCodedIndex, metadataToken, readMetadataString, sha256Bytes, @@ -242,18 +241,18 @@ export const parseTypes = ( definition.methodStart, definition.methodEnd, ); - const extendsReason = metadataCodedTokenInvalidReason( + const extendsIndex = decodeMetadataCodedIndex( extendsRaw, 2, [2, 1, 27], layout.rowCounts, ); - if (extendsReason !== null) + if (extendsIndex.kind === "invalid") issues.push({ code: "invalid-row", scope: `metadata.TypeDef:${token}`, offset: extendsOffset, - detail: `TypeDef Extends coded index 0x${extendsRaw.toString(16)} is invalid: ${extendsReason}`, + detail: `TypeDef Extends coded index 0x${extendsRaw.toString(16)} is invalid: ${extendsIndex.reason}`, }); const enclosingRow = nesting.enclosing.get(row); items.push({ @@ -263,12 +262,7 @@ export const parseTypes = ( name, full_name: definition.fullName, flags, - extends_token: metadataCodedToken( - extendsRaw, - 2, - [2, 1, 27], - layout.rowCounts, - ), + extends_token: extendsIndex.kind === "valid" ? extendsIndex.token : null, enclosing_type_token: enclosingRow === undefined ? null : metadataToken(2, enclosingRow), name_sha256: null, @@ -530,20 +524,17 @@ const readTypeOperand = ( ): ReturnType => { 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) + const decoded = decodeMetadataCodedIndex( + token.value, + 2, + tables, + layout.rowCounts, + ); + if (decoded.kind !== "valid") throw managedFailure( "invalid-row", "signature", - `TypeDefOrRefOrSpec operand 0x${token.value.toString(16)} is invalid: ${reason}`, + `TypeDefOrRefOrSpec operand 0x${token.value.toString(16)} is invalid: ${decoded.kind === "null" ? "type operand is null" : decoded.reason}`, offset, ); } diff --git a/src/dotnet/ManagedMemberRows.ts b/src/dotnet/ManagedMemberRows.ts index bfe5c950fe..9a94157896 100644 --- a/src/dotnet/ManagedMemberRows.ts +++ b/src/dotnet/ManagedMemberRows.ts @@ -25,8 +25,7 @@ import { import { metadataToken, metadataRowCursor, - metadataCodedToken, - metadataCodedTokenInvalidReason, + decodeMetadataCodedIndex, readMetadataBlob, readMetadataString, } from "./ManagedMetadataHeaps.js"; @@ -172,21 +171,21 @@ export const parseExportedTypes = ( layout.codedIndexSize("Implementation"), ); const token = metadataToken(39, row); - const implementationReason = metadataCodedTokenInvalidReason( + const implementation = decodeMetadataCodedIndex( implementationRaw, 2, [38, 35, 39], layout.rowCounts, ); - if (implementationReason !== null || implementationRaw === 0) + if (implementation.kind !== "valid") issues.push({ code: "invalid-row", scope: `metadata.ExportedType:${token}`, offset: implementationOffset, detail: - implementationReason === null + implementation.kind === "null" ? "ExportedType implementation coded index 0x0 is null, but the Implementation column must reference a row" - : `ExportedType implementation coded index 0x${implementationRaw.toString(16)} is invalid: ${implementationReason}`, + : `ExportedType implementation coded index 0x${implementationRaw.toString(16)} is invalid: ${implementation.reason}`, }); exportedTypes.push({ token, @@ -196,12 +195,8 @@ export const parseExportedTypes = ( flags, forwarder: (flags & TYPE_FORWARDER_FLAG) !== 0, type_def_id: typeDefId, - implementation_token: metadataCodedToken( - implementationRaw, - 2, - [38, 35, 39], - layout.rowCounts, - ), + implementation_token: + implementation.kind === "valid" ? implementation.token : null, }); }, issues); } @@ -241,28 +236,23 @@ export const parseMemberRefs = ( layout.blob?.size ?? 0, ); const token = metadataToken(10, row); - const parentReason = metadataCodedTokenInvalidReason( + const parent = decodeMetadataCodedIndex( parentRaw, 3, [2, 1, 26, 6, 27], layout.rowCounts, ); - if (parentReason !== null || parentRaw === 0) + if (parent.kind !== "valid") issues.push({ code: "invalid-row", scope: `metadata.MemberRef:${token}`, offset: cursor.start, detail: - parentReason === null + parent.kind === "null" ? "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}`, + : `MemberRef parent coded index 0x${parentRaw.toString(16)} is invalid: ${parent.reason}`, }); - const parentToken = metadataCodedToken( - parentRaw, - 3, - [2, 1, 26, 6, 27], - layout.rowCounts, - ); + const parentToken = parent.kind === "valid" ? parent.token : null; const parentName = typeName(parentToken); const decoded = budgetedSignature( sig, diff --git a/src/dotnet/ManagedMetadataHeaps.ts b/src/dotnet/ManagedMetadataHeaps.ts index 50e5d56a83..3ffe809e9e 100644 --- a/src/dotnet/ManagedMetadataHeaps.ts +++ b/src/dotnet/ManagedMetadataHeaps.ts @@ -392,37 +392,53 @@ export const metadataRowCursor = ( ); }; -/** Decode a metadata coded index without signed 32-bit coercion. */ -export const metadataCodedToken = ( - raw: number, - bits: number, - tables: readonly (number | undefined)[], - rowCounts: readonly number[], -): string | null => { - if (raw === 0) return null; - const tag = raw & (2 ** bits - 1); - const row = Math.floor(raw / 2 ** bits); - const table = tables[tag]; - return table === undefined || row === 0 || row > (rowCounts[table] ?? 0) - ? null - : metadataToken(table, row); -}; +export type ManagedCodedIndex = + | { readonly kind: "null" } + | { + readonly kind: "valid"; + readonly token: string; + readonly table: number; + readonly row: number; + } + | { + readonly kind: "invalid"; + readonly reason: string; + readonly table: number | undefined; + readonly row: number; + }; -/** Explain why a non-null coded index cannot name an admitted metadata row. */ -export const metadataCodedTokenInvalidReason = ( +/** Decode and validate a metadata coded index without signed row coercion. */ +export const decodeMetadataCodedIndex = ( raw: number, bits: number, tables: readonly (number | undefined)[], rowCounts: readonly number[], -): string | null => { - if (raw === 0) return null; +): ManagedCodedIndex => { + if (raw === 0) return { kind: "null" }; const tag = raw & (2 ** bits - 1); const row = Math.floor(raw / 2 ** bits); const table = tables[tag]; - if (table === undefined) return `coded index tag ${tag} is not defined`; - if (row === 0) return `coded index selects row zero in table ${table}`; + if (table === undefined) + return { + kind: "invalid", + reason: `coded index tag ${tag} is not defined`, + table, + row, + }; + if (row === 0) + return { + kind: "invalid", + reason: `coded index selects row zero in table ${table}`, + table, + row, + }; const count = rowCounts[table] ?? 0; - return row > count - ? `coded index selects row ${row} in table ${table}, which has ${count} rows` - : null; + if (row > count) + return { + kind: "invalid", + reason: `coded index selects row ${row} in table ${table}, which has ${count} rows`, + table, + row, + }; + return { kind: "valid", token: metadataToken(table, row), table, row }; }; diff --git a/src/dotnet/ManagedMetadataInventoryRows.ts b/src/dotnet/ManagedMetadataInventoryRows.ts index 79563afb62..6bf3ccbb0b 100644 --- a/src/dotnet/ManagedMetadataInventoryRows.ts +++ b/src/dotnet/ManagedMetadataInventoryRows.ts @@ -16,8 +16,8 @@ import { } from "./ManagedMetadataLayout.js"; import { metadataRowCursor, - metadataCodedToken, - metadataCodedTokenInvalidReason, + decodeMetadataCodedIndex, + type ManagedCodedIndex, metadataToken, readMetadataBlob, readMetadataGuid, @@ -404,29 +404,28 @@ const declaringTypeForMethod = ( const attributeTypeName = ( bytes: Buffer, layout: ManagedMetadataLayout, - rawType: number, + constructor: Extract, memberLists: TypeMemberListFacts, typeName: ManagedMetadataTypeNameLookup, ): string | null => { - const tag = rawType & 7; - const row = Math.floor(rawType / 8); - if (row === 0) return null; - if (tag === 2) + const { table, row } = constructor; + if (table === 6) return declaringTypeForMethod(bytes, layout, row, memberLists, typeName); - if (tag !== 3) return null; - const memberRefs = layout.table(10); - if (memberRefs === undefined || row > memberRefs.rowCount) return null; const cursor = metadataRowCursor(bytes, layout, 10, row); - const parent = cursor.readIndex(layout.codedIndexSize("MemberRefParent")); - const parentTag = parent & 7; - const parentRow = Math.floor(parent / 8); - if (parentTag === 0) return typeName(2, parentRow); - if (parentTag === 1) return typeName(1, parentRow); - if (parentTag === 3) + const parent = decodeMetadataCodedIndex( + cursor.readIndex(layout.codedIndexSize("MemberRefParent")), + 3, + [2, 1, 26, 6, 27], + layout.rowCounts, + ); + if (parent.kind === "null") return null; + if (parent.table === 2 || parent.table === 1) + return typeName(parent.table, parent.row); + if (parent.table === 6) return declaringTypeForMethod( bytes, layout, - parentRow, + parent.row, memberLists, typeName, ); @@ -510,59 +509,48 @@ export const readCustomAttribute = ( cursor.readIndex(layout.blobIndexSize), heapExtent, ); - const parent = metadataCodedToken( + const parent = decodeMetadataCodedIndex( parentRaw, 5, HAS_CUSTOM_ATTRIBUTE_TABLES, layout.rowCounts, ); - if (parent === null) { - const parentReason = metadataCodedTokenInvalidReason( - parentRaw, - 5, - HAS_CUSTOM_ATTRIBUTE_TABLES, - layout.rowCounts, - ); + if (parent.kind !== "valid") { throw managedFailure( "invalid-row", `metadata.CustomAttribute:${metadataToken(12, row)}`, - `CustomAttribute parent coded index 0x${parentRaw.toString(16)} is invalid${parentReason === null ? " because the required parent is null" : `: ${parentReason}`}`, + `CustomAttribute parent coded index 0x${parentRaw.toString(16)} is invalid${parent.kind === "null" ? " because the required parent is null" : `: ${parent.reason}`}`, cursor.start, ); } - const constructor = metadataCodedToken( + const constructor = decodeMetadataCodedIndex( typeRaw, 3, CUSTOM_ATTRIBUTE_TYPE_TABLES, layout.rowCounts, ); - if (constructor === null) { - const typeReason = metadataCodedTokenInvalidReason( - typeRaw, - 3, - CUSTOM_ATTRIBUTE_TYPE_TABLES, - layout.rowCounts, - ); + if (constructor.kind !== "valid") { issues.push({ code: "invalid-row", scope: `metadata.CustomAttribute:${metadataToken(12, row)}`, offset: typeOffset, detail: - typeReason === null + constructor.kind === "null" ? "CustomAttribute constructor coded index 0x0 is null, but Type must reference a MethodDef or MemberRef row" - : `CustomAttribute constructor coded index 0x${typeRaw.toString(16)} is invalid: ${typeReason}`, + : `CustomAttribute constructor coded index 0x${typeRaw.toString(16)} is invalid: ${constructor.reason}`, }); } let typeName: string | null = null; - if (constructor !== null) + if (constructor.kind === "valid") typeName = readManagedValue( - () => attributeTypeName(bytes, layout, typeRaw, memberLists, typeNames), + () => + attributeTypeName(bytes, layout, constructor, memberLists, typeNames), issues, ) ?? null; return { - parent_token: parent, - constructor_token: constructor, + parent_token: parent.token, + constructor_token: constructor.kind === "valid" ? constructor.token : null, type_name: typeName, value_length: value.length, value_sha256: managedDecodeBudget(layout).digest(value, "sha256"), @@ -603,28 +591,22 @@ export const readResource = ({ const implementationRaw = cursor.readIndex( layout.codedIndexSize("Implementation"), ); - const implementationToken = metadataCodedToken( + const implementation = decodeMetadataCodedIndex( implementationRaw, 2, [38, 35, 39], layout.rowCounts, ); - const implementationReason = metadataCodedTokenInvalidReason( - implementationRaw, - 2, - [38, 35, 39], - layout.rowCounts, - ); - if (implementationReason !== null) + if (implementation.kind === "invalid") issues.push({ code: "invalid-row", scope: `metadata.ManifestResource:${metadataToken(40, row)}`, offset: implementationOffset, - detail: `ManifestResource implementation coded index 0x${implementationRaw.toString(16)} is invalid: ${implementationReason}`, + detail: `ManifestResource implementation coded index 0x${implementationRaw.toString(16)} is invalid: ${implementation.reason}`, }); let dataLength: number | null = null; let dataSha256: string | null = null; - if (implementationRaw === 0) { + if (implementation.kind === "null") { if (directory === null || declaredOffset > directory.size - 4) { issues.push({ code: "invalid-resource", @@ -655,8 +637,9 @@ export const readResource = ({ flags, visibility: (flags & 7) === 1 ? "public" : (flags & 7) === 2 ? "private" : "unknown", - implementation_token: implementationToken, - embedded: implementationRaw === 0, + implementation_token: + implementation.kind === "valid" ? implementation.token : null, + embedded: implementation.kind === "null", declared_offset: declaredOffset, data_length: dataLength, data_sha256: dataSha256, diff --git a/src/dotnet/ManagedMethodSpecs.ts b/src/dotnet/ManagedMethodSpecs.ts index fd97737dc3..37d9917d5b 100644 --- a/src/dotnet/ManagedMethodSpecs.ts +++ b/src/dotnet/ManagedMethodSpecs.ts @@ -9,8 +9,7 @@ import { } from "./ManagedMemberInspectorCore.js"; import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js"; import { - metadataCodedToken, - metadataCodedTokenInvalidReason, + decodeMetadataCodedIndex, metadataRowCursor, readMetadataBlob, } from "./ManagedMetadataHeaps.js"; @@ -84,23 +83,23 @@ const readMethodSpec = ( ); const methodRaw = cursor.readIndex(layout.codedIndexSize("MethodDefOrRef")); const instantiation = cursor.readIndex(layout.blobIndexSize); - const methodReason = metadataCodedTokenInvalidReason( + const method = decodeMetadataCodedIndex( methodRaw, 1, [6, 10], layout.rowCounts, ); - if (methodReason !== null || methodRaw === 0) + if (method.kind !== "valid") issues.push({ code: "invalid-row", scope, offset: cursor.start, detail: - methodReason === null + method.kind === "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}`, + : `MethodSpec method coded index 0x${methodRaw.toString(16)} is invalid: ${method.reason}`, }); - const generic = metadataCodedToken(methodRaw, 1, [6, 10], layout.rowCounts); + const generic = method.kind === "valid" ? method.token : null; 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. diff --git a/src/dotnet/ManagedNativeBoundaryHelpers.ts b/src/dotnet/ManagedNativeBoundaryHelpers.ts index 12bc15c492..495dc4089b 100644 --- a/src/dotnet/ManagedNativeBoundaryHelpers.ts +++ b/src/dotnet/ManagedNativeBoundaryHelpers.ts @@ -11,8 +11,7 @@ import { import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js"; import { metadataRowCursor, - metadataCodedToken, - metadataCodedTokenInvalidReason, + decodeMetadataCodedIndex, metadataToken, readMetadataString, } from "./ManagedMetadataHeaps.js"; @@ -169,28 +168,24 @@ export const parseImplMaps = ({ const memberForwardedRaw = cursor.readIndex( layout.codedIndexSize("MemberForwarded"), ); - const memberForwardedReason = metadataCodedTokenInvalidReason( + const memberForwarded = decodeMetadataCodedIndex( memberForwardedRaw, 1, [4, 6], layout.rowCounts, ); - if (memberForwardedReason !== null || memberForwardedRaw === 0) + if (memberForwarded.kind !== "valid") issues.push({ code: "invalid-row", scope: `metadata.ImplMap:${metadataToken(28, row)}`, offset: memberForwardedOffset, detail: - memberForwardedReason === null + memberForwarded.kind === "null" ? "ImplMap MemberForwarded coded index 0x0 is null, but the column must reference a Field or MethodDef row" - : `ImplMap MemberForwarded coded index 0x${memberForwardedRaw.toString(16)} is invalid: ${memberForwardedReason}`, + : `ImplMap MemberForwarded coded index 0x${memberForwardedRaw.toString(16)} is invalid: ${memberForwarded.reason}`, }); - const memberToken = metadataCodedToken( - memberForwardedRaw, - 1, - [4, 6], - layout.rowCounts, - ); + const memberToken = + memberForwarded.kind === "valid" ? memberForwarded.token : null; const importName = readMetadataString( bytes, layout, diff --git a/src/dotnet/ManagedTypeReferences.ts b/src/dotnet/ManagedTypeReferences.ts index 6b436a410c..340a0bc681 100644 --- a/src/dotnet/ManagedTypeReferences.ts +++ b/src/dotnet/ManagedTypeReferences.ts @@ -1,7 +1,7 @@ import { managedDecodeBudget } from "./ManagedDecodeBudget.js"; import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js"; import { - metadataCodedTokenInvalidReason, + decodeMetadataCodedIndex, metadataRowCursor, metadataToken, readMetadataString, @@ -46,25 +46,25 @@ export const managedTypeReferences = ( readManagedValue(() => { const cursor = metadataRowCursor(bytes, layout, 1, row); const raw = cursor.readIndex(layout.codedIndexSize("ResolutionScope")); - const reason = metadataCodedTokenInvalidReason( + const scope = decodeMetadataCodedIndex( raw, 2, [0, 26, 35, 1], layout.rowCounts, ); - if (reason !== null) + if (scope.kind === "invalid") issues.push({ code: "invalid-row", scope: `metadata.TypeRef:${metadataToken(1, row)}`, offset: cursor.start, - detail: `TypeRef ResolutionScope coded index 0x${raw.toString(16)} is invalid: ${reason}`, + detail: `TypeRef ResolutionScope coded index 0x${raw.toString(16)} is invalid: ${scope.reason}`, }); return { nameIndex: cursor.readIndex(layout.stringIndexSize), namespaceIndex: cursor.readIndex(layout.stringIndexSize), enclosingRow: - raw !== 0 && (raw & 3) === 3 ? Math.floor(raw / 4) : null, - scopeValid: reason === null, + scope.kind !== "null" && scope.table === 1 ? scope.row : null, + scopeValid: scope.kind !== "invalid", }; }, issues) ?? null; rows.set(row, value);