fix(managed): retain nested names and ReadyToRun observations

Resolve attribute display names from validated nesting and parsed member facts, admit composed names before allocation, and preserve unknown malformed chains. Keep ReadyToRun classification ahead of metadata hints while retaining each observed hint.

Fixes #6815
Fixes #6810
This commit is contained in:
morluto
2026-10-24 00:11:41 +08:00
parent ddd7ac7b84
commit 87fa1b613d
7 changed files with 424 additions and 96 deletions
+7
View File
@@ -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,
+127
View File
@@ -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 });
+51 -40
View File
@@ -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 };
};
+8
View File
@@ -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);
+6
View File
@@ -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,
+161 -56
View File
@@ -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<ManagedArtifactInspection["module"]>;
type AssemblyIdentity = NonNullable<ManagedArtifactInspection["assembly"]>;
@@ -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<number, ManagedType>();
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<string> = 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 {
+64
View File
@@ -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: [] },
},
});
},
);