Files
reverse-engineering-anything/src/dotnet/ManagedTypeNameDigests.ts
T
1438422dfe fix(managed): preserve enclosing identity in member comparisons (#3079)
Keep same-named nested members paired by their source-owned enclosing identity, including selected inventories that omit the parent TypeDefs. Share the cached, bounded nominal type-name digest owner with signature resolution; preserve current unknown identity through matching, remapping and absence instead of repairing it with display names or historical compatibility.

Verification: 457 related tests passed after preserving TypeDef visibility/parent applicability and coherent fixtures; the complete static/architecture gate and real pinned-SDK MCP nested/official .NET DLL workflows passed after a sigrok-experiment-only main refresh. Documentation checks passed after the compatibility fix. Full CoreLib inspection retains the pre-existing explicit representation limit. No target assembly execution; local verification used two CPUs and an 8 GiB RSS guard.

Co-authored-by: 8rulerstar <190670431+8rulerstar@users.noreply.github.com>
Co-authored-by: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-14 23:20:07 +08:00

212 lines
7.8 KiB
TypeScript

import { createHash } from "node:crypto";
import { managedDecodeBudget } from "./ManagedDecodeBudget.js";
import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js";
import type {
parseTypes,
SignatureTypeName,
} from "./ManagedMemberInspectorCore.js";
import { metadataToken } from "./ManagedMetadataHeaps.js";
import type { ManagedTypeReferenceLookup } from "./ManagedTypeReferences.js";
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
import { managedFailure, readManagedValue } from "./ManagedReaderFailure.js";
const TYPE_REF = 1;
const TYPE_DEF = 2;
/** One type's own namespace and name, and its enclosing type's row if nested. */
interface TypeLink {
readonly namespace: string;
readonly name: string;
readonly enclosingRow: number | null;
}
/**
* Deepest type nesting REA names, counting the outermost type as 1. Real
* assemblies nest a few levels; the cap bounds the walk a hostile chain of
* NestedClass rows or TypeRef ResolutionScopes can force per operand.
*/
export const MAX_TYPE_NESTING_DEPTH = 64;
/** Digest prefixes that keep a top-level type apart from a nested one. */
const TOP_LEVEL = Buffer.from([0]);
const NESTED = Buffer.from([1]);
/**
* Separates a namespace from a name. #Strings entries end at NUL, so neither
* holds one, and `A` + `B.C` stays apart from `A.B` + `C`.
*/
const NAME_SEPARATOR = Buffer.from([0]);
interface NamedRow {
readonly digest: Buffer;
readonly depth: number;
}
/**
* Digest each row's type name after its enclosing types' names: a top-level
* type hashes its namespace and name, each framed, and a nested type hashes
* its enclosing type's digest and its own namespace and name, as
* `Outer/Inner` would read. Each
* row is read and digested once, so naming a chain is linear in its length.
* A gap, a cycle, or nesting deeper than `MAX_TYPE_NESTING_DEPTH` has no
* digest. `retain` is charged once per row read and once per remembered row.
*/
export const nestedTypeDigests = (
link: (row: number) => TypeLink | null,
retain: () => void,
): ((row: number) => Buffer | null) => {
const named = new Map<number, NamedRow | null>();
const remember = (row: number, value: NamedRow | null): void => {
retain();
named.set(row, value);
};
// Rows a walk read but could not name, such as the parents of a type nested
// past the cap, are still read only once.
const links = new Map<number, TypeLink | null>();
const readLink = (row: number): TypeLink | null => {
const known = links.get(row);
if (known !== undefined) return known;
const read = link(row);
retain();
links.set(row, read);
return read;
};
return (row) => {
// Walk out to a named row or the outermost type, at most past the cap.
const path: { readonly row: number; readonly link: TypeLink }[] = [];
const seen = new Set<number>();
let outer: NamedRow | null = null;
for (let current: number | null = row; current !== null;) {
const known = named.get(current);
if (known !== undefined) {
if (known === null) {
for (const step of path) remember(step.row, null);
return null;
}
outer = known;
break;
}
if (seen.has(current)) {
// Every row on a cycle, and every row nested in one, has no name.
for (const step of path) remember(step.row, null);
return null;
}
if (path.length === MAX_TYPE_NESTING_DEPTH) {
// The row the walk started from is nested past the cap.
remember(row, null);
return null;
}
const step = readLink(current);
if (step === null) {
remember(current, null);
for (const visited of path) remember(visited.row, null);
return null;
}
seen.add(current);
path.push({ row: current, link: step });
current = step.enclosingRow;
}
// Name inward from the outermost row; a row past the cap, and every row
// nested in it, has no name.
let inner: NamedRow | null = outer;
let exceeded = false;
for (const step of path.reverse()) {
const depth = (inner?.depth ?? 0) + 1;
exceeded ||= depth > MAX_TYPE_NESTING_DEPTH;
if (exceeded) {
remember(step.row, null);
continue;
}
const hash = createHash("sha256");
if (inner === null) hash.update(TOP_LEVEL);
else hash.update(NESTED).update(inner.digest);
hash
.update(step.link.namespace)
.update(NAME_SEPARATOR)
.update(step.link.name);
inner = { digest: hash.digest(), depth };
remember(step.row, inner);
}
return exceeded ? null : (inner?.digest ?? null);
};
};
/**
* Name signature type operands independently of their metadata rows: a TypeDef
* or TypeRef by namespace and name, after its enclosing types for a nested
* type, as in `Outer/Inner`. A TypeDef and a TypeRef with the same name are
* the same nominal name, and a TypeRef's resolution scope assembly is not part of its
* name, so a type forwarded between assemblies keeps its name. A TypeSpec, an
* unreadable row, cyclic nesting, or nesting deeper than the cap has no name,
* so its declaration and signature operands have no resolved digest; an unreadable row's failure is
* retained in `issues`, once per row. Each coded index and each row is
* named and digested once, so repeated operands cost a lookup.
*/
export const managedTypeNameDigests = (
layout: ManagedMetadataLayout,
issues: ManagedParseIssue[],
parsedTypes: Pick<ReturnType<typeof parseTypes>, "types" | "nesting">,
reference: ManagedTypeReferenceLookup,
): SignatureTypeName => {
const retain = () => managedDecodeBudget(layout).reserve(128, null);
managedDecodeBudget(layout).reserve(parsedTypes.types.length * 128, null);
const types = new Map<string, (typeof parsedTypes.types)[number]>();
for (const type of parsedTypes.types) types.set(type.token, type);
const definitions = nestedTypeDigests((row) => {
if (!parsedTypes.nesting.complete) return null;
const type = types.get(metadataToken(TYPE_DEF, row));
if (
type === undefined &&
(row < 1 || row > (layout.table(TYPE_DEF)?.rowCount ?? 0))
) {
readManagedValue(() => {
throw managedFailure(
"invalid-row",
`metadata.signature.type:${TYPE_DEF}:${row}`,
`Signature references an unavailable TypeDef row ${row}`,
);
}, issues);
return null;
}
if (type === undefined) return null;
const enclosingRow = parsedTypes.nesting.enclosing.get(row) ?? null;
// ECMA-335 TypeAttributes.VisibilityMask: 0/1 are top-level, 2..7 nested.
const nested = (type.flags & 0x7) >= 2;
if (nested !== (enclosingRow !== null)) {
managedDecodeBudget(layout).reserve(512, type.row_offset);
readManagedValue(() => {
throw managedFailure(
"invalid-row",
`metadata.TypeDef:${type.token}`,
nested
? `Nested TypeDef ${type.token} has no NestedClass enclosing entry`
: `Top-level TypeDef ${type.token} has a NestedClass enclosing entry`,
type.row_offset,
);
}, issues);
return null;
}
return {
namespace: type.namespace,
name: type.name,
enclosingRow,
};
}, retain);
const references = nestedTypeDigests((row) => {
const value = reference(row);
return value?.scopeValid === true ? value : null;
}, retain);
const digests = new Map<number, Buffer | null>();
return (codedIndex) => {
let digest = digests.get(codedIndex);
if (digest === undefined) {
managedDecodeBudget(layout).reserve(256, null);
const row = codedIndex >>> 2;
const table = codedIndex & 3;
digest =
table === 0 ? definitions(row) : table === 1 ? references(row) : null;
digests.set(codedIndex, digest);
}
return digest;
};
};