diff --git a/docs/managed-code-analysis.md b/docs/managed-code-analysis.md index e349f6e9ad..c9f656d7ea 100644 --- a/docs/managed-code-analysis.md +++ b/docs/managed-code-analysis.md @@ -182,6 +182,9 @@ declares API compatibility across multiple .NET implementations. REA preserves that target in `target_frameworks` and leaves `runtime_family` as `unknown` unless independent markers identify a runtime family, such as a ReadyToRun signature. +When ReadyToRun is observed, `runtime_family` remains `modern-dotnet` even if +Unity or target-framework hints are also present; the separate hint evidence +is retained in the classification evidence list. ### Resource admission @@ -542,6 +545,10 @@ preserving readable member and attribute facts. The retained inverse indexes are admitted against the existing representation budget before allocation. +Custom-attribute `type_name` uses `Outer+Nested` when its local TypeDef or +TypeRef enclosing chain is readable. It remains `null` when the constructor's +type or required enclosing facts cannot be resolved. + `exported_types` reports ExportedType rows as declared. Facade assemblies such as .NET `System.Runtime.dll` provide their types this way: `forwarder: true` marks a type forwarder, and `implementation_token` names the AssemblyRef, File, diff --git a/src/dotnet/ManagedArtifactInspector.test.ts b/src/dotnet/ManagedArtifactInspector.test.ts index 50de3a39b1..814e15e991 100644 --- a/src/dotnet/ManagedArtifactInspector.test.ts +++ b/src/dotnet/ManagedArtifactInspector.test.ts @@ -479,6 +479,100 @@ describe("managed artifact coded indexes", () => { }); describe("managed artifact reference inventory", () => { + it("qualifies local custom-attribute names with their enclosing types", () => { + const bytes = buildManagedPeFixture({ + additionalEmptyTypes: 1, + customAttributeTypeRaw: (1 << 3) | 2, + nestedTypes: [[2, 1]], + typeName: "InnerAttribute", + typeNamespace: "", + }); + const result = inspectManagedArtifactBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + + expect(result.attributes[0]?.type_name).toBe("Empty0+InnerAttribute"); + expect(result.coverage).toEqual({ state: "complete", issues: [] }); + }); + + it("qualifies referenced custom-attribute names from TypeRef scopes", () => { + const bytes = buildManagedPeFixture({ + customAttributeTypeRaw: (1 << 3) | 3, + memberRefParentRaw: (3 << 3) | 1, + typeReferences: [ + { namespace: "Fixture", name: "Outer" }, + { namespace: "", name: "InnerAttribute", enclosingRow: 2 }, + ], + }); + const result = inspectManagedArtifactBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + + expect(result.attributes[0]?.type_name).toBe( + "Fixture.Outer+InnerAttribute", + ); + expect(result.coverage).toEqual({ state: "complete", issues: [] }); + }); + + it.each([ + { + label: "duplicate NestedClass rows", + additionalEmptyTypes: 1, + nestedTypes: [ + [2, 1], + [2, 1], + ] as const, + coverage: "partial", + }, + { + label: "a cyclic NestedClass chain", + additionalEmptyTypes: 2, + nestedTypes: [ + [2, 3], + [3, 2], + ] as const, + coverage: "complete", + }, + ])("leaves a nested attribute name unknown for $label", (fixture) => { + const bytes = buildManagedPeFixture({ + additionalEmptyTypes: fixture.additionalEmptyTypes, + customAttributeTypeRaw: (1 << 3) | 2, + nestedTypes: fixture.nestedTypes, + typeName: "InnerAttribute", + typeNamespace: "", + }); + const result = inspectManagedArtifactBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + + expect(result.attributes[0]?.type_name).toBeNull(); + expect(result.coverage.state).toBe(fixture.coverage); + }); + + it("leaves a custom-attribute name unknown when nesting exceeds its cap", () => { + const nestedTypes: (readonly [number, number])[] = Array.from( + { length: 64 }, + (_, index) => [65 - index, 64 - index], + ); + const bytes = buildManagedPeFixture({ + additionalEmptyTypes: 64, + customAttributeTypeRaw: (1 << 3) | 2, + nestedTypes, + typeName: "InnerAttribute", + typeNamespace: "", + }); + const result = inspectManagedArtifactBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + + expect(result.attributes[0]?.type_name).toBeNull(); + expect(result.coverage).toEqual({ state: "complete", issues: [] }); + }); + it("returns all references inline while classifying from the full inventory", () => { const bytes = buildManagedPeFixture({ references: ["System.Runtime", "UnityEngine.CoreModule"], @@ -548,6 +642,39 @@ describe("managed fixture data layout", () => { }); describe("managed implementation classification", () => { + it.each([ + { + hint: "Unity AssemblyRef", + options: { references: ["UnityEngine.CoreModule"] }, + evidence: "unity-reference-observed", + }, + { + hint: ".NET Framework target", + options: { targetFramework: ".NETFramework,Version=v4.8" }, + evidence: "target-framework-observed", + }, + ])( + "keeps ReadyToRun classification and evidence with a $hint", + ({ options, evidence }) => { + const bytes = buildManagedPeFixture({ readyToRun: true, ...options }); + const result = inspectManagedArtifactBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + + expect(result.classification).toMatchObject({ + runtime_family: "modern-dotnet", + implementation: "cil-and-ready-to-run", + }); + expect(result.classification.evidence).toEqual( + expect.arrayContaining([ + expect.objectContaining({ code: "ready-to-run-signature-observed" }), + expect.objectContaining({ code: evidence }), + ]), + ); + }, + ); + it("classifies crossgen2 ReadyToRun output without ILONLY as modern .NET", () => { // crossgen2 writes IL_LIBRARY | STRONGNAMESIGNED and clears ILONLY. const bytes = buildManagedPeFixture({ readyToRun: true, cliFlags: 0xc }); diff --git a/src/dotnet/ManagedArtifactInspector.ts b/src/dotnet/ManagedArtifactInspector.ts index e5e06f678c..b39d269fd5 100644 --- a/src/dotnet/ManagedArtifactInspector.ts +++ b/src/dotnet/ManagedArtifactInspector.ts @@ -189,9 +189,57 @@ const runtimeFamilyFor = ( const hasUnity = inventory.referenceNames.some( (name) => name === "UnityEngine" || name.startsWith("UnityEngine."), ); + const hasFrameworkTarget = frameworks.some((value) => + value.startsWith(".NETFramework,"), + ); + const hasModernTarget = frameworks.some((value) => + value.startsWith(".NETCoreApp,"), + ); + const hasStandardTarget = frameworks.some((value) => + value.startsWith(".NETStandard,"), + ); + if (cli.readyToRunSignature) + evidence.push({ + code: "ready-to-run-signature-observed", + detail: "The managed native header has the ReadyToRun signature.", + file_offset: layout.rvaToOffset( + cli.managedNativeHeader.rva, + 4, + "cli.managed-native-header", + ), + }); + if (hasUnity) + evidence.push({ + code: "unity-reference-observed", + detail: + "The admitted AssemblyRef table contains a UnityEngine reference.", + file_offset: metadata.table(35)?.offset ?? null, + }); + if (hasFrameworkTarget) + evidence.push({ + code: "target-framework-observed", + detail: + "An assembly-level TargetFrameworkAttribute names .NET Framework.", + file_offset: metadata.table(12)?.offset ?? null, + }); + if (hasModernTarget) + evidence.push({ + code: "target-framework-observed", + detail: "An assembly-level TargetFrameworkAttribute names modern .NET.", + file_offset: metadata.table(12)?.offset ?? null, + }); + if (hasStandardTarget) + evidence.push({ + code: "target-framework-observed", + detail: + "An assembly-level TargetFrameworkAttribute names .NET Standard, an API specification that does not identify a runtime family.", + file_offset: metadata.table(12)?.offset ?? null, + }); let runtimeFamily: ManagedArtifactInspection["classification"]["runtime_family"] = "unknown"; - if (runtimeFamilyFromCliHeader(cli) === "mixed-clr-native") { + if (cli.readyToRunSignature) { + runtimeFamily = "modern-dotnet"; + } else if (runtimeFamilyFromCliHeader(cli) === "mixed-clr-native") { runtimeFamily = "mixed-clr-native"; evidence.push({ code: "cli-il-only-clear", @@ -201,47 +249,10 @@ const runtimeFamilyFor = ( }); } else if (hasUnity) { runtimeFamily = "unity-mono"; - evidence.push({ - code: "unity-reference-observed", - detail: - "The admitted AssemblyRef table contains a UnityEngine reference.", - file_offset: metadata.table(35)?.offset ?? null, - }); - } else if (frameworks.some((value) => value.startsWith(".NETFramework,"))) { + } else if (hasFrameworkTarget) { runtimeFamily = "dotnet-framework"; - evidence.push({ - code: "target-framework-observed", - detail: - "An assembly-level TargetFrameworkAttribute names .NET Framework.", - file_offset: metadata.table(12)?.offset ?? null, - }); - } else if ( - frameworks.some((value) => value.startsWith(".NETCoreApp,")) || - cli.readyToRunSignature - ) { + } else if (hasModernTarget) { runtimeFamily = "modern-dotnet"; - evidence.push({ - code: cli.readyToRunSignature - ? "ready-to-run-signature-observed" - : "target-framework-observed", - detail: cli.readyToRunSignature - ? "The managed native header has the ReadyToRun signature." - : "An assembly-level TargetFrameworkAttribute names modern .NET.", - file_offset: cli.readyToRunSignature - ? layout.rvaToOffset( - cli.managedNativeHeader.rva, - 4, - "cli.managed-native-header", - ) - : (metadata.table(12)?.offset ?? null), - }); - } else if (frameworks.some((value) => value.startsWith(".NETStandard,"))) { - evidence.push({ - code: "target-framework-observed", - detail: - "An assembly-level TargetFrameworkAttribute names .NET Standard, an API specification that does not identify a runtime family.", - file_offset: metadata.table(12)?.offset ?? null, - }); } return { runtime_family: runtimeFamily, evidence }; }; diff --git a/src/dotnet/ManagedMemberInspector.ts b/src/dotnet/ManagedMemberInspector.ts index 1720e08e4f..68f5638a31 100644 --- a/src/dotnet/ManagedMemberInspector.ts +++ b/src/dotnet/ManagedMemberInspector.ts @@ -18,6 +18,7 @@ import { readManagedMetadataInventory, readManagedModuleIdentity, } from "./ManagedMetadataInventory.js"; +import { managedMetadataTypeNames } from "./ManagedMetadataInventoryRows.js"; import { managedPeInspectionEnvelope } from "./ManagedPeInspectionEnvelope.js"; import { readManagedMetadataLayout } from "./ManagedMetadataLayout.js"; import { @@ -262,6 +263,13 @@ export const inspectManagedMembersBytes = ( readManagedResourceDirectory(pe, issues), memberLists, identity, + managedMetadataTypeNames( + bytes, + layout, + issues, + parsedTypes, + typeReferences, + ), ); appendManagedParseIssues(issues, inventory.issues); const exportedTypes = parseExportedTypes(bytes, layout); diff --git a/src/dotnet/ManagedMetadataInventory.ts b/src/dotnet/ManagedMetadataInventory.ts index 1ffec64051..3d55371cb1 100644 --- a/src/dotnet/ManagedMetadataInventory.ts +++ b/src/dotnet/ManagedMetadataInventory.ts @@ -13,6 +13,8 @@ import { readCustomAttribute, readModule, readResource, + managedMetadataTypeNames, + type ManagedMetadataTypeNameLookup, } from "./ManagedMetadataInventoryRows.js"; import { metadataToken } from "./ManagedMetadataHeaps.js"; import type { TypeMemberListFacts } from "./ManagedTypeMemberLists.js"; @@ -166,6 +168,7 @@ const readAttributes = ( layout: ManagedMetadataLayout, issues: ManagedParseIssue[], memberLists: TypeMemberListFacts, + typeNames: ManagedMetadataTypeNameLookup, ): { readonly attributes: readonly CustomAttribute[]; readonly targetFrameworks: readonly string[]; @@ -180,6 +183,7 @@ const readAttributes = ( heapExtent(layout), issues, memberLists, + typeNames, ), issues, ), @@ -207,6 +211,7 @@ export const readManagedMetadataInventory = ( ManagedMetadataInventory, "module" | "issues" > = readManagedModuleIdentity(bytes, layout), + typeNames?: ManagedMetadataTypeNameLookup, ): ManagedMetadataInventory => { const issues: ManagedParseIssue[] = [...memberLists.issues]; issues.push(...identity.issues); @@ -228,6 +233,7 @@ export const readManagedMetadataInventory = ( layout, issues, memberLists, + typeNames ?? managedMetadataTypeNames(bytes, layout, issues), ); return { module, diff --git a/src/dotnet/ManagedMetadataInventoryRows.ts b/src/dotnet/ManagedMetadataInventoryRows.ts index 4e3559388a..79563afb62 100644 --- a/src/dotnet/ManagedMetadataInventoryRows.ts +++ b/src/dotnet/ManagedMetadataInventoryRows.ts @@ -1,4 +1,8 @@ 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"; @@ -20,7 +24,9 @@ import { readMetadataString, } from "./ManagedMetadataHeaps.js"; import { managedFailure, readManagedValue } from "./ManagedReaderFailure.js"; +import type { ManagedTypeReferenceLookup } from "./ManagedTypeReferences.js"; import type { ManagedResourceDirectory } from "./ManagedMetadataInventory.js"; +import type { ManagedType } from "./ManagedMemberInspectorCore.js"; type ModuleIdentity = NonNullable; type AssemblyIdentity = NonNullable; @@ -229,48 +235,161 @@ export const readAssemblyReference = ( }; }; -interface ReadTypeNameContext { - readonly bytes: Buffer; - readonly layout: ManagedMetadataLayout; - readonly table: 1 | 2; - readonly row: number; - readonly heapExtent: number; -} +export type ManagedMetadataTypeNameLookup = ( + table: 1 | 2, + row: number, +) => string | null; -const readTypeName = ({ - bytes, - layout, - table, - row, - heapExtent, -}: ReadTypeNameContext): string | null => { - const descriptor = layout.table(table); - if (descriptor === undefined || row < 1 || row > descriptor.rowCount) - return null; - const cursor = metadataRowCursor(bytes, layout, table, row); - if (table === 1) cursor.readIndex(layout.codedIndexSize("ResolutionScope")); - else cursor.readUInt32(); - const name = readMetadataString( - bytes, - layout, - cursor.readIndex(layout.stringIndexSize), - heapExtent, - ); - const namespace = readMetadataString( - bytes, - layout, - cursor.readIndex(layout.stringIndexSize), - heapExtent, - ); - return namespace.length === 0 ? name : `${namespace}.${name}`; +/** Resolve the display name from the same validated nesting facts used by member parsing. */ +export const managedMetadataTypeNames = ( + bytes: Buffer, + layout: ManagedMetadataLayout, + issues: ManagedParseIssue[], + parsedTypes?: { + readonly types: readonly ManagedType[]; + readonly nesting: ManagedTypeNesting; + }, + parsedReferences?: ManagedTypeReferenceLookup, +): ManagedMetadataTypeNameLookup => { + const nesting = + parsedTypes?.nesting ?? readManagedTypeNesting(bytes, layout, issues); + const references = + parsedReferences ?? managedTypeReferences(bytes, layout, issues); + 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); + } + const cache = new Map< + string, + { + readonly fullName: string; + readonly depth: number; + readonly utf8Bytes: number; + } | null + >(); + const depthLimit = Symbol("type-name-depth-limit"); + const resolve = ( + table: 1 | 2, + row: number, + active: ReadonlySet = new Set(), + ): + | { + readonly fullName: string; + readonly depth: number; + readonly utf8Bytes: number; + } + | null + | typeof depthLimit => { + const key = `${String(table)}:${String(row)}`; + if (cache.has(key)) return cache.get(key) ?? null; + const descriptor = layout.table(table); + if (descriptor === undefined || row < 1 || row > descriptor.rowCount) + return null; + if (active.has(key)) { + cache.set(key, null); + return null; + } + if (active.size >= MAX_TYPE_NESTING_DEPTH) return depthLimit; + managedDecodeBudget(layout).reserve(128, null); + let name: string; + let namespace: string; + let parentRow: number | null; + if (table === 1) { + const reference = references(row); + if (reference === null || !reference.scopeValid) { + cache.set(key, null); + return null; + } + ({ name, namespace, enclosingRow: parentRow } = reference); + } else { + if (!nesting.complete) { + cache.set(key, null); + return null; + } + const parsed = definitions.get(row); + if (parsedTypes !== undefined && parsed === undefined) { + cache.set(key, null); + return null; + } + let flags: number; + if (parsed !== undefined) { + name = parsed.name; + namespace = parsed.namespace; + flags = parsed.flags; + } else { + const cursor = metadataRowCursor(bytes, layout, table, row); + flags = cursor.readUInt32(); + name = readMetadataString( + bytes, + layout, + cursor.readIndex(layout.stringIndexSize), + layout.strings.size, + ); + namespace = readMetadataString( + bytes, + layout, + cursor.readIndex(layout.stringIndexSize), + layout.strings.size, + ); + } + parentRow = nesting.enclosing.get(row) ?? null; + const isNested = (flags & 0x7) >= 2; + if (isNested !== (parentRow !== null)) { + cache.set(key, null); + return null; + } + } + let fullName: string | null; + let depth = 1; + let utf8Bytes = 0; + const nameBytes = Buffer.byteLength(name, "utf8"); + if (parentRow === null) { + if (namespace.length === 0) { + fullName = name; + utf8Bytes = nameBytes; + } else { + utf8Bytes = Buffer.byteLength(namespace, "utf8") + 1 + nameBytes; + managedDecodeBudget(layout).reserveString( + utf8Bytes, + metadataRowOffset(layout, table, row), + ); + fullName = `${namespace}.${name}`; + } + } else { + const parents = new Set(active); + parents.add(key); + const parent = resolve(table, parentRow, parents); + if (parent === depthLimit) return depthLimit; + if (parent === null) fullName = null; + else { + depth = parent.depth + 1; + utf8Bytes = parent.utf8Bytes + 1 + nameBytes; + managedDecodeBudget(layout).reserveString( + utf8Bytes, + metadataRowOffset(layout, table, row), + ); + fullName = `${parent.fullName}+${name}`; + } + } + if (depth > MAX_TYPE_NESTING_DEPTH) fullName = null; + const named = fullName === null ? null : { fullName, depth, utf8Bytes }; + cache.set(key, named); + return named; + }; + return (table, row) => { + const named = resolve(table, row); + return named === null || named === depthLimit ? null : named.fullName; + }; }; const declaringTypeForMethod = ( bytes: Buffer, layout: ManagedMetadataLayout, methodRow: number, - heapExtent: number, memberLists: TypeMemberListFacts, + typeName: ManagedMetadataTypeNameLookup, ): string | null => { const owner = memberDeclaringTypeRowLookup( bytes, @@ -279,21 +398,21 @@ const declaringTypeForMethod = ( memberLists, )(methodRow); if (owner === 0) return null; - return readTypeName({ bytes, layout, table: 2, row: owner, heapExtent }); + return typeName(2, owner); }; const attributeTypeName = ( bytes: Buffer, layout: ManagedMetadataLayout, rawType: number, - heapExtent: number, memberLists: TypeMemberListFacts, + typeName: ManagedMetadataTypeNameLookup, ): string | null => { const tag = rawType & 7; const row = Math.floor(rawType / 8); if (row === 0) return null; if (tag === 2) - return declaringTypeForMethod(bytes, layout, row, heapExtent, memberLists); + 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; @@ -301,29 +420,15 @@ const attributeTypeName = ( const parent = cursor.readIndex(layout.codedIndexSize("MemberRefParent")); const parentTag = parent & 7; const parentRow = Math.floor(parent / 8); - if (parentTag === 0) - return readTypeName({ - bytes, - layout, - table: 2, - row: parentRow, - heapExtent, - }); - if (parentTag === 1) - return readTypeName({ - bytes, - layout, - table: 1, - row: parentRow, - heapExtent, - }); + if (parentTag === 0) return typeName(2, parentRow); + if (parentTag === 1) return typeName(1, parentRow); if (parentTag === 3) return declaringTypeForMethod( bytes, layout, parentRow, - heapExtent, memberLists, + typeName, ); return null; }; @@ -389,6 +494,7 @@ export const readCustomAttribute = ( heapExtent: number, issues: ManagedParseIssue[], memberLists: TypeMemberListFacts, + typeNames: ManagedMetadataTypeNameLookup, ): CustomAttribute => { const cursor = metadataRowCursor(bytes, layout, 12, row); const parentRaw = cursor.readIndex( @@ -451,8 +557,7 @@ export const readCustomAttribute = ( if (constructor !== null) typeName = readManagedValue( - () => - attributeTypeName(bytes, layout, typeRaw, heapExtent, memberLists), + () => attributeTypeName(bytes, layout, typeRaw, memberLists, typeNames), issues, ) ?? null; return { diff --git a/tests/acceptance/managedCliOutput.test.ts b/tests/acceptance/managedCliOutput.test.ts index cefb178e0f..b83d237552 100644 --- a/tests/acceptance/managedCliOutput.test.ts +++ b/tests/acceptance/managedCliOutput.test.ts @@ -166,6 +166,70 @@ cliTest( }, }); } + + const hintedPath = join(root, "ready-to-run-with-runtime-hints.dll"); + const hintedTarget = ".NETFramework,Version=v4.8"; + await writeFile( + hintedPath, + buildManagedPeFixture({ + readyToRun: true, + targetFramework: hintedTarget, + references: ["UnityEngine.CoreModule"], + }), + ); + const hinted = await cli.run({ + arguments: ["inspect-managed-artifact", hintedPath, "--json"], + }); + expect(hinted.exitCode).toBe(0); + expect(hinted.json).toMatchObject({ + normalized_result: { + classification: { + runtime_family: "modern-dotnet", + implementation: "cil-and-ready-to-run", + evidence: expect.arrayContaining([ + expect.objectContaining({ + code: "ready-to-run-signature-observed", + }), + expect.objectContaining({ code: "unity-reference-observed" }), + expect.objectContaining({ code: "target-framework-observed" }), + ]), + }, + target_frameworks: [hintedTarget], + }, + }); + }, +); + +cliTest( + "managed CLI qualifies a local nested custom-attribute type name", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-managed-nested-attribute-"); + const path = join(root, "nested-attribute.dll"); + await writeFile( + path, + buildManagedPeFixture({ + additionalEmptyTypes: 1, + customAttributeTypeRaw: (1 << 3) | 2, + nestedTypes: [[2, 1]], + typeName: "InnerAttribute", + typeNamespace: "", + }), + ); + + const result = await cli.run({ + arguments: ["inspect-managed-artifact", path, "--json"], + }); + expect(result.exitCode).toBe(0); + expect(result.json).toMatchObject({ + normalized_result: { + attributes: [ + expect.objectContaining({ + type_name: "Empty0+InnerAttribute", + }), + ], + coverage: { state: "complete", issues: [] }, + }, + }); }, );