refactor(managed): share decoded type definition facts

This commit is contained in:
morluto committed 2026-10-24 01:11:58 +08:00
1 parent f276bcbf6c
commit d0432d63c9
4 files changed
+145 -124

No files matched your search

+41 -27
View File
@@ -2245,13 +2245,15 @@ it.each([
);
it.each([
{ facet: "fields", nameOffset: 2 },
{ facet: "methods", nameOffset: 8 },
{ facet: "member_refs", nameOffset: 2 },
{ facet: "types", nameOffset: 4 },
{ facet: "fields", nameOffsets: [2] },
{ facet: "methods", nameOffsets: [8] },
{ facet: "member_refs", nameOffsets: [2] },
{ facet: "types", nameOffsets: [4] },
{ facet: "types", nameOffsets: [6] },
{ facet: "types", nameOffsets: [4, 6] },
] as const)(
"preserves independent metadata when a $facet name is unreadable",
({ facet, nameOffset }) => {
"preserves independent metadata when $facet name columns $nameOffsets are unreadable",
({ facet, nameOffsets }) => {
const bytes = buildManagedPeFixture();
const original = inspectManagedMembersBytes(
bytes,
@@ -2259,7 +2261,8 @@ it.each([
);
const row = original[facet][0]?.row_offset;
if (row === undefined) throw new Error(`Fixture must contain ${facet}`);
bytes.writeUInt16LE(0xffff, row + nameOffset);
for (const [index, nameOffset] of nameOffsets.entries())
bytes.writeUInt16LE(0xffff - index, row + nameOffset);
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
@@ -2278,13 +2281,17 @@ it.each([
expect(result[remaining]).toHaveLength(original[remaining].length);
expect(result.coverage).toMatchObject({
state: "partial",
issues: [
issues: nameOffsets.map(() =>
expect.objectContaining({
code: "invalid-heap-index",
scope: "metadata.#Strings",
offset: expect.any(Number),
}),
],
),
});
expect(
new Set(result.coverage.issues.map(({ offset }) => offset)).size,
).toBe(nameOffsets.length);
if (facet === "types") {
expect(result.fields[0]).toMatchObject({
declaring_type_token: "0x02000001",
@@ -2327,25 +2334,32 @@ describe("managed member type selection", () => {
);
});
it("does not treat a name as absent when a TypeDef name is unreadable", () => {
const corrupt = Buffer.from(bytes);
const row = everything.types[0]?.row_offset ?? 0;
// Point the TypeDef Name past the #Strings heap.
corrupt.writeUInt16LE(0xffff, row + 4);
const result = inspectManagedMembersBytes(
corrupt,
managedPeFixtureTarget(corrupt),
{ typeNames: ["Fixture.Program"] },
);
it.each([
{ column: "name", offset: 4 },
{ column: "namespace", offset: 6 },
])(
"does not treat a name as absent when a TypeDef $column is unreadable",
({ offset }) => {
const corrupt = Buffer.from(bytes);
const row = everything.types[0]?.row_offset ?? 0;
corrupt.writeUInt16LE(0xffff, row + offset);
const result = inspectManagedMembersBytes(
corrupt,
managedPeFixtureTarget(corrupt),
{ typeNames: ["Fixture.Program"] },
);
expect(result.selection?.unmatched_type_names).toEqual(["Fixture.Program"]);
expect(result.coverage.state).toBe("partial");
// Its methods were never inspected, so neither were their literals.
expect(result.user_strings?.state).toBe("partial");
expect(result.limitations).toContain(
"1 TypeDef name(s) could not be read; a name in selection.unmatched_type_names may belong to one of them, so its absence is not observed.",
);
});
expect(result.selection?.unmatched_type_names).toEqual([
"Fixture.Program",
]);
expect(result.coverage.state).toBe("partial");
// Its methods were never inspected, so neither were their literals.
expect(result.user_strings?.state).toBe("partial");
expect(result.limitations).toContain(
"1 TypeDef name(s) could not be read; a name in selection.unmatched_type_names may belong to one of them, so its absence is not observed.",
);
},
);
it("omits members of unselected types without dropping member references", () => {
const result = inspectManagedMembersBytes(bytes, target, {
+2 -2
View File
@@ -28,7 +28,7 @@ import {
} from "./ManagedReaderFailure.js";
import {
parseTypes,
typeRanges,
readTypeDefinitions,
type TypeRange,
} from "./ManagedMemberInspectorCore.js";
import { edgeTargets } from "./ManagedMethodSpecs.js";
@@ -190,7 +190,7 @@ export const inspectManagedMembersBytes = (
const { layout, memberLists } = readMemberLayout(bytes, pe);
const identity = readManagedModuleIdentity(bytes, layout);
appendManagedParseIssues(issues, identity.issues);
const ranges = typeRanges(bytes, layout, issues, memberLists);
const ranges = readTypeDefinitions(bytes, layout, issues, memberLists);
const {
tokens: selected,
selection,
+95 -93
View File
@@ -60,6 +60,13 @@ export interface TypeRange {
readonly methodEnd: number | null;
}
/** One TypeDef row's names and coordinates, including unreadable declarations. */
interface TypeDefinition extends TypeRange {
readonly row: number;
readonly name: string | undefined;
readonly namespace: string | undefined;
}
/**
* Name and declaring type of a field, method, member reference, or method
* specification, keyed by its token so edges resolve any target uniformly.
@@ -109,36 +116,38 @@ const rowRange = (
export const fullName = (namespace: string, name: string): string =>
namespace.length === 0 ? name : `${namespace}.${name}`;
const readTypeRange = (
const readTypeDefinition = (
bytes: Buffer,
layout: ManagedMetadataLayout,
row: number,
issues: ManagedParseIssue[],
memberLists: TypeMemberListFacts,
): TypeRange => {
): TypeDefinition => {
const cursor = metadataRowCursor(bytes, layout, 2, row);
cursor.readUInt32();
const nameIndex = cursor.readIndex(layout.stringIndexSize);
const namespaceIndex = cursor.readIndex(layout.stringIndexSize);
const declaredName =
readManagedValue(
() =>
fullName(
readMetadataString(
bytes,
layout,
namespaceIndex,
layout.strings.size,
),
readMetadataString(bytes, layout, nameIndex, layout.strings.size),
),
issues,
) ?? null;
// Keep both independent name failures, but decode each heap coordinate once.
const namespace = readManagedValue(
() =>
readMetadataString(bytes, layout, namespaceIndex, layout.strings.size),
issues,
);
const name = readManagedValue(
() => readMetadataString(bytes, layout, nameIndex, layout.strings.size),
issues,
);
cursor.readIndex(layout.codedIndexSize("TypeDefOrRef"));
const current = memberLists.entries[row - 1];
return {
row,
name,
namespace,
token: metadataToken(2, row),
fullName: declaredName,
fullName:
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,
@@ -146,18 +155,20 @@ const readTypeRange = (
};
};
export const typeRanges = (
export const readTypeDefinitions = (
bytes: Buffer,
layout: ManagedMetadataLayout,
issues: ManagedParseIssue[],
memberLists: TypeMemberListFacts,
): readonly TypeRange[] => {
): readonly TypeDefinition[] => {
issues.push(...memberLists.issues);
const typeTable = layout.table(2);
const ranges: TypeRange[] = [];
const definitions: TypeDefinition[] = [];
for (let row = 1; row <= (typeTable?.rowCount ?? 0); row += 1)
ranges.push(readTypeRange(bytes, layout, row, issues, memberLists));
return ranges;
definitions.push(
readTypeDefinition(bytes, layout, row, issues, memberLists),
);
return definitions;
};
/**
@@ -167,91 +178,82 @@ export const typeRanges = (
export const parseTypes = (
bytes: Buffer,
layout: ManagedMetadataLayout,
ranges: readonly TypeRange[],
definitions: readonly TypeDefinition[],
): {
readonly types: readonly ManagedType[];
readonly issues: readonly ManagedParseIssue[];
readonly nesting: ManagedTypeNesting;
} => {
const typeTable = layout.table(2);
const fields = memberListTable(layout, 4);
const methods = memberListTable(layout, 6);
const items: ManagedType[] = [];
const issues: ManagedParseIssue[] = [];
const nesting = readManagedTypeNesting(bytes, layout, issues);
for (let row = 1; row <= (typeTable?.rowCount ?? 0); row += 1) {
readManagedValue(() => {
const cursor = metadataRowCursor(bytes, layout, 2, row);
const flags = cursor.readUInt32();
const name = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
layout.strings.size,
);
const namespace = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
layout.strings.size,
);
const extendsOffset = cursor.offset;
const extendsRaw = cursor.readIndex(
layout.codedIndexSize("TypeDefOrRef"),
);
const currentRange = ranges[row - 1] ?? null;
const fieldRange = rowRange(
fields,
currentRange?.fieldStart ?? null,
currentRange?.fieldEnd ?? null,
);
const methodRange = rowRange(
methods,
currentRange?.methodStart ?? null,
currentRange?.methodEnd ?? null,
);
const extendsReason = metadataCodedTokenInvalidReason(
for (const definition of definitions) {
const { row, name, namespace, token } = definition;
if (
name === undefined ||
namespace === undefined ||
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 fieldRange = rowRange(
fields,
definition.fieldStart,
definition.fieldEnd,
);
const methodRange = rowRange(
methods,
definition.methodStart,
definition.methodEnd,
);
const extendsReason = metadataCodedTokenInvalidReason(
extendsRaw,
2,
[2, 1, 27],
layout.rowCounts,
);
if (extendsReason !== null)
issues.push({
code: "invalid-row",
scope: `metadata.TypeDef:${token}`,
offset: extendsOffset,
detail: `TypeDef Extends coded index 0x${extendsRaw.toString(16)} is invalid: ${extendsReason}`,
});
const enclosingRow = nesting.enclosing.get(row);
items.push({
token,
row_offset: cursor.start,
namespace,
name,
full_name: definition.fullName,
flags,
extends_token: metadataCodedToken(
extendsRaw,
2,
[2, 1, 27],
layout.rowCounts,
);
if (extendsReason !== null)
issues.push({
code: "invalid-row",
scope: `metadata.TypeDef:${metadataToken(2, row)}`,
offset: extendsOffset,
detail: `TypeDef Extends coded index 0x${extendsRaw.toString(16)} is invalid: ${extendsReason}`,
});
const enclosingRow = nesting.enclosing.get(row);
items.push({
token: metadataToken(2, row),
row_offset: cursor.start,
namespace,
name,
full_name: fullName(namespace, name),
flags,
extends_token: metadataCodedToken(
extendsRaw,
2,
[2, 1, 27],
layout.rowCounts,
),
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,
},
});
}, issues);
),
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,
},
});
}
return { types: items, issues, nesting };
};
@@ -10,7 +10,7 @@ import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
import {
parseTypes,
signature,
typeRanges,
readTypeDefinitions,
} from "./ManagedMemberInspectorCore.js";
import {
metadataRowOffset,
@@ -169,7 +169,12 @@ describe("resolved member signature digests", () => {
const parsedTypes = parseTypes(
bytes,
layout,
typeRanges(bytes, layout, issues, readTypeMemberListFacts(bytes, layout)),
readTypeDefinitions(
bytes,
layout,
issues,
readTypeMemberListFacts(bytes, layout),
),
);
const resolve = managedTypeNameDigests(
layout,