refactor(ida): consolidate pagination progress checks

This commit is contained in:
morluto committed 2026-10-27 12:49:07 +08:00
1 parent 8f894546c8
commit 21b6ea7fbd
3 files changed
+58 -23

No files matched your search

+44
View File
@@ -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 });
}
},
);
+3 -10
View File
@@ -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;
}
+11 -13
View File
@@ -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<typeof cursorSchema>,
/** 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<typeof cursorSchema>,
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(),