Files
reverse-engineering-anything/src/dotnet/ManagedMemberOwnership.test.ts
T
7d92d93a55 fix(managed): share first-match member list ownership (#1955)
Inventory attribute lookups and the members inspector each interpreted
TypeDef member-list ownership. The members lookup binary-searched ordered
ranges but fell back to a per-member scan of every type when malformed
lists were unordered, which was quadratic (8 s for 100,000 types).

Both now use firstMatchListOwners, one near-linear first-match index over
one-based list positions. Remove the ordered/fallback split and the
production declaringType scan, which remains only as the test oracle. The
inventory reserves only its retained index.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-12 05:06:41 +08:00

151 lines
5.3 KiB
TypeScript

import { fc, it } from "@fast-check/vitest";
import { describe, expect, vi } from "vitest";
import { inspectManagedArtifactBytes } from "./ManagedArtifactInspector.js";
import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
import { managedDecodeBudget } from "./ManagedDecodeBudget.js";
import { memberDeclaringTypeRowLookup } from "./ManagedMemberOwnership.js";
import { memberListIndexSize } from "./ManagedMemberPointers.js";
import { readManagedMetadataLayout } from "./ManagedMetadataLayout.js";
import {
buildManagedMemberPointerFixture,
buildManagedPeFixture,
managedPeFixtureTarget,
} from "./ManagedPe.fixture.js";
import { readManagedPeLayout } from "./ManagedPeReader.js";
import { ManagedReaderFailure } from "./ManagedReaderFailure.js";
const metadataLayout = (bytes: Buffer) => {
const pe = readManagedPeLayout(bytes);
if (pe.cli === null) throw new Error("Fixture omitted CLI metadata");
return readManagedMetadataLayout(
bytes,
pe.rvaToOffset(pe.cli.metadata.rva, pe.cli.metadata.size, "cli.metadata"),
pe.cli.metadata.size,
);
};
describe("managed declaring type ownership", () => {
it.prop([
fc.tuple(
fc.tuple(
...Array.from({ length: 3 }, () => fc.integer({ min: 0, max: 6 })),
),
fc.tuple(
...Array.from({ length: 3 }, () => fc.integer({ min: 0, max: 6 })),
),
),
])(
"retains first-match field and method owners for arbitrary metadata lists",
([fields, methods]) => {
const bytes = buildManagedMemberPointerFixture();
const layout = metadataLayout(bytes);
const types = layout.table(2);
if (types === undefined) throw new Error("Fixture omitted TypeDef");
for (let row = 0; row < 3; row++) {
const fieldOffset =
types.offset +
row * types.rowSize +
4 +
2 * layout.stringIndexSize +
layout.codedIndexSize("TypeDefOrRef");
bytes.writeUInt16LE(fields[row] ?? 0, fieldOffset);
bytes.writeUInt16LE(
methods[row] ?? 0,
fieldOffset + memberListIndexSize(layout, 4),
);
}
const target = managedPeFixtureTarget(bytes);
const members = inspectManagedMembersBytes(bytes, target);
const artifact = inspectManagedArtifactBytes(bytes, target);
for (const [table, starts] of [
["fields", fields],
["methods", methods],
] as const) {
// The fixture's pointers reorder physical rows 1,2 to list positions 2,1.
const expected = [2, 1].map((position) => {
const owner = starts.findIndex(
(start, index) =>
position >= start && position < (starts[index + 1] ?? 3),
);
return owner < 0 ? null : members.types[owner]?.full_name;
});
expect(members[table].map((member) => member.declaring_type)).toEqual(
expected,
);
}
expect(artifact.attributes[0]?.type_name).toBe(
members.methods[0]?.declaring_type,
);
},
);
it("indexes large unordered member lists once per layout", () => {
const count = 100_000;
const bytes = buildManagedPeFixture({
additionalEmptyTypes: count - 1,
methodRowCount: count,
});
const layout = metadataLayout(bytes);
const types = layout.table(2);
if (types === undefined) throw new Error("Fixture omitted TypeDef");
const methodOffset =
types.offset +
4 +
2 * layout.stringIndexSize +
layout.codedIndexSize("TypeDefOrRef") +
memberListIndexSize(layout, 4);
// The first reversed range forces first-match semantics, with the owner last.
// 100,000 MethodDef rows require a four-byte method-list index.
bytes.writeUInt32LE(2, methodOffset);
const start = performance.now();
const lookup = memberDeclaringTypeRowLookup(bytes, layout, 6);
expect(
Array.from({ length: count }, (_, row) => lookup(row + 1)).every(
(owner) => owner === count,
),
).toBe(true);
expect(performance.now() - start).toBeLessThan(2000);
expect(memberDeclaringTypeRowLookup(bytes, layout, 6)).toBe(lookup);
expect([lookup(0), lookup(count + 1)]).toEqual([0, 0]);
});
it("admits ownership storage before allocation and retains failed admission", () => {
const bytes = buildManagedPeFixture();
const layout = metadataLayout(bytes);
const budget = managedDecodeBudget(layout);
const capacityFailure = new ManagedReaderFailure({
code: "resource-limit",
scope: "metadata.representation",
offset: layout.table(2)?.offset ?? null,
detail: "No remaining ownership index capacity",
});
const admission = vi.spyOn(budget, "reserve").mockImplementation(() => {
throw capacityFailure;
});
let allocations = 0;
vi.stubGlobal(
"Uint32Array",
new Proxy(Uint32Array, {
construct(target, args, newTarget) {
allocations++;
return Reflect.construct(target, args, newTarget);
},
}),
);
try {
expect(() => memberDeclaringTypeRowLookup(bytes, layout, 6)).toThrow(
capacityFailure,
);
expect(() => memberDeclaringTypeRowLookup(bytes, layout, 6)).toThrow(
capacityFailure,
);
expect(admission).toHaveBeenCalledTimes(1);
expect(allocations).toBe(0);
} finally {
vi.unstubAllGlobals();
admission.mockRestore();
}
});
});