refactor(managed): centralize MemberRef row facts

This commit is contained in:
morluto committed 2026-10-27 01:29:43 +08:00
1 parent 939009f6f9
commit c09fa01b37
7 files changed
+179 -26

No files matched your search

@@ -583,6 +583,35 @@ describe("managed artifact coded indexes", () => {
}, 30_000); }, 30_000);
}); });
it.each([false, true])(
"resolves an attribute type without reading malformed constructor heaps (bad name %s)",
(badName) => {
const bytes = buildManagedPeFixture();
const layout = metadataLayoutFor(bytes);
const row = layout.table(10)!;
const nameColumn = row.offset + layout.codedIndexSize("MemberRefParent");
const signatureColumn = nameColumn + layout.stringIndexSize;
if (badName) {
if (layout.stringIndexSize === 2) bytes.writeUInt16LE(0xffff, nameColumn);
else bytes.writeUInt32LE(0xffffffff, nameColumn);
}
if (layout.blobIndexSize === 2)
bytes.writeUInt16LE(0xffff, signatureColumn);
else bytes.writeUInt32LE(0xffffffff, signatureColumn);
const result = inspectManagedArtifactBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.attributes[0]).toMatchObject({
constructor_token: "0x0a000001",
type_name: "System.Runtime.Versioning.TargetFrameworkAttribute",
decoded_fixed_string: ".NETCoreApp,Version=v8.0",
});
expect(result.coverage).toEqual({ state: "complete", issues: [] });
},
);
describe("managed artifact reference inventory", () => { describe("managed artifact reference inventory", () => {
it("qualifies local custom-attribute names with their enclosing types", () => { it("qualifies local custom-attribute names with their enclosing types", () => {
const bytes = buildManagedPeFixture({ const bytes = buildManagedPeFixture({
+99
View File
@@ -571,6 +571,56 @@ const genericRef = Buffer.from([0x30, 0x01, 0x00, 0x01]);
// GENERIC, one generic parameter, no parameters, void. // GENERIC, one generic parameter, no parameters, void.
const genericDef = Buffer.from([0x10, 0x01, 0x00, 0x01]); const genericDef = Buffer.from([0x10, 0x01, 0x00, 0x01]);
it.each([
{ badName: true, parent: 0, expectedHeap: "#Strings" },
{ badName: false, parent: 0, expectedHeap: "#Blob" },
{ badName: true, parent: 17, expectedHeap: "#Strings" },
{ badName: false, parent: 17, expectedHeap: "#Blob" },
])(
"reports $expectedHeap before invalid MemberRef parent $parent (bad name $badName)",
({ badName, parent, expectedHeap }) => {
const bytes = buildManagedPeFixture({ memberRefParentRaw: parent });
const pe = readManagedPeLayout(bytes);
if (pe.cli === null) throw new Error("fixture has no CLI header");
const { rva, size } = pe.cli.metadata;
const layout = readManagedMetadataLayout(
bytes,
pe.rvaToOffset(rva, size, "metadata"),
size,
);
const nameColumn =
metadataRowOffset(layout, 10, 1) +
layout.codedIndexSize("MemberRefParent");
if (badName) {
if (layout.stringIndexSize === 2) bytes.writeUInt16LE(0xffff, nameColumn);
else bytes.writeUInt32LE(0xffffffff, nameColumn);
}
const signatureColumn = nameColumn + layout.stringIndexSize;
if (layout.blobIndexSize === 2)
bytes.writeUInt16LE(0xffff, signatureColumn);
else bytes.writeUInt32LE(0xffffffff, signatureColumn);
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.member_refs).toEqual([]);
expect(result.coverage.state).toBe("partial");
expect(
result.coverage.issues.filter(
({ scope }) =>
scope === "metadata.#Strings" ||
scope === "metadata.#Blob" ||
scope === "metadata.MemberRef:0x0a000001",
),
).toEqual([
expect.objectContaining({
code: "invalid-heap-index",
scope: `metadata.${expectedHeap}`,
}),
]);
},
);
describe("managed TypeSpec declaring names", () => { describe("managed TypeSpec declaring names", () => {
it("names a generic TypeSpec parent by its generic type and leaves other TypeSpecs unknown", () => { it("names a generic TypeSpec parent by its generic type and leaves other TypeSpecs unknown", () => {
// GENERICINST CLASS TypeRef(1) <string> // GENERICINST CLASS TypeRef(1) <string>
@@ -956,6 +1006,55 @@ describe("managed MethodSpec call targets", () => {
}); });
}); });
it("reads MethodSpec arity from an unnamed MemberRef independently of parent admission", () => {
const bytes = buildManagedPeFixture({
memberRefSignature: genericRef,
memberRefParentRaw: 17,
methodSpecMethods: [(1 << 1) | 1],
methodSpecInstantiation: Buffer.from([0x0a, 0x01, 0x0e]),
ilBody: tokenBody([[call, 0x2b00_0001]]),
});
const pe = readManagedPeLayout(bytes);
if (pe.cli === null) throw new Error("fixture has no CLI header");
const { rva, size } = pe.cli.metadata;
const layout = readManagedMetadataLayout(
bytes,
pe.rvaToOffset(rva, size, "metadata"),
size,
);
const nameColumn =
metadataRowOffset(layout, 10, 1) + layout.codedIndexSize("MemberRefParent");
if (layout.stringIndexSize === 2) bytes.writeUInt16LE(0xffff, nameColumn);
else bytes.writeUInt32LE(0xffffffff, nameColumn);
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.member_refs).toEqual([]);
expect(result.call_edges).toMatchObject([
{
target_token: "0x2b000001",
target_kind: "method-spec",
target_name: null,
target_declaring_type: null,
},
]);
expect(result.coverage).toMatchObject({
state: "partial",
issues: [
expect.objectContaining({
code: "invalid-heap-index",
scope: "metadata.#Strings",
}),
],
});
expect(
result.coverage.issues.some(({ scope }) =>
scope.startsWith("metadata.MethodSpec:"),
),
).toBe(false);
});
describe("managed MethodDef heap admission", () => { describe("managed MethodDef heap admission", () => {
it("keeps names from unpaged methods without reading their signatures or bodies", () => { it("keeps names from unpaged methods without reading their signatures or bodies", () => {
const bytes = buildMultiMethodManagedPeFixture({ const bytes = buildMultiMethodManagedPeFixture({
+33
View File
@@ -0,0 +1,33 @@
import {
MEMBER_REF_PARENT_CODED_INDEX,
type ManagedMetadataLayout,
} from "./ManagedMetadataLayout.js";
import {
decodeMetadataCodedIndex,
metadataRowCursor,
metadataToken,
} from "./ManagedMetadataHeaps.js";
/** Scalar MemberRef facts; consumers own heap admission and parent diagnostics. */
export const readMemberReferenceRowFacts = (
bytes: Buffer,
layout: ManagedMetadataLayout,
row: number,
) => {
// Layout admission bounds the complete row independently of its heap values.
const cursor = metadataRowCursor(bytes, layout, 10, row);
const parentRaw = cursor.readIndex(layout.codedIndexSize("MemberRefParent"));
return {
token: metadataToken(10, row),
rowOffset: cursor.start,
parentRaw,
parent: decodeMetadataCodedIndex(
parentRaw,
MEMBER_REF_PARENT_CODED_INDEX.bits,
MEMBER_REF_PARENT_CODED_INDEX.tables,
layout.rowCounts,
),
nameIndex: cursor.readIndex(layout.stringIndexSize),
signatureIndex: cursor.readIndex(layout.blobIndexSize),
};
};
+7 -15
View File
@@ -1,3 +1,4 @@
import { readMemberReferenceRowFacts } from "./ManagedMemberReferenceRows.js";
import { readMethodDefinitionRow } from "./ManagedMethodDefinitionRows.js"; import { readMethodDefinitionRow } from "./ManagedMethodDefinitionRows.js";
import { jsonStringByteLength } from "../domain/jsonStringByteLength.js"; import { jsonStringByteLength } from "../domain/jsonStringByteLength.js";
import { isManagedMemberReferenceInstruction } from "../domain/managed/managedMemberInstructionFacts.js"; import { isManagedMemberReferenceInstruction } from "../domain/managed/managedMemberInstructionFacts.js";
@@ -234,34 +235,25 @@ export const parseMemberRefs = (
const table = layout.table(10); const table = layout.table(10);
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) { for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
readManagedValue(() => { readManagedValue(() => {
const cursor = metadataRowCursor(bytes, layout, 10, row); const { token, rowOffset, parentRaw, parent, nameIndex, signatureIndex } =
const parentRaw = cursor.readIndex( readMemberReferenceRowFacts(bytes, layout, row);
layout.codedIndexSize("MemberRefParent"),
);
const name = readMetadataString( const name = readMetadataString(
bytes, bytes,
layout, layout,
cursor.readIndex(layout.stringIndexSize), nameIndex,
layout.strings.size, layout.strings.size,
); );
const sig = readMetadataBlob( const sig = readMetadataBlob(
bytes, bytes,
layout, layout,
cursor.readIndex(layout.blobIndexSize), signatureIndex,
layout.blob?.size ?? 0, layout.blob?.size ?? 0,
); );
const token = metadataToken(10, row);
const parent = decodeMetadataCodedIndex(
parentRaw,
3,
[2, 1, 26, 6, 27],
layout.rowCounts,
);
if (parent.kind !== "valid") if (parent.kind !== "valid")
issues.push({ issues.push({
code: "invalid-row", code: "invalid-row",
scope: `metadata.MemberRef:${token}`, scope: `metadata.MemberRef:${token}`,
offset: cursor.start, offset: rowOffset,
detail: detail:
parent.kind === "null" parent.kind === "null"
? "MemberRef parent coded index 0x0 is null, but the Class column must reference a row" ? "MemberRef parent coded index 0x0 is null, but the Class column must reference a row"
@@ -278,7 +270,7 @@ export const parseMemberRefs = (
); );
refs.push({ refs.push({
token, token,
row_offset: cursor.start, row_offset: rowOffset,
parent_token: parentToken, parent_token: parentToken,
parent_name: parentName, parent_name: parentName,
name, name,
+2 -7
View File
@@ -1,3 +1,4 @@
import { readMemberReferenceRowFacts } from "./ManagedMemberReferenceRows.js";
import { readTypeDefinitionRowFacts } from "./ManagedTypeDefinitionRows.js"; import { readTypeDefinitionRowFacts } from "./ManagedTypeDefinitionRows.js";
import { managedDecodeBudget } from "./ManagedDecodeBudget.js"; import { managedDecodeBudget } from "./ManagedDecodeBudget.js";
import { managedTypeReferences } from "./ManagedTypeReferences.js"; import { managedTypeReferences } from "./ManagedTypeReferences.js";
@@ -408,13 +409,7 @@ const attributeTypeName = (
const { table, row } = constructor; const { table, row } = constructor;
if (table === 6) if (table === 6)
return declaringTypeForMethod(bytes, layout, row, memberLists, typeName); return declaringTypeForMethod(bytes, layout, row, memberLists, typeName);
const cursor = metadataRowCursor(bytes, layout, 10, row); const { parent } = readMemberReferenceRowFacts(bytes, layout, row);
const parent = decodeMetadataCodedIndex(
cursor.readIndex(layout.codedIndexSize("MemberRefParent")),
3,
[2, 1, 26, 6, 27],
layout.rowCounts,
);
if (parent.kind === "null") return null; if (parent.kind === "null") return null;
if (parent.table === 2 || parent.table === 1) if (parent.table === 2 || parent.table === 1)
return typeName(parent.table, parent.row); return typeName(parent.table, parent.row);
+3
View File
@@ -118,6 +118,9 @@ const CODED_INDEXES = {
TypeOrMethodDef: { bits: 1, tables: [2, 6] }, TypeOrMethodDef: { bits: 1, tables: [2, 6] },
} as const; } as const;
/** Shared MemberRef parent tag mapping, also used to select its column width. */
export const MEMBER_REF_PARENT_CODED_INDEX = CODED_INDEXES.MemberRefParent;
const SUPPORTED_TABLE_MASK = (1n << BigInt(METADATA_TABLE_NAMES.length)) - 1n; const SUPPORTED_TABLE_MASK = (1n << BigInt(METADATA_TABLE_NAMES.length)) - 1n;
/** Check a range within `bytes`, which start at artifact file offset `base`. */ /** Check a range within `bytes`, which start at artifact file offset `base`. */
+6 -4
View File
@@ -1,3 +1,4 @@
import { readMemberReferenceRowFacts } from "./ManagedMemberReferenceRows.js";
import { readMethodDefinitionRow } from "./ManagedMethodDefinitionRows.js"; import { readMethodDefinitionRow } from "./ManagedMethodDefinitionRows.js";
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js"; import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js";
@@ -163,10 +164,11 @@ const methodSignatureBlob = (
if (table === 6) { if (table === 6) {
signatureIndex = readMethodDefinitionRow(bytes, layout, row).signatureIndex; signatureIndex = readMethodDefinitionRow(bytes, layout, row).signatureIndex;
} else { } else {
const cursor = metadataRowCursor(bytes, layout, table, row); signatureIndex = readMemberReferenceRowFacts(
cursor.readIndex(layout.codedIndexSize("MemberRefParent")); bytes,
cursor.readIndex(layout.stringIndexSize); layout,
signatureIndex = cursor.readIndex(layout.blobIndexSize); row,
).signatureIndex;
} }
return readMetadataBlob( return readMetadataBlob(
bytes, bytes,