mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
2d84c800bc
Filter class names at the native owner before serialization, and resume matching pages through contiguous inventory positions without a second index or query-result cache. Preserve selected source Evidence, query identity, complete top-level coverage and shared CLI/MCP behavior. Fixes #4776. Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
167 lines
4.8 KiB
TypeScript
167 lines
4.8 KiB
TypeScript
import { z } from "zod";
|
|
import { AnalysisOutputError } from "../domain/analysisErrorCore.js";
|
|
import { jsonValueSchema } from "../domain/jsonValue.js";
|
|
import type { AndroidOperation } from "../domain/android/androidAnalysis.js";
|
|
import { dexDescriptorSchema } from "../domain/android/androidAnalysis.js";
|
|
|
|
const count = z.number().int().nonnegative();
|
|
/** Selector failures authored by REA's bridge, separate from arbitrary engine errors. */
|
|
export const jadxInputFailureSchema = z.strictObject({
|
|
kind: z.literal("invalid-input"),
|
|
field: z.enum(["class_name", "method_name", "overload_index"]),
|
|
message: z.string().min(1),
|
|
});
|
|
/** Values observed inside the exact JVM producing the analysis. */
|
|
export const jadxRuntimeSchema = z.object({
|
|
engine_reported_version: z.string().min(1),
|
|
max_heap_bytes: z.number().int().positive().safe(),
|
|
available_processors: z.number().int().positive(),
|
|
java_version: z.string().min(1),
|
|
metadata_scope: z.literal("parsed_members"),
|
|
});
|
|
export const jadxLoadSchema = z.object({
|
|
state: z.literal("LOADED"),
|
|
apk_path: z.string(),
|
|
class_count: count,
|
|
resource_count: count,
|
|
threads: z.literal(1),
|
|
resources: z.literal("full"),
|
|
});
|
|
export const jadxAppSchema = z.object({
|
|
package: z.string(),
|
|
version_name: z.string(),
|
|
version_code: z.string(),
|
|
min_sdk: z.string(),
|
|
target_sdk: z.string(),
|
|
permissions: z.array(z.string()),
|
|
});
|
|
export const jadxClassPageSchema = z.object({
|
|
scope: z.literal("top_level"),
|
|
total_classes: count,
|
|
next_offset: count,
|
|
offset: count,
|
|
limit: count,
|
|
items: z.array(z.string()),
|
|
});
|
|
export const jadxClassSchema = z.object({
|
|
full_name: z.string(),
|
|
name: z.string(),
|
|
method_count: count,
|
|
field_count: count,
|
|
inner_class_count: count,
|
|
methods: z.array(
|
|
z.object({
|
|
name: z.string(),
|
|
signature: z.string(),
|
|
dex_descriptor: dexDescriptorSchema,
|
|
is_constructor: z.boolean(),
|
|
def_pos: z.number().int(),
|
|
}),
|
|
),
|
|
fields: z.array(
|
|
z.object({ name: z.string(), type: z.string(), def_pos: z.number().int() }),
|
|
),
|
|
inner_classes: z.array(z.string()),
|
|
});
|
|
export const jadxMethodSchema = z.object({
|
|
class_name: z.string(),
|
|
method_name: z.string(),
|
|
full_name: z.string(),
|
|
overload_index: count,
|
|
overload_count: count,
|
|
mode: z.enum(["java", "smali"]),
|
|
fell_back: z.boolean(),
|
|
markers: z.array(z.string()),
|
|
body: z.string(),
|
|
});
|
|
export const jadxXrefsSchema = z.object({
|
|
target: z.string(),
|
|
overload_index: count.optional(),
|
|
reported_signature: z.string().optional(),
|
|
total: count,
|
|
count,
|
|
resolve_line: z.literal(false),
|
|
items: z.array(
|
|
z.object({
|
|
kind: z.string(),
|
|
name: z.string(),
|
|
full_name: z.string(),
|
|
containing_class: z.string(),
|
|
top_class: z.string(),
|
|
def_pos: z.number().int(),
|
|
direct_invokes_exhaustive: z.boolean().optional(),
|
|
direct_invokes: z
|
|
.array(
|
|
z.object({
|
|
dex_entry: z.string().min(1),
|
|
source_raw_method_id: z.string().min(1),
|
|
dex_code_unit_offset: count,
|
|
dex_method_index: count,
|
|
opcode: z.string().regex(/^INVOKE_/u),
|
|
}),
|
|
)
|
|
.optional(),
|
|
}),
|
|
),
|
|
});
|
|
const toolResponse = z.object({
|
|
content: z
|
|
.array(z.object({ type: z.literal("text"), text: z.string() }))
|
|
.length(1),
|
|
isError: z.boolean().optional(),
|
|
});
|
|
|
|
/** Parse the upstream text-only envelope, preserving errors at the producing boundary. */
|
|
export const parseJadxEnvelope = (
|
|
response: unknown,
|
|
operation: AndroidOperation,
|
|
): { text: string; failed: boolean } => {
|
|
const parsed = toolResponse.safeParse(response);
|
|
if (!parsed.success || parsed.data.content[0] === undefined)
|
|
throw new AnalysisOutputError(
|
|
operation,
|
|
"JADX returned an unsupported MCP content envelope",
|
|
);
|
|
return {
|
|
text: parsed.data.content[0].text,
|
|
failed: parsed.data.isError === true,
|
|
};
|
|
};
|
|
|
|
/** Decode an upstream JSON text payload without trusting its contents. */
|
|
export const parseJadxJson = (
|
|
text: string,
|
|
operation: AndroidOperation,
|
|
): z.infer<typeof jsonValueSchema> => {
|
|
try {
|
|
return jsonValueSchema.parse(JSON.parse(text));
|
|
} catch (cause) {
|
|
throw new AnalysisOutputError(
|
|
operation,
|
|
"JADX returned malformed JSON text",
|
|
{ cause },
|
|
);
|
|
}
|
|
};
|
|
|
|
/** Upstream byte truncation is reported rather than promoted to a complete source. */
|
|
export const normalizeJadxText = (
|
|
text: string,
|
|
): {
|
|
status: "complete" | "partial";
|
|
text: string;
|
|
reported_total_bytes: number | null;
|
|
} => {
|
|
const marker =
|
|
/\n\n\.\.\. \[truncated: exceeds (\d+) bytes, total (\d+) bytes\]$/u.exec(
|
|
text,
|
|
);
|
|
const total = marker?.[2] === undefined ? null : Number(marker[2]);
|
|
return {
|
|
status: marker === null ? "complete" : "partial",
|
|
text,
|
|
reported_total_bytes:
|
|
total !== null && Number.isSafeInteger(total) ? total : null,
|
|
};
|
|
};
|