fix(android): report DEX static values of static final fields (#6981)

* fix(android): report DEX static values of static final fields

Fixes #6397

* fix(android): request DEX static values only for class inspection

* fix(android): range-check wide DEX static values

* fix(android): move the bridge to version 7 after main took 6

---------

Co-authored-by: Abelo9996 <Abelo9996@users.noreply.github.com>
This commit is contained in:
Abel YagubyanandAbelo9996 authored and GitHub committed 2026-10-25 02:17:13 +08:00
1 parent fcebd79ed8
commit 955c7c80ec
12 files changed
+451 -17

No files matched your search

+68 -10
View File
@@ -3,6 +3,8 @@ import com.google.gson.*;
import io.modelcontextprotocol.kotlin.sdk.server.*;
import io.modelcontextprotocol.kotlin.sdk.types.*;
import jadx.api.*;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.api.plugins.input.data.attributes.JadxAttrType;
import jadx.api.plugins.input.data.ICodeReader;
import jadx.api.plugins.input.data.IMethodRef;
import jadx.api.plugins.input.insns.InsnIndexType;
@@ -40,7 +42,8 @@ class ReaJadxBridge {
private record MethodSnapshot(String name, String rawName, String shortId, List<String> bodyAbsenceMarkers,
Map<String, Object> summary) {}
private record ClassSnapshot(JavaClass node, List<MethodSnapshot> methods, Map<String, Object> summary) {}
private record ClassSnapshot(JavaClass node, List<MethodSnapshot> methods, Map<String, Object> summary,
Map<String, Object> summaryWithValues) {}
private record ManifestSnapshot(String text, Map<String, Object> summary) {}
private static final class InputFailure extends IllegalArgumentException {
@@ -142,6 +145,14 @@ class ReaJadxBridge {
return value.getAsString();
}
private static boolean flag(JsonObject args, String name) {
JsonElement value = args.get(name);
if (value == null) return false;
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isBoolean())
throw new IllegalArgumentException(name + " must be a boolean");
return value.getAsBoolean();
}
private static int integer(JsonObject args, String name, int fallback) {
JsonElement value = args.get(name);
if (value == null) return fallback;
@@ -263,17 +274,38 @@ class ReaJadxBridge {
"dex_descriptor", descriptor,
"is_constructor", method.isConstructor(), "def_pos", 0));
}).toList();
List<Map<String, Object>> fields = node.getFields().stream()
.sorted(Comparator.comparing(FieldNode::getAlias))
.map(field -> Map.<String, Object>of("name", field.getAlias(),
"type", field.getType().toString(), "def_pos", 0)).toList();
List<FieldNode> orderedFields = new ArrayList<>(node.getFields());
orderedFields.sort(Comparator.comparing(FieldNode::getAlias));
List<Map<String, Object>> fields = new ArrayList<>();
List<Map<String, Object>> valuedFields = new ArrayList<>();
boolean anyValue = false;
for (FieldNode field : orderedFields) {
Map<String, Object> entry = Map.of("name", field.getAlias(),
"type", field.getType().toString(), "def_pos", 0);
fields.add(entry);
Map<String, Object> value = dexStaticValue(field);
if (value == null) valuedFields.add(entry);
else {
anyValue = true;
Map<String, Object> valued = new LinkedHashMap<>(entry);
valued.put("dex_static_value", value);
valuedFields.add(Collections.unmodifiableMap(valued));
}
}
List<String> inner = node.getInnerClasses().stream().map(ClassNode::getFullName).toList();
Map<String, Object> summary = Map.of(
"full_name", cls.getFullName(), "raw_name", cls.getRawName(), "name", cls.getName(),
"method_count", methods.size(), "field_count", fields.size(),
"inner_class_count", inner.size(), "inner_classes", inner,
"methods", methods.stream().map(MethodSnapshot::summary).toList(), "fields", fields);
ClassSnapshot snapshot = new ClassSnapshot(cls, methods, summary);
"methods", methods.stream().map(MethodSnapshot::summary).toList(), "fields", List.copyOf(fields));
// DEX static values are opt-in, so method and reference lookups keep the member-only reply.
Map<String, Object> summaryWithValues = summary;
if (anyValue) {
Map<String, Object> copy = new LinkedHashMap<>(summary);
copy.put("fields", List.copyOf(valuedFields));
summaryWithValues = Collections.unmodifiableMap(copy);
}
ClassSnapshot snapshot = new ClassSnapshot(cls, methods, summary, summaryWithValues);
index.computeIfAbsent(cls.getFullName(), ignored -> new ArrayList<>()).add(snapshot);
// Keep the binary identity supplied by JADX; '$' can also be a literal
// name character, so replacing separators would invent lookup aliases.
@@ -293,6 +325,28 @@ class ReaJadxBridge {
return candidates.get(0);
}
/** A static final field's class-definition static value; a static initializer can still assign it. */
private static Map<String, Object> dexStaticValue(FieldNode field) {
if (!field.getAccessFlags().isStatic() || !field.getAccessFlags().isFinal()) return null;
EncodedValue encoded = field.get(JadxAttrType.CONSTANT_VALUE);
if (encoded == null) return null;
Object value = encoded.getValue();
// JADX also attaches the shared null to static finals without a value, so null is never reported.
// Wide and floating values keep their Java text instead of a lossy JSON number.
return switch (encoded.getType()) {
case ENCODED_BOOLEAN -> Map.of("type", "boolean", "value", value);
case ENCODED_BYTE -> Map.of("type", "byte", "value", value);
case ENCODED_SHORT -> Map.of("type", "short", "value", value);
case ENCODED_CHAR -> Map.of("type", "char", "value", (int) (Character) value);
case ENCODED_INT -> Map.of("type", "int", "value", value);
case ENCODED_LONG -> Map.of("type", "long", "value", value.toString());
case ENCODED_FLOAT -> Map.of("type", "float", "value", value.toString());
case ENCODED_DOUBLE -> Map.of("type", "double", "value", value.toString());
case ENCODED_STRING -> Map.of("type", "string", "value", value);
default -> null;
};
}
private List<MethodSnapshot> methods(ClassSnapshot cls, JsonObject args) {
String name = string(args, "method_name");
List<MethodSnapshot> result = cls.methods().stream().filter(method -> method.name().equals(name)).toList();
@@ -443,7 +497,8 @@ class ReaJadxBridge {
"items", items));
}
ClassSnapshot cls = selected(args);
if (name.equals("get_class_summary")) return json(cls.summary());
if (name.equals("get_class_summary"))
return json(flag(args, "dex_static_values") ? cls.summaryWithValues() : cls.summary());
if (name.equals("get_method_body")) {
List<MethodSnapshot> overloads = methods(cls, args);
int index = integer(args, "overload_index", 0);
@@ -526,7 +581,7 @@ class ReaJadxBridge {
SessionHolder holder = new SessionHolder(defaults);
ServerCapabilities capabilities = new ServerCapabilities(
new ServerCapabilities.Tools(null), null, null, null, null, null, null, Map.of());
Server server = new Server(new Implementation("rea-jadx-bridge", "6", null, null, null),
Server server = new Server(new Implementation("rea-jadx-bridge", "7", null, null, null),
new ServerOptions(capabilities), "", ignored -> Unit.INSTANCE);
SessionToolsKt.registerSessionTools(server, holder);
ClassToolsKt.registerClassTools(server, holder);
@@ -568,9 +623,12 @@ class ReaJadxBridge {
if (name.equals("list_classes")) properties.put("query",
new kotlinx.serialization.json.JsonObject(Map.of(
"type", kotlinx.serialization.json.JsonElementKt.JsonPrimitive("string"))));
if (name.equals("get_class_summary")) properties.put("dex_static_values",
new kotlinx.serialization.json.JsonObject(Map.of(
"type", kotlinx.serialization.json.JsonElementKt.JsonPrimitive("boolean"))));
String description = switch (name) {
case "list_classes" -> "Pages of case-sensitive matching top-level class names; offset and next_offset are inventory positions, limit bounds matching rows. Omitted or empty query inventories all names.";
case "get_class_summary" -> "Parsed member metadata for one exact class name; no source generation.";
case "get_class_summary" -> "Parsed member metadata for one exact class name; no source generation. Set dex_static_values to include encoded static final field values.";
case "get_method_body" -> "Source for an exact class and stable same-name overload index; static initializers use smali to preserve folded field initialization.";
default -> "Complete static using-node inventory for an exact class or selected method overload; one node may contain multiple invoke instructions, and source lines are unresolved.";
};
+15
View File
@@ -194,6 +194,21 @@ queries accept the same `overload_index`; the bridge resolves the selected
method node and the result reports its index and displayed signature. These
indices are tied to the current artifact and engine, not stable across builds.
`inspect_android_class` also reports `dex_static_value` for each `static final`
field whose class definition encodes a value in its DEX static values, so
compile-time constants are readable even when the class has no `<clinit>`.
Each value is typed: `boolean`, `byte`, `short`, `char` (UTF-16 code unit) and
`int` values are JSON booleans or numbers, while `long`, `float` and `double`
values are strings holding Java's text for the value, so 64-bit values stay
exact. The field reports `null` when no value is encoded, the encoded value is
null or of another kind, or the field is not `static final`. The value is read from parsed
metadata without generating source, and a static initializer can still assign
the field; use `inspect_android_method` on `<clinit>` for that evidence.
Only `inspect_android_class` asks the bridge for these values.
`inspect_android_method` and `trace_android_references` read the member-only
class summary, so large encoded strings cannot push their member lookup past
the 8 MiB protocol frame limit.
Smali fallbacks select one complete block using the original method name and DEX
descriptor, before applying the source byte limit. This preserves the selected
overload even when Java code generation renames a method. If the selected Java
+4
View File
@@ -93,6 +93,10 @@ export const verifyAndroidMetadata = async ({
result.stdout,
/MERGED_METHOD_REGRESSION_PASS exact=true overloaded=true handlers=true/u,
);
assert.match(
result.stdout,
/CONSTANT_FIELD_REGRESSION_PASS encoded=9 absent=4 opt_in=true/u,
);
assert.match(
result.stdout,
/DIRECT_INVOKE_REGRESSION_PASS nodes=1 offsets=0,3 cross_dex=true/u,
+6
View File
@@ -71,8 +71,13 @@ const classSummary = async (
{ operation: "inspect_android_package" | "search_android_classes" }
>,
): Promise<ClassSummary> => {
// Only class inspection reports DEX static values; method and reference
// lookups keep the member-only summary so large constants cannot block them.
const raw = await tools.json("get_class_summary", {
class_name: request.input.class_name,
...(request.operation === "inspect_android_class"
? { dex_static_values: true }
: {}),
});
if (
raw !== null &&
@@ -293,6 +298,7 @@ export const analyzeJadxRequest = async (
fields: summary.fields.map((field) => ({
name: field.name,
reported_type: field.type,
dex_static_value: field.dex_static_value ?? null,
})),
inner_classes: summary.inner_classes,
});
+10 -2
View File
@@ -2,7 +2,10 @@ import { z } from "zod";
import { AnalysisOutputError } from "../domain/analysisErrorCore.js";
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
import type { AndroidOperation } from "../domain/android/androidAnalysis.js";
import { dexDescriptorSchema } from "../domain/android/androidAnalysis.js";
import {
dexDescriptorSchema,
dexStaticValueSchema,
} from "../domain/android/androidAnalysis.js";
const count = z.int().nonnegative();
/** Typed allocation failures authored before upstream erases the JVM exception. */
@@ -96,7 +99,12 @@ export const jadxClassSchema = z.object({
}),
),
fields: z.array(
z.object({ name: z.string(), type: z.string(), def_pos: z.int() }),
z.object({
name: z.string(),
type: z.string(),
def_pos: z.int(),
dex_static_value: dexStaticValueSchema.optional(),
}),
),
inner_classes: z.array(z.string()),
});
+1 -1
View File
@@ -9,7 +9,7 @@ export const JADX_RELEASE = Object.freeze({
/** Identity of REA's metadata bridge, distinct from the producing engine. */
export const JADX_BRIDGE_IDENTITY = Object.freeze({
name: "rea-jadx-bridge",
version: "6",
version: "7",
});
/** Remediation when `REA_JADX_MCP_JAR` is missing or not an absolute path. */
@@ -51,7 +51,7 @@ export const ANDROID_TOOL_CONTRACTS = [
name: "inspect_android_class",
...toolContractMetadata("inspect_android_class"),
description:
"Inspect one exact Android class's parsed methods, fields and inner classes, including synthetic members. Returns display signatures and exact DEX prototypes without generating source. A prototype is not a complete raw method ID; overload indices remain consistent within the artifact and engine session.",
"Inspect one exact Android class's parsed methods, fields and inner classes, including synthetic members. Returns display signatures, exact DEX prototypes and encoded static final field values without generating source. A prototype is not a complete raw method ID; overload indices remain consistent within the artifact and engine session.",
kind: "android-provider",
inputSchema: androidInputSchemas.inspect_android_class,
outputSchema: evidenceResultOf(androidResultSchemas.inspect_android_class),
+51 -1
View File
@@ -137,6 +137,51 @@ export const dexDescriptorSchema = z
/^\((?:\[*(?:[ZBSCIJFD]|L[^;\x28\x29]+;))*\)(?:V|\[*(?:[ZBSCIJFD]|L[^;\x28\x29]+;))$/u,
);
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(),
@@ -196,7 +241,12 @@ export const androidResultSchemas = {
class_name: z.string(),
methods: z.array(method),
fields: z.array(
z.strictObject({ name: z.string(), reported_type: z.string() }),
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()),
}),
+212
View File
@@ -9,6 +9,7 @@ import {
androidInputSchemas,
androidResultSchemas,
} from "../../../src/domain/android/androidAnalysis.js";
import type { JsonValue } from "../../../src/domain/jsonValue.js";
const summary = {
full_name: "fixture.Target",
@@ -440,6 +441,217 @@ it("publishes exact DEX prototypes instead of discarding bridge metadata", async
});
});
const constantFields = [
{
name: "MAX_SIZE",
type: "int",
def_pos: 0,
dex_static_value: { type: "int", value: 1024 },
},
{
name: "PACKET_FLAG_CONFIG",
type: "long",
def_pos: 0,
dex_static_value: { type: "long", value: "-9223372036854775808" },
},
{
name: "TAG",
type: "java.lang.String",
def_pos: 0,
dex_static_value: { type: "string", value: "flags" },
},
{ name: "INSTANCE", type: "fixture.Target", def_pos: 0 },
];
const inspectClassWithFields = (fields: JsonValue[]) =>
analyzeJadxRequest(
{
listClasses: () =>
Promise.reject(new Error("unexpected class inventory call")),
json: () =>
Promise.resolve({ ...summary, field_count: fields.length, fields }),
text: () => Promise.reject(new Error("unexpected text call")),
},
{
operation: "inspect_android_class",
input: androidInputSchemas.inspect_android_class.parse({
path: "/fixture.apk",
class_name: "fixture.Target",
}),
},
{} as never,
engine,
);
it("reports DEX static values of static final fields without a static initializer", async () => {
const result = await inspectClassWithFields(constantFields);
expect(result).toMatchObject({
fields: [
{
name: "MAX_SIZE",
reported_type: "int",
dex_static_value: { type: "int", value: 1024 },
},
{
name: "PACKET_FLAG_CONFIG",
reported_type: "long",
dex_static_value: { type: "long", value: "-9223372036854775808" },
},
{
name: "TAG",
reported_type: "java.lang.String",
dex_static_value: { type: "string", value: "flags" },
},
{
name: "INSTANCE",
reported_type: "fixture.Target",
dex_static_value: null,
},
],
});
});
it.each([
{ type: "long", value: -9223372036854775808 },
{ type: "int", value: "1024" },
{ type: "double", value: 0.5 },
{ type: "enum", value: "fixture.Mode.ON" },
])(
"refuses a lossy or unsupported DEX static value $type",
async (dex_static_value) => {
await expect(
inspectClassWithFields([
{ name: "VALUE", type: "int", def_pos: 0, dex_static_value },
]),
).rejects.toThrow("JADX class summary failed or was malformed");
},
);
it.each([
{ type: "byte", value: -128 },
{ type: "byte", value: 127 },
{ type: "short", value: -32_768 },
{ type: "char", value: 65_535 },
{ type: "int", value: -2_147_483_648 },
{ type: "int", value: 2_147_483_647 },
{ type: "long", value: "-9223372036854775808" },
{ type: "long", value: "9223372036854775807" },
{ type: "float", value: "3.4028235E38" },
{ type: "float", value: "-3.4028235E38" },
{ type: "float", value: "1.4E-45" },
{ type: "float", value: "-0.0" },
{ type: "float", value: "NaN" },
{ type: "float", value: "-Infinity" },
{ type: "double", value: "1.7976931348623157E308" },
{ type: "double", value: "4.9E-324" },
{ type: "double", value: "0.0" },
{ type: "double", value: "Infinity" },
])(
"accepts the boundary DEX static value $type $value",
async (dex_static_value) => {
await expect(
inspectClassWithFields([
{ name: "VALUE", type: "int", def_pos: 0, dex_static_value },
]),
).resolves.toMatchObject({ fields: [{ dex_static_value }] });
},
);
it.each([
{ type: "byte", value: 128 },
{ type: "short", value: -32_769 },
{ type: "char", value: -1 },
{ type: "int", value: 2_147_483_648 },
{ type: "long", value: "9223372036854775808" },
{ type: "long", value: "-9223372036854775809" },
{ type: "long", value: "-0" },
{ type: "float", value: "1.0E40" },
{ type: "float", value: "3.4028236E38" },
{ type: "float", value: "1.0E-50" },
{ type: "double", value: "1.8E308" },
{ type: "double", value: "1.0E-400" },
])(
"refuses an out-of-range DEX static value $type $value",
async (dex_static_value) => {
await expect(
inspectClassWithFields([
{ name: "VALUE", type: "int", def_pos: 0, dex_static_value },
]),
).rejects.toThrow("JADX class summary failed or was malformed");
},
);
it.each([
{ operation: "inspect_android_class", input: {}, values: true },
{
operation: "inspect_android_method",
input: { method_name: "choose", overload_index: 1 },
values: false,
},
{
operation: "trace_android_references",
input: { method_name: "choose", overload_index: 1 },
values: false,
},
{ operation: "trace_android_references", input: {}, values: false },
] as const)(
"requests DEX static values for $operation only when inspecting a class",
async ({ operation, input, values }) => {
const calls: Record<string, unknown>[] = [];
const reply = (name: string): JsonValue => {
if (name === "get_class_summary") return summary;
if (name === "get_method_body")
return {
class_name: "fixture.Target",
method_name: "choose",
full_name: "fixture.Target.choose",
overload_index: 1,
overload_count: 2,
dex_descriptor: "(I)V",
mode: "java",
fell_back: false,
markers: [],
body: "void choose(int value) {}",
};
return {
target:
name === "get_xrefs_to_class"
? "fixture.Target"
: "fixture.Target.choose",
...(name === "get_xrefs_to_class"
? {}
: { overload_index: 1, reported_signature: "choose(int): void" }),
total: 0,
count: 0,
resolve_line: false,
items: [],
};
};
await analyzeJadxRequest(
{
listClasses: () =>
Promise.reject(new Error("unexpected class inventory call")),
json: (name, args) => {
calls.push({ name, ...args });
return Promise.resolve(reply(name));
},
text: () => Promise.reject(new Error("unexpected text call")),
},
{
operation,
input: { path: "/fixture.apk", class_name: "fixture.Target", ...input },
} as never,
{} as never,
engine,
);
expect(calls[0]).toEqual({
name: "get_class_summary",
class_name: "fixture.Target",
...(values ? { dex_static_values: true } : {}),
});
},
);
it("traces only the selected overloaded method and checks the producer identity", async () => {
const calls: Record<string, unknown>[] = [];
const tools: JadxToolPort = {
@@ -233,7 +233,8 @@ public class ReaJadxMetadataRegression {
Path.of(sources, "MixedInitialization.smali").toString(),
Path.of(sources, "NativeOverload.smali").toString(),
Path.of(sources, "AbstractShape.smali").toString(),
Path.of(sources, "MergedMethod.smali").toString())), "Initializer fixture assembly failed");
Path.of(sources, "MergedMethod.smali").toString(),
Path.of(sources, "ConstantFields.smali").toString())), "Initializer fixture assembly failed");
Path apk = directory.resolve("initializers.apk");
try (ZipOutputStream zip = new ZipOutputStream(Files.newOutputStream(apk))) {
zip.putNextEntry(new ZipEntry("classes.dex"));
@@ -363,6 +364,7 @@ public class ReaJadxMetadataRegression {
"Ordinary executable method no longer produces Java");
verifyNoBodyMethods(handle, bridge, holder.current());
verifyMergedMethods(handle, bridge);
verifyConstantFields(handle, bridge, holder.current());
SessionConfig bounded = new SessionConfig(96, 0, 90000L, 1, List.of(), List.of(), 64, ResourceMode.FULL);
BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
@@ -390,6 +392,69 @@ public class ReaJadxMetadataRegression {
System.out.println("INITIALIZER_REGRESSION_PASS folded=2 bounded=true");
}
private static void verifyConstantFields(Method handle, Object bridge, JadxSession session) throws Exception {
Map<String, String> encoded = Map.of(
"ENABLED", "{\"type\":\"boolean\",\"value\":true}",
"LETTER", "{\"type\":\"char\",\"value\":65}",
"MAX_SIZE", "{\"type\":\"int\",\"value\":1024}",
"PACKET_FLAG_CONFIG", "{\"type\":\"long\",\"value\":\"-9223372036854775808\"}",
"RATIO", "{\"type\":\"double\",\"value\":\"0.5\"}",
"SCALE", "{\"type\":\"float\",\"value\":\"1.5\"}",
"SMALL", "{\"type\":\"byte\",\"value\":127}",
"TAG", "{\"type\":\"string\",\"value\":\"flags\"}",
"WIDE", "{\"type\":\"short\",\"value\":32767}");
// Null, unencoded, non-final and instance fields have no reportable DEX static value.
Set<String> absent = Set.of("NOTHING", "INSTANCE", "COUNTER", "width");
JsonObject valuesInput = arguments("fixture.ConstantFields");
valuesInput.addProperty("dex_static_values", true);
JsonObject summary = call(handle, bridge, "get_class_summary", valuesInput);
ClassNode node = session.getClasses().stream()
.filter(cls -> cls.getFullName().equals("fixture.ConstantFields"))
.findFirst().orElseThrow().getClassNode();
require(node.getState() == ProcessState.NOT_LOADED, "Constant field metadata generated source");
JsonArray fields = summary.getAsJsonArray("fields");
require(fields.size() == encoded.size() + absent.size(), "Constant field inventory changed: " + fields);
for (JsonElement entry : fields) {
JsonObject field = entry.getAsJsonObject();
String name = field.get("name").getAsString();
require(encoded.containsKey(name) || absent.contains(name), "Unexpected field: " + name);
JsonElement value = field.get("dex_static_value");
require(encoded.containsKey(name) ? JsonParser.parseString(encoded.get(name)).equals(value) : value == null,
"Wrong DEX static value for " + name + ": " + field);
}
// Fields assigned only by <clinit> carry a JADX null placeholder, not a DEX value.
for (String[] assigned : new String[][] {
{ "fixture.InitializationPolicy", "POLICY" }, { "fixture.MixedInitialization", "LABEL" } }) {
JsonObject assignedInput = arguments(assigned[0]);
assignedInput.addProperty("dex_static_values", true);
for (JsonElement entry : call(handle, bridge, "get_class_summary", assignedInput)
.getAsJsonArray("fields")) {
JsonObject field = entry.getAsJsonObject();
if (field.get("name").getAsString().equals(assigned[1]))
require(!field.has("dex_static_value"), "Initializer-assigned field reported a value: " + field);
}
}
require(summary.equals(call(handle, bridge, "get_class_summary", valuesInput)),
"Repeated constant field summary changed");
// Method and reference lookups omit the flag and must get the member-only summary.
JsonObject members = call(handle, bridge, "get_class_summary", arguments("fixture.ConstantFields"));
JsonObject stripped = summary.deepCopy();
for (JsonElement entry : stripped.getAsJsonArray("fields")) entry.getAsJsonObject().remove("dex_static_value");
require(members.equals(stripped), "Default class summary carried DEX static values: " + members);
JsonObject explicitOff = arguments("fixture.ConstantFields");
explicitOff.addProperty("dex_static_values", false);
require(members.equals(call(handle, bridge, "get_class_summary", explicitOff)),
"dex_static_values=false returned DEX static values");
JsonObject malformed = arguments("fixture.ConstantFields");
malformed.addProperty("dex_static_values", "true");
boolean refused = false;
try { call(handle, bridge, "get_class_summary", malformed); }
catch (AssertionError failure) { refused = failure.getCause() instanceof IllegalArgumentException; }
require(refused, "Non-boolean dex_static_values was accepted");
System.out.println("CONSTANT_FIELD_REGRESSION_PASS encoded=" + encoded.size() + " absent=" + absent.size()
+ " opt_in=true");
}
private static void verifyMergedMethods(Method handle, Object bridge) throws Exception {
JsonObject input = methodArguments("fixture.MergedMethod", "highlightText");
JsonObject body = call(handle, bridge, "get_method_body", input);
@@ -0,0 +1,16 @@
.class public Lfixture/ConstantFields;
.super Ljava/lang/Object;
.field public static final ENABLED:Z = true
.field public static final LETTER:C = 'A'
.field public static final MAX_SIZE:I = 0x400
.field public static final PACKET_FLAG_CONFIG:J = -0x8000000000000000L
.field public static final RATIO:D = 0.5
.field public static final SCALE:F = 1.5f
.field public static final SMALL:B = 0x7ft
.field public static final TAG:Ljava/lang/String; = "flags"
.field public static final WIDE:S = 0x7fffs
.field public static final NOTHING:Ljava/lang/String; = null
.field public static final INSTANCE:Lfixture/ConstantFields;
.field public static COUNTER:I = 0x7
.field public final width:I
@@ -320,7 +320,7 @@ it("preserves executable arguments through the public runner", async () => {
it("preserves command stderr, warnings and exit status independently of supervisor notices", async () => {
const directory = await createTestTempDirectory("rea-runner-diagnostics-");
const stderr = 'authored command stderr: \"quotes\" & 雪\n';
const stderr = 'authored command stderr: "quotes" & 雪\n';
const result = await execute(directory, process.execPath, [
"-e",
`