refactor(managed): retain TypeDef row ownership through projections

This commit is contained in:
morluto
2026-10-24 05:32:59 +08:00
parent 15edc71914
commit f470350528
5 changed files with 131 additions and 40 deletions
+3 -7
View File
@@ -209,16 +209,12 @@ export const inspectManagedMembersBytes = (
const { operand: signatureTypeName, declaration: declarationTypeName } =
managedTypeNameDigests(layout, issues, parsedTypes, typeReferences);
const types = parsedTypes.types
.filter((type) => selected === null || selected.has(type.token))
.map((type) => {
.filter(({ type }) => selected === null || selected.has(type.token))
.map(({ row, type }) => {
managedDecodeBudget(layout).reserve(512, type.row_offset);
return {
...type,
name_sha256:
declarationTypeName(
2,
Number.parseInt(type.token.slice(4), 16),
)?.toString("hex") ?? null,
name_sha256: declarationTypeName(2, row)?.toString("hex") ?? null,
};
});
const envelope = managedPeInspectionEnvelope(
+32 -21
View File
@@ -36,6 +36,12 @@ import { memberListTable } from "./ManagedMemberPointers.js";
import type { TypeMemberListFacts } from "./ManagedTypeMemberLists.js";
export type ManagedType = ManagedMemberInspection["types"][number];
/** A renderable TypeDef projection paired with its original source row. */
export interface DecodedTypeDefinition {
readonly row: number;
readonly type: ManagedType;
}
export type ManagedExportedType = NonNullable<
ManagedMemberInspection["exported_types"]
>[number];
@@ -225,13 +231,13 @@ export const parseTypes = (
layout: ManagedMetadataLayout,
definitions: readonly TypeDefinition[],
): {
readonly types: readonly ManagedType[];
readonly types: readonly DecodedTypeDefinition[];
readonly issues: readonly ManagedParseIssue[];
readonly nesting: ManagedTypeNesting;
} => {
const fields = memberListTable(layout, 4);
const methods = memberListTable(layout, 6);
const items: ManagedType[] = [];
const items: DecodedTypeDefinition[] = [];
const issues: ManagedParseIssue[] = [];
const nesting = readManagedTypeNesting(bytes, layout, issues);
for (const definition of definitions) {
@@ -272,26 +278,31 @@ 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);
items.push({
token,
row_offset: cursor.start,
namespace,
name,
full_name: definition.fullName,
flags,
extends_token: extendsIndex.kind === "valid" ? extendsIndex.token : null,
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,
row,
type: {
token,
row_offset: cursor.start,
namespace,
name,
full_name: definition.fullName,
flags,
extends_token:
extendsIndex.kind === "valid" ? extendsIndex.token : null,
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,
},
},
});
}
@@ -7,6 +7,17 @@ import {
} from "./ManagedPe.fixture.js";
import { buildManagedMemberCapacityFixture } from "../../tests/fixtures/managedMemberCapacity.js";
import { readManagedPeLayout } from "./ManagedPeReader.js";
import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js";
import {
parseTypes,
readTypeDefinitions,
} from "./ManagedMemberInspectorCore.js";
import {
readManagedMetadataInventory,
readManagedResourceDirectory,
} from "./ManagedMetadataInventory.js";
import { managedMetadataTypeNames } from "./ManagedMetadataInventoryRows.js";
import { readTypeMemberListFacts } from "./ManagedTypeMemberLists.js";
import {
metadataRowOffset,
readManagedMetadataLayout,
@@ -206,3 +217,81 @@ it("does not turn an unreadable requested type name into observed absence during
]);
expect(result.limitations.join(" ")).toContain("absence is not observed");
});
it("keeps later type identity, member ownership and constructor names at their original row", () => {
const bytes = buildManagedPeFixture({
additionalEmptyTypes: 1,
customAttributeTypeRaw: (1 << 3) | 2,
typeName: "LookupAttribute",
extendsRaw: 0,
methodName: ".ctor",
methodFlags: 0x1886,
methodSignature: Buffer.from([0x20, 0x01, 0x01, 0x0e]),
});
const healthy = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
const owner = healthy.types.find(({ token }) => token === "0x02000002");
expect(owner?.full_name).toBe("Fixture.LookupAttribute");
expect(owner?.name_sha256).toMatch(/^[0-9a-f]{64}$/u);
expect(healthy.fields).toHaveLength(1);
expect(healthy.methods).toHaveLength(1);
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);
// Only the preceding empty type's name is unreadable; row 2 still owns
// FieldDef 1 and MethodDef 1, including the local attribute constructor.
bytes.writeUInt16LE(0xffff, metadataRowOffset(layout, 2, 1) + 4);
const target = managedPeFixtureTarget(bytes);
const result = inspectManagedMembersBytes(bytes, target, {
typeNames: ["Fixture.LookupAttribute", "Fixture.Empty0"],
});
expect(result.types).toEqual([owner]);
expect(result.types[0]).toMatchObject({
token: "0x02000002",
row_offset: metadataRowOffset(layout, 2, 2),
name_sha256: owner?.name_sha256,
field_list: { first_row: 1, last_row: 1, count: 1 },
method_list: { first_row: 1, last_row: 1, count: 1 },
});
expect(result.fields).toEqual(healthy.fields);
expect(result.methods).toEqual(healthy.methods);
for (const member of [...result.fields, ...result.methods])
expect(member).toMatchObject({
declaring_type_token: "0x02000002",
declaring_type: "Fixture.LookupAttribute",
});
expect(result.artifact.sha256).toBe(target.sha256);
expect(result.coverage.state).toBe("partial");
expect(result.selection?.unmatched_type_names).toEqual(["Fixture.Empty0"]);
expect(result.limitations.join(" ")).toContain("absence is not observed");
// Member inspection uses the parsed-type name owner for its metadata
// inventory, whose attribute names are not included in the member report.
const issues: ManagedParseIssue[] = [];
const memberLists = readTypeMemberListFacts(bytes, layout);
const definitions = readTypeDefinitions(bytes, layout, issues, memberLists);
const parsedTypes = parseTypes(bytes, layout, definitions);
const names = managedMetadataTypeNames(bytes, layout, issues, parsedTypes);
const inventory = readManagedMetadataInventory(
bytes,
layout,
readManagedResourceDirectory(pe, issues),
memberLists,
undefined,
names,
);
expect(inventory.attributes).toMatchObject([
{
constructor_token: "0x06000001",
type_name: "Fixture.LookupAttribute",
},
]);
});
+3 -8
View File
@@ -2,7 +2,6 @@ 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";
@@ -27,7 +26,7 @@ import {
import { managedFailure, readManagedValue } from "./ManagedReaderFailure.js";
import type { ManagedTypeReferenceLookup } from "./ManagedTypeReferences.js";
import type { ManagedResourceDirectory } from "./ManagedMetadataInventory.js";
import type { ManagedType } from "./ManagedMemberInspectorCore.js";
import type { ManagedType, parseTypes } from "./ManagedMemberInspectorCore.js";
type ModuleIdentity = NonNullable<ManagedArtifactInspection["module"]>;
type AssemblyIdentity = NonNullable<ManagedArtifactInspection["assembly"]>;
@@ -246,10 +245,7 @@ export const managedMetadataTypeNames = (
bytes: Buffer,
layout: ManagedMetadataLayout,
issues: ManagedParseIssue[],
parsedTypes?: {
readonly types: readonly ManagedType[];
readonly nesting: ManagedTypeNesting;
},
parsedTypes?: Pick<ReturnType<typeof parseTypes>, "types" | "nesting">,
parsedReferences?: ManagedTypeReferenceLookup,
): ManagedMetadataTypeNameLookup => {
const nesting =
@@ -259,8 +255,7 @@ export const managedMetadataTypeNames = (
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);
for (const { row, type } of parsedTypes.types) definitions.set(row, type);
}
const cache = new Map<
string,
+4 -4
View File
@@ -4,10 +4,10 @@ import { managedDecodeBudget } from "./ManagedDecodeBudget.js";
import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js";
import type {
parseTypes,
ManagedType,
SignatureTypeName,
DeclarationTypeName,
} from "./ManagedMemberInspectorCore.js";
import { metadataToken } from "./ManagedMetadataHeaps.js";
import type { ManagedTypeReferenceLookup } from "./ManagedTypeReferences.js";
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
import { managedFailure, readManagedValue } from "./ManagedReaderFailure.js";
@@ -153,11 +153,11 @@ export const managedTypeNameDigests = (
} => {
const retain = () => managedDecodeBudget(layout).reserve(128, null);
managedDecodeBudget(layout).reserve(parsedTypes.types.length * 128, null);
const types = new Map<string, (typeof parsedTypes.types)[number]>();
for (const type of parsedTypes.types) types.set(type.token, type);
const types = new Map<number, ManagedType>();
for (const { row, type } of parsedTypes.types) types.set(row, type);
const definitions = nestedTypeDigests((row) => {
if (!parsedTypes.nesting.complete) return null;
const type = types.get(metadataToken(TYPE_DEF, row));
const type = types.get(row);
if (
type === undefined &&
(row < 1 || row > (layout.table(TYPE_DEF)?.rowCount ?? 0))