From 9fe54c4073056587d4cdddd4ade06b993dc833d4 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 14 Oct 2026 09:47:29 +0800 Subject: [PATCH] fix(dotnet): honor bounded method row selections --- src/dotnet/ManagedMemberInspector.test.ts | 59 +++++++++++++++++++++++ src/dotnet/ManagedMemberInspector.ts | 16 ++++++ src/dotnet/ManagedMemberRows.ts | 17 +++++-- 3 files changed, 87 insertions(+), 5 deletions(-) diff --git a/src/dotnet/ManagedMemberInspector.test.ts b/src/dotnet/ManagedMemberInspector.test.ts index 3aee06e73..ea4c41ac7 100644 --- a/src/dotnet/ManagedMemberInspector.test.ts +++ b/src/dotnet/ManagedMemberInspector.test.ts @@ -17,6 +17,7 @@ import { signature, type ManagedMethod } from "./ManagedMemberInspectorCore.js"; import { ManagedReaderFailure } from "./ManagedReaderFailure.js"; import { managedMemberInspectionSchema } from "../domain/managed/managedArtifact.js"; import { comparisonLimitations } from "../domain/managed/managedMemberComparisonCoverage.js"; +import { buildManagedPeFixture as buildMultiMethodManagedPeFixture } from "../../scripts/fixtures/managed/pe.mjs"; import { buildManagedPeFixture, @@ -1735,3 +1736,61 @@ describe("managed member type selection", () => { expect(result.member_refs).toEqual(everything.member_refs); }); }); + +describe("managed member method paging", () => { + it("returns only the requested MethodDef rows", () => { + const image = buildMultiMethodManagedPeFixture({ + methods: [ + { name: "First", body: Buffer.from([0x2a]) }, + { name: "Second", body: Buffer.from([0x2a]) }, + { name: "Third", body: Buffer.from([0x2a]) }, + ], + }); + const all = inspectManagedMembersBytes( + image, + managedPeFixtureTarget(image), + ); + expect(all.methods.map(({ name }) => name)).toEqual([ + "First", + "Second", + "Third", + ]); + + const page = inspectManagedMembersBytes( + image, + managedPeFixtureTarget(image), + { methodOffset: 1, methodLimit: 1 }, + ); + + expect(page.methods.map(({ name, token }) => ({ name, token }))).toEqual([ + { name: "Second", token: "0x06000002" }, + ]); + expect(page.coverage.state).toBe("complete"); + + const offsetOnly = inspectManagedMembersBytes( + image, + managedPeFixtureTarget(image), + { methodOffset: 1 }, + ); + expect(offsetOnly.methods.map(({ name }) => name)).toEqual([ + "Second", + "Third", + ]); + + const emptyPage = inspectManagedMembersBytes( + image, + managedPeFixtureTarget(image), + { methodOffset: 1, methodLimit: 0 }, + ); + expect(emptyPage.methods).toEqual([]); + }); + + it("rejects invalid method page bounds", () => { + const image = buildManagedPeFixture(); + expect(() => + inspectManagedMembersBytes(image, managedPeFixtureTarget(image), { + methodOffset: -1, + }), + ).toThrow(RangeError); + }); +}); diff --git a/src/dotnet/ManagedMemberInspector.ts b/src/dotnet/ManagedMemberInspector.ts index 1b018522a..d6464b09d 100644 --- a/src/dotnet/ManagedMemberInspector.ts +++ b/src/dotnet/ManagedMemberInspector.ts @@ -160,12 +160,26 @@ export const inspectManagedMembersBytes = ( target: BinaryTarget, { typeNames, + methodOffset = 0, + methodLimit = Number.MAX_SAFE_INTEGER, userStrings: readUserStrings = true, }: { readonly typeNames?: readonly string[]; + /** Zero-based offset into MethodDef rows. */ + readonly methodOffset?: number; + readonly methodLimit?: number; readonly userStrings?: boolean; } = {}, ): ManagedMemberInspection => { + if ( + !Number.isSafeInteger(methodOffset) || + methodOffset < 0 || + !Number.isSafeInteger(methodLimit) || + methodLimit < 0 + ) + throw new RangeError( + "methodOffset and methodLimit must be non-negative safe integers", + ); const pe = readManagedPeLayout(bytes); if (pe.cli === null) return unavailable(target, bytes, pe.cliIssue, typeNames); @@ -196,6 +210,8 @@ export const inspectManagedMembersBytes = ( pe, ranges, selected, + methodOffset, + methodLimit, }); appendManagedParseIssues(issues, fields.issues, methods.issues); // #US literals sit beside the member inventory and carry their own diff --git a/src/dotnet/ManagedMemberRows.ts b/src/dotnet/ManagedMemberRows.ts index 700f723e6..0f2bcc633 100644 --- a/src/dotnet/ManagedMemberRows.ts +++ b/src/dotnet/ManagedMemberRows.ts @@ -257,6 +257,8 @@ interface ParseMethodsInput { readonly pe: ManagedPeLayout; readonly ranges: readonly TypeRange[]; readonly selected?: SelectedTypeTokens; + readonly methodOffset?: number; + readonly methodLimit?: number; } export const parseMethods = ({ @@ -265,6 +267,8 @@ export const parseMethods = ({ pe, ranges, selected = null, + methodOffset = 0, + methodLimit = Number.MAX_SAFE_INTEGER, }: ParseMethodsInput): { readonly methods: readonly ManagedMethod[]; readonly core: ReadonlyMap; @@ -292,12 +296,14 @@ export const parseMethods = ({ ? undefined : readManagedValue(() => ownerRow(row), issues); const declared = owner === undefined ? undefined : ranges[owner - 1]; + const inPage = row > methodOffset && row - methodOffset <= methodLimit; const mayBeSelected = - selected === null || - owner === undefined || - isSelected(selected, declared) || - // A type whose name is unreadable may be one the caller requested. - declared?.fullName === null; + inPage && + (selected === null || + owner === undefined || + isSelected(selected, declared) || + // A type whose name is unreadable may be one the caller requested. + declared?.fullName === null); const produced = methods.length; readManagedValue(() => { const cursor = metadataRowCursor(bytes, layout, 6, row); @@ -318,6 +324,7 @@ export const parseMethods = ({ kind: "method", } as const; core.set(token, member); + if (!inPage) return; if (!isSelected(selected, declared)) return; const sig = readMetadataBlob( bytes,