refactor(managed): centralize TypeDef row facts across consumers

This commit is contained in:
morluto committed 2026-10-27 00:21:00 +08:00
1 parent 8423c5c6d0
commit 2cdc8cb92e
5 files changed
+173 -44

No files matched your search

+19 -24
View File
@@ -23,7 +23,6 @@ import type {
MetadataTableLayout,
} from "./ManagedMetadataLayout.js";
import {
metadataRowCursor,
readManagedCompressedUnsigned,
decodeMetadataCodedIndex,
metadataCodedIndexCoordinate,
@@ -34,7 +33,11 @@ import {
sha256Bytes,
} from "./ManagedMetadataHeaps.js";
import { memberListTable } from "./ManagedMemberPointers.js";
import type { TypeMemberListFacts } from "./ManagedTypeMemberLists.js";
import type {
TypeMemberListEntry,
TypeMemberListFacts,
} from "./ManagedTypeMemberLists.js";
import type { TypeDefinitionRowFacts } from "./ManagedTypeDefinitionRows.js";
export type ManagedType = ManagedMemberInspection["types"][number];
/** A renderable TypeDef projection paired with its original source row. */
@@ -72,6 +75,7 @@ export interface TypeRange {
/** One TypeDef row's names and coordinates, including unreadable declarations. */
interface TypeDefinition extends TypeRange {
readonly source: TypeDefinitionRowFacts;
readonly row: number;
readonly name: string | undefined;
readonly namespace: string | undefined;
@@ -173,12 +177,10 @@ const readTypeDefinition = (
layout: ManagedMetadataLayout,
row: number,
issues: ManagedParseIssue[],
memberLists: TypeMemberListFacts,
current: TypeMemberListEntry,
): TypeDefinition => {
const cursor = metadataRowCursor(bytes, layout, 2, row);
cursor.readUInt32();
const nameIndex = cursor.readIndex(layout.stringIndexSize);
const namespaceIndex = cursor.readIndex(layout.stringIndexSize);
const { source } = current;
const { nameIndex, namespaceIndex } = source;
// Keep both independent name failures, but decode each heap coordinate once.
const namespace = readManagedValue(
() =>
@@ -189,9 +191,8 @@ const readTypeDefinition = (
() => readMetadataString(bytes, layout, nameIndex, layout.strings.size),
issues,
);
cursor.readIndex(layout.codedIndexSize("TypeDefOrRef"));
const current = memberLists.entries[row - 1];
return {
source,
row,
name,
namespace,
@@ -200,10 +201,10 @@ const readTypeDefinition = (
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,
methodEnd: current?.method.end ?? null,
fieldStart: current.field.start.value,
fieldEnd: current.field.end,
methodStart: current.method.start.value,
methodEnd: current.method.end,
};
};
@@ -214,11 +215,10 @@ export const readTypeDefinitions = (
memberLists: TypeMemberListFacts,
): readonly TypeDefinition[] => {
appendManagedParseIssues(issues, memberLists.issues);
const typeTable = layout.table(2);
const definitions: TypeDefinition[] = [];
for (let row = 1; row <= (typeTable?.rowCount ?? 0); row += 1)
for (const [index, entry] of memberLists.entries.entries())
definitions.push(
readTypeDefinition(bytes, layout, row, issues, memberLists),
readTypeDefinition(bytes, layout, index + 1, issues, entry),
);
return definitions;
};
@@ -249,12 +249,7 @@ export const parseTypes = (
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 { flags, extendsRaw, extendsOffset, rowOffset } = definition.source;
const fieldRange = rowRange(
fields,
definition.fieldStart,
@@ -279,12 +274,12 @@ 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);
managedDecodeBudget(layout).reserve(128, rowOffset);
items.push({
row,
type: {
token,
row_offset: cursor.start,
row_offset: rowOffset,
namespace,
name,
full_name: definition.fullName,
@@ -295,3 +295,84 @@ it("keeps later type identity, member ownership and constructor names at their o
},
]);
});
it("keeps independent name failures ahead of conditional Extends diagnostics", () => {
const bytes = buildManagedPeFixture({ extendsRaw: 12 });
const readable = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
const row = readable.types[0]?.row_offset;
if (row === undefined) throw new Error("Expected TypeDef fixture");
expect(readable.coverage.issues).toContainEqual(
expect.objectContaining({
code: "invalid-row",
scope: "metadata.TypeDef:0x02000001",
offset: row + 8,
detail: expect.stringContaining("Extends"),
}),
);
bytes.writeUInt16LE(0xffff, row + 4);
bytes.writeUInt16LE(0xfffe, row + 6);
const partial = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(partial.types).toEqual([]);
expect(partial.fields).toHaveLength(1);
expect(partial.methods).toHaveLength(1);
for (const member of [...partial.fields, ...partial.methods])
expect(member).toMatchObject({
declaring_type_token: "0x02000001",
declaring_type: null,
});
expect(partial.module).toEqual(readable.module);
expect(partial.coverage).toMatchObject({
state: "partial",
issues: [
expect.objectContaining({
code: "invalid-heap-index",
scope: "metadata.#Strings",
}),
expect.objectContaining({
code: "invalid-heap-index",
scope: "metadata.#Strings",
}),
],
});
expect(
new Set(partial.coverage.issues.map(({ offset }) => offset)).size,
).toBe(2);
});
it("reads inventory names on demand without admitting TypeDef member lists or Extends", () => {
const bytes = buildManagedPeFixture({
additionalEmptyTypes: 1,
typeName: "Lookup",
});
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);
const earlier = metadataRowOffset(layout, 2, 1);
bytes.writeUInt16LE(0xffff, earlier + 4);
bytes.writeUInt16LE(0xfffe, earlier + 6);
const selected = metadataRowOffset(layout, 2, 2);
bytes.writeUInt16LE(0xffff, selected + 8);
bytes.writeUInt16LE(0, selected + 10);
bytes.writeUInt16LE(0, selected + 12);
const issues: ManagedParseIssue[] = [];
const names = managedMetadataTypeNames(bytes, layout, issues);
expect(names(2, 2)).toBe("Fixture.Lookup");
expect(issues).toEqual([]);
expect(readTypeMemberListFacts(bytes, layout).issues).toEqual(
expect.arrayContaining([
expect.objectContaining({ code: "invalid-row", offset: selected + 10 }),
expect.objectContaining({ code: "invalid-row", offset: selected + 12 }),
]),
);
});
+5 -4
View File
@@ -1,3 +1,4 @@
import { readTypeDefinitionRowFacts } from "./ManagedTypeDefinitionRows.js";
import { managedDecodeBudget } from "./ManagedDecodeBudget.js";
import { managedTypeReferences } from "./ManagedTypeReferences.js";
import { MAX_TYPE_NESTING_DEPTH } from "./ManagedTypeNameDigests.js";
@@ -315,18 +316,18 @@ export const managedMetadataTypeNames = (
namespace = parsed.namespace;
flags = parsed.flags;
} else {
const cursor = metadataRowCursor(bytes, layout, table, row);
flags = cursor.readUInt32();
const source = readTypeDefinitionRowFacts(bytes, layout, row);
flags = source.flags;
name = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
source.nameIndex,
layout.strings.size,
);
namespace = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
source.namespaceIndex,
layout.strings.size,
);
}
+46
View File
@@ -0,0 +1,46 @@
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
import { metadataRowCursor } from "./ManagedMetadataHeaps.js";
import { memberListIndexSize } from "./ManagedMemberPointers.js";
/** Scalar TypeDef source facts; consumers own heap and coded-index admission. */
export interface TypeDefinitionRowFacts {
readonly rowOffset: number;
readonly flags: number;
readonly nameIndex: number;
readonly namespaceIndex: number;
readonly extendsRaw: number;
readonly extendsOffset: number;
readonly fieldListRaw: number;
readonly fieldListOffset: number;
readonly methodListRaw: number;
readonly methodListOffset: number;
}
export const readTypeDefinitionRowFacts = (
bytes: Buffer,
layout: ManagedMetadataLayout,
row: number,
): TypeDefinitionRowFacts => {
const cursor = metadataRowCursor(bytes, layout, 2, row);
const flags = cursor.readUInt32();
const nameIndex = cursor.readIndex(layout.stringIndexSize);
const namespaceIndex = cursor.readIndex(layout.stringIndexSize);
const extendsOffset = cursor.offset;
const extendsRaw = cursor.readIndex(layout.codedIndexSize("TypeDefOrRef"));
const fieldListOffset = cursor.offset;
const fieldListRaw = cursor.readIndex(memberListIndexSize(layout, 4));
const methodListOffset = cursor.offset;
const methodListRaw = cursor.readIndex(memberListIndexSize(layout, 6));
return {
rowOffset: cursor.start,
flags,
nameIndex,
namespaceIndex,
extendsRaw,
extendsOffset,
fieldListRaw,
fieldListOffset,
methodListRaw,
methodListOffset,
};
};
+22 -16
View File
@@ -1,13 +1,17 @@
import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js";
import { managedDecodeBudget } from "./ManagedDecodeBudget.js";
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
import { metadataRowCursor, metadataToken } from "./ManagedMetadataHeaps.js";
import { metadataToken } from "./ManagedMetadataHeaps.js";
import {
memberListIndexSize,
memberListTable,
validatedMemberListStart,
} from "./ManagedMemberPointers.js";
import {
readTypeDefinitionRowFacts,
type TypeDefinitionRowFacts,
} from "./ManagedTypeDefinitionRows.js";
export type MemberDefinitionTable = 4 | 6;
export interface TypeMemberListStart {
@@ -23,6 +27,7 @@ export interface TypeMemberListRange {
}
export interface TypeMemberListEntry {
readonly source: TypeDefinitionRowFacts;
readonly field: TypeMemberListRange;
readonly method: TypeMemberListRange;
}
@@ -39,33 +44,33 @@ export const readTypeMemberListFacts = (
): TypeMemberListFacts => {
const fields = memberListTable(layout, 4);
const methods = memberListTable(layout, 6);
const rows: { field: TypeMemberListStart; method: TypeMemberListStart }[] =
[];
const rows: {
source: TypeDefinitionRowFacts;
field: TypeMemberListStart;
method: TypeMemberListStart;
}[] = [];
const issues: ManagedParseIssue[] = [];
const types = layout.table(2);
const typeCount = types?.rowCount ?? 0;
// Peak retained shape includes both the parsed-start and range arrays, their
// nested records, plus each eventual JSON object/key projection. Malformed
// Peak retained shape includes shared scalar source facts, the parsed-start
// and range arrays, plus each eventual JSON object/key projection. Malformed
// row diagnostics are admitted separately before they are allocated.
managedDecodeBudget(layout).reserve(
typeCount * 512 + 256,
typeCount * 640 + 256,
types?.offset ?? null,
);
for (let row = 1; row <= typeCount; row += 1) {
const cursor = metadataRowCursor(bytes, layout, 2, row);
cursor.readUInt32();
cursor.readIndex(layout.stringIndexSize);
cursor.readIndex(layout.stringIndexSize);
cursor.readIndex(layout.codedIndexSize("TypeDefOrRef"));
const source = readTypeDefinitionRowFacts(bytes, layout, row);
const field = {
offset: cursor.offset,
raw: cursor.readIndex(memberListIndexSize(layout, 4)),
offset: source.fieldListOffset,
raw: source.fieldListRaw,
};
const method = {
offset: cursor.offset,
raw: cursor.readIndex(memberListIndexSize(layout, 6)),
offset: source.methodListOffset,
raw: source.methodListRaw,
};
const entry = {
source,
field: { ...field, value: validatedMemberListStart(field.raw, fields) },
method: {
...method,
@@ -116,6 +121,7 @@ export const readTypeMemberListFacts = (
const entries: TypeMemberListEntry[] = rows.map((entry, index) => {
const next = rows[index + 1];
return {
source: entry.source,
field: {
start: {
...entry.field,