fix(managed): unify metadata row and coded-index ownership

Share Field scalar facts and use one coded-index tag registry for widths and decoding. Preserve reserved constructor tags, generic-instantiation restrictions, heap failure order and selected-field signature admission.

Derive native import member kind from the validated MemberForwarded coordinate independently of name lookup, retaining known field or method identity when its display name is unreadable.

Validation: 368 focused source, CLI and MCP tests passed; five lost-kind regressions reproduced on the previous commit. Heap-width, coded-width, reserved-tag and selection boundaries are covered. Typecheck and lint passed.
This commit is contained in:
morluto
2026-10-27 02:46:25 +08:00
parent 741f2de751
commit 416f16e2ed
11 changed files with 360 additions and 60 deletions
+25
View File
@@ -0,0 +1,25 @@
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
import { metadataRowCursor, metadataToken } from "./ManagedMetadataHeaps.js";
/** Scalar Field facts; consumers own heap admission and field selection. */
export const readFieldDefinitionRow = (
bytes: Buffer,
layout: ManagedMetadataLayout,
row: number,
): {
readonly token: string;
readonly rowOffset: number;
readonly flags: number;
readonly nameIndex: number;
readonly signatureIndex: number;
} => {
// Layout admission bounds the complete row independently of its heap values.
const cursor = metadataRowCursor(bytes, layout, 4, row);
return {
token: metadataToken(4, row),
rowOffset: cursor.start,
flags: cursor.readUInt16(),
nameIndex: cursor.readIndex(layout.stringIndexSize),
signatureIndex: cursor.readIndex(layout.blobIndexSize),
};
};
+71
View File
@@ -2863,3 +2863,74 @@ describe("managed member method paging", () => {
).toThrow(RangeError);
});
});
it("retains names of unselected fields without admitting their malformed signatures", () => {
const bytes = buildManagedNestedTypeFixture({ topLevelTypes: true });
const pe = readManagedPeLayout(bytes);
if (pe.cli === null) throw new Error("Expected CLI fixture");
const { rva, size } = pe.cli.metadata;
const layout = readManagedMetadataLayout(
bytes,
pe.rvaToOffset(rva, size, "cli.metadata"),
size,
);
const field = metadataRowOffset(layout, 4, 1);
bytes.writeUIntLE(
layout.blobIndexSize === 2 ? 0xffff : 0xffffffff,
field + 2 + layout.stringIndexSize,
layout.blobIndexSize,
);
const method = metadataRowOffset(layout, 6, 2);
const methodRva = bytes.readUInt32LE(method);
const body = tokenBody([[ldsfld, 0x04000001]]);
bytes.set(body, pe.rvaToOffset(methodRva, body.length, "fixture CIL"));
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
{
typeNames: ["OtherInner"],
},
);
expect(result.fields).toMatchObject([{ token: "0x04000002", name: "F" }]);
expect(result.methods).toMatchObject([{ token: "0x06000002", name: "M" }]);
expect(result.field_accesses).toMatchObject([
{
field_token: "0x04000001",
field_name: "F",
field_declaring_type: "Inner",
},
]);
expect(
result.coverage.issues.filter(
({ scope }) =>
scope === "metadata.#Blob" || scope === "Field:0x04000001.signature",
),
).toEqual([]);
});
it.each([false, true])(
"keeps ordinary TypeSpec operands distinct from GENERICINST heads (generic=%s)",
(generic) => {
const bytes = buildManagedPeFixture({
typeSpecSignatures: [Buffer.from([0x15, 0x12, 0x05, 0x01, 0x0e])],
fieldSignature: Buffer.from(
generic ? [0x06, 0x15, 0x12, 0x06, 0x01, 0x0e] : [0x06, 0x12, 0x06],
),
});
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.fields[0]?.signature).toMatchObject({
kind: "field",
parse_status: generic ? "malformed" : "decoded",
field_type: generic ? null : "class:6",
});
const issues = result.coverage.issues.filter(
({ scope }) => scope === "Field:0x04000001.signature",
);
expect(issues).toEqual(
generic ? [expect.objectContaining({ code: "invalid-row" })] : [],
);
},
);
+23 -9
View File
@@ -18,9 +18,10 @@ import {
readManagedTypeNesting,
type ManagedTypeNesting,
} from "./ManagedTypeNesting.js";
import type {
ManagedMetadataLayout,
MetadataTableLayout,
import {
MANAGED_CODED_INDEXES,
type ManagedMetadataLayout,
type MetadataTableLayout,
} from "./ManagedMetadataLayout.js";
import {
readManagedCompressedUnsigned,
@@ -262,8 +263,8 @@ export const parseTypes = (
);
const extendsIndex = decodeMetadataCodedIndex(
extendsRaw,
2,
[2, 1, 27],
MANAGED_CODED_INDEXES.TypeDefOrRef.bits,
MANAGED_CODED_INDEXES.TypeDefOrRef.tables,
layout.rowCounts,
);
if (extendsIndex.kind === "invalid")
@@ -518,18 +519,22 @@ const POSITION_ELEMENTS: ReadonlyMap<number, readonly SignaturePosition[]> =
[0x10, ["return", "parameter", "local", "field"]],
]);
/** GENERICINST names a TypeDef or TypeRef, excluding the family's TypeSpec alternative. */
const GENERIC_TYPE_DEFINITION_TABLES: readonly number[] =
MANAGED_CODED_INDEXES.TypeDefOrRef.tables.slice(0, 2);
/** Decode a type operand once; spec callers also validate its metadata coordinate. */
const readTypeOperand = (
blob: Buffer,
offset: number,
layout?: ManagedMetadataLayout,
tables: readonly number[] = [2, 1, 27],
tables: readonly number[] = MANAGED_CODED_INDEXES.TypeDefOrRef.tables,
): Omit<TypeOperand, "offset"> => {
const token = readCompressed(blob, offset);
if (layout !== undefined) {
const decoded = decodeMetadataCodedIndex(
token.value,
2,
MANAGED_CODED_INDEXES.TypeDefOrRef.bits,
tables,
layout.rowCounts,
);
@@ -542,7 +547,11 @@ const readTypeOperand = (
);
return { ...token, coordinate: { table: decoded.table, row: decoded.row } };
}
const { table, row } = metadataCodedIndexCoordinate(token.value, 2, tables);
const { table, row } = metadataCodedIndexCoordinate(
token.value,
MANAGED_CODED_INDEXES.TypeDefOrRef.bits,
tables,
);
return { ...token, coordinate: { table, row } };
};
@@ -695,7 +704,12 @@ const readTypeSignature = (
"generic instantiation does not name a class or value type",
cursor + 1,
);
const token = readTypeOperand(blob, cursor + 2, layout, [2, 1]);
const token = readTypeOperand(
blob,
cursor + 2,
layout,
GENERIC_TYPE_DEFINITION_TABLES,
);
if (
cursor === offset &&
(token.coordinate.table === 1 || token.coordinate.table === 2)
+3 -3
View File
@@ -1,5 +1,5 @@
import {
MEMBER_REF_PARENT_CODED_INDEX,
MANAGED_CODED_INDEXES,
type ManagedMetadataLayout,
} from "./ManagedMetadataLayout.js";
import {
@@ -23,8 +23,8 @@ export const readMemberReferenceRowFacts = (
parentRaw,
parent: decodeMetadataCodedIndex(
parentRaw,
MEMBER_REF_PARENT_CODED_INDEX.bits,
MEMBER_REF_PARENT_CODED_INDEX.tables,
MANAGED_CODED_INDEXES.MemberRefParent.bits,
MANAGED_CODED_INDEXES.MemberRefParent.tables,
layout.rowCounts,
),
nameIndex: cursor.readIndex(layout.stringIndexSize),
+12 -9
View File
@@ -1,3 +1,4 @@
import { readFieldDefinitionRow } from "./ManagedFieldDefinitionRows.js";
import { readMemberReferenceRowFacts } from "./ManagedMemberReferenceRows.js";
import { readMethodDefinitionRow } from "./ManagedMethodDefinitionRows.js";
import { jsonStringByteLength } from "../domain/jsonStringByteLength.js";
@@ -9,7 +10,10 @@ import {
} from "./ManagedDecodeBudget.js";
import type { ManagedPeLayout } from "./ManagedPeReader.js";
import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js";
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
import {
MANAGED_CODED_INDEXES,
type ManagedMetadataLayout,
} from "./ManagedMetadataLayout.js";
import {
metadataSignature,
memberTargetIdentity,
@@ -83,12 +87,12 @@ export const parseFields = (
);
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
readManagedValue(() => {
const cursor = metadataRowCursor(bytes, layout, 4, row);
const flags = cursor.readUInt16();
const { token, rowOffset, flags, nameIndex, signatureIndex } =
readFieldDefinitionRow(bytes, layout, row);
const name = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
nameIndex,
layout.strings.size,
);
const owner =
@@ -96,7 +100,6 @@ export const parseFields = (
? 0
: (readManagedValue(() => ownerRow(row), issues) ?? 0);
const declared = ranges[owner - 1];
const token = metadataToken(4, row);
core.set(token, {
token,
name,
@@ -107,7 +110,7 @@ export const parseFields = (
const sig = readMetadataBlob(
bytes,
layout,
cursor.readIndex(layout.blobIndexSize),
signatureIndex,
layout.blob?.size ?? 0,
);
const decoded = budgetedSignature(
@@ -131,7 +134,7 @@ export const parseFields = (
});
fields.push({
token,
row_offset: cursor.start,
row_offset: rowOffset,
declaring_type_token: declared?.token ?? null,
declaring_type: declared?.fullName ?? null,
name,
@@ -180,8 +183,8 @@ export const parseExportedTypes = (
const token = metadataToken(39, row);
const implementation = decodeMetadataCodedIndex(
implementationRaw,
2,
[38, 35, 39],
MANAGED_CODED_INDEXES.Implementation.bits,
MANAGED_CODED_INDEXES.Implementation.tables,
layout.rowCounts,
);
if (implementation.kind !== "valid")
+7 -13
View File
@@ -12,6 +12,7 @@ import type {
ManagedParseIssue,
} from "../domain/managed/managedArtifact.js";
import {
MANAGED_CODED_INDEXES,
metadataRowOffset,
type ManagedMetadataLayout,
} from "./ManagedMetadataLayout.js";
@@ -36,13 +37,6 @@ type AssemblyReference = ManagedArtifactInspection["references"][number];
type ManagedResource = ManagedArtifactInspection["resources"][number];
type CustomAttribute = ManagedArtifactInspection["attributes"][number];
const HAS_CUSTOM_ATTRIBUTE_TABLES = [
6, 4, 1, 2, 8, 9, 10, 0, 14, 23, 20, 17, 26, 27, 32, 35, 38, 39, 40, 42, 44,
43,
] as const;
const CUSTOM_ATTRIBUTE_TYPE_TABLES = [undefined, undefined, 6, 10] as const;
const publicKeyIdentity = (
bytes: Buffer,
kind: "public-key" | "public-key-token",
@@ -475,8 +469,8 @@ export const readCustomAttribute = (
);
const parent = decodeMetadataCodedIndex(
parentRaw,
5,
HAS_CUSTOM_ATTRIBUTE_TABLES,
MANAGED_CODED_INDEXES.HasCustomAttribute.bits,
MANAGED_CODED_INDEXES.HasCustomAttribute.tables,
layout.rowCounts,
);
if (parent.kind !== "valid") {
@@ -489,8 +483,8 @@ export const readCustomAttribute = (
}
const constructor = decodeMetadataCodedIndex(
typeRaw,
3,
CUSTOM_ATTRIBUTE_TYPE_TABLES,
MANAGED_CODED_INDEXES.CustomAttributeType.bits,
MANAGED_CODED_INDEXES.CustomAttributeType.tables,
layout.rowCounts,
);
if (constructor.kind !== "valid") {
@@ -557,8 +551,8 @@ export const readResource = ({
);
const implementation = decodeMetadataCodedIndex(
implementationRaw,
2,
[38, 35, 39],
MANAGED_CODED_INDEXES.Implementation.bits,
MANAGED_CODED_INDEXES.Implementation.tables,
layout.rowCounts,
);
if (implementation.kind === "invalid")
+8 -8
View File
@@ -94,9 +94,10 @@ export interface ManagedMetadataLayout {
codedIndexSize(name: CodedIndexName): 2 | 4;
}
export type CodedIndexName = keyof typeof CODED_INDEXES;
export type CodedIndexName = keyof typeof MANAGED_CODED_INDEXES;
const CODED_INDEXES = {
/** ECMA-335 coded-index tag positions, shared by width and coordinate interpretation. */
export const MANAGED_CODED_INDEXES = {
TypeDefOrRef: { bits: 2, tables: [2, 1, 27] },
HasConstant: { bits: 2, tables: [4, 8, 23] },
HasCustomAttribute: {
@@ -113,14 +114,11 @@ const CODED_INDEXES = {
MethodDefOrRef: { bits: 1, tables: [6, 10] },
MemberForwarded: { bits: 1, tables: [4, 6] },
Implementation: { bits: 2, tables: [38, 35, 39] },
CustomAttributeType: { bits: 3, tables: [6, 10] },
CustomAttributeType: { bits: 3, tables: [undefined, undefined, 6, 10] },
ResolutionScope: { bits: 2, tables: [0, 26, 35, 1] },
TypeOrMethodDef: { bits: 1, tables: [2, 6] },
} 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;
/** Check a range within `bytes`, which start at artifact file offset `base`. */
@@ -207,10 +205,12 @@ const codedSize = (
name: CodedIndexName,
rowCounts: readonly number[],
): 2 | 4 => {
const descriptor = CODED_INDEXES[name];
const descriptor = MANAGED_CODED_INDEXES[name];
let maximum = 0;
for (const table of descriptor.tables)
for (const table of descriptor.tables) {
if (table === undefined) continue;
maximum = Math.max(maximum, rowCounts[table] ?? 0);
}
return maximum < 2 ** (16 - descriptor.bits) ? 2 : 4;
};
+6 -3
View File
@@ -9,7 +9,10 @@ import {
type DecodedMethod,
type MemberCore,
} from "./ManagedMemberInspectorCore.js";
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
import {
MANAGED_CODED_INDEXES,
type ManagedMetadataLayout,
} from "./ManagedMetadataLayout.js";
import {
decodeMetadataCodedIndex,
type MetadataTokenFact,
@@ -84,8 +87,8 @@ const readMethodSpec = (
const instantiation = cursor.readIndex(layout.blobIndexSize);
const method = decodeMetadataCodedIndex(
methodRaw,
1,
[6, 10],
MANAGED_CODED_INDEXES.MethodDefOrRef.bits,
MANAGED_CODED_INDEXES.MethodDefOrRef.tables,
layout.rowCounts,
);
if (method.kind !== "valid")
+17 -12
View File
@@ -1,3 +1,4 @@
import { readFieldDefinitionRow } from "./ManagedFieldDefinitionRows.js";
import {
readMethodDefinitionRow,
type ManagedMethodImplementationFlags,
@@ -12,7 +13,10 @@ import {
type ManagedNativeBoundaryInspection,
type ManagedParseIssue,
} from "../domain/managed/managedArtifact.js";
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
import {
MANAGED_CODED_INDEXES,
type ManagedMetadataLayout,
} from "./ManagedMetadataLayout.js";
import {
metadataRowCursor,
decodeMetadataCodedIndex,
@@ -92,19 +96,15 @@ export const parseFields = (
const table = layout.table(4);
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
readManagedValue(() => {
const cursor = metadataRowCursor(bytes, layout, 4, row);
const flags = cursor.readUInt16();
const name = readMetadataString(
const { token, rowOffset, flags, nameIndex } = readFieldDefinitionRow(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
heapExtent,
row,
);
cursor.readIndex(layout.blobIndexSize);
const token = metadataToken(4, row);
const name = readMetadataString(bytes, layout, nameIndex, heapExtent);
fields.set(token, {
token,
rowOffset: cursor.start,
rowOffset,
kind: "field",
name,
flags,
@@ -181,8 +181,8 @@ export const parseImplMaps = ({
);
const memberForwarded = decodeMetadataCodedIndex(
memberForwardedRaw,
1,
[4, 6],
MANAGED_CODED_INDEXES.MemberForwarded.bits,
MANAGED_CODED_INDEXES.MemberForwarded.tables,
layout.rowCounts,
);
if (memberForwarded.kind !== "valid")
@@ -226,7 +226,12 @@ export const parseImplMaps = ({
mapping_flags: mappingFlags,
mapping_flags_hex: flagsHex(mappingFlags),
member_token: memberToken,
member_kind: member?.kind ?? "unknown",
member_kind:
memberForwarded.kind !== "valid"
? "unknown"
: memberForwarded.table === 4
? "field"
: "method",
member_name: member?.name ?? null,
import_name: importName,
import_scope_token: importScopeToken,
@@ -598,3 +598,185 @@ it.each([
expect(members.methods[0]?.body.il_sha256).toEqual(expect.any(String));
},
);
it.each(
[2, 4].flatMap((stringWidth) =>
[2, 4].flatMap((blobWidth) =>
[false, true].map((badName) => ({ stringWidth, blobWidth, badName })),
),
),
)(
"keeps Field heap admission consumer-specific with string=$stringWidth/blob=$blobWidth/badName=$badName",
({ stringWidth, blobWidth, badName }) => {
const bytes = buildManagedPeFixture({
pinvoke: { memberForwardedRaw: 2 },
methodFlags: 0x0016,
...(stringWidth === 4 ? { assemblyName: "A".repeat(65536) } : {}),
...(blobWidth === 4
? { standaloneSignatures: [Buffer.alloc(65536)] }
: {}),
});
const pe = readManagedPeLayout(bytes);
if (pe.cli === null) throw new Error("Expected CLI fixture");
const { rva, size } = pe.cli.metadata;
const layout = readManagedMetadataLayout(
bytes,
pe.rvaToOffset(rva, size, "cli.metadata"),
size,
);
expect(layout.stringIndexSize).toBe(stringWidth);
expect(layout.blobIndexSize).toBe(blobWidth);
const row = metadataRowOffset(layout, 4, 1);
if (badName)
bytes.writeUIntLE(
stringWidth === 2 ? 0xffff : 0xffffffff,
row + 2,
stringWidth,
);
bytes.writeUIntLE(
blobWidth === 2 ? 0xffff : 0xffffffff,
row + 2 + stringWidth,
blobWidth,
);
const target = managedPeFixtureTarget(bytes);
const members = inspectManagedMembersBytes(bytes, target);
const native = inspectManagedNativeBoundariesBytes(bytes, target);
expect(members.fields).toEqual([]);
expect(
members.coverage.issues.filter(
({ scope }) =>
scope === "metadata.#Strings" || scope === "metadata.#Blob",
),
).toEqual([
expect.objectContaining({
code: "invalid-heap-index",
scope: badName ? "metadata.#Strings" : "metadata.#Blob",
}),
]);
expect(native.pinvoke_imports).toMatchObject([
{
member_kind: "field",
member_token: "0x04000001",
member_name: badName ? null : "counter",
},
]);
expect(
native.coverage.issues.filter(
({ scope }) =>
scope === "metadata.#Strings" || scope === "metadata.#Blob",
),
).toEqual(
badName
? [
expect.objectContaining({
code: "invalid-heap-index",
scope: "metadata.#Strings",
}),
]
: [],
);
if (!badName)
expect(members.field_accesses).toContainEqual(
expect.objectContaining({
field_token: "0x04000001",
field_name: "counter",
}),
);
},
);
it.each([8, 9])(
"keeps reserved CustomAttributeType tag %i distinct from a valid constructor",
(constructor) => {
const value = Buffer.from([1, 0, 0, 0, 0]);
const bytes = buildManagedPeFixture({
customAttributeTypeRaw: constructor,
customAttributeValue: value,
});
const result = inspectManagedArtifactBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.attributes).toMatchObject([
{
parent_token: "0x20000001",
constructor_token: null,
type_name: null,
value_length: value.length,
value_sha256: createHash("sha256").update(value).digest("hex"),
},
]);
expect(result.coverage.issues).toContainEqual(
expect.objectContaining({
code: "invalid-row",
scope: "metadata.CustomAttribute:0x0c000001",
detail: expect.stringContaining(
`coded index tag ${constructor - 8} is not defined`,
),
}),
);
},
);
it.each([
{ options: { methodRowCount: 8191 }, table: 6, count: 8191, width: 2 },
{ options: { methodRowCount: 8192 }, table: 6, count: 8192, width: 4 },
{ options: { memberRefRowCount: 8191 }, table: 10, count: 8191, width: 2 },
{ options: { memberRefRowCount: 8192 }, table: 10, count: 8192, width: 4 },
{ options: { moduleRowCount: 8192 }, table: 0, count: 8192, width: 2 },
])(
"sizes CustomAttributeType from real target-table $table count $count",
({ options, table, count, width }) => {
const bytes = buildManagedPeFixture(options);
const pe = readManagedPeLayout(bytes);
if (pe.cli === null) throw new Error("Expected CLI fixture");
const { rva, size } = pe.cli.metadata;
const layout = readManagedMetadataLayout(
bytes,
pe.rvaToOffset(rva, size, "cli.metadata"),
size,
);
expect(layout.table(table)?.rowCount).toBe(count);
expect(layout.codedIndexSize("CustomAttributeType")).toBe(width);
},
);
it("retains an ImplMap method identity independently of its unreadable name", () => {
const bytes = buildManagedPeFixture({ pinvoke: {} });
const pe = readManagedPeLayout(bytes);
if (pe.cli === null) throw new Error("Expected CLI fixture");
const { rva, size } = pe.cli.metadata;
const layout = readManagedMetadataLayout(
bytes,
pe.rvaToOffset(rva, size, "cli.metadata"),
size,
);
const row = metadataRowOffset(layout, 6, 1);
bytes.writeUIntLE(
layout.stringIndexSize === 2 ? 0xffff : 0xffffffff,
row + 8,
layout.stringIndexSize,
);
const result = inspectManagedNativeBoundariesBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.pinvoke_imports).toMatchObject([
{
member_token: "0x06000001",
member_kind: "method",
member_name: null,
import_name: "MessageBoxW",
import_scope_name: "user32.dll",
},
]);
expect(result.coverage).toMatchObject({
state: "partial",
issues: [
expect.objectContaining({
code: "invalid-heap-index",
scope: "metadata.#Strings",
}),
],
});
});
+6 -3
View File
@@ -7,7 +7,10 @@ import {
readMetadataString,
} from "./ManagedMetadataHeaps.js";
import { readManagedValue } from "./ManagedReaderFailure.js";
import { metadataRowOffset } from "./ManagedMetadataLayout.js";
import {
MANAGED_CODED_INDEXES,
metadataRowOffset,
} from "./ManagedMetadataLayout.js";
import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js";
export interface ManagedTypeReference {
@@ -48,8 +51,8 @@ export const managedTypeReferences = (
const raw = cursor.readIndex(layout.codedIndexSize("ResolutionScope"));
const scope = decodeMetadataCodedIndex(
raw,
2,
[0, 26, 35, 1],
MANAGED_CODED_INDEXES.ResolutionScope.bits,
MANAGED_CODED_INDEXES.ResolutionScope.tables,
layout.rowCounts,
);
if (scope.kind === "invalid")