Files
reverse-engineering-anything/src/android/JadxProtocol.ts
T
郭立 Guo Li 7590b50801 perf(schema): reduce retained integer validator allocations (#4360)
Construct integer formats directly while preserving bounds, refinements, metadata and current shared schema owners. Keep Incur-dependent CLI numeric coercion declarations unchanged.

Verify canonical advertisements, 126 numeric boundary cases, 974 actual CLI parser cases, broad source tests and real public-artifact SDK workflows. Record 72 normal tasks and 12 separate owned-inspector heap diagnostics with whole-family resource bounds; distinguish SDK spans and asymmetric audit overhead. Generate numerical report sections from receipts and reject stale tables through documentation conformance checks.

Closes #6236. Current-head CI and Greptile pass; actionable review findings are addressed. Codex review was unavailable due repository usage limits.
2026-10-22 05:11:08 +00:00

194 lines
5.7 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.int().nonnegative();
/** Typed allocation failures authored before upstream erases the JVM exception. */
const jadxMemoryFailureSchema = z.strictObject({
kind: z.literal("jvm-memory-failure"),
memory_area: z.enum(["heap", "unknown"]),
exception_type: z.literal("java.lang.OutOfMemoryError"),
message: z.string().optional(),
max_heap_bytes: z.int().positive().safe(),
});
/** Selector failures authored by REA's bridge, separate from arbitrary engine errors. */
const jadxInputFailureSchema = z.strictObject({
kind: z.literal("invalid-input"),
field: z.enum(["class_name", "method_name", "overload_index"]),
message: z.string().min(1),
});
const bridgeFailureSchema = z.discriminatedUnion("kind", [
jadxInputFailureSchema,
jadxMemoryFailureSchema,
]);
/** Arbitrary upstream failure text stays text unless it matches a named bridge contract. */
export const parseJadxFailure = (
text: string,
): z.infer<typeof bridgeFailureSchema> | undefined => {
let candidate: unknown;
try {
candidate = JSON.parse(text);
} catch {
return undefined;
}
const parsed = bridgeFailureSchema.safeParse(candidate);
return parsed.success ? parsed.data : undefined;
};
/** 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.int().positive().safe(),
available_processors: z.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.int(),
}),
),
fields: z.array(
z.object({ name: z.string(), type: z.string(), def_pos: z.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,
dex_descriptor: dexDescriptorSchema,
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.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,
};
};