Files
rea/src/dotnet/ManagedMemberRows.ts
T
morluto 416f16e2ed 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.
2026-10-27 02:46:26 +08:00

591 lines
18 KiB
TypeScript

import { readFieldDefinitionRow } from "./ManagedFieldDefinitionRows.js";
import { readMemberReferenceRowFacts } from "./ManagedMemberReferenceRows.js";
import { readMethodDefinitionRow } from "./ManagedMethodDefinitionRows.js";
import { jsonStringByteLength } from "../domain/jsonStringByteLength.js";
import { isManagedMemberReferenceInstruction } from "../domain/managed/managedMemberInstructionFacts.js";
import {
admitManagedProjection,
managedDecodeBudget,
type ManagedDecodeBudget,
} from "./ManagedDecodeBudget.js";
import type { ManagedPeLayout } from "./ManagedPeReader.js";
import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js";
import {
MANAGED_CODED_INDEXES,
type ManagedMetadataLayout,
} from "./ManagedMetadataLayout.js";
import {
metadataSignature,
memberTargetIdentity,
signatureGenericArity,
type ManagedCallEdge,
type ManagedExportedType,
type ManagedField,
type ManagedFieldAccess,
type ManagedMemberRef,
type ManagedMethod,
type DecodedMethod,
type MemberCore,
type SignatureTypeName,
type DeclarationTypeName,
type TypeRange,
} from "./ManagedMemberInspectorCore.js";
import {
metadataToken,
type MetadataTokenFact,
metadataRowCursor,
decodeMetadataCodedIndex,
readMetadataBlob,
readMetadataString,
} from "./ManagedMetadataHeaps.js";
import {
appendManagedParseIssues,
readManagedValue,
} from "./ManagedReaderFailure.js";
import { methodBody } from "./ManagedMethodBodyReader.js";
import { memberDeclaringTypeRowLookup } from "./ManagedMemberOwnership.js";
import type { TypeMemberListFacts } from "./ManagedTypeMemberLists.js";
/**
* Declaring-type tokens whose members are decoded in full; null selects every
* type. Unselected members keep only their name so edges stay labelled.
*/
export type SelectedTypeTokens = ReadonlySet<string> | null;
const isSelected = (
selected: SelectedTypeTokens,
declared: { readonly token: string } | undefined,
): boolean =>
selected === null || (declared !== undefined && selected.has(declared.token));
export const parseFields = (
bytes: Buffer,
layout: ManagedMetadataLayout,
ranges: readonly TypeRange[],
memberLists: TypeMemberListFacts,
{
signatureTypeName,
declarationTypeName,
selected = null,
}: {
readonly signatureTypeName: SignatureTypeName;
readonly declarationTypeName: DeclarationTypeName;
readonly selected?: SelectedTypeTokens;
},
): {
readonly fields: readonly ManagedField[];
readonly core: ReadonlyMap<string, MemberCore>;
readonly issues: readonly ManagedParseIssue[];
} => {
const fields: ManagedField[] = [];
const core = new Map<string, MemberCore>();
const issues: ManagedParseIssue[] = [];
const table = layout.table(4);
const ownerRow = readManagedValue(
() => memberDeclaringTypeRowLookup(bytes, layout, 4, memberLists),
issues,
);
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
readManagedValue(() => {
const { token, rowOffset, flags, nameIndex, signatureIndex } =
readFieldDefinitionRow(bytes, layout, row);
const name = readMetadataString(
bytes,
layout,
nameIndex,
layout.strings.size,
);
const owner =
ownerRow === undefined
? 0
: (readManagedValue(() => ownerRow(row), issues) ?? 0);
const declared = ranges[owner - 1];
core.set(token, {
token,
name,
declaringType: declared?.fullName ?? null,
kind: "field",
});
if (!isSelected(selected, declared)) return;
const sig = readMetadataBlob(
bytes,
layout,
signatureIndex,
layout.blob?.size ?? 0,
);
const decoded = budgetedSignature(
sig,
layout,
signatureTypeName,
issues,
{ scope: `Field:${token}.signature`, kind: "field" },
);
core.set(token, {
token,
name,
declaringType: declared?.fullName ?? null,
kind: "field",
identitySha256: memberTargetIdentity(
name,
declared === undefined ? null : { table: 2, row: owner },
decoded,
declarationTypeName,
),
});
fields.push({
token,
row_offset: rowOffset,
declaring_type_token: declared?.token ?? null,
declaring_type: declared?.fullName ?? null,
name,
flags,
signature: decoded,
});
}, issues);
}
return { fields, core, issues };
};
const TYPE_FORWARDER_FLAG = 0x0020_0000;
/** Read ExportedType rows, including type forwarders, as declared. */
export const parseExportedTypes = (
bytes: Buffer,
layout: ManagedMetadataLayout,
): {
readonly exportedTypes: readonly ManagedExportedType[];
readonly issues: readonly ManagedParseIssue[];
} => {
const exportedTypes: ManagedExportedType[] = [];
const issues: ManagedParseIssue[] = [];
const table = layout.table(39);
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
readManagedValue(() => {
const cursor = metadataRowCursor(bytes, layout, 39, row);
const flags = cursor.readUInt32();
const typeDefId = 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 implementationOffset = cursor.offset;
const implementationRaw = cursor.readIndex(
layout.codedIndexSize("Implementation"),
);
const token = metadataToken(39, row);
const implementation = decodeMetadataCodedIndex(
implementationRaw,
MANAGED_CODED_INDEXES.Implementation.bits,
MANAGED_CODED_INDEXES.Implementation.tables,
layout.rowCounts,
);
if (implementation.kind !== "valid")
issues.push({
code: "invalid-row",
scope: `metadata.ExportedType:${token}`,
offset: implementationOffset,
detail:
implementation.kind === "null"
? "ExportedType implementation coded index 0x0 is null, but the Implementation column must reference a row"
: `ExportedType implementation coded index 0x${implementationRaw.toString(16)} is invalid: ${implementation.reason}`,
});
exportedTypes.push({
token,
row_offset: cursor.start,
namespace,
name,
flags,
forwarder: (flags & TYPE_FORWARDER_FLAG) !== 0,
type_def_id: typeDefId,
implementation_token:
implementation.kind === "valid" ? implementation.token : null,
});
}, issues);
}
return { exportedTypes, issues };
};
export const parseMemberRefs = (
bytes: Buffer,
layout: ManagedMetadataLayout,
{
typeName,
signatureTypeName,
declarationTypeName,
}: {
readonly typeName: (
reference: Pick<MetadataTokenFact, "table" | "row" | "token"> | null,
) => string | null;
readonly signatureTypeName: SignatureTypeName;
readonly declarationTypeName: DeclarationTypeName;
},
): {
readonly refs: readonly ManagedMemberRef[];
readonly core: ReadonlyMap<string, MemberCore>;
readonly issues: readonly ManagedParseIssue[];
} => {
const refs: ManagedMemberRef[] = [];
const core = new Map<string, MemberCore>();
const issues: ManagedParseIssue[] = [];
const table = layout.table(10);
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
readManagedValue(() => {
const { token, rowOffset, parentRaw, parent, nameIndex, signatureIndex } =
readMemberReferenceRowFacts(bytes, layout, row);
const name = readMetadataString(
bytes,
layout,
nameIndex,
layout.strings.size,
);
const sig = readMetadataBlob(
bytes,
layout,
signatureIndex,
layout.blob?.size ?? 0,
);
if (parent.kind !== "valid")
issues.push({
code: "invalid-row",
scope: `metadata.MemberRef:${token}`,
offset: rowOffset,
detail:
parent.kind === "null"
? "MemberRef parent coded index 0x0 is null, but the Class column must reference a row"
: `MemberRef parent coded index 0x${parentRaw.toString(16)} is invalid: ${parent.reason}`,
});
const parentToken = parent.kind === "valid" ? parent.token : null;
const parentName = typeName(parent.kind === "valid" ? parent : null);
const decoded = budgetedSignature(
sig,
layout,
signatureTypeName,
issues,
{ scope: `MemberRef:${token}.signature` },
);
refs.push({
token,
row_offset: rowOffset,
parent_token: parentToken,
parent_name: parentName,
name,
signature: decoded,
});
core.set(token, {
token,
name,
declaringType: parentName,
kind: decoded.kind,
genericArity: signatureGenericArity(decoded),
identitySha256: memberTargetIdentity(
name,
parent.kind === "valid" ? parent : null,
decoded,
declarationTypeName,
),
});
}, issues);
}
return { refs, core, issues };
};
interface ParseMethodsInput {
readonly bytes: Buffer;
readonly layout: ManagedMetadataLayout;
readonly pe: ManagedPeLayout;
readonly ranges: readonly TypeRange[];
readonly signatureTypeName: SignatureTypeName;
readonly declarationTypeName: DeclarationTypeName;
readonly memberRefKinds: ReadonlyMap<string, "field" | "method">;
readonly memberLists: TypeMemberListFacts;
readonly selected?: SelectedTypeTokens;
readonly methodOffset?: number;
readonly methodLimit?: number;
}
export const parseMethods = ({
bytes,
layout,
pe,
ranges,
signatureTypeName,
declarationTypeName,
memberRefKinds,
memberLists,
selected = null,
methodOffset = 0,
methodLimit = Number.MAX_SAFE_INTEGER,
}: ParseMethodsInput): {
readonly methods: readonly DecodedMethod[];
readonly core: ReadonlyMap<string, MemberCore>;
readonly issues: readonly ManagedParseIssue[];
/**
* Methods that may belong to a selected type but whose CIL was not fully
* scanned: rows that produced no method, or bodies whose decoding stopped.
*/
readonly unscannedMethods: number;
} => {
const methods: DecodedMethod[] = [];
const core = new Map<string, MemberCore>();
const issues: ManagedParseIssue[] = [];
let unscannedMethods = 0;
const table = layout.table(6);
const ownerRow = readManagedValue(
() => memberDeclaringTypeRowLookup(bytes, layout, 6, memberLists),
issues,
);
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
// The owner is read first: a row that produced no method hides a selected
// body only when its owner is selected, unknown, or unreadably named.
const owner =
ownerRow === undefined
? undefined
: readManagedValue(() => ownerRow(row), issues);
const declared = owner === undefined ? undefined : ranges[owner - 1];
const inPage = row > methodOffset && row - methodOffset <= methodLimit;
const mayBeSelected =
inPage &&
(selected === null ||
owner === undefined ||
owner === 0 ||
isSelected(selected, declared) ||
// A type whose name is unreadable may be one the caller requested.
declared?.fullName === null);
const produced = methods.length;
readManagedValue(() => {
const {
token,
rowOffset,
rva,
implFlags,
flags,
nameIndex,
signatureIndex,
implementation,
} = readMethodDefinitionRow(bytes, layout, row);
const name = readMetadataString(
bytes,
layout,
nameIndex,
layout.strings.size,
);
const member = {
token,
name,
declaringType: declared?.fullName ?? null,
kind: "method",
} as const;
core.set(token, member);
if (!inPage) return;
if (!isSelected(selected, declared)) return;
const sig = readMetadataBlob(
bytes,
layout,
signatureIndex,
layout.blob?.size ?? 0,
);
const decoded = budgetedSignature(
sig,
layout,
signatureTypeName,
issues,
{ scope: `MethodDef:${token}.signature`, kind: "method" },
);
core.set(token, {
...member,
genericArity: signatureGenericArity(decoded),
identitySha256: memberTargetIdentity(
name,
declared === undefined || owner === undefined
? null
: { table: 2, row: owner },
decoded,
declarationTypeName,
),
});
const read = methodBody(bytes, pe, rva, {
implementation,
budget: managedDecodeBudget(layout),
metadataLayout: layout,
methodToken: token,
memberRefKinds,
});
appendManagedParseIssues(issues, read.issues);
if (!read.scanned) unscannedMethods += 1;
managedDecodeBudget(layout).reserve(128, rowOffset);
methods.push({
tokenInstructions: read.tokenInstructions,
catchTokens: read.catchTokens,
method: {
token,
row_offset: rowOffset,
declaring_type_token: declared?.token ?? null,
declaring_type: declared?.fullName ?? null,
name,
rva,
impl_flags: implFlags,
flags,
signature: decoded,
body: read.body,
},
});
}, issues);
if (mayBeSelected && methods.length === produced) unscannedMethods += 1;
}
return { methods, core, issues, unscannedMethods };
};
/**
* Member kinds each operand kind may name. A MemberRef whose signature does
* not decode keeps its name on calls, as before, but is never named a field.
*/
const NAMEABLE_KINDS = new Map<string, ReadonlySet<MemberCore["kind"]>>([
["method", new Set(["method", "unknown"])],
["field", new Set(["field"])],
]);
const targetKind = (table: number): ManagedCallEdge["target_kind"] =>
table === 6
? "method-def"
: table === 0x0a
? "member-ref"
: table === 0x2b
? "method-spec"
: "unknown";
/** The member an operand names, if the operand kind can name that member's kind. */
const namedTarget = (
members: ReadonlyMap<string, MemberCore>,
operandKind: string,
operand: string,
): MemberCore | undefined => {
const member = members.get(operand);
return member !== undefined &&
NAMEABLE_KINDS.get(operandKind)?.has(member.kind) === true
? member
: undefined;
};
export const edges = (
methods: readonly DecodedMethod[],
members: ReadonlyMap<string, MemberCore>,
budget: ManagedDecodeBudget,
): {
readonly callEdges: readonly ManagedCallEdge[];
readonly fieldAccesses: readonly ManagedFieldAccess[];
} => {
const callEdges: ManagedCallEdge[] = [];
const fieldAccesses: ManagedFieldAccess[] = [];
for (const { method, tokenInstructions } of methods) {
const derives = tokenInstructions.some((instruction) =>
isManagedMemberReferenceInstruction({
opcode: instruction.opcode,
operand_kind: instruction.operandKind,
operand: instruction.operand,
}),
);
if (derives) budget.admitTransient(callerNameBytes(method), null);
// Built once per method, only after its exact size was admitted.
const caller = derives ? callerName(method) : method.name;
for (const instruction of tokenInstructions) {
const anchor = {
opcode: instruction.opcode,
operand_kind: instruction.operandKind,
operand: instruction.operand,
};
if (!isManagedMemberReferenceInstruction(anchor)) continue;
const { operand } = anchor;
const target = namedTarget(members, anchor.operand_kind, operand);
if (anchor.operand_kind === "method") {
// Each derived record is admitted before the graph grows further.
callEdges.push(
admitManagedProjection(
{
caller_token: method.token,
il_offset: instruction.offset,
target_identity_sha256: target?.identitySha256 ?? null,
caller,
opcode: anchor.opcode,
target_token: operand,
target_kind: targetKind(instruction.reference.table),
target_name: target?.name ?? null,
target_declaring_type: target?.declaringType ?? null,
},
budget,
),
);
}
if (anchor.operand_kind === "field") {
fieldAccesses.push(
admitManagedProjection(
{
method_token: method.token,
il_offset: instruction.offset,
target_identity_sha256: target?.identitySha256 ?? null,
method: caller,
opcode: anchor.opcode,
field_token: operand,
field_name: target?.name ?? null,
field_declaring_type: target?.declaringType ?? null,
},
budget,
),
);
}
}
// Every record now reserves the name it carries.
if (derives) budget.releaseTransient();
}
return { callEdges, fieldAccesses };
};
const callerName = (method: ManagedMethod): string =>
method.declaring_type === null
? method.name
: `${method.declaring_type}.${method.name}`;
/** JSON bytes of `callerName`, from its parts: the joining dot adds one byte. */
const callerNameBytes = (method: ManagedMethod): number =>
method.declaring_type === null
? jsonStringByteLength(method.name)
: jsonStringByteLength(method.declaring_type) +
jsonStringByteLength(method.name) -
1;
const budgetedSignature = (
bytes: Buffer,
layout: ManagedMetadataLayout,
signatureTypeName: SignatureTypeName,
issues: ManagedParseIssue[],
owner: { readonly scope: string; readonly kind?: "field" | "method" },
): ReturnType<typeof metadataSignature> => {
const budget = managedDecodeBudget(layout);
budget.admitTransient(
bytes.length * 256,
layout.blob?.offset ?? layout.rootOffset,
);
try {
return admitManagedProjection(
metadataSignature(bytes, {
rawSha256: budget.digest(bytes, "sha256"),
typeName: signatureTypeName,
layout,
issues,
scope: owner.scope,
...(owner.kind === undefined ? {} : { expectedKind: owner.kind }),
}),
budget,
);
} finally {
budget.releaseTransient();
}
};