mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
fix(android): preserve executable static initializer evidence (#1876)
Return the exact static initializer disassembly before Java projection can lose field assignments. Retain byte limits and representation markers, and verify fully folded and mixed initialization through the real CLI/MCP lane. Closes #1846 Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Co-authored-by: Kaoru0822 <[email protected]>
This commit is contained in:
co-authored by
factory-droid[bot]
Kaoru0822
parent
ea40f717b2
commit
efbc25fbea
@@ -126,6 +126,20 @@ class ReaJadxBridge {
|
||||
return result;
|
||||
}
|
||||
|
||||
private JadxSession.SmartCode methodBody(JadxSession current, ClassSnapshot cls,
|
||||
MethodSnapshot selected, JavaMethod method) {
|
||||
if (!selected.shortId().equals("<clinit>()V"))
|
||||
return current.getMethodBodySmart(method, current.getMaxSourceBytes(), true);
|
||||
// Java code generation can fold all or part of <clinit> into field declarations.
|
||||
// Its method text cannot establish coverage, even when a static block remains.
|
||||
JadxSession.MethodSmali smali = current.getMethodSmali(cls.node(), method.getName());
|
||||
if (!smali.getFound() || smali.getBlocks().size() != 1)
|
||||
throw new IllegalArgumentException("Static initializer disassembly requires one exact method: " + selected.shortId());
|
||||
return new JadxSession.SmartCode(
|
||||
TextUtilKt.truncateToBytes(smali.getBlocks().get(0), current.getMaxSourceBytes()),
|
||||
"smali", true, List.of("static_initializer_disassembly"));
|
||||
}
|
||||
|
||||
private CallToolResult handle(String name, JsonObject args) {
|
||||
if (name.equals("rea_jvm_status")) return json(Map.of(
|
||||
"engine_reported_version", BuildInfo.VERSION,
|
||||
@@ -150,7 +164,7 @@ class ReaJadxBridge {
|
||||
int index = integer(args, "overload_index", 0);
|
||||
if (index >= overloads.size()) throw new InputFailure("overload_index", "overload_index out of range: " + index);
|
||||
JavaMethod method = method(cls, overloads.get(index));
|
||||
JadxSession.SmartCode code = current.getMethodBodySmart(method, current.getMaxSourceBytes(), true);
|
||||
JadxSession.SmartCode code = methodBody(current, cls, overloads.get(index), method);
|
||||
return json(Map.of("class_name", cls.summary().get("full_name"),
|
||||
"method_name", overloads.get(index).name(), "full_name", method.getFullName(),
|
||||
"overload_index", index, "overload_count", overloads.size(),
|
||||
@@ -211,7 +225,7 @@ class ReaJadxBridge {
|
||||
String description = switch (name) {
|
||||
case "list_classes" -> "Indexed pages of all top-level class names, with offset and limit.";
|
||||
case "get_class_summary" -> "Parsed member metadata for one exact class name; no source generation.";
|
||||
case "get_method_body" -> "Source for an exact class and stable same-name overload index, with smali fallback.";
|
||||
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 references to an exact class or unique method; source lines are unresolved.";
|
||||
};
|
||||
Tool narrowed = new Tool(name, new ToolSchema(null, new JsonObject(properties),
|
||||
|
||||
@@ -94,6 +94,12 @@ fallback for an overloaded method instead of attributing all bodies to one
|
||||
selected overload. Native/abstract methods with no decompiled body report
|
||||
`body_status: "not_available"` without inferring which implementation flag caused it.
|
||||
|
||||
Static initializers (`<clinit>`) return smali with the
|
||||
`static_initializer_disassembly` marker. Java decompilation can move their
|
||||
instructions into field declarations, leaving an empty or incomplete method
|
||||
body. The selected method's disassembly preserves those instructions and retains
|
||||
the same source byte limit and explicit truncation reporting.
|
||||
|
||||
## Interpret the result
|
||||
|
||||
- Package inspection retains decoded manifest XML, summary declarations,
|
||||
|
||||
@@ -7,13 +7,16 @@ import { promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { parseEvidence } from "../../../dist/domain/evidence.js";
|
||||
import { hashAndroidFile } from "../../../dist/android/AndroidTargetSnapshot.js";
|
||||
import { hashAndroidFile } from "./digest.mjs";
|
||||
import { JADX_RELEASE } from "../../../dist/android/JadxRelease.js";
|
||||
import fixture from "../../fixtures/android/apidemos.json" with { type: "json" };
|
||||
import { verifyAndroidCliCancellation } from "./cli-cancellation.mjs";
|
||||
import { verifyAndroidMetadata } from "./metadata.mjs";
|
||||
import { verifyAndroidFailureRecovery } from "./failure-recovery.mjs";
|
||||
import { resolveJadxConfiguration } from "../../../dist/android/JadxConfiguration.js";
|
||||
import {
|
||||
resolveJadxConfigurationInputs,
|
||||
validateJadxConfiguration,
|
||||
} from "../../../dist/android/JadxConfiguration.js";
|
||||
import { verifyWindowsAndroidLifecycle } from "./windows-lifecycle.mjs";
|
||||
|
||||
const repository = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||
@@ -35,10 +38,16 @@ assert.equal(
|
||||
JADX_RELEASE.sha256,
|
||||
"Real Android lane requires the audited headless JADX release JAR",
|
||||
);
|
||||
const { java, jvmArguments } = await resolveJadxConfiguration(
|
||||
const configuration = await resolveJadxConfigurationInputs(
|
||||
process.env,
|
||||
"inspect_android_package",
|
||||
);
|
||||
await validateJadxConfiguration(
|
||||
configuration,
|
||||
process.env,
|
||||
"inspect_android_package",
|
||||
);
|
||||
const { java, jvmArguments } = configuration;
|
||||
const execute = promisify(execFile);
|
||||
await execute(java, ["-version"], { timeout: 10_000 });
|
||||
const entrypoint =
|
||||
@@ -52,6 +61,8 @@ await verifyAndroidMetadata({
|
||||
repository,
|
||||
execute,
|
||||
jvmArguments,
|
||||
entrypoint,
|
||||
environment,
|
||||
bridge: resolve(dirname(entrypoint), "../bridge/android/ReaJadxBridge.java"),
|
||||
});
|
||||
const tasks = [
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { createReadStream } from "node:fs";
|
||||
|
||||
/** Verify artifact identity without depending on private runtime snapshot helpers. */
|
||||
export const hashAndroidFile = async (path) => {
|
||||
const hash = createHash("sha256");
|
||||
for await (const bytes of createReadStream(path)) hash.update(bytes);
|
||||
return hash.digest("hex");
|
||||
};
|
||||
@@ -0,0 +1,130 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { parseEvidence } from "../../../dist/domain/evidence.js";
|
||||
import { hashAndroidFile } from "./digest.mjs";
|
||||
|
||||
/** Verify folded field assignments and remaining static-block calls through both public adapters. */
|
||||
export const verifyAndroidInitializers = async ({
|
||||
apk,
|
||||
repository,
|
||||
entrypoint,
|
||||
environment,
|
||||
execute,
|
||||
}) => {
|
||||
const digest = await hashAndroidFile(apk);
|
||||
const cases = [
|
||||
{
|
||||
class_name: "fixture.InitializationPolicy",
|
||||
witnesses: [
|
||||
"declared-policy",
|
||||
"->singletonList",
|
||||
"->POLICY:Ljava/util/List;",
|
||||
],
|
||||
},
|
||||
{
|
||||
class_name: "fixture.MixedInitialization",
|
||||
witnesses: ["field-value", "->LABEL:Ljava/lang/String;", "->touch()V"],
|
||||
},
|
||||
];
|
||||
const check = (value, scenario) => {
|
||||
const evidence = parseEvidence(value);
|
||||
assert.equal(evidence.subject.digest.sha256, digest);
|
||||
const result = evidence.normalized_result;
|
||||
assert.equal(result.class_name, scenario.class_name);
|
||||
assert.equal(result.method.name, "<clinit>");
|
||||
assert.equal(result.overload_count, 1);
|
||||
assert.equal(result.body_status, "available");
|
||||
assert.equal(result.representation, "smali");
|
||||
assert.equal(result.fell_back, true);
|
||||
assert.ok(result.markers.includes("static_initializer_disassembly"));
|
||||
assert.equal(result.source.status, "complete");
|
||||
for (const witness of scenario.witnesses)
|
||||
assert.ok(
|
||||
result.source.text.includes(witness),
|
||||
`Missing initializer evidence: ${witness}`,
|
||||
);
|
||||
return evidence;
|
||||
};
|
||||
const cli = new Map();
|
||||
for (const scenario of cases) {
|
||||
const response = await execute(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
"inspect-android-method",
|
||||
apk,
|
||||
scenario.class_name,
|
||||
"<clinit>",
|
||||
"--format",
|
||||
"json",
|
||||
],
|
||||
{
|
||||
cwd: repository,
|
||||
env: environment,
|
||||
timeout: 150_000,
|
||||
maxBuffer: 8 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
cli.set(scenario.class_name, check(JSON.parse(response.stdout), scenario));
|
||||
}
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
cwd: repository,
|
||||
env: environment,
|
||||
stderr: "pipe",
|
||||
});
|
||||
let stderr = "";
|
||||
transport.stderr?.on("data", (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
const client = new Client({
|
||||
name: "rea-real-android-initializers",
|
||||
version: "1",
|
||||
});
|
||||
try {
|
||||
await client.connect(transport, { timeout: 30_000 });
|
||||
for (const scenario of cases) {
|
||||
const response = await client.callTool(
|
||||
{
|
||||
name: "inspect_android_method",
|
||||
arguments: {
|
||||
path: apk,
|
||||
class_name: scenario.class_name,
|
||||
method_name: "<clinit>",
|
||||
},
|
||||
},
|
||||
{ timeout: 150_000 },
|
||||
);
|
||||
assert.notEqual(response.isError, true, JSON.stringify(response));
|
||||
const evidence = check(response.structuredContent, scenario);
|
||||
assert.deepEqual(
|
||||
evidence.normalized_result,
|
||||
cli.get(scenario.class_name).normalized_result,
|
||||
);
|
||||
assert.deepEqual(
|
||||
evidence.parameters,
|
||||
cli.get(scenario.class_name).parameters,
|
||||
);
|
||||
assert.deepEqual(
|
||||
evidence.provider,
|
||||
cli.get(scenario.class_name).provider,
|
||||
);
|
||||
}
|
||||
await client.ping();
|
||||
} catch (cause) {
|
||||
process.stderr.write(stderr);
|
||||
throw cause;
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
assert.equal(
|
||||
await hashAndroidFile(apk),
|
||||
digest,
|
||||
"Static inspection changed fixture bytes",
|
||||
);
|
||||
console.log(
|
||||
"PASS real CLI/MCP static initializer evidence, identity, parity and unchanged APK",
|
||||
);
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { delimiter, join, resolve } from "node:path";
|
||||
import { verifyAndroidInitializers } from "./initializers.mjs";
|
||||
|
||||
/** Prove cold metadata stays unmaterialized and overload identities survive code generation. */
|
||||
export const verifyAndroidMetadata = async ({
|
||||
@@ -11,6 +12,8 @@ export const verifyAndroidMetadata = async ({
|
||||
bridge,
|
||||
repository,
|
||||
execute,
|
||||
entrypoint,
|
||||
environment,
|
||||
jvmArguments = [],
|
||||
}) => {
|
||||
const javac =
|
||||
@@ -55,6 +58,8 @@ export const verifyAndroidMetadata = async ({
|
||||
`${classes}${delimiter}${jar}`,
|
||||
"ReaJadxMetadataRegression",
|
||||
apk,
|
||||
resolve(repository, "tests/fixtures/android/static-initializers"),
|
||||
classes,
|
||||
],
|
||||
{ timeout: 120_000, maxBuffer: 8 * 1024 * 1024 },
|
||||
);
|
||||
@@ -62,9 +67,23 @@ export const verifyAndroidMetadata = async ({
|
||||
result.stdout,
|
||||
/METADATA_REGRESSION_PASS classes=\d+ overloads=3/u,
|
||||
);
|
||||
assert.match(
|
||||
result.stdout,
|
||||
/INITIALIZER_REGRESSION_PASS folded=2 bounded=true/u,
|
||||
);
|
||||
console.log(
|
||||
"PASS real cold metadata, full generic types and stable overloaded method selection",
|
||||
);
|
||||
console.log(
|
||||
"PASS real folded and mixed static initializers, absent bodies and bounded smali",
|
||||
);
|
||||
await verifyAndroidInitializers({
|
||||
apk: resolve(classes, "initializers.apk"),
|
||||
repository,
|
||||
entrypoint,
|
||||
environment,
|
||||
execute,
|
||||
});
|
||||
} finally {
|
||||
await rm(classes, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -13,7 +13,10 @@ import { dirname, join } from "node:path";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { resolveJadxConfiguration } from "../../../dist/android/JadxConfiguration.js";
|
||||
import {
|
||||
resolveJadxConfigurationInputs,
|
||||
validateJadxConfiguration,
|
||||
} from "../../../dist/android/JadxConfiguration.js";
|
||||
|
||||
/** Verify real Windows cancellation, disconnect and abrupt CLI owner exit. */
|
||||
export async function verifyWindowsAndroidLifecycle(options) {
|
||||
@@ -36,7 +39,12 @@ export async function verifyWindowsAndroidLifecycle(options) {
|
||||
}
|
||||
|
||||
async function verifyJavaSelection(environment) {
|
||||
const selected = await resolveJadxConfiguration(
|
||||
const selected = await resolveJadxConfigurationInputs(
|
||||
environment,
|
||||
"inspect_android_package",
|
||||
);
|
||||
await validateJadxConfiguration(
|
||||
selected,
|
||||
environment,
|
||||
"inspect_android_package",
|
||||
);
|
||||
@@ -51,7 +59,12 @@ async function verifyJavaSelection(environment) {
|
||||
// The owned junction borrows the selected JDK directory without copying it.
|
||||
await symlink(dirname(selected.java), alias, "junction");
|
||||
pathEnvironment.Path = `"${alias}"`;
|
||||
const onPath = await resolveJadxConfiguration(
|
||||
const onPath = await resolveJadxConfigurationInputs(
|
||||
pathEnvironment,
|
||||
"inspect_android_package",
|
||||
);
|
||||
await validateJadxConfiguration(
|
||||
onPath,
|
||||
pathEnvironment,
|
||||
"inspect_android_package",
|
||||
);
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import com.atxx.jhmcp.*;
|
||||
import com.android.tools.smali.smali.Smali;
|
||||
import com.android.tools.smali.smali.SmaliOptions;
|
||||
import com.google.gson.*;
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult;
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.TextContent;
|
||||
import jadx.api.*;
|
||||
import jadx.core.dex.nodes.*;
|
||||
import java.lang.reflect.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.zip.*;
|
||||
import kotlin.coroutines.EmptyCoroutineContext;
|
||||
import kotlinx.coroutines.BuildersKt;
|
||||
|
||||
@@ -37,6 +42,86 @@ public class ReaJadxMetadataRegression {
|
||||
return value;
|
||||
}
|
||||
|
||||
private static Path initializerFixture(String sources, String output) throws Exception {
|
||||
Path directory = Path.of(output);
|
||||
SmaliOptions options = new SmaliOptions();
|
||||
options.apiLevel = 28;
|
||||
options.jobs = 1;
|
||||
options.outputDexFile = directory.resolve("initializers.dex").toString();
|
||||
require(Smali.assemble(options, List.of(
|
||||
Path.of(sources, "InitializationPolicy.smali").toString(),
|
||||
Path.of(sources, "MixedInitialization.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"));
|
||||
Files.copy(Path.of(options.outputDexFile), zip);
|
||||
zip.closeEntry();
|
||||
}
|
||||
return apk;
|
||||
}
|
||||
|
||||
private static JsonObject methodArguments(String className, String methodName) {
|
||||
JsonObject value = arguments(className);
|
||||
value.addProperty("method_name", methodName);
|
||||
value.addProperty("overload_index", 0);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void verifyInitializers(SessionHolder holder, SessionConfig config,
|
||||
Constructor<?> constructor, Method handle, Path apk) throws Exception {
|
||||
BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
|
||||
(scope, continuation) -> holder.unload(continuation));
|
||||
BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
|
||||
(scope, continuation) -> holder.load(apk.toString(), config, continuation));
|
||||
Object bridge = constructor.newInstance(holder);
|
||||
String[] classNames = { "fixture.InitializationPolicy", "fixture.MixedInitialization" };
|
||||
String[][] witnesses = {
|
||||
{ "declared-policy", "->singletonList", "->POLICY:Ljava/util/List;" },
|
||||
{ "field-value", "->LABEL:Ljava/lang/String;", "->touch()V" }
|
||||
};
|
||||
for (int index = 0; index < classNames.length; index++) {
|
||||
JsonObject summary = call(handle, bridge, "get_class_summary", arguments(classNames[index]));
|
||||
JsonObject input = methodArguments(classNames[index], "<clinit>");
|
||||
JsonObject body = call(handle, bridge, "get_method_body", input);
|
||||
String text = body.get("body").getAsString();
|
||||
for (String witness : witnesses[index]) require(text.contains(witness), "Missing initializer instruction: " + witness);
|
||||
require(body.get("mode").getAsString().equals("smali"), "Static initializer must preserve DEX instructions");
|
||||
require(body.get("fell_back").getAsBoolean(), "Disassembly fallback was not reported");
|
||||
require(body.getAsJsonArray("markers").contains(new JsonPrimitive("static_initializer_disassembly")),
|
||||
"Static initializer representation reason was not reported");
|
||||
require(text.contains(".method static constructor <clinit>()V") && text.contains(".end method"),
|
||||
"Initializer method boundaries missing");
|
||||
require(body.get("overload_index").getAsInt() == 0 && body.get("overload_count").getAsInt() == 1,
|
||||
"Initializer identity changed");
|
||||
require(body.equals(call(handle, bridge, "get_method_body", input)), "Repeated initializer body changed");
|
||||
require(summary.equals(call(handle, bridge, "get_class_summary", arguments(classNames[index]))),
|
||||
"Frozen initializer metadata changed after source generation");
|
||||
}
|
||||
for (String name : List.of("missing", "originalNative")) {
|
||||
JsonObject body = call(handle, bridge, "get_method_body", methodArguments(classNames[0], name));
|
||||
String text = body.get("body").getAsString();
|
||||
require(body.get("mode").getAsString().equals("java") && !text.contains("{") && !text.contains(".method"),
|
||||
"Executable body was fabricated for " + name + ": " + text);
|
||||
}
|
||||
JsonObject ordinary = call(handle, bridge, "get_method_body", methodArguments(classNames[1], "touch"));
|
||||
require(ordinary.get("mode").getAsString().equals("java") && ordinary.get("body").getAsString().contains("COUNT"),
|
||||
"Ordinary executable method no longer produces Java");
|
||||
|
||||
SessionConfig bounded = new SessionConfig(96, 0, 90000L, 1, List.of(), List.of(), 64, ResourceMode.FULL);
|
||||
BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
|
||||
(scope, continuation) -> holder.unload(continuation));
|
||||
BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
|
||||
(scope, continuation) -> holder.load(apk.toString(), bounded, continuation));
|
||||
JsonObject partial = call(handle, constructor.newInstance(holder), "get_method_body",
|
||||
methodArguments(classNames[0], "<clinit>"));
|
||||
require(partial.get("mode").getAsString().equals("smali")
|
||||
&& partial.get("body").getAsString().contains("[truncated: exceeds 96 bytes, total "),
|
||||
"Static initializer ignored the source byte limit or omitted its truncation marker");
|
||||
String prefix = partial.get("body").getAsString().split("\n\n\\.\\.\\. \\[truncated:", 2)[0];
|
||||
require(prefix.getBytes(StandardCharsets.UTF_8).length <= 96, "Initializer text exceeded its byte limit");
|
||||
System.out.println("INITIALIZER_REGRESSION_PASS folded=2 bounded=true");
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
SessionConfig config = new SessionConfig(1048576, 0, 90000L, 1, List.of(), List.of(), 64, ResourceMode.FULL);
|
||||
SessionHolder holder = new SessionHolder(config);
|
||||
@@ -89,6 +174,7 @@ public class ReaJadxMetadataRegression {
|
||||
"Frozen summary changed after code generation");
|
||||
require(apiSummary.equals(call(handle, bridge, "get_class_summary", apiArgs)), "Unrelated frozen summary changed");
|
||||
System.out.println("METADATA_REGRESSION_PASS classes=" + states.size() + " overloads=" + signatures.size());
|
||||
verifyInitializers(holder, config, constructor, handle, initializerFixture(args[1], args[2]));
|
||||
} finally {
|
||||
BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
|
||||
(scope, continuation) -> holder.unload(continuation));
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
.class public abstract Lfixture/InitializationPolicy;
|
||||
.super Ljava/lang/Object;
|
||||
|
||||
.field public static final POLICY:Ljava/util/List;
|
||||
|
||||
.method static constructor <clinit>()V
|
||||
.registers 1
|
||||
const-string v0, "declared-policy"
|
||||
invoke-static {v0}, Ljava/util/Collections;->singletonList(Ljava/lang/Object;)Ljava/util/List;
|
||||
move-result-object v0
|
||||
sput-object v0, Lfixture/InitializationPolicy;->POLICY:Ljava/util/List;
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public constructor <init>()V
|
||||
.registers 1
|
||||
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public abstract missing()V
|
||||
.end method
|
||||
|
||||
.method public native originalNative()V
|
||||
.end method
|
||||
@@ -0,0 +1,21 @@
|
||||
.class public Lfixture/MixedInitialization;
|
||||
.super Ljava/lang/Object;
|
||||
|
||||
.field public static final LABEL:Ljava/lang/String;
|
||||
.field public static COUNT:I
|
||||
|
||||
.method static constructor <clinit>()V
|
||||
.registers 1
|
||||
const-string v0, "field-value"
|
||||
sput-object v0, Lfixture/MixedInitialization;->LABEL:Ljava/lang/String;
|
||||
invoke-static {}, Lfixture/MixedInitialization;->touch()V
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public static touch()V
|
||||
.registers 1
|
||||
sget v0, Lfixture/MixedInitialization;->COUNT:I
|
||||
add-int/lit8 v0, v0, 0x1
|
||||
sput v0, Lfixture/MixedInitialization;->COUNT:I
|
||||
return-void
|
||||
.end method
|
||||
Reference in New Issue
Block a user