Files
rea/src/domain/android/androidAnalysis.ts
T
gensokyo-devandhhang 0ffed7a48c feat(android): trace callers of external DEX methods (#8154)
Resolve exact external method targets from directly encoded DEX invokes.
Preserve complete caller identity, bounded position evidence and follow-up
selectors. Report aggregate position coverage independently of the caller
inventory, and reuse bounded prototype facts within the exact session.

Absorbs #8096. Fixes #7811.

Co-authored-by: hhang <[email protected]>
2026-10-31 01:07:18 +08:00

339 lines
12 KiB
TypeScript

import { z } from "zod";
import {
absoluteLocalPathStringSchema,
localPathStringSchema,
} from "../localPath.js";
import { androidManifestComponentsSchema } from "./androidManifestComponents.js";
import {
androidViewRequestSchema,
validateAndroidPackageDelivery,
} from "./androidPackageQuery.js";
import { androidMappingReportSchema } from "./androidMapping.js";
import { dexDescriptorSchema } from "./dexTypes.js";
import {
androidJniSelectionSchema,
androidJniReportSchema,
} from "./androidJniBindings.js";
import { androidJniViewRequestSchema } from "./androidJniQuery.js";
const target = {
path: localPathStringSchema.describe(
"Local APK path; the target is never executed",
),
};
const classTarget = {
...target,
class_name: z.string().min(1).describe("Exact fully qualified class name"),
};
const mappingPath = {
mapping_path: absoluteLocalPathStringSchema
.optional()
.describe(
"Absolute local R8/ProGuard mapping.txt; names are annotated, never replaced",
),
};
/** Caller intent for static Android inspection, independent of its engine. */
export const androidInputSchemas = {
inspect_android_package: z.strictObject(target),
inspect_android_jni_bindings: androidJniSelectionSchema.extend(target),
search_android_classes: z.strictObject({
...target,
query: z
.string()
.describe("Case-sensitive class-name substring; empty lists all classes"),
}),
inspect_android_class: z.strictObject({ ...classTarget, ...mappingPath }),
inspect_android_method: z.strictObject({
...classTarget,
...mappingPath,
method_name: z.string().min(1),
overload_index: z
.int()
.nonnegative()
.optional()
.describe(
"Explicit index among same-name methods; required when ambiguous",
),
}),
trace_android_references: z.strictObject({
...classTarget,
class_name: classTarget.class_name.describe(
"Exact APK-defined class, or an externally referenced DEX class when method_name is selected",
),
method_name: z
.string()
.min(1)
.optional()
.describe(
"Omit for class references; select ambiguous methods with overload_index",
),
overload_index: z
.int()
.nonnegative()
.optional()
.describe(
"Index among same-name APK methods or observed external DEX prototypes; requires method_name",
),
}),
} as const;
/** Optional retained delivery is separate from the engine's APK request. */
export const androidPackageInspectionInputSchema =
androidInputSchemas.inspect_android_package
.extend({
detail: z.enum(["complete", "summary"]).optional(),
view: androidViewRequestSchema.optional(),
})
.superRefine(validateAndroidPackageDelivery);
/** Delivery is an adapter/application choice, never a provider analysis option. */
export const androidSymbolInspectionInputSchemas = {
inspect_android_class: androidInputSchemas.inspect_android_class.extend({
detail: z.enum(["complete", "summary"]).optional(),
}),
inspect_android_method: androidInputSchemas.inspect_android_method.extend({
detail: z.enum(["complete", "summary"]).optional(),
}),
} as const;
/** JNI selection changes analysis; delivery only selects retained views. */
export const androidJniInspectionInputSchema =
androidInputSchemas.inspect_android_jni_bindings
.extend({
detail: z.enum(["complete", "summary"]).default("summary"),
view: androidJniViewRequestSchema.optional(),
})
.superRefine(validateAndroidPackageDelivery);
/** Supported static Android operations. */
export type AndroidOperation = keyof typeof androidInputSchemas;
/** Validated Android requests carry their selected operation. */
export type AndroidRequest = {
[Name in AndroidOperation]: {
operation: Name;
input: z.infer<(typeof androidInputSchemas)[Name]>;
};
}[AndroidOperation];
/** Validate operation and input together so their types remain correlated. */
export const androidRequestSchema = z.discriminatedUnion("operation", [
z.strictObject({
operation: z.literal("inspect_android_jni_bindings"),
input: androidInputSchemas.inspect_android_jni_bindings,
}),
z.strictObject({
operation: z.literal("inspect_android_package"),
input: androidInputSchemas.inspect_android_package,
}),
z.strictObject({
operation: z.literal("search_android_classes"),
input: androidInputSchemas.search_android_classes,
}),
z.strictObject({
operation: z.literal("inspect_android_class"),
input: androidInputSchemas.inspect_android_class,
}),
z.strictObject({
operation: z.literal("inspect_android_method"),
input: androidInputSchemas.inspect_android_method,
}),
z.strictObject({
operation: z.literal("trace_android_references"),
input: androidInputSchemas.trace_android_references,
}),
]);
const textCoverage = z.strictObject({
status: z.enum(["complete", "partial"]),
text: z.string(),
reported_total_bytes: z.int().nonnegative().nullable(),
});
const integral = <Type extends string>(type: Type, min: number, max: number) =>
z.strictObject({
type: z.literal(type),
value: z.int().min(min).max(max),
});
const LONG_MIN = -(2n ** 63n);
const LONG_MAX = 2n ** 63n - 1n;
/** Finite text must name a value of the given width; only zero text may round to zero. */
const floating = <Type extends "float" | "double">(
type: Type,
round: (value: number) => number,
) =>
z.strictObject({
type: z.literal(type),
value: z
.string()
.regex(/^(?:NaN|-?Infinity|-?\d+\.\d+(?:E-?\d+)?)$/u)
.refine((text) => {
if (/^(?:NaN|-?Infinity)$/u.test(text)) return true;
const rounded = round(Number(text));
return (
Number.isFinite(rounded) &&
(rounded !== 0 || /^-?0\.0+(?:E-?\d+)?$/u.test(text))
);
}),
});
/** A static final field's DEX static value; wide and floating values keep Java text. */
export const dexStaticValueSchema = z.discriminatedUnion("type", [
z.strictObject({ type: z.literal("boolean"), value: z.boolean() }),
integral("byte", -128, 127),
integral("short", -32_768, 32_767),
integral("char", 0, 65_535),
integral("int", -2_147_483_648, 2_147_483_647),
z.strictObject({
type: z.literal("long"),
value: z
.string()
.regex(/^(?:0|-?[1-9]\d*)$/u)
.refine((text) => BigInt(text) >= LONG_MIN && BigInt(text) <= LONG_MAX),
}),
floating("float", Math.fround),
floating("double", (value) => value),
z.strictObject({ type: z.literal("string"), value: z.string() }),
]);
const method = z.strictObject({
name: z.string(),
reported_signature: z.string(),
overload_index: z.int().nonnegative(),
is_constructor: z.boolean(),
dex_descriptor: dexDescriptorSchema,
});
/** Exact member selector shared with follow-up APK method inspection. */
export const androidMethodSelectorSchema = z.strictObject({
class_name: z.string().min(1),
method_name: z.string().min(1),
overload_index: z.int().nonnegative(),
});
const engine = z.strictObject({
name: z.string().min(1),
version: z.string().nullable(),
artifact_sha256: z.string().regex(/^[a-f0-9]{64}$/u),
source_revision: z.string().nullable(),
worker_count: z.int().positive(),
heap_limit_mib: z.int().positive().nullable(),
});
const common = { engine };
/** Portable observations with explicit decompiler and identity limitations. */
export const androidResultSchemas = {
inspect_android_jni_bindings: androidJniReportSchema.extend(common),
inspect_android_package: z.strictObject({
...common,
package_name: z.string().nullable(),
version_name: z.string().nullable(),
version_code: z.string().nullable(),
min_sdk: z.string().nullable(),
target_sdk: z.string().nullable(),
permissions: z.array(z.string()),
manifest: textCoverage,
// Evidence retained before the component projection existed re-parses
// with an explicit unavailable inventory; current results always carry it.
components: androidManifestComponentsSchema.default(() => ({
status: "unavailable" as const,
items: [],
limitations: [
"This Evidence predates the manifest component projection; run inspect_android_package again to obtain it.",
],
})),
class_count: z.int().nonnegative(),
resource_count: z.int().nonnegative(),
signature_verification: z.literal("not_performed"),
}),
search_android_classes: z.strictObject({
...common,
query: z.string(),
total_classes: z.int().nonnegative(),
matches: z.array(z.string()),
duplicate_names: z.array(
z.strictObject({
name: z.string(),
occurrences: z.int().min(2),
}),
),
coverage: z.literal("complete"),
}),
inspect_android_class: z.strictObject({
...common,
class_name: z.string(),
methods: z.array(method),
fields: z.array(
z.strictObject({
name: z.string(),
reported_type: z.string(),
/** Null unless a static final field encodes a supported non-null value; initializers are not applied. */
dex_static_value: dexStaticValueSchema.nullable(),
}),
),
inner_classes: z.array(z.string()),
mapping: androidMappingReportSchema.exactOptional(),
}),
inspect_android_method: z.strictObject({
...common,
class_name: z.string(),
method: method,
overload_count: z.int().positive(),
representation: z.enum(["java", "smali"]),
body_status: z.enum(["available", "not_available"]),
fell_back: z.boolean(),
markers: z.array(z.string()),
source: textCoverage,
mapping: androidMappingReportSchema.exactOptional(),
}),
trace_android_references: z.strictObject({
...common,
/** Older retained Evidence omits this fact; omission does not mean external. */
target_definition: z.enum(["defined", "external"]).exactOptional(),
/** External results inventory directly encoded invokes, not reflection or dispatch. */
reference_scope: z.literal("direct_dex_invokes").exactOptional(),
class_name: z.string(),
method_name: z.string().nullable(),
overload_index: z.int().nonnegative().nullable(),
reported_signature: z.string().nullable(),
dex_descriptor: dexDescriptorSchema.nullable(),
total: z.int().nonnegative(),
/** Position coverage is separate from the complete using-node inventory.
* Historical Evidence may omit this aggregate; omission is unexamined. */
direct_invokes_coverage: z
.strictObject({
complete_using_nodes: z.int().nonnegative(),
partial_using_nodes: z.int().nonnegative(),
unknown_using_nodes: z.int().nonnegative(),
recorded_sites: z.int().nonnegative(),
})
.exactOptional(),
references: z.array(
z.strictObject({
kind: z.string(),
name: z.string(),
reported_full_name: z.string(),
containing_class: z.string(),
top_class: z.string(),
source_line: z.null(),
dex_offset: z.null(),
/** An explicit follow-up selector accepted by inspect_android_method. */
source_selector: androidMethodSelectorSchema.exactOptional(),
/** false means transport-bounded instruction evidence; null is unknown. */
direct_invokes_exhaustive: z.boolean().nullable(),
/** Raw direct invoke instructions inside this using node, when available. */
direct_invokes: z
.array(
z.strictObject({
dex_entry: z.string().min(1),
source_raw_method_id: z.string().min(1),
dex_code_unit_offset: z.int().nonnegative(),
dex_method_index: z.int().nonnegative(),
opcode: z.string().regex(/^INVOKE_/u),
}),
)
.nullable(),
}),
),
coverage: z.literal("complete"),
reference_unit: z.literal("using_node"),
}),
} as const;