From 21b6ea7fbdc441d391dfa0b83fb69ee0b21ade38 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:49:07 +0800 Subject: [PATCH] refactor(ida): consolidate pagination progress checks --- src/ida/IdaOperationRunner.test.ts | 44 ++++++++++++++++++++++++++++++ src/ida/IdaOperationRunner.ts | 13 ++------- src/ida/IdaProtocolValues.ts | 24 ++++++++-------- 3 files changed, 58 insertions(+), 23 deletions(-) diff --git a/src/ida/IdaOperationRunner.test.ts b/src/ida/IdaOperationRunner.test.ts index aacd1a9553..bd932e8425 100644 --- a/src/ida/IdaOperationRunner.test.ts +++ b/src/ida/IdaOperationRunner.test.ts @@ -188,3 +188,47 @@ it.each(["legacy", "modern", "native"] as const)( } }, ); + +it.each([ + ["legacy", "functions"], + ["modern", "functions"], + ["native", "functions"], + ["legacy", "strings"], + ["modern", "strings"], + ["native", "strings"], +] as const)( + "%s %s inventory rejects a repeated or backward offset before another request", + async (protocol, inventory) => { + const { root, target } = await createIdaTarget(); + const producer = new RecordingIdaMcp(target); + try { + for (const next of [2, 1]) { + let pages = 0; + producer.overrideCall = (tool) => { + if ( + ![ + "list_functions", + "list_funcs", + "list_strings", + "find_regex", + ].includes(tool) + ) + return undefined; + const nextOffset = pages++ === 0 ? 2 : next; + if (tool === "find_regex") + return { matches: [], cursor: { next: nextOffset } }; + const page = { data: [], next_offset: nextOffset }; + return tool === "list_funcs" ? [page] : page; + }; + const runner = new IdaOperationRunner(producer, protocol); + await expect(runner[inventory]()).rejects.toMatchObject({ + _tag: "AnalysisProtocolError", + }); + expect(pages).toBe(2); + } + } finally { + await producer.close(); + await rm(root, { recursive: true, force: true }); + } + }, +); diff --git a/src/ida/IdaOperationRunner.ts b/src/ida/IdaOperationRunner.ts index a447057362..41df1f2741 100644 --- a/src/ida/IdaOperationRunner.ts +++ b/src/ida/IdaOperationRunner.ts @@ -54,6 +54,7 @@ import { modernPageSchema, modernXrefsSchema, nextOffset, + nextPageOffset, surveySegmentsSchema, type IdaDisassembly, type IdaFunction, @@ -160,11 +161,7 @@ export class IdaOperationRunner { if (value === undefined) throw new AnalysisProtocolError("IDA MCP omitted its function page."); result.push(...value.data.map((fn) => idaFunctionSchema.parse(fn))); - offset = value.next_offset; - if (offset !== null && offset <= current) - throw new AnalysisProtocolError( - "IDA MCP function pagination did not advance.", - ); + offset = nextPageOffset(value.next_offset, current); } return result; } @@ -349,7 +346,7 @@ export class IdaOperationRunner { const s = legacyStringSchema.parse(item); result.push({ address: s.address, value: s.string }); } - offset = page.next_offset; + offset = nextPageOffset(page.next_offset, current); } else { const page = idaStringsPageSchema.parse( await this.call("find_regex", { @@ -370,10 +367,6 @@ export class IdaOperationRunner { ); offset = nextOffset(page.cursor, current); } - if (offset !== null && offset <= current) - throw new AnalysisProtocolError( - "IDA MCP string pagination did not advance.", - ); } return result; } diff --git a/src/ida/IdaProtocolValues.ts b/src/ida/IdaProtocolValues.ts index 7faf6915b1..509c2cce9d 100644 --- a/src/ida/IdaProtocolValues.ts +++ b/src/ida/IdaProtocolValues.ts @@ -61,30 +61,28 @@ export const cursorSchema = z.union([ z.object({ next: z.int().nonnegative() }), z.object({ done: z.literal(true) }), ]); -export const nextOffset = ( - cursor: z.infer, +/** Both legacy pages and modern cursors must finish or advance strictly. */ +export const nextPageOffset = ( + next: number | null, current: number, ): number | null => { - if ("done" in cursor) return null; - if (cursor.next <= current) + if (next !== null && next <= current) throw new AnalysisProtocolError( "IDA MCP pagination did not advance its offset.", ); - return cursor.next; + return next; }; +export const nextOffset = ( + cursor: z.infer, + current: number, +): number | null => + nextPageOffset("done" in cursor ? null : cursor.next, current); export const legacyPageSchema = z.object({ data: z.array(jsonValueSchema), next_offset: z.int().nonnegative().nullable(), }); -export const modernPageSchema = z - .array( - z.object({ - data: z.array(jsonValueSchema), - next_offset: z.int().nonnegative().nullable(), - }), - ) - .length(1); +export const modernPageSchema = z.array(legacyPageSchema).length(1); const modernLookupRowSchema = z.object({ query: z.string(),