Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ec127c3cb | |||
| 7a3b7c55c9 | |||
| b66293a2f7 | |||
| abcaafa89a | |||
| cf25cc4faa | |||
| b57001d4a7 | |||
| 83decc2473 | |||
| 92faa569be | |||
| c5b731169d | |||
| f0a8ef81d3 | |||
| 994973ac01 | |||
| c9622c0771 | |||
| 8551c6c903 | |||
| 9a9ac4308e | |||
| e784cbdd09 | |||
| 2744c4bfb6 | |||
| e4f4c1b84a | |||
| e5fa818b5c | |||
| b22b554a69 | |||
| e9b8060889 | |||
| 1c2b2c072c | |||
| 3d451912ee | |||
| fe91d774fa | |||
| d8306cb1c0 | |||
| 909cf0a576 | |||
| 8fe1ee11e4 |
@@ -0,0 +1,5 @@
|
||||
jdk:
|
||||
- openjdk11
|
||||
install:
|
||||
- echo "Jitpack is not supported. Use artifacts from Maven Central (https://search.maven.org/search?q=jadx), check usage help at https://github.com/skylot/jadx/wiki/Use-jadx-as-a-library"
|
||||
- ./gradlew intentional-fail
|
||||
@@ -84,6 +84,11 @@ options:
|
||||
--output-format - can be 'java' or 'json', default: java
|
||||
-e, --export-gradle - save as android gradle project
|
||||
-j, --threads-count - processing threads count, default: 4
|
||||
-m, --decompilation-mode - code output mode:
|
||||
'auto' - trying best options (default)
|
||||
'restructure' - restore code structure (normal java code)
|
||||
'simple' - simplified instructions (linear, with goto's)
|
||||
'fallback' - raw instructions without modifications
|
||||
--show-bad-code - show inconsistent code (incorrectly decompiled)
|
||||
--no-imports - disable use of imports, always write entire package name
|
||||
--no-debug-info - disable debug info
|
||||
@@ -115,7 +120,7 @@ options:
|
||||
--fs-case-sensitive - treat filesystem as case sensitive, false by default
|
||||
--cfg - save methods control flow graph to dot file
|
||||
--raw-cfg - save methods control flow graph (use raw instructions)
|
||||
-f, --fallback - make simple dump (using goto instead of 'if', 'for', etc)
|
||||
-f, --fallback - set '--decompilation-mode' to 'fallback' (deprecated)
|
||||
--use-dx - use dx/d8 to convert java bytecode
|
||||
--comments-level - set code comments level, values: error, warn, info, debug, user-only, none, default: info
|
||||
--log-level - set log level, values: quiet, progress, error, warn, info, debug, default: progress
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="desktop">
|
||||
<id>com.github.skylot.jadx</id>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>Apache-2.0</project_license>
|
||||
<name>JADX</name>
|
||||
<summary>Dex to Java decompiler</summary>
|
||||
<description>
|
||||
<p>Command line and GUI tools for producing Java source code from Android Dex and Apk files</p>
|
||||
<ul>
|
||||
<li>decompile Dalvik bytecode to java classes from APK, dex, aar, aab and zip files</li>
|
||||
<li>decode AndroidManifest.xml and other resources from resources.arsc</li>
|
||||
<li>deobfuscator included</li>
|
||||
<li>view decompiled code with highlighted syntax</li>
|
||||
<li>jump to declaration</li>
|
||||
<li>find usage</li>
|
||||
<li>full text search</li>
|
||||
<li>smali debugger</li>
|
||||
</ul>
|
||||
</description>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://user-images.githubusercontent.com/118523/142730720-839f017e-38db-423e-b53f-39f5f0a0316f.png</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<content_rating type="oars-1.1" />
|
||||
<launchable type="desktop-id">com.github.skylot.jadx.desktop</launchable>
|
||||
<url type="homepage">https://github.com/skylot/jadx</url>
|
||||
<url type="bugtracker">https://github.com/skylot/jadx/issues</url>
|
||||
<releases>
|
||||
<release version="1.3.4" date="2022-03-20" />
|
||||
</releases>
|
||||
</component>
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
plugins {
|
||||
id 'com.github.ben-manes.versions' version '0.42.0'
|
||||
id 'com.diffplug.spotless' version '6.3.0'
|
||||
id 'com.diffplug.spotless' version '6.4.2'
|
||||
}
|
||||
|
||||
ext.jadxVersion = System.getenv('JADX_VERSION') ?: "dev"
|
||||
@@ -43,6 +43,7 @@ allprojects {
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
maxParallelForks = Runtime.runtime.availableProcessors()
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
||||
@@ -66,6 +66,7 @@ publishing {
|
||||
}
|
||||
|
||||
signing {
|
||||
required { gradle.taskGraph.hasTask("publish") }
|
||||
sign publishing.publications.mavenJava
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
org.gradle.warning.mode=all
|
||||
org.gradle.parallel=true
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=e5444a57cda4a95f90b0c9446a9e1b47d3d7f69057765bfb54bd4f482542d548
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip
|
||||
distributionSha256Sum=29e49b10984e585d8118b7d0bc452f944e386458df27371b49b4ac1dec4b7fda
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -17,7 +17,7 @@ dependencies {
|
||||
application {
|
||||
applicationName = 'jadx'
|
||||
mainClass.set('jadx.cli.JadxCLI')
|
||||
applicationDefaultJvmArgs = ['-Xms128M', '-Xmx4g', '-XX:+UseG1GC']
|
||||
applicationDefaultJvmArgs = ['-Xms128M', '-XX:MaxRAMPercentage=70.0', '-XX:+UseG1GC']
|
||||
}
|
||||
|
||||
applicationDistribution.with {
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.beust.jcommander.IStringConverter;
|
||||
import com.beust.jcommander.Parameter;
|
||||
|
||||
import jadx.api.CommentsLevel;
|
||||
import jadx.api.DecompilationMode;
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.JadxArgs.RenameEnum;
|
||||
import jadx.api.JadxArgs.UseKotlinMethodsForVarNames;
|
||||
@@ -58,6 +59,17 @@ public class JadxCLIArgs {
|
||||
@Parameter(names = { "-j", "--threads-count" }, description = "processing threads count")
|
||||
protected int threadsCount = JadxArgs.DEFAULT_THREADS_COUNT;
|
||||
|
||||
@Parameter(
|
||||
names = { "-m", "--decompilation-mode" },
|
||||
description = "code output mode:"
|
||||
+ "\n 'auto' - trying best options (default)"
|
||||
+ "\n 'restructure' - restore code structure (normal java code)"
|
||||
+ "\n 'simple' - simplified instructions (linear, with goto's)"
|
||||
+ "\n 'fallback' - raw instructions without modifications",
|
||||
converter = DecompilationModeConverter.class
|
||||
)
|
||||
protected DecompilationMode decompilationMode = DecompilationMode.AUTO;
|
||||
|
||||
@Parameter(names = { "--show-bad-code" }, description = "show inconsistent code (incorrectly decompiled)")
|
||||
protected boolean showInconsistentCode = false;
|
||||
|
||||
@@ -148,7 +160,7 @@ public class JadxCLIArgs {
|
||||
@Parameter(names = { "--raw-cfg" }, description = "save methods control flow graph (use raw instructions)")
|
||||
protected boolean rawCfgOutput = false;
|
||||
|
||||
@Parameter(names = { "-f", "--fallback" }, description = "make simple dump (using goto instead of 'if', 'for', etc)")
|
||||
@Parameter(names = { "-f", "--fallback" }, description = "set '--decompilation-mode' to 'fallback' (deprecated)")
|
||||
protected boolean fallbackMode = false;
|
||||
|
||||
@Parameter(names = { "--use-dx" }, description = "use dx/d8 to convert java bytecode")
|
||||
@@ -236,7 +248,11 @@ public class JadxCLIArgs {
|
||||
args.setThreadsCount(threadsCount);
|
||||
args.setSkipSources(skipSources);
|
||||
args.setSkipResources(skipResources);
|
||||
args.setFallbackMode(fallbackMode);
|
||||
if (fallbackMode) {
|
||||
args.setDecompilationMode(DecompilationMode.FALLBACK);
|
||||
} else {
|
||||
args.setDecompilationMode(decompilationMode);
|
||||
}
|
||||
args.setShowInconsistentCode(showInconsistentCode);
|
||||
args.setCfgOutput(cfgOutput);
|
||||
args.setRawCFGOutput(rawCfgOutput);
|
||||
@@ -313,6 +329,10 @@ public class JadxCLIArgs {
|
||||
return useDx;
|
||||
}
|
||||
|
||||
public DecompilationMode getDecompilationMode() {
|
||||
return decompilationMode;
|
||||
}
|
||||
|
||||
public boolean isShowInconsistentCode() {
|
||||
return showInconsistentCode;
|
||||
}
|
||||
@@ -493,6 +513,19 @@ public class JadxCLIArgs {
|
||||
}
|
||||
}
|
||||
|
||||
public static class DecompilationModeConverter implements IStringConverter<DecompilationMode> {
|
||||
@Override
|
||||
public DecompilationMode convert(String value) {
|
||||
try {
|
||||
return DecompilationMode.valueOf(value.toUpperCase());
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException(
|
||||
'\'' + value + "' is unknown, possible values are: "
|
||||
+ JadxCLIArgs.enumValuesString(DecompilationMode.values()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static String enumValuesString(Enum<?>[] values) {
|
||||
return Stream.of(values)
|
||||
.map(v -> v.name().replace('_', '-').toLowerCase(Locale.ROOT))
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package jadx.api;
|
||||
|
||||
public enum DecompilationMode {
|
||||
/**
|
||||
* Trying best options (default)
|
||||
*/
|
||||
AUTO,
|
||||
|
||||
/**
|
||||
* Restore code structure (normal java code)
|
||||
*/
|
||||
RESTRUCTURE,
|
||||
|
||||
/**
|
||||
* Simplified instructions (linear with goto's)
|
||||
*/
|
||||
SIMPLE,
|
||||
|
||||
/**
|
||||
* Raw instructions without modifications
|
||||
*/
|
||||
FALLBACK
|
||||
}
|
||||
@@ -38,7 +38,6 @@ public class JadxArgs {
|
||||
private boolean cfgOutput = false;
|
||||
private boolean rawCFGOutput = false;
|
||||
|
||||
private boolean fallbackMode = false;
|
||||
private boolean showInconsistentCode = false;
|
||||
|
||||
private boolean useImports = true;
|
||||
@@ -85,6 +84,8 @@ public class JadxArgs {
|
||||
|
||||
private OutputFormatEnum outputFormat = OutputFormatEnum.JAVA;
|
||||
|
||||
private DecompilationMode decompilationMode = DecompilationMode.AUTO;
|
||||
|
||||
private ICodeData codeData;
|
||||
|
||||
private CommentsLevel commentsLevel = CommentsLevel.INFO;
|
||||
@@ -175,11 +176,17 @@ public class JadxArgs {
|
||||
}
|
||||
|
||||
public boolean isFallbackMode() {
|
||||
return fallbackMode;
|
||||
return decompilationMode == DecompilationMode.FALLBACK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecated: use 'decompilation mode' property
|
||||
*/
|
||||
@Deprecated
|
||||
public void setFallbackMode(boolean fallbackMode) {
|
||||
this.fallbackMode = fallbackMode;
|
||||
if (fallbackMode) {
|
||||
this.decompilationMode = DecompilationMode.FALLBACK;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isShowInconsistentCode() {
|
||||
@@ -422,6 +429,14 @@ public class JadxArgs {
|
||||
this.outputFormat = outputFormat;
|
||||
}
|
||||
|
||||
public DecompilationMode getDecompilationMode() {
|
||||
return decompilationMode;
|
||||
}
|
||||
|
||||
public void setDecompilationMode(DecompilationMode decompilationMode) {
|
||||
this.decompilationMode = decompilationMode;
|
||||
}
|
||||
|
||||
public ICodeCache getCodeCache() {
|
||||
return codeCache;
|
||||
}
|
||||
@@ -493,9 +508,7 @@ public class JadxArgs {
|
||||
+ ", outDirSrc=" + outDirSrc
|
||||
+ ", outDirRes=" + outDirRes
|
||||
+ ", threadsCount=" + threadsCount
|
||||
+ ", cfgOutput=" + cfgOutput
|
||||
+ ", rawCFGOutput=" + rawCFGOutput
|
||||
+ ", fallbackMode=" + fallbackMode
|
||||
+ ", decompilationMode=" + decompilationMode
|
||||
+ ", showInconsistentCode=" + showInconsistentCode
|
||||
+ ", useImports=" + useImports
|
||||
+ ", skipResources=" + skipResources
|
||||
@@ -520,6 +533,8 @@ public class JadxArgs {
|
||||
+ ", codeWriter=" + codeWriterProvider.apply(this).getClass().getSimpleName()
|
||||
+ ", useDxInput=" + useDxInput
|
||||
+ ", pluginOptions=" + pluginOptions
|
||||
+ ", cfgOutput=" + cfgOutput
|
||||
+ ", rawCFGOutput=" + rawCFGOutput
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ import jadx.api.plugins.input.data.ILoadResult;
|
||||
import jadx.api.plugins.options.JadxPluginOptions;
|
||||
import jadx.core.Jadx;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.attributes.nodes.InlinedAttr;
|
||||
import jadx.core.dex.attributes.nodes.LineAttrNode;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.FieldNode;
|
||||
@@ -483,12 +485,12 @@ public final class JadxDecompiler implements Closeable {
|
||||
if (parentClass.contains(AFlag.DONT_GENERATE)) {
|
||||
return null;
|
||||
}
|
||||
if (parentClass != cls) {
|
||||
JavaClass parentJavaClass = classesMap.get(parentClass);
|
||||
if (parentJavaClass == null) {
|
||||
getClasses();
|
||||
parentJavaClass = classesMap.get(parentClass);
|
||||
}
|
||||
JavaClass parentJavaClass = classesMap.get(parentClass);
|
||||
if (parentJavaClass == null) {
|
||||
getClasses();
|
||||
parentJavaClass = classesMap.get(parentClass);
|
||||
}
|
||||
if (parentJavaClass != null) {
|
||||
loadJavaClass(parentJavaClass);
|
||||
javaClass = classesMap.get(cls);
|
||||
if (javaClass != null) {
|
||||
@@ -512,7 +514,9 @@ public final class JadxDecompiler implements Closeable {
|
||||
return null;
|
||||
}
|
||||
// parent class not loaded yet
|
||||
JavaClass javaClass = getJavaClassByNode(mth.getParentClass().getTopParentClass());
|
||||
ClassNode parentClass = mth.getParentClass();
|
||||
ClassNode codeCls = getCodeParentClass(parentClass);
|
||||
JavaClass javaClass = getJavaClassByNode(codeCls);
|
||||
if (javaClass == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -521,12 +525,26 @@ public final class JadxDecompiler implements Closeable {
|
||||
if (javaMethod != null) {
|
||||
return javaMethod;
|
||||
}
|
||||
if (mth.getParentClass().hasNotGeneratedParent()) {
|
||||
if (parentClass.hasNotGeneratedParent()) {
|
||||
return null;
|
||||
}
|
||||
throw new JadxRuntimeException("JavaMethod not found by MethodNode: " + mth);
|
||||
}
|
||||
|
||||
private ClassNode getCodeParentClass(ClassNode cls) {
|
||||
ClassNode codeCls;
|
||||
InlinedAttr inlinedAttr = cls.get(AType.INLINED);
|
||||
if (inlinedAttr != null) {
|
||||
codeCls = inlinedAttr.getInlineCls().getTopParentClass();
|
||||
} else {
|
||||
codeCls = cls.getTopParentClass();
|
||||
}
|
||||
if (codeCls == cls) {
|
||||
return codeCls;
|
||||
}
|
||||
return getCodeParentClass(codeCls);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private JavaField getJavaFieldByNode(FieldNode fld) {
|
||||
JavaField javaField = fieldsMap.get(fld);
|
||||
|
||||
@@ -2,9 +2,12 @@ package jadx.api;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
|
||||
@@ -14,6 +17,7 @@ import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.utils.Utils;
|
||||
|
||||
public final class JavaMethod implements JavaNode {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JavaMethod.class);
|
||||
private final MethodNode mth;
|
||||
private final JavaClass parent;
|
||||
|
||||
@@ -73,7 +77,14 @@ public final class JavaMethod implements JavaNode {
|
||||
}
|
||||
JadxDecompiler decompiler = getDeclaringClass().getRootDecompiler();
|
||||
return ovrdAttr.getRelatedMthNodes().stream()
|
||||
.map(m -> ((JavaMethod) decompiler.convertNode(m)))
|
||||
.map(m -> {
|
||||
JavaMethod javaMth = (JavaMethod) decompiler.convertNode(m);
|
||||
if (javaMth == null) {
|
||||
LOG.warn("Failed convert to java method: {}", m);
|
||||
}
|
||||
return javaMth;
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.CommentsLevel;
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.visitors.AnonymousClassVisitor;
|
||||
import jadx.core.dex.visitors.AttachCommentsVisitor;
|
||||
import jadx.core.dex.visitors.AttachMethodDetails;
|
||||
@@ -32,6 +33,7 @@ import jadx.core.dex.visitors.InitCodeVariables;
|
||||
import jadx.core.dex.visitors.InlineMethods;
|
||||
import jadx.core.dex.visitors.MarkMethodsForInline;
|
||||
import jadx.core.dex.visitors.MethodInvokeVisitor;
|
||||
import jadx.core.dex.visitors.MethodVisitor;
|
||||
import jadx.core.dex.visitors.ModVisitor;
|
||||
import jadx.core.dex.visitors.MoveInlineVisitor;
|
||||
import jadx.core.dex.visitors.OverrideMethodVisitor;
|
||||
@@ -60,8 +62,10 @@ import jadx.core.dex.visitors.rename.CodeRenameVisitor;
|
||||
import jadx.core.dex.visitors.rename.RenameVisitor;
|
||||
import jadx.core.dex.visitors.shrink.CodeShrinkVisitor;
|
||||
import jadx.core.dex.visitors.ssa.SSATransform;
|
||||
import jadx.core.dex.visitors.typeinference.FinishTypeInference;
|
||||
import jadx.core.dex.visitors.typeinference.TypeInferenceVisitor;
|
||||
import jadx.core.dex.visitors.usage.UsageInfoVisitor;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
|
||||
public class Jadx {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Jadx.class);
|
||||
@@ -69,21 +73,20 @@ public class Jadx {
|
||||
private Jadx() {
|
||||
}
|
||||
|
||||
static {
|
||||
if (Consts.DEBUG) {
|
||||
LOG.info("debug enabled");
|
||||
public static List<IDexTreeVisitor> getPassesList(JadxArgs args) {
|
||||
switch (args.getDecompilationMode()) {
|
||||
case AUTO:
|
||||
case RESTRUCTURE:
|
||||
return getRegionsModePasses(args);
|
||||
case SIMPLE:
|
||||
return getSimpleModePasses(args);
|
||||
case FALLBACK:
|
||||
return getFallbackPassesList();
|
||||
default:
|
||||
throw new JadxRuntimeException("Unknown decompilation mode: " + args.getDecompilationMode());
|
||||
}
|
||||
}
|
||||
|
||||
public static List<IDexTreeVisitor> getFallbackPassesList() {
|
||||
List<IDexTreeVisitor> passes = new ArrayList<>();
|
||||
passes.add(new AttachTryCatchVisitor());
|
||||
passes.add(new AttachCommentsVisitor());
|
||||
passes.add(new ProcessInstructionsVisitor());
|
||||
passes.add(new FallbackModeVisitor());
|
||||
return passes;
|
||||
}
|
||||
|
||||
public static List<IDexTreeVisitor> getPreDecompilePassesList() {
|
||||
List<IDexTreeVisitor> passes = new ArrayList<>();
|
||||
passes.add(new SignatureProcessor());
|
||||
@@ -95,12 +98,8 @@ public class Jadx {
|
||||
return passes;
|
||||
}
|
||||
|
||||
public static List<IDexTreeVisitor> getPassesList(JadxArgs args) {
|
||||
if (args.isFallbackMode()) {
|
||||
return getFallbackPassesList();
|
||||
}
|
||||
public static List<IDexTreeVisitor> getRegionsModePasses(JadxArgs args) {
|
||||
List<IDexTreeVisitor> passes = new ArrayList<>();
|
||||
|
||||
// instructions IR
|
||||
passes.add(new CheckCode());
|
||||
if (args.isDebugInfo()) {
|
||||
@@ -132,6 +131,7 @@ public class Jadx {
|
||||
if (args.isDebugInfo()) {
|
||||
passes.add(new DebugInfoApplyVisitor());
|
||||
}
|
||||
passes.add(new FinishTypeInference());
|
||||
if (args.getUseKotlinMethodsForVarNames() != JadxArgs.UseKotlinMethodsForVarNames.DISABLE) {
|
||||
passes.add(new ProcessKotlinInternals());
|
||||
}
|
||||
@@ -178,6 +178,61 @@ public class Jadx {
|
||||
return passes;
|
||||
}
|
||||
|
||||
public static List<IDexTreeVisitor> getSimpleModePasses(JadxArgs args) {
|
||||
List<IDexTreeVisitor> passes = new ArrayList<>();
|
||||
if (args.isDebugInfo()) {
|
||||
passes.add(new DebugInfoAttachVisitor());
|
||||
}
|
||||
passes.add(new AttachTryCatchVisitor());
|
||||
if (args.getCommentsLevel() != CommentsLevel.NONE) {
|
||||
passes.add(new AttachCommentsVisitor());
|
||||
}
|
||||
passes.add(new AttachMethodDetails());
|
||||
passes.add(new ProcessInstructionsVisitor());
|
||||
|
||||
passes.add(new BlockSplitter());
|
||||
passes.add(new MethodVisitor(mth -> mth.add(AFlag.DISABLE_BLOCKS_LOCK)));
|
||||
if (args.isRawCFGOutput()) {
|
||||
passes.add(DotGraphVisitor.dumpRaw());
|
||||
}
|
||||
passes.add(new MethodVisitor(mth -> mth.add(AFlag.DISABLE_BLOCKS_LOCK)));
|
||||
passes.add(new BlockProcessor());
|
||||
passes.add(new SSATransform());
|
||||
passes.add(new MoveInlineVisitor());
|
||||
passes.add(new ConstructorVisitor());
|
||||
passes.add(new InitCodeVariables());
|
||||
passes.add(new ConstInlineVisitor());
|
||||
passes.add(new TypeInferenceVisitor());
|
||||
if (args.isDebugInfo()) {
|
||||
passes.add(new DebugInfoApplyVisitor());
|
||||
}
|
||||
passes.add(new FinishTypeInference());
|
||||
passes.add(new CodeRenameVisitor());
|
||||
passes.add(new DeboxingVisitor());
|
||||
passes.add(new ModVisitor());
|
||||
passes.add(new CodeShrinkVisitor());
|
||||
passes.add(new ReSugarCode());
|
||||
passes.add(new CodeShrinkVisitor());
|
||||
passes.add(new SimplifyVisitor());
|
||||
passes.add(new MethodVisitor(mth -> mth.remove(AFlag.DONT_GENERATE)));
|
||||
if (args.isRawCFGOutput()) {
|
||||
passes.add(DotGraphVisitor.dumpRaw());
|
||||
}
|
||||
if (args.isCfgOutput()) {
|
||||
passes.add(DotGraphVisitor.dump());
|
||||
}
|
||||
return passes;
|
||||
}
|
||||
|
||||
public static List<IDexTreeVisitor> getFallbackPassesList() {
|
||||
List<IDexTreeVisitor> passes = new ArrayList<>();
|
||||
passes.add(new AttachTryCatchVisitor());
|
||||
passes.add(new AttachCommentsVisitor());
|
||||
passes.add(new ProcessInstructionsVisitor());
|
||||
passes.add(new FallbackModeVisitor());
|
||||
return passes;
|
||||
}
|
||||
|
||||
public static final String VERSION_DEV = "dev";
|
||||
|
||||
private static String version;
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
package jadx.core;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.ICodeInfo;
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.core.codegen.CodeGen;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.LoadStage;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.dex.visitors.DepthTraversal;
|
||||
import jadx.core.dex.visitors.IDexTreeVisitor;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
@@ -18,13 +24,17 @@ import static jadx.core.dex.nodes.ProcessState.NOT_LOADED;
|
||||
import static jadx.core.dex.nodes.ProcessState.PROCESS_COMPLETE;
|
||||
import static jadx.core.dex.nodes.ProcessState.PROCESS_STARTED;
|
||||
|
||||
public final class ProcessClass {
|
||||
public class ProcessClass {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ProcessClass.class);
|
||||
|
||||
private ProcessClass() {
|
||||
private final List<IDexTreeVisitor> passes;
|
||||
|
||||
public ProcessClass(JadxArgs args) {
|
||||
this.passes = Jadx.getPassesList(args);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static ICodeInfo process(ClassNode cls, boolean codegen) {
|
||||
private ICodeInfo process(ClassNode cls, boolean codegen) {
|
||||
if (!codegen && cls.getState() == PROCESS_COMPLETE) {
|
||||
// nothing to do
|
||||
return null;
|
||||
@@ -58,7 +68,7 @@ public final class ProcessClass {
|
||||
}
|
||||
if (cls.getState() == LOADED) {
|
||||
cls.setState(PROCESS_STARTED);
|
||||
for (IDexTreeVisitor visitor : cls.root().getPasses()) {
|
||||
for (IDexTreeVisitor visitor : passes) {
|
||||
DepthTraversal.visit(visitor, cls);
|
||||
}
|
||||
cls.setState(PROCESS_COMPLETE);
|
||||
@@ -83,7 +93,7 @@ public final class ProcessClass {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static ICodeInfo generateCode(ClassNode cls) {
|
||||
public ICodeInfo generateCode(ClassNode cls) {
|
||||
ClassNode topParentClass = cls.getTopParentClass();
|
||||
if (topParentClass != cls) {
|
||||
return generateCode(topParentClass);
|
||||
@@ -107,4 +117,19 @@ public final class ProcessClass {
|
||||
throw new JadxRuntimeException("Failed to generate code for class: " + cls.getFullName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public void initPasses(RootNode root) {
|
||||
for (IDexTreeVisitor pass : passes) {
|
||||
try {
|
||||
pass.init(root);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Visitor init failed: {}", pass.getClass().getSimpleName(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: make passes list private and not visible
|
||||
public List<IDexTreeVisitor> getPasses() {
|
||||
return passes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,7 +356,7 @@ public class ClassGen {
|
||||
badCode = false;
|
||||
}
|
||||
MethodGen mthGen;
|
||||
if (badCode || fallback || mth.contains(AType.JADX_ERROR) || mth.getRegion() == null) {
|
||||
if (badCode || fallback || mth.contains(AType.JADX_ERROR)) {
|
||||
mthGen = MethodGen.getFallbackMethodGen(mth);
|
||||
} else {
|
||||
mthGen = new MethodGen(this, mth);
|
||||
|
||||
@@ -20,6 +20,7 @@ import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.attributes.nodes.FieldReplaceAttr;
|
||||
import jadx.core.dex.attributes.nodes.GenericInfoAttr;
|
||||
import jadx.core.dex.attributes.nodes.LoopLabelAttr;
|
||||
import jadx.core.dex.attributes.nodes.MethodReplaceAttr;
|
||||
import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr;
|
||||
import jadx.core.dex.info.ClassInfo;
|
||||
import jadx.core.dex.info.FieldInfo;
|
||||
@@ -50,6 +51,7 @@ import jadx.core.dex.instructions.args.RegisterArg;
|
||||
import jadx.core.dex.instructions.args.SSAVar;
|
||||
import jadx.core.dex.instructions.mods.ConstructorInsn;
|
||||
import jadx.core.dex.instructions.mods.TernaryInsn;
|
||||
import jadx.core.dex.nodes.BlockNode;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.FieldNode;
|
||||
import jadx.core.dex.nodes.InsnNode;
|
||||
@@ -517,7 +519,7 @@ public class InsnGen {
|
||||
code.add(' ');
|
||||
code.add(ifInsn.getOp().getSymbol()).add(' ');
|
||||
addArg(code, insn.getArg(1));
|
||||
code.add(") goto ").add(MethodGen.getLabelName(ifInsn.getTarget()));
|
||||
code.add(") goto ").add(MethodGen.getLabelName(ifInsn));
|
||||
break;
|
||||
|
||||
case GOTO:
|
||||
@@ -538,13 +540,24 @@ public class InsnGen {
|
||||
code.add(") {");
|
||||
code.incIndent();
|
||||
int[] keys = sw.getKeys();
|
||||
int[] targets = sw.getTargets();
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
code.startLine("case ").add(Integer.toString(keys[i])).add(": goto ");
|
||||
code.add(MethodGen.getLabelName(targets[i])).add(';');
|
||||
int size = keys.length;
|
||||
BlockNode[] targetBlocks = sw.getTargetBlocks();
|
||||
if (targetBlocks != null) {
|
||||
for (int i = 0; i < size; i++) {
|
||||
code.startLine("case ").add(Integer.toString(keys[i])).add(": goto ");
|
||||
code.add(MethodGen.getLabelName(targetBlocks[i])).add(';');
|
||||
}
|
||||
code.startLine("default: goto ");
|
||||
code.add(MethodGen.getLabelName(sw.getDefTargetBlock())).add(';');
|
||||
} else {
|
||||
int[] targets = sw.getTargets();
|
||||
for (int i = 0; i < size; i++) {
|
||||
code.startLine("case ").add(Integer.toString(keys[i])).add(": goto ");
|
||||
code.add(MethodGen.getLabelName(targets[i])).add(';');
|
||||
}
|
||||
code.startLine("default: goto ");
|
||||
code.add(MethodGen.getLabelName(sw.getDefaultCaseOffset())).add(';');
|
||||
}
|
||||
code.startLine("default: goto ");
|
||||
code.add(MethodGen.getLabelName(sw.getDefaultCaseOffset())).add(';');
|
||||
code.decIndent();
|
||||
code.startLine('}');
|
||||
break;
|
||||
@@ -682,19 +695,27 @@ public class InsnGen {
|
||||
throw new JadxRuntimeException("Constructor 'self' invoke must be removed!");
|
||||
}
|
||||
MethodNode callMth = mth.root().resolveMethod(insn.getCallMth());
|
||||
MethodNode refMth = callMth;
|
||||
if (callMth != null) {
|
||||
MethodReplaceAttr replaceAttr = callMth.get(AType.METHOD_REPLACE);
|
||||
if (replaceAttr != null) {
|
||||
refMth = replaceAttr.getReplaceMth();
|
||||
}
|
||||
}
|
||||
|
||||
if (insn.isSuper()) {
|
||||
code.attachAnnotation(callMth);
|
||||
code.attachAnnotation(refMth);
|
||||
code.add("super");
|
||||
} else if (insn.isThis()) {
|
||||
code.attachAnnotation(callMth);
|
||||
code.attachAnnotation(refMth);
|
||||
code.add("this");
|
||||
} else {
|
||||
code.add("new ");
|
||||
if (callMth == null || callMth.contains(AFlag.DONT_GENERATE)) {
|
||||
if (refMth == null || refMth.contains(AFlag.DONT_GENERATE)) {
|
||||
// use class reference if constructor method is missing (default constructor)
|
||||
code.attachAnnotation(mth.root().resolveClass(insn.getCallMth().getDeclClass()));
|
||||
} else {
|
||||
code.attachAnnotation(callMth);
|
||||
code.attachAnnotation(refMth);
|
||||
}
|
||||
mgen.getClassGen().addClsName(code, insn.getClassType());
|
||||
GenericInfoAttr genericInfoAttr = insn.get(AType.GENERIC_INFO);
|
||||
|
||||
@@ -6,11 +6,13 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.CommentsLevel;
|
||||
import jadx.api.ICodeWriter;
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.data.annotations.InsnCodeOffset;
|
||||
import jadx.api.data.annotations.VarDeclareRef;
|
||||
import jadx.api.plugins.input.data.AccessFlags;
|
||||
@@ -32,13 +34,14 @@ import jadx.core.dex.instructions.args.ArgType;
|
||||
import jadx.core.dex.instructions.args.CodeVar;
|
||||
import jadx.core.dex.instructions.args.RegisterArg;
|
||||
import jadx.core.dex.instructions.args.SSAVar;
|
||||
import jadx.core.dex.nodes.BlockNode;
|
||||
import jadx.core.dex.nodes.InsnNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.dex.trycatch.CatchAttr;
|
||||
import jadx.core.dex.trycatch.ExceptionHandler;
|
||||
import jadx.core.dex.visitors.DepthTraversal;
|
||||
import jadx.core.dex.visitors.IDexTreeVisitor;
|
||||
import jadx.core.utils.CodeGenUtils;
|
||||
import jadx.core.utils.InsnUtils;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.exceptions.CodegenException;
|
||||
import jadx.core.utils.exceptions.JadxOverflowException;
|
||||
@@ -252,12 +255,28 @@ public class MethodGen {
|
||||
}
|
||||
|
||||
public void addInstructions(ICodeWriter code) throws CodegenException {
|
||||
if (mth.root().getArgs().isFallbackMode()) {
|
||||
addFallbackMethodCode(code, FALLBACK_MODE);
|
||||
} else if (classGen.isFallbackMode()) {
|
||||
dumpInstructions(code);
|
||||
} else {
|
||||
addRegionInsns(code);
|
||||
JadxArgs args = mth.root().getArgs();
|
||||
switch (args.getDecompilationMode()) {
|
||||
case AUTO:
|
||||
if (classGen.isFallbackMode()) {
|
||||
// TODO: try simple mode first
|
||||
dumpInstructions(code);
|
||||
} else {
|
||||
addRegionInsns(code);
|
||||
}
|
||||
break;
|
||||
|
||||
case RESTRUCTURE:
|
||||
addRegionInsns(code);
|
||||
break;
|
||||
|
||||
case SIMPLE:
|
||||
addSimpleMethodCode(code);
|
||||
break;
|
||||
|
||||
case FALLBACK:
|
||||
addFallbackMethodCode(code, FALLBACK_MODE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,6 +298,59 @@ public class MethodGen {
|
||||
}
|
||||
}
|
||||
|
||||
private void addSimpleMethodCode(ICodeWriter code) {
|
||||
if (mth.getBasicBlocks() == null) {
|
||||
code.startLine("// Blocks not ready for simple mode, using fallback");
|
||||
addFallbackMethodCode(code, FALLBACK_MODE);
|
||||
return;
|
||||
}
|
||||
JadxArgs args = mth.root().getArgs();
|
||||
ICodeWriter tmpCode = args.getCodeWriterProvider().apply(args);
|
||||
try {
|
||||
tmpCode.setIndent(code.getIndent());
|
||||
generateSimpleCode(tmpCode);
|
||||
code.add(tmpCode);
|
||||
} catch (Exception e) {
|
||||
mth.addError("Simple mode code generation failed", e);
|
||||
CodeGenUtils.addError(code, "Simple mode code generation failed", e);
|
||||
dumpInstructions(code);
|
||||
}
|
||||
}
|
||||
|
||||
private void generateSimpleCode(ICodeWriter code) throws CodegenException {
|
||||
SimpleModeHelper helper = new SimpleModeHelper(mth);
|
||||
List<BlockNode> blocks = helper.prepareBlocks();
|
||||
InsnGen insnGen = new InsnGen(this, true);
|
||||
for (BlockNode block : blocks) {
|
||||
if (block.contains(AFlag.DONT_GENERATE)) {
|
||||
continue;
|
||||
}
|
||||
if (helper.isNeedStartLabel(block)) {
|
||||
code.decIndent();
|
||||
code.startLine(getLabelName(block)).add(':');
|
||||
code.incIndent();
|
||||
}
|
||||
for (InsnNode insn : block.getInstructions()) {
|
||||
if (!insn.contains(AFlag.DONT_GENERATE)) {
|
||||
if (insn.getResult() != null) {
|
||||
CodeVar codeVar = insn.getResult().getSVar().getCodeVar();
|
||||
if (!codeVar.isDeclared()) {
|
||||
insn.add(AFlag.DECLARE_VAR);
|
||||
codeVar.setDeclared(true);
|
||||
}
|
||||
}
|
||||
InsnCodeOffset.attach(code, insn);
|
||||
insnGen.makeInsn(insn, code);
|
||||
addCatchComment(code, insn, false);
|
||||
CodeGenUtils.addCodeComments(code, mth, insn);
|
||||
}
|
||||
}
|
||||
if (helper.isNeedEndGoto(block)) {
|
||||
code.startLine("goto ").add(getLabelName(block.getSuccessors().get(0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void dumpInstructions(ICodeWriter code) {
|
||||
if (mth.checkCommentsLevel(CommentsLevel.ERROR)) {
|
||||
code.startLine("/*");
|
||||
@@ -353,60 +425,81 @@ public class MethodGen {
|
||||
|
||||
public static void addFallbackInsns(ICodeWriter code, MethodNode mth, InsnNode[] insnArr, FallbackOption option) {
|
||||
int startIndent = code.getIndent();
|
||||
InsnGen insnGen = new InsnGen(getFallbackMethodGen(mth), true);
|
||||
MethodGen methodGen = getFallbackMethodGen(mth);
|
||||
InsnGen insnGen = new InsnGen(methodGen, true);
|
||||
InsnNode prevInsn = null;
|
||||
for (InsnNode insn : insnArr) {
|
||||
if (insn == null) {
|
||||
continue;
|
||||
}
|
||||
if (insn.contains(AType.JADX_ERROR)) {
|
||||
for (JadxError error : insn.getAll(AType.JADX_ERROR)) {
|
||||
code.startLine("// ").add(error.getError());
|
||||
}
|
||||
continue;
|
||||
methodGen.dumpInsn(code, insnGen, option, startIndent, prevInsn, insn);
|
||||
prevInsn = insn;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean dumpInsn(ICodeWriter code, InsnGen insnGen, FallbackOption option, int startIndent,
|
||||
@Nullable InsnNode prevInsn, InsnNode insn) {
|
||||
if (insn.contains(AType.JADX_ERROR)) {
|
||||
for (JadxError error : insn.getAll(AType.JADX_ERROR)) {
|
||||
code.startLine("// ").add(error.getError());
|
||||
}
|
||||
if (option != BLOCK_DUMP && needLabel(insn, prevInsn)) {
|
||||
return true;
|
||||
}
|
||||
if (option != BLOCK_DUMP && needLabel(insn, prevInsn)) {
|
||||
code.decIndent();
|
||||
code.startLine(getLabelName(insn.getOffset()) + ':');
|
||||
code.incIndent();
|
||||
}
|
||||
if (insn.getType() == InsnType.NOP) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
boolean escapeComment = isCommentEscapeNeeded(insn, option);
|
||||
if (escapeComment) {
|
||||
code.decIndent();
|
||||
code.startLine(getLabelName(insn.getOffset()) + ':');
|
||||
code.startLine("*/");
|
||||
code.startLine("// ");
|
||||
} else {
|
||||
code.startLineWithNum(insn.getSourceLine());
|
||||
}
|
||||
InsnCodeOffset.attach(code, insn);
|
||||
RegisterArg resArg = insn.getResult();
|
||||
if (resArg != null) {
|
||||
ArgType varType = resArg.getInitType();
|
||||
if (varType.isTypeKnown()) {
|
||||
code.add(varType.toString()).add(' ');
|
||||
}
|
||||
}
|
||||
insnGen.makeInsn(insn, code, InsnGen.Flags.INLINE);
|
||||
if (escapeComment) {
|
||||
code.startLine("/*");
|
||||
code.incIndent();
|
||||
}
|
||||
if (insn.getType() == InsnType.NOP) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
boolean escapeComment = isCommentEscapeNeeded(insn, option);
|
||||
if (escapeComment) {
|
||||
code.decIndent();
|
||||
code.startLine("*/");
|
||||
code.startLine("// ");
|
||||
} else {
|
||||
code.startLineWithNum(insn.getSourceLine());
|
||||
}
|
||||
InsnCodeOffset.attach(code, insn);
|
||||
RegisterArg resArg = insn.getResult();
|
||||
if (resArg != null) {
|
||||
ArgType varType = resArg.getInitType();
|
||||
if (varType.isTypeKnown()) {
|
||||
code.add(varType.toString()).add(' ');
|
||||
}
|
||||
}
|
||||
insnGen.makeInsn(insn, code, InsnGen.Flags.INLINE);
|
||||
if (escapeComment) {
|
||||
code.startLine("/*");
|
||||
code.incIndent();
|
||||
}
|
||||
addCatchComment(code, insn, true);
|
||||
CodeGenUtils.addCodeComments(code, mth, insn);
|
||||
} catch (Exception e) {
|
||||
LOG.debug("Error generate fallback instruction: ", e.getCause());
|
||||
code.setIndent(startIndent);
|
||||
code.startLine("// error: " + insn);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
CatchAttr catchAttr = insn.get(AType.EXC_CATCH);
|
||||
if (catchAttr != null) {
|
||||
code.add(" // " + catchAttr);
|
||||
}
|
||||
CodeGenUtils.addCodeComments(code, mth, insn);
|
||||
} catch (Exception e) {
|
||||
LOG.debug("Error generate fallback instruction: ", e.getCause());
|
||||
code.setIndent(startIndent);
|
||||
code.startLine("// error: " + insn);
|
||||
private void addCatchComment(ICodeWriter code, InsnNode insn, boolean raw) {
|
||||
CatchAttr catchAttr = insn.get(AType.EXC_CATCH);
|
||||
if (catchAttr == null) {
|
||||
return;
|
||||
}
|
||||
code.add(" // Catch:");
|
||||
for (ExceptionHandler handler : catchAttr.getHandlers()) {
|
||||
code.add(' ');
|
||||
classGen.useClass(code, handler.getArgType());
|
||||
code.add(" -> ");
|
||||
if (raw) {
|
||||
code.add(getLabelName(handler.getHandlerOffset()));
|
||||
} else {
|
||||
code.add(getLabelName(handler.getHandlerBlock()));
|
||||
}
|
||||
prevInsn = insn;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,7 +542,22 @@ public class MethodGen {
|
||||
return new MethodGen(clsGen, mth);
|
||||
}
|
||||
|
||||
public static String getLabelName(BlockNode block) {
|
||||
return String.format("L%d", block.getId());
|
||||
}
|
||||
|
||||
public static String getLabelName(IfNode insn) {
|
||||
BlockNode thenBlock = insn.getThenBlock();
|
||||
if (thenBlock != null) {
|
||||
return getLabelName(thenBlock);
|
||||
}
|
||||
return getLabelName(insn.getTarget());
|
||||
}
|
||||
|
||||
public static String getLabelName(int offset) {
|
||||
return "L_" + InsnUtils.formatOffset(offset);
|
||||
if (offset < 0) {
|
||||
return String.format("LB_%x", -offset);
|
||||
}
|
||||
return String.format("L%x", offset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,13 +268,17 @@ public class NameGen {
|
||||
|
||||
private String makeNameFromInvoke(MethodInfo callMth) {
|
||||
String name = callMth.getName();
|
||||
ArgType declType = callMth.getDeclClass().getType();
|
||||
if ("getInstance".equals(name)) {
|
||||
// e.g. Cipher.getInstance
|
||||
return makeNameForType(declType);
|
||||
}
|
||||
if (name.startsWith("get") || name.startsWith("set")) {
|
||||
return fromName(name.substring(3));
|
||||
}
|
||||
if ("iterator".equals(name)) {
|
||||
return "it";
|
||||
}
|
||||
ArgType declType = callMth.getDeclClass().getType();
|
||||
if ("toString".equals(name)) {
|
||||
return makeNameForType(declType);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package jadx.core.codegen;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.BitSet;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.instructions.IfNode;
|
||||
import jadx.core.dex.instructions.TargetInsnNode;
|
||||
import jadx.core.dex.nodes.BlockNode;
|
||||
import jadx.core.dex.nodes.InsnNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.dex.trycatch.ExceptionHandler;
|
||||
import jadx.core.dex.visitors.blocks.BlockProcessor;
|
||||
import jadx.core.dex.visitors.blocks.BlockSplitter;
|
||||
import jadx.core.utils.BlockUtils;
|
||||
|
||||
public class SimpleModeHelper {
|
||||
|
||||
private final MethodNode mth;
|
||||
|
||||
private final BitSet startLabel;
|
||||
private final BitSet endGoto;
|
||||
|
||||
public SimpleModeHelper(MethodNode mth) {
|
||||
this.mth = mth;
|
||||
this.startLabel = BlockUtils.newBlocksBitSet(mth);
|
||||
this.endGoto = BlockUtils.newBlocksBitSet(mth);
|
||||
}
|
||||
|
||||
public List<BlockNode> prepareBlocks() {
|
||||
removeEmptyBlocks();
|
||||
List<BlockNode> blocksList = getSortedBlocks();
|
||||
blocksList.removeIf(b -> b.equals(mth.getEnterBlock()) || b.equals(mth.getExitBlock()));
|
||||
unbindExceptionHandlers();
|
||||
if (blocksList.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
@Nullable
|
||||
BlockNode prev = null;
|
||||
int blocksCount = blocksList.size();
|
||||
for (int i = 0; i < blocksCount; i++) {
|
||||
BlockNode block = blocksList.get(i);
|
||||
BlockNode nextBlock = i + 1 == blocksCount ? null : blocksList.get(i + 1);
|
||||
List<BlockNode> preds = block.getPredecessors();
|
||||
int predsCount = preds.size();
|
||||
if (predsCount > 1) {
|
||||
startLabel.set(block.getId());
|
||||
} else if (predsCount == 1 && prev != null) {
|
||||
if (!prev.equals(preds.get(0))) {
|
||||
startLabel.set(block.getId());
|
||||
if (prev.getSuccessors().size() == 1 && !mth.isPreExitBlocks(prev)) {
|
||||
endGoto.set(prev.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
InsnNode lastInsn = BlockUtils.getLastInsn(block);
|
||||
if (lastInsn instanceof TargetInsnNode) {
|
||||
processTargetInsn(block, lastInsn, nextBlock);
|
||||
}
|
||||
if (block.contains(AType.EXC_HANDLER)) {
|
||||
startLabel.set(block.getId());
|
||||
}
|
||||
if (nextBlock == null && !mth.isPreExitBlocks(block)) {
|
||||
endGoto.set(block.getId());
|
||||
}
|
||||
prev = block;
|
||||
}
|
||||
if (mth.isVoidReturn()) {
|
||||
int last = blocksList.size() - 1;
|
||||
if (blocksList.get(last).contains(AFlag.RETURN)) {
|
||||
// remove trailing return
|
||||
blocksList.remove(last);
|
||||
}
|
||||
}
|
||||
return blocksList;
|
||||
}
|
||||
|
||||
private void removeEmptyBlocks() {
|
||||
for (BlockNode block : mth.getBasicBlocks()) {
|
||||
if (block.getInstructions().isEmpty()
|
||||
&& block.getPredecessors().size() > 0
|
||||
&& block.getSuccessors().size() == 1) {
|
||||
BlockNode successor = block.getSuccessors().get(0);
|
||||
List<BlockNode> predecessors = block.getPredecessors();
|
||||
BlockSplitter.removeConnection(block, successor);
|
||||
if (predecessors.size() == 1) {
|
||||
BlockSplitter.replaceConnection(predecessors.get(0), block, successor);
|
||||
} else {
|
||||
for (BlockNode pred : new ArrayList<>(predecessors)) {
|
||||
BlockSplitter.replaceConnection(pred, block, successor);
|
||||
}
|
||||
}
|
||||
block.add(AFlag.REMOVE);
|
||||
}
|
||||
}
|
||||
BlockProcessor.removeMarkedBlocks(mth);
|
||||
}
|
||||
|
||||
private void unbindExceptionHandlers() {
|
||||
if (mth.isNoExceptionHandlers()) {
|
||||
return;
|
||||
}
|
||||
for (ExceptionHandler handler : mth.getExceptionHandlers()) {
|
||||
BlockNode handlerBlock = handler.getHandlerBlock();
|
||||
if (handlerBlock != null) {
|
||||
BlockSplitter.removePredecessors(handlerBlock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processTargetInsn(BlockNode block, InsnNode lastInsn, @Nullable BlockNode next) {
|
||||
if (lastInsn instanceof IfNode) {
|
||||
IfNode ifInsn = (IfNode) lastInsn;
|
||||
BlockNode thenBlock = ifInsn.getThenBlock();
|
||||
if (Objects.equals(next, thenBlock)) {
|
||||
ifInsn.invertCondition();
|
||||
startLabel.set(ifInsn.getThenBlock().getId());
|
||||
} else {
|
||||
startLabel.set(thenBlock.getId());
|
||||
}
|
||||
ifInsn.normalize();
|
||||
} else {
|
||||
for (BlockNode successor : block.getSuccessors()) {
|
||||
startLabel.set(successor.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isNeedStartLabel(BlockNode block) {
|
||||
return startLabel.get(block.getId());
|
||||
}
|
||||
|
||||
public boolean isNeedEndGoto(BlockNode block) {
|
||||
return endGoto.get(block.getId());
|
||||
}
|
||||
|
||||
// DFS sort blocks to reduce goto count
|
||||
private List<BlockNode> getSortedBlocks() {
|
||||
List<BlockNode> list = new ArrayList<>(mth.getBasicBlocks().size());
|
||||
BlockUtils.dfsVisit(mth, list::add);
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,7 @@ public class Deobfuscator {
|
||||
for (MethodNode mth : cls.getMethods()) {
|
||||
MethodInfo methodInfo = mth.getMethodInfo();
|
||||
if (methodInfo.hasAlias()) {
|
||||
deobfPresets.getFldPresetMap().put(methodInfo.getRawFullId(), methodInfo.getAlias());
|
||||
deobfPresets.getMthPresetMap().put(methodInfo.getRawFullId(), methodInfo.getAlias());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,8 @@ public enum AFlag {
|
||||
|
||||
METHOD_CANDIDATE_FOR_INLINE,
|
||||
|
||||
DISABLE_BLOCKS_LOCK,
|
||||
|
||||
// Class processing flags
|
||||
RESTART_CODEGEN, // codegen must be executed again
|
||||
RELOAD_AT_CODEGEN_STAGE, // class can't be analyzed at 'process' stage => unload before 'codegen' stage
|
||||
|
||||
@@ -11,6 +11,7 @@ import jadx.core.dex.attributes.nodes.EnumMapAttr;
|
||||
import jadx.core.dex.attributes.nodes.FieldReplaceAttr;
|
||||
import jadx.core.dex.attributes.nodes.ForceReturnAttr;
|
||||
import jadx.core.dex.attributes.nodes.GenericInfoAttr;
|
||||
import jadx.core.dex.attributes.nodes.InlinedAttr;
|
||||
import jadx.core.dex.attributes.nodes.JadxCommentsAttr;
|
||||
import jadx.core.dex.attributes.nodes.JadxError;
|
||||
import jadx.core.dex.attributes.nodes.JumpInfo;
|
||||
@@ -20,6 +21,7 @@ import jadx.core.dex.attributes.nodes.LoopLabelAttr;
|
||||
import jadx.core.dex.attributes.nodes.MethodBridgeAttr;
|
||||
import jadx.core.dex.attributes.nodes.MethodInlineAttr;
|
||||
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
|
||||
import jadx.core.dex.attributes.nodes.MethodReplaceAttr;
|
||||
import jadx.core.dex.attributes.nodes.MethodTypeVarsAttr;
|
||||
import jadx.core.dex.attributes.nodes.PhiListAttr;
|
||||
import jadx.core.dex.attributes.nodes.RegDebugInfoAttr;
|
||||
@@ -55,6 +57,7 @@ public final class AType<T extends IJadxAttribute> implements IJadxAttrType<T> {
|
||||
public static final AType<EnumMapAttr> ENUM_MAP = new AType<>();
|
||||
public static final AType<ClassTypeVarsAttr> CLASS_TYPE_VARS = new AType<>();
|
||||
public static final AType<AnonymousClassAttr> ANONYMOUS_CLASS = new AType<>();
|
||||
public static final AType<InlinedAttr> INLINED = new AType<>();
|
||||
|
||||
// field
|
||||
public static final AType<FieldInitInsnAttr> FIELD_INIT_INSN = new AType<>();
|
||||
@@ -63,11 +66,12 @@ public final class AType<T extends IJadxAttribute> implements IJadxAttrType<T> {
|
||||
// method
|
||||
public static final AType<LocalVarsDebugInfoAttr> LOCAL_VARS_DEBUG_INFO = new AType<>();
|
||||
public static final AType<MethodInlineAttr> METHOD_INLINE = new AType<>();
|
||||
public static final AType<MethodReplaceAttr> METHOD_REPLACE = new AType<>();
|
||||
public static final AType<MethodBridgeAttr> BRIDGED_BY = new AType<>();
|
||||
public static final AType<SkipMethodArgsAttr> SKIP_MTH_ARGS = new AType<>();
|
||||
public static final AType<MethodOverrideAttr> METHOD_OVERRIDE = new AType<>();
|
||||
public static final AType<MethodTypeVarsAttr> METHOD_TYPE_VARS = new AType<>();
|
||||
public static final AType<AttrList<TryCatchBlockAttr>> TRY_BLOCKS_LIST = new AType<>();
|
||||
public static final AType<MethodBridgeAttr> BRIDGED_BY = new AType<>();
|
||||
|
||||
// region
|
||||
public static final AType<DeclareVariablesAttr> DECLARE_VARIABLES = new AType<>();
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package jadx.core.dex.attributes.nodes;
|
||||
|
||||
import jadx.api.plugins.input.data.attributes.IJadxAttrType;
|
||||
import jadx.api.plugins.input.data.attributes.IJadxAttribute;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
|
||||
public class InlinedAttr implements IJadxAttribute {
|
||||
|
||||
private final ClassNode inlineCls;
|
||||
|
||||
public InlinedAttr(ClassNode inlineCls) {
|
||||
this.inlineCls = inlineCls;
|
||||
}
|
||||
|
||||
public ClassNode getInlineCls() {
|
||||
return inlineCls;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IJadxAttrType<InlinedAttr> getAttrType() {
|
||||
return AType.INLINED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "INLINED: " + inlineCls;
|
||||
}
|
||||
}
|
||||
@@ -50,11 +50,7 @@ public class JadxError implements Comparable<JadxError> {
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder str = new StringBuilder();
|
||||
str.append("JadxError: ");
|
||||
if (error != null) {
|
||||
str.append(error);
|
||||
str.append(' ');
|
||||
}
|
||||
str.append("JadxError: ").append(error).append(' ');
|
||||
if (cause != null) {
|
||||
str.append(cause.getClass());
|
||||
str.append(':');
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package jadx.core.dex.attributes.nodes;
|
||||
|
||||
import jadx.api.plugins.input.data.attributes.PinnedAttribute;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
|
||||
/**
|
||||
* Calls of method should be replaced by provided method (used for synthetic methods redirect)
|
||||
*/
|
||||
public class MethodReplaceAttr extends PinnedAttribute {
|
||||
|
||||
private final MethodNode replaceMth;
|
||||
|
||||
public MethodReplaceAttr(MethodNode replaceMth) {
|
||||
this.replaceMth = replaceMth;
|
||||
}
|
||||
|
||||
public MethodNode getReplaceMth() {
|
||||
return replaceMth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AType<MethodReplaceAttr> getAttrType() {
|
||||
return AType.METHOD_REPLACE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "REPLACED_BY: " + replaceMth;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import java.util.List;
|
||||
import jadx.api.plugins.input.insns.InsnData;
|
||||
import jadx.core.dex.instructions.args.ArgType;
|
||||
import jadx.core.dex.instructions.args.InsnArg;
|
||||
import jadx.core.dex.instructions.args.LiteralArg;
|
||||
import jadx.core.dex.instructions.args.PrimitiveType;
|
||||
import jadx.core.dex.nodes.BlockNode;
|
||||
import jadx.core.dex.nodes.InsnNode;
|
||||
@@ -70,6 +71,15 @@ public class IfNode extends GotoNode {
|
||||
elseBlock = tmp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change 'a != false' to 'a == true'
|
||||
*/
|
||||
public void normalize() {
|
||||
if (getOp() == IfOp.NE && getArg(1).isFalse()) {
|
||||
changeCondition(IfOp.EQ, getArg(0), LiteralArg.litTrue());
|
||||
}
|
||||
}
|
||||
|
||||
public void changeCondition(IfOp op, InsnArg arg1, InsnArg arg2) {
|
||||
this.op = op;
|
||||
setArg(0, arg1);
|
||||
|
||||
@@ -192,6 +192,11 @@ public class SSAVar {
|
||||
return usedInPhi;
|
||||
}
|
||||
|
||||
public boolean isAssignInPhi() {
|
||||
InsnNode assignInsn = getAssignInsn();
|
||||
return assignInsn != null && assignInsn.getType() == InsnType.PHI;
|
||||
}
|
||||
|
||||
public boolean isUsedInPhi() {
|
||||
return usedInPhi != null && !usedInPhi.isEmpty();
|
||||
}
|
||||
|
||||
@@ -16,9 +16,11 @@ import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.DecompilationMode;
|
||||
import jadx.api.ICodeCache;
|
||||
import jadx.api.ICodeInfo;
|
||||
import jadx.api.ICodeWriter;
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.plugins.input.data.IClassData;
|
||||
import jadx.api.plugins.input.data.IFieldData;
|
||||
import jadx.api.plugins.input.data.IMethodData;
|
||||
@@ -34,6 +36,7 @@ import jadx.core.Consts;
|
||||
import jadx.core.ProcessClass;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.attributes.nodes.InlinedAttr;
|
||||
import jadx.core.dex.attributes.nodes.NotificationAttrNode;
|
||||
import jadx.core.dex.info.AccessInfo;
|
||||
import jadx.core.dex.info.AccessInfo.AFType;
|
||||
@@ -304,6 +307,26 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
|
||||
return decompile(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* WARNING: Slow operation! Use with caution!
|
||||
*/
|
||||
public ICodeInfo decompileWithMode(DecompilationMode mode) {
|
||||
DecompilationMode baseMode = root.getArgs().getDecompilationMode();
|
||||
if (mode == baseMode) {
|
||||
return decompile(true);
|
||||
}
|
||||
JadxArgs args = root.getArgs();
|
||||
try {
|
||||
unload();
|
||||
args.setDecompilationMode(mode);
|
||||
ProcessClass process = new ProcessClass(args);
|
||||
process.initPasses(root);
|
||||
return process.generateCode(this);
|
||||
} finally {
|
||||
args.setDecompilationMode(baseMode);
|
||||
}
|
||||
}
|
||||
|
||||
public ICodeInfo getCode() {
|
||||
return decompile(true);
|
||||
}
|
||||
@@ -355,7 +378,7 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
|
||||
return code;
|
||||
}
|
||||
}
|
||||
ICodeInfo codeInfo = ProcessClass.generateCode(this);
|
||||
ICodeInfo codeInfo = root.getProcessClasses().generateCode(this);
|
||||
codeCache.add(clsRawName, codeInfo);
|
||||
return codeInfo;
|
||||
}
|
||||
@@ -611,6 +634,7 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
|
||||
if (inlinedClasses.isEmpty()) {
|
||||
inlinedClasses = new ArrayList<>(5);
|
||||
}
|
||||
cls.addAttr(new InlinedAttr(this));
|
||||
inlinedClasses.add(cls);
|
||||
}
|
||||
|
||||
|
||||
@@ -336,6 +336,14 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
|
||||
return exitBlock.getPredecessors();
|
||||
}
|
||||
|
||||
public boolean isPreExitBlocks(BlockNode block) {
|
||||
List<BlockNode> successors = block.getSuccessors();
|
||||
if (successors.size() == 1) {
|
||||
return successors.get(0).equals(exitBlock);
|
||||
}
|
||||
return exitBlock.getPredecessors().contains(block);
|
||||
}
|
||||
|
||||
public void registerLoop(LoopInfo loop) {
|
||||
if (loops.isEmpty()) {
|
||||
loops = new ArrayList<>(5);
|
||||
|
||||
@@ -22,6 +22,7 @@ import jadx.api.data.ICodeData;
|
||||
import jadx.api.plugins.input.data.IClassData;
|
||||
import jadx.api.plugins.input.data.ILoadResult;
|
||||
import jadx.core.Jadx;
|
||||
import jadx.core.ProcessClass;
|
||||
import jadx.core.clsp.ClspGraph;
|
||||
import jadx.core.dex.info.ClassInfo;
|
||||
import jadx.core.dex.info.ConstStorage;
|
||||
@@ -51,9 +52,9 @@ public class RootNode {
|
||||
|
||||
private final JadxArgs args;
|
||||
private final List<IDexTreeVisitor> preDecompilePasses;
|
||||
private final List<IDexTreeVisitor> passes;
|
||||
private final List<ICodeDataUpdateListener> codeDataUpdateListeners = new ArrayList<>();
|
||||
|
||||
private final ProcessClass processClasses;
|
||||
private final ErrorsCounter errorsCounter = new ErrorsCounter();
|
||||
private final StringUtils stringUtils;
|
||||
private final ConstStorage constValues;
|
||||
@@ -76,7 +77,7 @@ public class RootNode {
|
||||
public RootNode(JadxArgs args) {
|
||||
this.args = args;
|
||||
this.preDecompilePasses = Jadx.getPreDecompilePassesList();
|
||||
this.passes = Jadx.getPassesList(args);
|
||||
this.processClasses = new ProcessClass(this.getArgs());
|
||||
this.stringUtils = new StringUtils(args);
|
||||
this.constValues = new ConstStorage(args);
|
||||
this.typeUpdate = new TypeUpdate(this);
|
||||
@@ -460,18 +461,16 @@ public class RootNode {
|
||||
return null;
|
||||
}
|
||||
|
||||
public ProcessClass getProcessClasses() {
|
||||
return processClasses;
|
||||
}
|
||||
|
||||
public List<IDexTreeVisitor> getPasses() {
|
||||
return passes;
|
||||
return processClasses.getPasses();
|
||||
}
|
||||
|
||||
public void initPasses() {
|
||||
for (IDexTreeVisitor pass : passes) {
|
||||
try {
|
||||
pass.init(this);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Visitor init failed: {}", pass.getClass().getSimpleName(), e);
|
||||
}
|
||||
}
|
||||
processClasses.initPasses(this);
|
||||
}
|
||||
|
||||
public ICodeWriter makeCodeWriter() {
|
||||
|
||||
@@ -4,7 +4,6 @@ import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.instructions.IfNode;
|
||||
import jadx.core.dex.instructions.IfOp;
|
||||
import jadx.core.dex.instructions.args.InsnArg;
|
||||
import jadx.core.dex.instructions.args.LiteralArg;
|
||||
|
||||
public final class Compare {
|
||||
private final IfNode insn;
|
||||
@@ -35,13 +34,8 @@ public final class Compare {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change 'a != false' to 'a == true'
|
||||
*/
|
||||
public void normalize() {
|
||||
if (getOp() == IfOp.NE && getB().isFalse()) {
|
||||
insn.changeCondition(IfOp.EQ, getA(), LiteralArg.litTrue());
|
||||
}
|
||||
insn.normalize();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -11,6 +11,7 @@ import jadx.api.plugins.input.data.AccessFlags;
|
||||
import jadx.core.Consts;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.attributes.nodes.FieldReplaceAttr;
|
||||
import jadx.core.dex.attributes.nodes.MethodReplaceAttr;
|
||||
import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr;
|
||||
import jadx.core.dex.info.AccessInfo;
|
||||
import jadx.core.dex.info.ClassInfo;
|
||||
@@ -31,6 +32,7 @@ import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.FieldNode;
|
||||
import jadx.core.dex.nodes.InsnNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.dex.visitors.usage.UsageInfoVisitor;
|
||||
import jadx.core.utils.BlockUtils;
|
||||
import jadx.core.utils.InsnRemover;
|
||||
import jadx.core.utils.exceptions.JadxException;
|
||||
@@ -155,7 +157,7 @@ public class ClassModifier extends AbstractVisitor {
|
||||
return;
|
||||
}
|
||||
// remove synthetic constructor for inner classes
|
||||
if (af.isConstructor()) {
|
||||
if (mth.isConstructor() && mth.contains(AFlag.METHOD_CANDIDATE_FOR_INLINE)) {
|
||||
InsnNode insn = BlockUtils.getOnlyOneInsnFromMth(mth);
|
||||
if (insn != null) {
|
||||
List<RegisterArg> args = mth.getArgRegs();
|
||||
@@ -210,7 +212,14 @@ public class ClassModifier extends AbstractVisitor {
|
||||
SkipMethodArgsAttr.skipArg(mth, i);
|
||||
}
|
||||
}
|
||||
mth.add(AFlag.DONT_GENERATE);
|
||||
MethodInfo callMth = constr.getCallMth();
|
||||
MethodNode callMthNode = cls.root().resolveMethod(callMth);
|
||||
if (callMthNode != null) {
|
||||
mth.addAttr(new MethodReplaceAttr(callMthNode));
|
||||
mth.add(AFlag.DONT_GENERATE);
|
||||
// code generation order should be already fixed for marked methods
|
||||
UsageInfoVisitor.replaceMethodUsage(callMthNode, mth);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package jadx.core.dex.visitors;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.utils.exceptions.JadxException;
|
||||
|
||||
public class MethodVisitor implements IDexTreeVisitor {
|
||||
|
||||
private final Consumer<MethodNode> visitor;
|
||||
|
||||
public MethodVisitor(Consumer<MethodNode> visitor) {
|
||||
this.visitor = visitor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(MethodNode mth) throws JadxException {
|
||||
visitor.accept(mth);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(RootNode root) throws JadxException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(ClassNode cls) throws JadxException {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,13 @@ public class MoveInlineVisitor extends AbstractVisitor {
|
||||
if (resultArg.sameRegAndSVar(moveArg)) {
|
||||
return true;
|
||||
}
|
||||
if (moveArg.isRegister()) {
|
||||
RegisterArg moveReg = (RegisterArg) moveArg;
|
||||
if (moveReg.getSVar().isAssignInPhi()) {
|
||||
// don't mix already merged variables
|
||||
return false;
|
||||
}
|
||||
}
|
||||
SSAVar ssaVar = resultArg.getSVar();
|
||||
if (ssaVar.isUsedInPhi()) {
|
||||
return deleteMove(mth, move);
|
||||
|
||||
@@ -73,6 +73,7 @@ public class ProcessAnonymous extends AbstractVisitor {
|
||||
return;
|
||||
}
|
||||
ClassNode outerCls = anonymousConstructor.getUseIn().get(0).getParentClass();
|
||||
outerCls.addInlinedClass(cls);
|
||||
cls.addAttr(new AnonymousClassAttr(outerCls, baseType));
|
||||
cls.add(AFlag.DONT_GENERATE);
|
||||
anonymousConstructor.add(AFlag.ANONYMOUS_CONSTRUCTOR);
|
||||
|
||||
@@ -45,16 +45,17 @@ public class ProcessMethodsForInline extends AbstractVisitor {
|
||||
}
|
||||
AccessInfo accessFlags = mth.getAccessFlags();
|
||||
boolean isSynthetic = accessFlags.isSynthetic() || mth.getName().contains("$");
|
||||
return isSynthetic && accessFlags.isStatic();
|
||||
return isSynthetic && (accessFlags.isStatic() || mth.isConstructor());
|
||||
}
|
||||
|
||||
private static void fixClassDependencies(MethodNode mth) {
|
||||
ClassNode parentClass = mth.getTopParentClass();
|
||||
for (MethodNode useInMth : mth.getUseIn()) {
|
||||
// remove possible cross dependency to force class with inline method to be processed before its
|
||||
// usage
|
||||
// remove possible cross dependency
|
||||
// to force class with inline method to be processed before its usage
|
||||
ClassNode useTopCls = useInMth.getTopParentClass();
|
||||
parentClass.setDependencies(ListUtils.safeRemoveAndTrim(parentClass.getDependencies(), useTopCls));
|
||||
useTopCls.addCodegenDep(parentClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,12 +149,7 @@ public class BlockExceptionHandler {
|
||||
continue;
|
||||
}
|
||||
firstInsn.remove(AType.EXC_HANDLER);
|
||||
TmpEdgeAttr tmpEdgeAttr = block.get(AType.TMP_EDGE);
|
||||
if (tmpEdgeAttr != null) {
|
||||
// remove temp connection
|
||||
BlockSplitter.removeConnection(tmpEdgeAttr.getBlock(), block);
|
||||
block.remove(AType.TMP_EDGE);
|
||||
}
|
||||
removeTmpConnection(block);
|
||||
|
||||
ExceptionHandler excHandler = excHandlerAttr.getHandler();
|
||||
if (block.getPredecessors().isEmpty()) {
|
||||
@@ -176,6 +171,19 @@ public class BlockExceptionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
protected static void removeTmpConnections(MethodNode mth) {
|
||||
mth.getBasicBlocks().forEach(BlockExceptionHandler::removeTmpConnection);
|
||||
}
|
||||
|
||||
private static void removeTmpConnection(BlockNode block) {
|
||||
TmpEdgeAttr tmpEdgeAttr = block.get(AType.TMP_EDGE);
|
||||
if (tmpEdgeAttr != null) {
|
||||
// remove temp connection
|
||||
BlockSplitter.removeConnection(tmpEdgeAttr.getBlock(), block);
|
||||
block.remove(AType.TMP_EDGE);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<TryCatchBlockAttr> prepareTryBlocks(MethodNode mth) {
|
||||
Map<ExceptionHandler, List<BlockNode>> blocksByHandler = new HashMap<>();
|
||||
for (BlockNode block : mth.getBasicBlocks()) {
|
||||
|
||||
@@ -77,7 +77,9 @@ public class BlockProcessor extends AbstractVisitor {
|
||||
processNestedLoops(mth);
|
||||
|
||||
updateCleanSuccessors(mth);
|
||||
mth.finishBasicBlocks();
|
||||
if (!mth.contains(AFlag.DISABLE_BLOCKS_LOCK)) {
|
||||
mth.finishBasicBlocks();
|
||||
}
|
||||
}
|
||||
|
||||
static void updateCleanSuccessors(MethodNode mth) {
|
||||
@@ -686,7 +688,7 @@ public class BlockProcessor extends AbstractVisitor {
|
||||
return false;
|
||||
}
|
||||
|
||||
static void removeMarkedBlocks(MethodNode mth) {
|
||||
public static void removeMarkedBlocks(MethodNode mth) {
|
||||
mth.getBasicBlocks().removeIf(block -> {
|
||||
if (block.contains(AFlag.REMOVE)) {
|
||||
if (!block.getPredecessors().isEmpty() || !block.getSuccessors().isEmpty()) {
|
||||
|
||||
@@ -142,7 +142,7 @@ public class BlockSplitter extends AbstractVisitor {
|
||||
return block;
|
||||
}
|
||||
|
||||
static void connect(BlockNode from, BlockNode to) {
|
||||
public static void connect(BlockNode from, BlockNode to) {
|
||||
if (!from.getSuccessors().contains(to)) {
|
||||
from.getSuccessors().add(to);
|
||||
}
|
||||
@@ -151,19 +151,19 @@ public class BlockSplitter extends AbstractVisitor {
|
||||
}
|
||||
}
|
||||
|
||||
static void removeConnection(BlockNode from, BlockNode to) {
|
||||
public static void removeConnection(BlockNode from, BlockNode to) {
|
||||
from.getSuccessors().remove(to);
|
||||
to.getPredecessors().remove(from);
|
||||
}
|
||||
|
||||
static void removePredecessors(BlockNode block) {
|
||||
public static void removePredecessors(BlockNode block) {
|
||||
for (BlockNode pred : block.getPredecessors()) {
|
||||
pred.getSuccessors().remove(block);
|
||||
}
|
||||
block.getPredecessors().clear();
|
||||
}
|
||||
|
||||
static void replaceConnection(BlockNode source, BlockNode oldDest, BlockNode newDest) {
|
||||
public static void replaceConnection(BlockNode source, BlockNode oldDest, BlockNode newDest) {
|
||||
removeConnection(source, oldDest);
|
||||
connect(source, newDest);
|
||||
replaceTarget(source, oldDest, newDest);
|
||||
|
||||
@@ -56,24 +56,11 @@ public class DebugInfoApplyVisitor extends AbstractVisitor {
|
||||
mth.remove(AType.LOCAL_VARS_DEBUG_INFO);
|
||||
}
|
||||
processMethodParametersAttribute(mth);
|
||||
checkTypes(mth);
|
||||
} catch (Exception e) {
|
||||
mth.addWarnComment("Failed to apply debug info", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkTypes(MethodNode mth) {
|
||||
if (mth.isNoCode() || mth.getSVars().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
mth.getSVars().forEach(var -> {
|
||||
ArgType type = var.getTypeInfo().getType();
|
||||
if (!type.isTypeKnown()) {
|
||||
mth.addWarnComment("Type inference failed for: " + var.getDetailedVarInfo(mth));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void applyDebugInfo(MethodNode mth) {
|
||||
if (Consts.DEBUG_TYPE_INFERENCE) {
|
||||
LOG.info("Apply debug info for method: {}", mth);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package jadx.core.dex.visitors.typeinference;
|
||||
|
||||
import jadx.core.dex.instructions.args.ArgType;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.dex.visitors.AbstractVisitor;
|
||||
import jadx.core.dex.visitors.JadxVisitor;
|
||||
|
||||
@JadxVisitor(
|
||||
name = "Finish Type Inference",
|
||||
desc = "Check used types",
|
||||
runAfter = {
|
||||
TypeInferenceVisitor.class
|
||||
}
|
||||
)
|
||||
public final class FinishTypeInference extends AbstractVisitor {
|
||||
|
||||
@Override
|
||||
public void visit(MethodNode mth) {
|
||||
if (mth.isNoCode() || mth.getSVars().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
mth.getSVars().forEach(var -> {
|
||||
ArgType type = var.getTypeInfo().getType();
|
||||
if (!type.isTypeKnown()) {
|
||||
mth.addWarnComment("Type inference failed for: " + var.getDetailedVarInfo(mth));
|
||||
}
|
||||
ArgType codeVarType = var.getCodeVar().getType();
|
||||
if (codeVarType == null) {
|
||||
var.getCodeVar().setType(ArgType.UNKNOWN);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+4
@@ -46,6 +46,10 @@ public final class TypeBoundCheckCastAssign implements ITypeBoundDynamic {
|
||||
return insn.getResult();
|
||||
}
|
||||
|
||||
public IndexInsnNode getInsn() {
|
||||
return insn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CHECK_CAST_ASSIGN{(" + insn.getIndex() + ") " + insn.getArg(0).getType() + "}";
|
||||
|
||||
+16
-2
@@ -1,5 +1,7 @@
|
||||
package jadx.core.dex.visitors.typeinference;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.core.dex.instructions.InvokeNode;
|
||||
import jadx.core.dex.instructions.args.ArgType;
|
||||
import jadx.core.dex.instructions.args.InsnArg;
|
||||
@@ -48,12 +50,24 @@ public final class TypeBoundInvokeAssign implements ITypeBoundDynamic {
|
||||
mthDeclType = instanceType;
|
||||
}
|
||||
ArgType resultGeneric = root.getTypeUtils().replaceClassGenerics(instanceType, mthDeclType, genericReturnType);
|
||||
if (resultGeneric != null && !resultGeneric.isWildcard()) {
|
||||
return resultGeneric;
|
||||
ArgType result = processResultType(resultGeneric);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
return invokeNode.getCallMth().getReturnType();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ArgType processResultType(@Nullable ArgType resultGeneric) {
|
||||
if (resultGeneric == null) {
|
||||
return null;
|
||||
}
|
||||
if (!resultGeneric.isWildcard()) {
|
||||
return resultGeneric;
|
||||
}
|
||||
return resultGeneric.getWildcardType();
|
||||
}
|
||||
|
||||
private InsnArg getInstanceArg() {
|
||||
return invokeNode.getArg(0);
|
||||
}
|
||||
|
||||
+56
-1
@@ -57,6 +57,7 @@ import jadx.core.dex.visitors.ssa.SSATransform;
|
||||
import jadx.core.utils.BlockUtils;
|
||||
import jadx.core.utils.InsnList;
|
||||
import jadx.core.utils.InsnUtils;
|
||||
import jadx.core.utils.ListUtils;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.exceptions.JadxOverflowException;
|
||||
|
||||
@@ -83,6 +84,7 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
|
||||
this.resolvers = Arrays.asList(
|
||||
this::initTypeBounds,
|
||||
this::runTypePropagation,
|
||||
this::tryRestoreTypeVarCasts,
|
||||
this::tryInsertCasts,
|
||||
this::tryDeduceTypes,
|
||||
this::trySplitConstInsns,
|
||||
@@ -520,7 +522,60 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ForLoopReplaceableByWhile")
|
||||
/**
|
||||
* Fix check casts to type var extend type:
|
||||
* <br>
|
||||
* {@code <T extends Comparable> T var = (Comparable) obj; => T var = (T) obj; }
|
||||
*/
|
||||
private boolean tryRestoreTypeVarCasts(MethodNode mth) {
|
||||
int changed = 0;
|
||||
List<SSAVar> mthSVars = mth.getSVars();
|
||||
for (SSAVar var : mthSVars) {
|
||||
changed += restoreTypeVarCasts(var);
|
||||
}
|
||||
if (changed == 0) {
|
||||
return false;
|
||||
}
|
||||
if (Consts.DEBUG_TYPE_INFERENCE) {
|
||||
mth.addDebugComment("Restore " + changed + " type vars casts");
|
||||
}
|
||||
initTypeBounds(mth);
|
||||
return runTypePropagation(mth);
|
||||
}
|
||||
|
||||
private int restoreTypeVarCasts(SSAVar var) {
|
||||
TypeInfo typeInfo = var.getTypeInfo();
|
||||
Set<ITypeBound> bounds = typeInfo.getBounds();
|
||||
if (!ListUtils.anyMatch(bounds, t -> t.getType().isGenericType())) {
|
||||
return 0;
|
||||
}
|
||||
List<ITypeBound> casts = ListUtils.filter(bounds, TypeBoundCheckCastAssign.class::isInstance);
|
||||
if (casts.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
ArgType bestType = selectBestTypeFromBounds(bounds).orElse(ArgType.UNKNOWN);
|
||||
if (!bestType.isGenericType()) {
|
||||
return 0;
|
||||
}
|
||||
List<ArgType> extendTypes = bestType.getExtendTypes();
|
||||
if (extendTypes.size() != 1) {
|
||||
return 0;
|
||||
}
|
||||
int fixed = 0;
|
||||
ArgType extendType = extendTypes.get(0);
|
||||
for (ITypeBound bound : casts) {
|
||||
TypeBoundCheckCastAssign cast = (TypeBoundCheckCastAssign) bound;
|
||||
ArgType castType = cast.getType();
|
||||
TypeCompareEnum result = typeUpdate.getTypeCompare().compareTypes(extendType, castType);
|
||||
if (result.isEqual() || result == TypeCompareEnum.NARROW_BY_GENERIC) {
|
||||
cast.getInsn().updateIndex(bestType);
|
||||
fixed++;
|
||||
}
|
||||
}
|
||||
return fixed;
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "ForLoopReplaceableByWhile", "ForLoopReplaceableByForEach" })
|
||||
private boolean tryInsertCasts(MethodNode mth) {
|
||||
int added = 0;
|
||||
List<SSAVar> mthSVars = mth.getSVars();
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package jadx.core.dex.visitors.usage;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import jadx.api.plugins.input.data.ICallSite;
|
||||
import jadx.api.plugins.input.data.ICodeReader;
|
||||
import jadx.api.plugins.input.data.IMethodHandle;
|
||||
@@ -18,6 +21,7 @@ import jadx.core.dex.visitors.AbstractVisitor;
|
||||
import jadx.core.dex.visitors.JadxVisitor;
|
||||
import jadx.core.dex.visitors.OverrideMethodVisitor;
|
||||
import jadx.core.dex.visitors.rename.RenameVisitor;
|
||||
import jadx.core.utils.ListUtils;
|
||||
import jadx.core.utils.input.InsnDataUtils;
|
||||
|
||||
@JadxVisitor(
|
||||
@@ -134,4 +138,11 @@ public class UsageInfoVisitor extends AbstractVisitor {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void replaceMethodUsage(MethodNode mergeIntoMth, MethodNode sourceMth) {
|
||||
List<MethodNode> mergedUsage = ListUtils.distinctMergeSortedLists(mergeIntoMth.getUseIn(), sourceMth.getUseIn());
|
||||
mergedUsage.remove(sourceMth);
|
||||
mergeIntoMth.setUseIn(mergedUsage);
|
||||
sourceMth.setUseIn(Collections.emptyList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,6 +453,31 @@ public class BlockUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static void dfsVisit(MethodNode mth, Consumer<BlockNode> visitor) {
|
||||
BitSet visited = newBlocksBitSet(mth);
|
||||
Deque<BlockNode> queue = new ArrayDeque<>();
|
||||
BlockNode enterBlock = mth.getEnterBlock();
|
||||
queue.addLast(enterBlock);
|
||||
visited.set(mth.getEnterBlock().getId());
|
||||
while (true) {
|
||||
BlockNode current = queue.pollLast();
|
||||
if (current == null) {
|
||||
return;
|
||||
}
|
||||
visitor.accept(current);
|
||||
List<BlockNode> successors = current.getSuccessors();
|
||||
int count = successors.size();
|
||||
for (int i = count - 1; i >= 0; i--) { // to preserve order in queue
|
||||
BlockNode next = successors.get(i);
|
||||
int nextId = next.getId();
|
||||
if (!visited.get(nextId)) {
|
||||
queue.addLast(next);
|
||||
visited.set(nextId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static List<BlockNode> collectPredecessors(MethodNode mth, BlockNode start, Collection<BlockNode> stopBlocks) {
|
||||
BitSet bs = newBlocksBitSet(mth);
|
||||
if (!stopBlocks.isEmpty()) {
|
||||
|
||||
@@ -35,18 +35,21 @@ public class CodeGenUtils {
|
||||
List<JadxError> errors = node.getAll(AType.JADX_ERROR);
|
||||
if (!errors.isEmpty()) {
|
||||
errors.stream().distinct().sorted().forEach(err -> {
|
||||
code.startLine("/* JADX ERROR: ").add(err.getError());
|
||||
Throwable cause = err.getCause();
|
||||
if (cause != null) {
|
||||
code.incIndent();
|
||||
Utils.appendStackTrace(code, cause);
|
||||
code.decIndent();
|
||||
}
|
||||
code.add("*/");
|
||||
addError(code, err.getError(), err.getCause());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static void addError(ICodeWriter code, String errMsg, Throwable cause) {
|
||||
code.startLine("/* JADX ERROR: ").add(errMsg);
|
||||
if (cause != null) {
|
||||
code.incIndent();
|
||||
Utils.appendStackTrace(code, cause);
|
||||
code.decIndent();
|
||||
}
|
||||
code.add("*/");
|
||||
}
|
||||
|
||||
public static void addComments(ICodeWriter code, NotificationAttrNode node) {
|
||||
JadxCommentsAttr commentsAttr = node.get(AType.JADX_COMMENTS);
|
||||
if (commentsAttr != null) {
|
||||
|
||||
@@ -112,7 +112,7 @@ public class ListUtils {
|
||||
return list;
|
||||
}
|
||||
|
||||
public static <T> List<T> filter(List<T> list, Predicate<T> filter) {
|
||||
public static <T> List<T> filter(Collection<T> list, Predicate<T> filter) {
|
||||
if (list == null || list.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
@@ -148,7 +148,7 @@ public class ListUtils {
|
||||
return found;
|
||||
}
|
||||
|
||||
public static <T> boolean allMatch(List<T> list, Predicate<T> test) {
|
||||
public static <T> boolean allMatch(Collection<T> list, Predicate<T> test) {
|
||||
if (list == null || list.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
@@ -160,7 +160,7 @@ public class ListUtils {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static <T> boolean anyMatch(List<T> list, Predicate<T> test) {
|
||||
public static <T> boolean anyMatch(Collection<T> list, Predicate<T> test) {
|
||||
if (list == null || list.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package jadx.core.utils.log;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Escape input from untrusted source before pass to logger.
|
||||
* Suggested by CodeQL: https://codeql.github.com/codeql-query-help/java/java-log-injection/
|
||||
*/
|
||||
public class LogUtils {
|
||||
|
||||
private static final Pattern ALFA_NUMERIC = Pattern.compile("\\w*");
|
||||
|
||||
public static String escape(String input) {
|
||||
if (input == null) {
|
||||
return "null";
|
||||
}
|
||||
if (ALFA_NUMERIC.matcher(input).matches()) {
|
||||
return input;
|
||||
}
|
||||
return input.replaceAll("\\W", ".");
|
||||
}
|
||||
|
||||
public static String escape(byte[] input) {
|
||||
if (input == null) {
|
||||
return "null";
|
||||
}
|
||||
return escape(new String(input, StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
@@ -212,7 +212,8 @@ public class ResTableParser extends CommonBinaryParser {
|
||||
/* int headerSize = */
|
||||
is.readInt16();
|
||||
/* int size = */
|
||||
is.readInt32();
|
||||
long chunkSize = is.readUInt32();
|
||||
long chunkEnd = start + chunkSize;
|
||||
|
||||
int id = is.readInt8();
|
||||
is.checkInt8(0, "type chunk, res0");
|
||||
@@ -231,10 +232,15 @@ public class ResTableParser extends CommonBinaryParser {
|
||||
for (int i = 0; i < entryCount; i++) {
|
||||
entryIndexes[i] = is.readInt32();
|
||||
}
|
||||
|
||||
is.checkPos(entriesStart, "Expected entry start");
|
||||
for (int i = 0; i < entryCount; i++) {
|
||||
if (entryIndexes[i] != NO_ENTRY) {
|
||||
if (is.getPos() >= chunkEnd) {
|
||||
// Certain resource obfuscated apps like com.facebook.orca have more entries defined
|
||||
// than actually fit into the chunk size -> ignore the remaining entries
|
||||
LOG.warn("End of chunk reached - ignoring remaining {} entries", entryCount - i);
|
||||
break;
|
||||
}
|
||||
parseEntry(pkg, id, i, config.getQualifiers());
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,13 @@
|
||||
package jadx.core.utils.log;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class LogUtilsTest {
|
||||
|
||||
@Test
|
||||
void escape() {
|
||||
assertThat(LogUtils.escape("Guest'%0AUser:'Admin")).isEqualTo("Guest..0AUser..Admin");
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import java.util.jar.JarOutputStream;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
@@ -219,7 +220,7 @@ public abstract class IntegrationTest extends TestUtils {
|
||||
return sortedClsNodes;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@NotNull
|
||||
public ClassNode searchCls(List<ClassNode> list, String clsName) {
|
||||
for (ClassNode cls : list) {
|
||||
if (cls.getClassInfo().getFullName().equals(clsName)) {
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
package jadx.tests.api.utils.assertj;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.assertj.core.api.AbstractObjectAssert;
|
||||
import org.assertj.core.api.Assertions;
|
||||
|
||||
import jadx.api.CodePosition;
|
||||
import jadx.api.ICodeInfo;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.ICodeNode;
|
||||
import jadx.tests.api.IntegrationTest;
|
||||
|
||||
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
|
||||
public class JadxClassNodeAssertions extends AbstractObjectAssert<JadxClassNodeAssertions, ClassNode> {
|
||||
public JadxClassNodeAssertions(ClassNode cls) {
|
||||
@@ -52,4 +57,22 @@ public class JadxClassNodeAssertions extends AbstractObjectAssert<JadxClassNodeA
|
||||
testInstance.runDecompiledAutoCheck(actual);
|
||||
return this;
|
||||
}
|
||||
|
||||
public void checkCodeAnnotationFor(String refStr, ICodeNode node) {
|
||||
checkCodeAnnotationFor(refStr, 0, node);
|
||||
}
|
||||
|
||||
public void checkCodeAnnotationFor(String refStr, int refOffset, ICodeNode node) {
|
||||
ICodeInfo code = actual.getCode();
|
||||
int codePos = code.getCodeStr().indexOf(refStr);
|
||||
assertThat(codePos).describedAs("String '%s' not found", refStr).isNotEqualTo(-1);
|
||||
int refPos = codePos + refOffset;
|
||||
for (Map.Entry<CodePosition, Object> entry : code.getAnnotations().entrySet()) {
|
||||
if (entry.getKey().getPos() == refPos) {
|
||||
Assertions.assertThat(entry.getValue()).isEqualTo(node);
|
||||
return;
|
||||
}
|
||||
}
|
||||
fail("Annotation for reference string: '%s' at position %d not found", refStr, refPos);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ public class TestFallbackMode extends IntegrationTest {
|
||||
|
||||
assertThat(code, containsString("public int test(int r2) {"));
|
||||
assertThat(code, containsOne("r1 = this;"));
|
||||
assertThat(code, containsOne("L_0x0000:"));
|
||||
assertThat(code, containsOne("L_0x0007:"));
|
||||
assertThat(code, containsOne("L0:"));
|
||||
assertThat(code, containsOne("L7:"));
|
||||
assertThat(code, containsOne("int r2 = r2 + 1"));
|
||||
assertThat(code, not(containsString("throw new UnsupportedOperationException")));
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@ package jadx.tests.integration.inner;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jadx.api.CommentsLevel;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.utils.ListUtils;
|
||||
import jadx.tests.api.SmaliTest;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
|
||||
|
||||
public class TestAnonymousClass14 extends SmaliTest {
|
||||
// @formatter:off
|
||||
@@ -44,11 +45,23 @@ public class TestAnonymousClass14 extends SmaliTest {
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
ClassNode clsNode = getClassNodeFromSmaliFiles("inner", "TestAnonymousClass14", "OuterCls");
|
||||
String code = clsNode.getCode().toString();
|
||||
code = code.replaceAll("/\\*.*?\\*/", ""); // remove block comments
|
||||
getArgs().setCommentsLevel(CommentsLevel.WARN);
|
||||
ClassNode outerCls = getClassNodeFromSmaliFiles("OuterCls");
|
||||
assertThat(outerCls).code()
|
||||
.doesNotContain("synthetic", "AnonymousClass1")
|
||||
.describedAs("only one constructor").containsOne("private TestCls(")
|
||||
.describedAs("constructor without args").containsOne("private TestCls() {");
|
||||
|
||||
assertThat(code, not(containsString("AnonymousClass1")));
|
||||
assertThat(code, not(containsString("synthetic")));
|
||||
MethodNode makeTestClsMth = outerCls.searchMethodByShortName("makeTestCls");
|
||||
assertThat(makeTestClsMth).isNotNull();
|
||||
|
||||
ClassNode testCls = searchCls(outerCls.getInnerClasses(), "TestCls");
|
||||
MethodNode ctrMth = ListUtils.filterOnlyOne(testCls.getMethods(),
|
||||
m -> m.isConstructor() && !m.getAccessFlags().isSynthetic());
|
||||
assertThat(ctrMth).isNotNull();
|
||||
assertThat(ctrMth.getUseIn()).hasSize(1);
|
||||
assertThat(ctrMth.getUseIn().get(0)).isEqualTo(makeTestClsMth);
|
||||
|
||||
assertThat(outerCls).checkCodeAnnotationFor("new TestCls();", 4, ctrMth);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,20 @@ package jadx.tests.integration.inner;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.api.CommentsLevel;
|
||||
import jadx.tests.api.IntegrationTest;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
|
||||
|
||||
public class TestOuterConstructorCall extends IntegrationTest {
|
||||
|
||||
@SuppressWarnings({ "InnerClassMayBeStatic", "unused" })
|
||||
public static class TestCls {
|
||||
private TestCls(Inner inner) {
|
||||
System.out.println(inner);
|
||||
}
|
||||
|
||||
private class Inner {
|
||||
@SuppressWarnings("unused")
|
||||
private TestCls test() {
|
||||
return new TestCls(this);
|
||||
}
|
||||
@@ -26,11 +24,11 @@ public class TestOuterConstructorCall extends IntegrationTest {
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
ClassNode cls = getClassNode(TestCls.class);
|
||||
String code = cls.getCode().toString();
|
||||
|
||||
assertThat(code, containsString("private class Inner {"));
|
||||
assertThat(code, containsString("return new TestOuterConstructorCall$TestCls(this);"));
|
||||
assertThat(code, not(containsString("synthetic")));
|
||||
getArgs().setCommentsLevel(CommentsLevel.WARN);
|
||||
assertThat(getClassNode(TestCls.class))
|
||||
.code()
|
||||
.containsOne("class Inner {")
|
||||
.containsOne("return new TestOuterConstructorCall$TestCls(this);")
|
||||
.doesNotContain("synthetic", "this$0");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package jadx.tests.integration.types;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jadx.tests.api.SmaliTest;
|
||||
import jadx.tests.api.extensions.profiles.TestProfile;
|
||||
import jadx.tests.api.extensions.profiles.TestWithProfiles;
|
||||
|
||||
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
|
||||
|
||||
/**
|
||||
* Issue 1238
|
||||
*/
|
||||
public class TestTypeResolver20 extends SmaliTest {
|
||||
|
||||
public static class TestCls {
|
||||
public interface Sequence<T> {
|
||||
Iterator<T> iterator();
|
||||
}
|
||||
|
||||
public static <T extends Comparable<? super T>> T max(Sequence<? extends T> seq) {
|
||||
Iterator<? extends T> it = seq.iterator();
|
||||
if (!it.hasNext()) {
|
||||
return null;
|
||||
}
|
||||
T t = it.next();
|
||||
while (it.hasNext()) {
|
||||
T next = it.next();
|
||||
if (t.compareTo(next) < 0) {
|
||||
t = next;
|
||||
}
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
private static class ArraySeq<T> implements Sequence<T> {
|
||||
private final List<T> list;
|
||||
|
||||
@SafeVarargs
|
||||
public ArraySeq(T... arr) {
|
||||
this.list = Arrays.asList(arr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<T> iterator() {
|
||||
return list.iterator();
|
||||
}
|
||||
}
|
||||
|
||||
public void check() {
|
||||
assertThat(max(new ArraySeq<>(2, 5, 3, 4))).isEqualTo(5);
|
||||
}
|
||||
}
|
||||
|
||||
@TestWithProfiles({ TestProfile.DX_J8, TestProfile.JAVA8 })
|
||||
public void test() {
|
||||
noDebugInfo();
|
||||
assertThat(getClassNode(TestCls.class))
|
||||
.code()
|
||||
.doesNotContain("next = next;")
|
||||
.containsOne("T next = it.next();");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSmali() {
|
||||
assertThat(getClassNodeFromSmaliFiles())
|
||||
.code()
|
||||
.containsOne("T next = it.next();")
|
||||
.containsOne("T next2 = it.next();");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
.class public interface abstract Lkotlin/sequences/Sequence;
|
||||
.super Ljava/lang/Object;
|
||||
.source "SourceFile"
|
||||
|
||||
.annotation system Ldalvik/annotation/Signature;
|
||||
value = {
|
||||
"<T:",
|
||||
"Ljava/lang/Object;",
|
||||
">",
|
||||
"Ljava/lang/Object;"
|
||||
}
|
||||
.end annotation
|
||||
|
||||
.method public abstract iterator()Ljava/util/Iterator;
|
||||
.annotation system Ldalvik/annotation/Signature;
|
||||
value = {
|
||||
"()",
|
||||
"Ljava/util/Iterator<",
|
||||
"TT;>;"
|
||||
}
|
||||
.end annotation
|
||||
.end method
|
||||
@@ -0,0 +1,61 @@
|
||||
.class public Ltypes/TestTypeResolver20;
|
||||
.super Ljava/lang/Object;
|
||||
.source "SourceFile"
|
||||
|
||||
|
||||
.method public static final max(Lkotlin/sequences/Sequence;)Ljava/lang/Comparable;
|
||||
.registers 4
|
||||
.annotation system Ldalvik/annotation/Signature;
|
||||
value = {
|
||||
"<T::",
|
||||
"Ljava/lang/Comparable<",
|
||||
"-TT;>;>(",
|
||||
"Lkotlin/sequences/Sequence<",
|
||||
"+TT;>;)TT;"
|
||||
}
|
||||
.end annotation
|
||||
|
||||
.line 1147
|
||||
invoke-interface {p0}, Lkotlin/sequences/Sequence;->iterator()Ljava/util/Iterator;
|
||||
move-result-object p0
|
||||
|
||||
.line 1148
|
||||
invoke-interface {p0}, Ljava/util/Iterator;->hasNext()Z
|
||||
move-result v0
|
||||
|
||||
if-nez v0, :cond_11
|
||||
|
||||
const/4 p0, 0x0
|
||||
return-object p0
|
||||
|
||||
.line 1149
|
||||
:cond_11
|
||||
invoke-interface {p0}, Ljava/util/Iterator;->next()Ljava/lang/Object;
|
||||
move-result-object v0
|
||||
check-cast v0, Ljava/lang/Comparable;
|
||||
|
||||
.line 1150
|
||||
:cond_17
|
||||
:goto_17
|
||||
invoke-interface {p0}, Ljava/util/Iterator;->hasNext()Z
|
||||
move-result v1
|
||||
|
||||
if-eqz v1, :cond_2b
|
||||
|
||||
.line 1151
|
||||
invoke-interface {p0}, Ljava/util/Iterator;->next()Ljava/lang/Object;
|
||||
move-result-object v1
|
||||
check-cast v1, Ljava/lang/Comparable;
|
||||
|
||||
.line 1152
|
||||
invoke-interface {v0, v1}, Ljava/lang/Comparable;->compareTo(Ljava/lang/Object;)I
|
||||
move-result v2
|
||||
|
||||
if-gez v2, :cond_17
|
||||
|
||||
move-object v0, v1
|
||||
goto :goto_17
|
||||
|
||||
:cond_2b
|
||||
return-object v0
|
||||
.end method
|
||||
@@ -1,6 +1,6 @@
|
||||
plugins {
|
||||
id 'application'
|
||||
id 'edu.sc.seis.launch4j' version '2.5.2'
|
||||
id 'edu.sc.seis.launch4j' version '2.5.3'
|
||||
id 'com.github.johnrengelman.shadow' version '7.1.2'
|
||||
id 'org.beryx.runtime' version '1.12.7'
|
||||
}
|
||||
@@ -34,6 +34,9 @@ dependencies {
|
||||
application {
|
||||
applicationName = 'jadx-gui'
|
||||
mainClass.set('jadx.gui.JadxGUI')
|
||||
// The option -XX:+UseG1GC is only relevant for Java 8. Starting with Java 9 G1GC is already the default GC
|
||||
applicationDefaultJvmArgs = ['-Xms128M', '-XX:MaxRAMPercentage=70.0', '-XX:+UseG1GC',
|
||||
'-Dawt.useSystemAAFontSettings=lcd', '-Dswing.aatext=true']
|
||||
}
|
||||
|
||||
applicationDistribution.with {
|
||||
@@ -62,8 +65,6 @@ shadowJar {
|
||||
}
|
||||
|
||||
startScripts {
|
||||
// The option -XX:+UseG1GC is only relevant for Java 8. Starting with Java 9 G1GC is already the default GC
|
||||
defaultJvmOpts = ['-Xms128M', '-Xmx4g', '-Dawt.useSystemAAFontSettings=lcd', '-Dswing.aatext=true', '-XX:+UseG1GC']
|
||||
doLast {
|
||||
def str = windowsScript.text
|
||||
str = str.replaceAll('java.exe', 'javaw.exe')
|
||||
|
||||
@@ -27,7 +27,10 @@ public class JadxGUI {
|
||||
if (!settings.overrideProvided(args)) {
|
||||
return;
|
||||
}
|
||||
LogHelper.initLogLevel(settings);
|
||||
LogHelper.setLogLevelsForDecompileStage();
|
||||
printSystemInfo();
|
||||
|
||||
LafManager.init(settings);
|
||||
NLS.setLocale(settings.getLangLocale());
|
||||
ExceptionDialog.registerUncaughtExceptionHandler();
|
||||
|
||||
@@ -11,6 +11,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.tree.DefaultMutableTreeNode;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
@@ -33,7 +34,6 @@ import jadx.gui.device.debugger.SmaliDebugger.RuntimeField;
|
||||
import jadx.gui.device.debugger.SmaliDebugger.RuntimeRegister;
|
||||
import jadx.gui.device.debugger.SmaliDebugger.RuntimeValue;
|
||||
import jadx.gui.device.debugger.SmaliDebugger.RuntimeVarInfo;
|
||||
import jadx.gui.device.debugger.SmaliDebugger.SmaliDebuggerException;
|
||||
import jadx.gui.device.debugger.smali.Smali;
|
||||
import jadx.gui.device.debugger.smali.SmaliRegister;
|
||||
import jadx.gui.treemodel.JClass;
|
||||
@@ -41,8 +41,7 @@ import jadx.gui.ui.panel.IDebugController;
|
||||
import jadx.gui.ui.panel.JDebuggerPanel;
|
||||
import jadx.gui.ui.panel.JDebuggerPanel.IListElement;
|
||||
import jadx.gui.ui.panel.JDebuggerPanel.ValueTreeNode;
|
||||
|
||||
import static jadx.gui.device.debugger.SmaliDebugger.RuntimeType;
|
||||
import jadx.gui.utils.NLS;
|
||||
|
||||
public final class DebugController implements SmaliDebugger.SuspendListener, IDebugController {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DebugController.class);
|
||||
@@ -72,23 +71,22 @@ public final class DebugController implements SmaliDebugger.SuspendListener, IDe
|
||||
private final ExecutorService updateQueue = Executors.newSingleThreadExecutor();
|
||||
private final ExecutorService lazyQueue = Executors.newSingleThreadExecutor();
|
||||
|
||||
/**
|
||||
* @param args at least 3 elements, host, port and android release version respectively.
|
||||
*/
|
||||
@Override
|
||||
public boolean startDebugger(JDebuggerPanel debuggerPanel, String[] args) {
|
||||
public boolean startDebugger(JDebuggerPanel debuggerPanel, String adbHost, int adbPort, int androidVer) {
|
||||
if (TYPE_MAP.isEmpty()) {
|
||||
initTypeMap();
|
||||
}
|
||||
this.debuggerPanel = debuggerPanel;
|
||||
debuggerPanel.resetUI();
|
||||
try {
|
||||
debugger = SmaliDebugger.attach(args[0], Integer.parseInt(args[1]), this);
|
||||
debugger = SmaliDebugger.attach(adbHost, adbPort, this);
|
||||
} catch (SmaliDebuggerException e) {
|
||||
JOptionPane.showMessageDialog(debuggerPanel.getMainWindow(), e.getMessage(),
|
||||
NLS.str("error_dialog.title"), JOptionPane.ERROR_MESSAGE);
|
||||
logErr(e);
|
||||
return false;
|
||||
}
|
||||
art = ArtAdapter.getAdapter(Integer.parseInt(args[2]));
|
||||
art = ArtAdapter.getAdapter(androidVer);
|
||||
resetAllInfo();
|
||||
hasResumed = false;
|
||||
run = debugger::resume;
|
||||
@@ -359,7 +357,7 @@ public final class DebugController implements SmaliDebugger.SuspendListener, IDe
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuspendEvent(SmaliDebugger.SuspendInfo info) {
|
||||
public void onSuspendEvent(SuspendInfo info) {
|
||||
if (!isDebugging()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package jadx.gui.device.debugger;
|
||||
|
||||
import io.github.hqktech.JDWP.Event.Composite.BreakpointEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.ClassPrepareEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.ClassUnloadEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.ExceptionEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.FieldAccessEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.FieldModificationEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.MethodEntryEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.MethodExitEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.MethodExitWithReturnValueEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.MonitorContendedEnterEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.MonitorContendedEnteredEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.MonitorWaitEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.MonitorWaitedEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.SingleStepEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.ThreadDeathEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.ThreadStartEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.VMDeathEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.VMStartEvent;
|
||||
|
||||
abstract class EventListenerAdapter {
|
||||
void onVMStart(VMStartEvent event) {
|
||||
}
|
||||
|
||||
void onVMDeath(VMDeathEvent event) {
|
||||
}
|
||||
|
||||
void onSingleStep(SingleStepEvent event) {
|
||||
}
|
||||
|
||||
void onBreakpoint(BreakpointEvent event) {
|
||||
}
|
||||
|
||||
void onMethodEntry(MethodEntryEvent event) {
|
||||
}
|
||||
|
||||
void onMethodExit(MethodExitEvent event) {
|
||||
}
|
||||
|
||||
void onMethodExitWithReturnValue(MethodExitWithReturnValueEvent event) {
|
||||
}
|
||||
|
||||
void onMonitorContendedEnter(MonitorContendedEnterEvent event) {
|
||||
}
|
||||
|
||||
void onMonitorContendedEntered(MonitorContendedEnteredEvent event) {
|
||||
}
|
||||
|
||||
void onMonitorWait(MonitorWaitEvent event) {
|
||||
}
|
||||
|
||||
void onMonitorWaited(MonitorWaitedEvent event) {
|
||||
}
|
||||
|
||||
void onException(ExceptionEvent event) {
|
||||
}
|
||||
|
||||
void onThreadStart(ThreadStartEvent event) {
|
||||
}
|
||||
|
||||
void onThreadDeath(ThreadDeathEvent event) {
|
||||
}
|
||||
|
||||
void onClassPrepare(ClassPrepareEvent event) {
|
||||
}
|
||||
|
||||
void onClassUnload(ClassUnloadEvent event) {
|
||||
}
|
||||
|
||||
void onFieldAccess(FieldAccessEvent event) {
|
||||
}
|
||||
|
||||
void onFieldModification(FieldModificationEvent event) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package jadx.gui.device.debugger;
|
||||
|
||||
import io.github.hqktech.JDWP;
|
||||
|
||||
public enum RuntimeType {
|
||||
ARRAY(91, "[]"),
|
||||
BYTE(66, "byte"),
|
||||
CHAR(67, "char"),
|
||||
OBJECT(76, "object"),
|
||||
FLOAT(70, "float"),
|
||||
DOUBLE(68, "double"),
|
||||
INT(73, "int"),
|
||||
LONG(74, "long"),
|
||||
SHORT(83, "short"),
|
||||
VOID(86, "void"),
|
||||
BOOLEAN(90, "boolean"),
|
||||
STRING(115, "string"),
|
||||
THREAD(116, "thread"),
|
||||
THREAD_GROUP(103, "thread_group"),
|
||||
CLASS_LOADER(108, "class_loader"),
|
||||
CLASS_OBJECT(99, "class_object");
|
||||
|
||||
private final int jdwpTag;
|
||||
private final String desc;
|
||||
|
||||
RuntimeType(int tag, String desc) {
|
||||
this.jdwpTag = tag;
|
||||
this.desc = desc;
|
||||
}
|
||||
|
||||
public int getTag() {
|
||||
return jdwpTag;
|
||||
}
|
||||
|
||||
public String getDesc() {
|
||||
return this.desc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a <code>JDWP.Tag</code> to a {@link RuntimeType}
|
||||
*
|
||||
* @param tag
|
||||
* @return
|
||||
* @throws SmaliDebuggerException
|
||||
*/
|
||||
public static RuntimeType fromJdwpTag(int tag) throws SmaliDebuggerException {
|
||||
switch (tag) {
|
||||
case JDWP.Tag.ARRAY:
|
||||
return RuntimeType.ARRAY;
|
||||
case JDWP.Tag.BYTE:
|
||||
return RuntimeType.BYTE;
|
||||
case JDWP.Tag.CHAR:
|
||||
return RuntimeType.CHAR;
|
||||
case JDWP.Tag.OBJECT:
|
||||
return RuntimeType.OBJECT;
|
||||
case JDWP.Tag.FLOAT:
|
||||
return RuntimeType.FLOAT;
|
||||
case JDWP.Tag.DOUBLE:
|
||||
return RuntimeType.DOUBLE;
|
||||
case JDWP.Tag.INT:
|
||||
return RuntimeType.INT;
|
||||
case JDWP.Tag.LONG:
|
||||
return RuntimeType.LONG;
|
||||
case JDWP.Tag.SHORT:
|
||||
return RuntimeType.SHORT;
|
||||
case JDWP.Tag.VOID:
|
||||
return RuntimeType.VOID;
|
||||
case JDWP.Tag.BOOLEAN:
|
||||
return RuntimeType.BOOLEAN;
|
||||
case JDWP.Tag.STRING:
|
||||
return RuntimeType.STRING;
|
||||
case JDWP.Tag.THREAD:
|
||||
return RuntimeType.THREAD;
|
||||
case JDWP.Tag.THREAD_GROUP:
|
||||
return RuntimeType.THREAD_GROUP;
|
||||
case JDWP.Tag.CLASS_LOADER:
|
||||
return RuntimeType.CLASS_LOADER;
|
||||
case JDWP.Tag.CLASS_OBJECT:
|
||||
return RuntimeType.CLASS_OBJECT;
|
||||
default:
|
||||
throw new SmaliDebuggerException("Unexpected value: " + tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,7 @@ import io.reactivex.annotations.NonNull;
|
||||
|
||||
import jadx.api.plugins.input.data.AccessFlags;
|
||||
import jadx.gui.device.debugger.smali.RegisterInfo;
|
||||
import jadx.gui.utils.IOUtils;
|
||||
import jadx.gui.utils.ObjectPool;
|
||||
|
||||
// TODO: Finish error notification, inner errors should be logged let user notice.
|
||||
@@ -83,9 +84,9 @@ public class SmaliDebugger {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SmaliDebugger.class);
|
||||
private final JDWP jdwp;
|
||||
private int localTcpPort;
|
||||
private InputStream inputStream;
|
||||
private OutputStream outputStream;
|
||||
private final int localTcpPort;
|
||||
private final InputStream inputStream;
|
||||
private final OutputStream outputStream;
|
||||
|
||||
// All event callbacks will be called in this queue, e.g. class prepare/unload
|
||||
private static final Executor EVENT_LISTENER_QUEUE = Executors.newSingleThreadExecutor();
|
||||
@@ -118,10 +119,13 @@ public class SmaliDebugger {
|
||||
private static final ICommandResult SKIP_RESULT = res -> {
|
||||
};
|
||||
|
||||
private SmaliDebugger(SuspendListener suspendListener, int localTcpPort, JDWP jdwp) {
|
||||
private SmaliDebugger(SuspendListener suspendListener, int localTcpPort, JDWP jdwp, InputStream inputStream,
|
||||
OutputStream outputStream) {
|
||||
this.jdwp = jdwp;
|
||||
this.localTcpPort = localTcpPort;
|
||||
this.suspendListener = suspendListener;
|
||||
this.inputStream = inputStream;
|
||||
this.outputStream = outputStream;
|
||||
|
||||
oneOffEventReq = jdwp.eventRequest().cmdSet().newCountRequest();
|
||||
oneOffEventReq.count = 1;
|
||||
@@ -135,6 +139,7 @@ public class SmaliDebugger {
|
||||
try {
|
||||
byte[] bytes = JDWP.IDSizes.encode().getBytes();
|
||||
JDWP.setPacketID(bytes, 1);
|
||||
LOG.debug("Connecting to ADB {}:{}", host, port);
|
||||
Socket socket = new Socket(host, port);
|
||||
InputStream inputStream = socket.getInputStream();
|
||||
OutputStream outputStream = socket.getOutputStream();
|
||||
@@ -143,9 +148,7 @@ public class SmaliDebugger {
|
||||
JDWP jdwp = initJDWP(outputStream, inputStream);
|
||||
socket.setSoTimeout(0); // set back to 0 so the decodingLoop won't break for timeout.
|
||||
|
||||
SmaliDebugger debugger = new SmaliDebugger(suspendListener, port, jdwp);
|
||||
debugger.inputStream = inputStream;
|
||||
debugger.outputStream = outputStream;
|
||||
SmaliDebugger debugger = new SmaliDebugger(suspendListener, port, jdwp, inputStream, outputStream);
|
||||
|
||||
debugger.decodingLoop();
|
||||
debugger.listenClassUnloadEvent();
|
||||
@@ -262,7 +265,7 @@ public class SmaliDebugger {
|
||||
for (int i = 0; i < values.size(); i++) {
|
||||
ObjectReference.GetValues.GetValuesReplyDataValues value = values.get(i);
|
||||
flds.get(i).setValue(value.value.idOrValue)
|
||||
.setType(getType(value.value.tag));
|
||||
.setType(RuntimeType.fromJdwpTag(value.value.tag));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,7 +505,7 @@ public class SmaliDebugger {
|
||||
ObjectReference.GetValues.GetValuesReplyData data =
|
||||
jdwp.objectReference().cmdGetValues().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE);
|
||||
fld.setValue(data.values.get(0).value.idOrValue)
|
||||
.setType(getType(data.values.get(0).value.tag));
|
||||
.setType(RuntimeType.fromJdwpTag(data.values.get(0).value.tag));
|
||||
}
|
||||
|
||||
private long createString(String localStr) throws SmaliDebuggerException {
|
||||
@@ -636,7 +639,7 @@ public class SmaliDebugger {
|
||||
byte[] buf;
|
||||
try {
|
||||
outputStream.write(JDWP.encodeHandShakePacket());
|
||||
buf = readBytes(inputStream, 14);
|
||||
buf = IOUtils.readNBytes(inputStream, 14);
|
||||
} catch (Exception e) {
|
||||
throw new SmaliDebuggerException("jdwp handshake failed", e);
|
||||
}
|
||||
@@ -1128,7 +1131,7 @@ public class SmaliDebugger {
|
||||
@Nullable
|
||||
private static Packet readPacket(InputStream inputStream) throws SmaliDebuggerException {
|
||||
try {
|
||||
byte[] header = readBytes(inputStream, JDWP.PACKET_HEADER_SIZE);
|
||||
byte[] header = IOUtils.readNBytes(inputStream, JDWP.PACKET_HEADER_SIZE);
|
||||
if (header == null) {
|
||||
// stream ended
|
||||
return null;
|
||||
@@ -1137,7 +1140,7 @@ public class SmaliDebugger {
|
||||
if (bodyLength <= 0) {
|
||||
return Packet.make(header);
|
||||
}
|
||||
byte[] body = readBytes(inputStream, bodyLength);
|
||||
byte[] body = IOUtils.readNBytes(inputStream, bodyLength);
|
||||
if (body == null) {
|
||||
throw new SmaliDebuggerException("Stream truncated");
|
||||
}
|
||||
@@ -1147,21 +1150,6 @@ public class SmaliDebugger {
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] readBytes(InputStream inputStream, int len) throws IOException {
|
||||
byte[] payload = new byte[len];
|
||||
int readSize = 0;
|
||||
while (true) {
|
||||
int read = inputStream.read(payload, readSize, len - readSize);
|
||||
if (read == -1) {
|
||||
return null;
|
||||
}
|
||||
readSize += read;
|
||||
if (readSize == len) {
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] concatBytes(byte[] buf1, byte[] buf2) {
|
||||
byte[] tempBuf = new byte[buf1.length + buf2.length];
|
||||
System.arraycopy(buf1, 0, tempBuf, 0, buf1.length);
|
||||
@@ -1183,62 +1171,6 @@ public class SmaliDebugger {
|
||||
void onCommandReply(Packet res) throws SmaliDebuggerException;
|
||||
}
|
||||
|
||||
private abstract class EventListenerAdapter {
|
||||
void onVMStart(VMStartEvent event) {
|
||||
}
|
||||
|
||||
void onVMDeath(VMDeathEvent event) {
|
||||
}
|
||||
|
||||
void onSingleStep(SingleStepEvent event) {
|
||||
}
|
||||
|
||||
void onBreakpoint(BreakpointEvent event) {
|
||||
}
|
||||
|
||||
void onMethodEntry(MethodEntryEvent event) {
|
||||
}
|
||||
|
||||
void onMethodExit(MethodExitEvent event) {
|
||||
}
|
||||
|
||||
void onMethodExitWithReturnValue(MethodExitWithReturnValueEvent event) {
|
||||
}
|
||||
|
||||
void onMonitorContendedEnter(MonitorContendedEnterEvent event) {
|
||||
}
|
||||
|
||||
void onMonitorContendedEntered(MonitorContendedEnteredEvent event) {
|
||||
}
|
||||
|
||||
void onMonitorWait(MonitorWaitEvent event) {
|
||||
}
|
||||
|
||||
void onMonitorWaited(MonitorWaitedEvent event) {
|
||||
}
|
||||
|
||||
void onException(ExceptionEvent event) {
|
||||
}
|
||||
|
||||
void onThreadStart(ThreadStartEvent event) {
|
||||
}
|
||||
|
||||
void onThreadDeath(ThreadDeathEvent event) {
|
||||
}
|
||||
|
||||
void onClassPrepare(ClassPrepareEvent event) {
|
||||
}
|
||||
|
||||
void onClassUnload(ClassUnloadEvent event) {
|
||||
}
|
||||
|
||||
void onFieldAccess(FieldAccessEvent event) {
|
||||
}
|
||||
|
||||
void onFieldModification(FieldModificationEvent event) {
|
||||
}
|
||||
}
|
||||
|
||||
public static class RuntimeField extends RuntimeValue {
|
||||
private final String name;
|
||||
private final String fldType;
|
||||
@@ -1296,46 +1228,7 @@ public class SmaliDebugger {
|
||||
}
|
||||
|
||||
private RuntimeRegister buildRegister(int num, int tag, ByteBuffer buf) throws SmaliDebuggerException {
|
||||
return new RuntimeRegister(num, getType(tag), buf);
|
||||
}
|
||||
|
||||
private RuntimeType getType(int tag) throws SmaliDebuggerException {
|
||||
switch (tag) {
|
||||
case JDWP.Tag.ARRAY:
|
||||
return RuntimeType.ARRAY;
|
||||
case JDWP.Tag.BYTE:
|
||||
return RuntimeType.BYTE;
|
||||
case JDWP.Tag.CHAR:
|
||||
return RuntimeType.CHAR;
|
||||
case JDWP.Tag.OBJECT:
|
||||
return RuntimeType.OBJECT;
|
||||
case JDWP.Tag.FLOAT:
|
||||
return RuntimeType.FLOAT;
|
||||
case JDWP.Tag.DOUBLE:
|
||||
return RuntimeType.DOUBLE;
|
||||
case JDWP.Tag.INT:
|
||||
return RuntimeType.INT;
|
||||
case JDWP.Tag.LONG:
|
||||
return RuntimeType.LONG;
|
||||
case JDWP.Tag.SHORT:
|
||||
return RuntimeType.SHORT;
|
||||
case JDWP.Tag.VOID:
|
||||
return RuntimeType.VOID;
|
||||
case JDWP.Tag.BOOLEAN:
|
||||
return RuntimeType.BOOLEAN;
|
||||
case JDWP.Tag.STRING:
|
||||
return RuntimeType.STRING;
|
||||
case JDWP.Tag.THREAD:
|
||||
return RuntimeType.THREAD;
|
||||
case JDWP.Tag.THREAD_GROUP:
|
||||
return RuntimeType.THREAD_GROUP;
|
||||
case JDWP.Tag.CLASS_LOADER:
|
||||
return RuntimeType.CLASS_LOADER;
|
||||
case JDWP.Tag.CLASS_OBJECT:
|
||||
return RuntimeType.CLASS_OBJECT;
|
||||
default:
|
||||
throw new SmaliDebuggerException("Unexpected value: " + tag);
|
||||
}
|
||||
return new RuntimeRegister(num, RuntimeType.fromJdwpTag(tag), buf);
|
||||
}
|
||||
|
||||
public static class RuntimeValue {
|
||||
@@ -1428,41 +1321,6 @@ public class SmaliDebugger {
|
||||
}
|
||||
}
|
||||
|
||||
public enum RuntimeType {
|
||||
ARRAY(91, "[]"),
|
||||
BYTE(66, "byte"),
|
||||
CHAR(67, "char"),
|
||||
OBJECT(76, "object"),
|
||||
FLOAT(70, "float"),
|
||||
DOUBLE(68, "double"),
|
||||
INT(73, "int"),
|
||||
LONG(74, "long"),
|
||||
SHORT(83, "short"),
|
||||
VOID(86, "void"),
|
||||
BOOLEAN(90, "boolean"),
|
||||
STRING(115, "string"),
|
||||
THREAD(116, "thread"),
|
||||
THREAD_GROUP(103, "thread_group"),
|
||||
CLASS_LOADER(108, "class_loader"),
|
||||
CLASS_OBJECT(99, "class_object");
|
||||
|
||||
private final int jdwpTag;
|
||||
private final String desc;
|
||||
|
||||
RuntimeType(int tag, String desc) {
|
||||
this.jdwpTag = tag;
|
||||
this.desc = desc;
|
||||
}
|
||||
|
||||
private int getTag() {
|
||||
return jdwpTag;
|
||||
}
|
||||
|
||||
public String getDesc() {
|
||||
return this.desc;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Frame {
|
||||
private final long id;
|
||||
private final long clsID;
|
||||
@@ -1503,35 +1361,6 @@ public class SmaliDebugger {
|
||||
void onUnloaded(String cls);
|
||||
}
|
||||
|
||||
public static class SmaliDebuggerException extends Exception {
|
||||
private final int errCode;
|
||||
private static final long serialVersionUID = -1111111202102191403L;
|
||||
|
||||
public SmaliDebuggerException(Exception e) {
|
||||
super(e);
|
||||
errCode = -1;
|
||||
}
|
||||
|
||||
public SmaliDebuggerException(String msg) {
|
||||
super(msg);
|
||||
this.errCode = -1;
|
||||
}
|
||||
|
||||
public SmaliDebuggerException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.errCode = -1;
|
||||
}
|
||||
|
||||
public SmaliDebuggerException(String msg, int errCode) {
|
||||
super(msg);
|
||||
this.errCode = errCode;
|
||||
}
|
||||
|
||||
public int getErrCode() {
|
||||
return errCode;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener for breakpoint, watch, step, etc.
|
||||
*/
|
||||
@@ -1543,99 +1372,4 @@ public class SmaliDebugger {
|
||||
void onSuspendEvent(SuspendInfo current);
|
||||
}
|
||||
|
||||
public static class SuspendInfo {
|
||||
private boolean terminated;
|
||||
private boolean newRound;
|
||||
private final InfoSetter updater = new InfoSetter();
|
||||
|
||||
public long getThreadID() {
|
||||
return updater.thread;
|
||||
}
|
||||
|
||||
public long getClassID() {
|
||||
return updater.clazz;
|
||||
}
|
||||
|
||||
public long getMethodID() {
|
||||
return updater.method;
|
||||
}
|
||||
|
||||
public long getOffset() {
|
||||
return updater.offset;
|
||||
}
|
||||
|
||||
private InfoSetter update() {
|
||||
updater.changed = false;
|
||||
updater.nextRound(newRound);
|
||||
this.newRound = false;
|
||||
return updater;
|
||||
}
|
||||
|
||||
// called by decodingLoop, to tell the updater even though the values are the same,
|
||||
// they are decoded from another packet, they should be treated as new.
|
||||
private void nextRound() {
|
||||
newRound = true;
|
||||
}
|
||||
|
||||
// according to JDWP document it's legal to fire two or more events on a same location,
|
||||
// e.g. one for single step and the other for breakpoint, so when this happened we only
|
||||
// want one of them.
|
||||
private boolean isAnythingChanged() {
|
||||
return updater.changed;
|
||||
}
|
||||
|
||||
public boolean isTerminated() {
|
||||
return terminated;
|
||||
}
|
||||
|
||||
private void setTerminated() {
|
||||
terminated = true;
|
||||
}
|
||||
|
||||
private static class InfoSetter {
|
||||
private long thread;
|
||||
private long clazz;
|
||||
private long method;
|
||||
private long offset; // code offset;
|
||||
private boolean changed;
|
||||
|
||||
private void nextRound(boolean newRound) {
|
||||
if (!changed) {
|
||||
changed = newRound;
|
||||
}
|
||||
}
|
||||
|
||||
private InfoSetter updateThread(long thread) {
|
||||
if (!changed) {
|
||||
changed = this.thread != thread;
|
||||
}
|
||||
this.thread = thread;
|
||||
return this;
|
||||
}
|
||||
|
||||
private InfoSetter updateClass(long clazz) {
|
||||
if (!changed) {
|
||||
changed = this.clazz != clazz;
|
||||
}
|
||||
this.clazz = clazz;
|
||||
return this;
|
||||
}
|
||||
|
||||
private InfoSetter updateMethod(long method) {
|
||||
if (!changed) {
|
||||
changed = this.method != method;
|
||||
}
|
||||
this.method = method;
|
||||
return this;
|
||||
}
|
||||
|
||||
private InfoSetter updateOffset(long offset) {
|
||||
if (!changed) {
|
||||
changed = this.offset != offset;
|
||||
}
|
||||
this.offset = offset;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package jadx.gui.device.debugger;
|
||||
|
||||
public class SmaliDebuggerException extends Exception {
|
||||
private final int errCode;
|
||||
private static final long serialVersionUID = -1111111202102191403L;
|
||||
|
||||
public SmaliDebuggerException(Exception e) {
|
||||
super(e);
|
||||
errCode = -1;
|
||||
}
|
||||
|
||||
public SmaliDebuggerException(String msg) {
|
||||
super(msg);
|
||||
this.errCode = -1;
|
||||
}
|
||||
|
||||
public SmaliDebuggerException(String msg, Exception e) {
|
||||
super(msg, e);
|
||||
errCode = -1;
|
||||
}
|
||||
|
||||
public SmaliDebuggerException(String msg, int errCode) {
|
||||
super(msg);
|
||||
this.errCode = errCode;
|
||||
}
|
||||
|
||||
public int getErrCode() {
|
||||
return errCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package jadx.gui.device.debugger;
|
||||
|
||||
public class SuspendInfo {
|
||||
private boolean terminated;
|
||||
private boolean newRound;
|
||||
private final InfoSetter updater = new InfoSetter();
|
||||
|
||||
public long getThreadID() {
|
||||
return updater.thread;
|
||||
}
|
||||
|
||||
public long getClassID() {
|
||||
return updater.clazz;
|
||||
}
|
||||
|
||||
public long getMethodID() {
|
||||
return updater.method;
|
||||
}
|
||||
|
||||
public long getOffset() {
|
||||
return updater.offset;
|
||||
}
|
||||
|
||||
InfoSetter update() {
|
||||
updater.changed = false;
|
||||
updater.nextRound(newRound);
|
||||
this.newRound = false;
|
||||
return updater;
|
||||
}
|
||||
|
||||
// called by decodingLoop, to tell the updater even though the values are the same,
|
||||
// they are decoded from another packet, they should be treated as new.
|
||||
void nextRound() {
|
||||
newRound = true;
|
||||
}
|
||||
|
||||
// according to JDWP document it's legal to fire two or more events on a same location,
|
||||
// e.g. one for single step and the other for breakpoint, so when this happened we only
|
||||
// want one of them.
|
||||
boolean isAnythingChanged() {
|
||||
return updater.changed;
|
||||
}
|
||||
|
||||
public boolean isTerminated() {
|
||||
return terminated;
|
||||
}
|
||||
|
||||
void setTerminated() {
|
||||
terminated = true;
|
||||
}
|
||||
|
||||
static class InfoSetter {
|
||||
private long thread;
|
||||
private long clazz;
|
||||
private long method;
|
||||
private long offset; // code offset;
|
||||
private boolean changed;
|
||||
|
||||
void nextRound(boolean newRound) {
|
||||
if (!changed) {
|
||||
changed = newRound;
|
||||
}
|
||||
}
|
||||
|
||||
InfoSetter updateThread(long thread) {
|
||||
if (!changed) {
|
||||
changed = this.thread != thread;
|
||||
}
|
||||
this.thread = thread;
|
||||
return this;
|
||||
}
|
||||
|
||||
InfoSetter updateClass(long clazz) {
|
||||
if (!changed) {
|
||||
changed = this.clazz != clazz;
|
||||
}
|
||||
this.clazz = clazz;
|
||||
return this;
|
||||
}
|
||||
|
||||
InfoSetter updateMethod(long method) {
|
||||
if (!changed) {
|
||||
changed = this.method != method;
|
||||
}
|
||||
this.method = method;
|
||||
return this;
|
||||
}
|
||||
|
||||
InfoSetter updateOffset(long offset) {
|
||||
if (!changed) {
|
||||
changed = this.offset != offset;
|
||||
}
|
||||
this.offset = offset;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,27 @@
|
||||
package jadx.gui.device.protocol;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.reactivex.annotations.NonNull;
|
||||
|
||||
import jadx.core.utils.StringUtils;
|
||||
import jadx.core.utils.log.LogUtils;
|
||||
import jadx.gui.utils.IOUtils;
|
||||
|
||||
public class ADB {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ADB.class);
|
||||
@@ -28,14 +30,28 @@ public class ADB {
|
||||
private static final String DEFAULT_ADDR = "localhost";
|
||||
|
||||
private static final String CMD_FEATURES = "000dhost:features";
|
||||
private static final String CMD_TRACK_JDWP = "000atrack-jdwp";
|
||||
private static final String CMD_TRACK_DEVICES = "0014host:track-devices-l";
|
||||
private static final byte[] OKAY = "OKAY".getBytes();
|
||||
private static final byte[] FAIL = "FAIL".getBytes();
|
||||
|
||||
private static boolean isOkay(InputStream stream) throws IOException {
|
||||
byte[] buf = new byte[4];
|
||||
stream.read(buf, 0, 4);
|
||||
return Arrays.equals(buf, OKAY);
|
||||
static boolean isOkay(InputStream stream) throws IOException {
|
||||
byte[] buf = IOUtils.readNBytes(stream, 4);
|
||||
if (Arrays.equals(buf, OKAY)) {
|
||||
return true;
|
||||
}
|
||||
if (Arrays.equals(buf, FAIL)) {
|
||||
// Observed that after FAIL the length in hex follows and afterwards an error message,
|
||||
// but it is unclear if this is true for all cases where isOkay is used.
|
||||
// int msgLen = Integer.parseInt(new String(IOUtils.readNBytes(stream, 4)), 16);
|
||||
// byte[] errorMsg = IOUtils.readNBytes(stream, msgLen);
|
||||
// LOG.error("isOkay failed: received error message: {}", new String(errorMsg));
|
||||
LOG.error("isOkay failed");
|
||||
return false;
|
||||
}
|
||||
if (buf == null) {
|
||||
throw new IOException("isOkay failed - steam ended");
|
||||
}
|
||||
throw new IOException("isOkay failed - unexpected response " + new String(buf));
|
||||
}
|
||||
|
||||
public static byte[] exec(String cmd, OutputStream outputStream, InputStream inputStream) throws IOException {
|
||||
@@ -43,11 +59,9 @@ public class ADB {
|
||||
}
|
||||
|
||||
public static byte[] exec(String cmd) throws IOException {
|
||||
byte[] res;
|
||||
Socket socket = connect();
|
||||
res = exec(cmd, socket.getOutputStream(), socket.getInputStream());
|
||||
socket.close();
|
||||
return res;
|
||||
try (Socket socket = connect()) {
|
||||
return exec(cmd, socket.getOutputStream(), socket.getInputStream());
|
||||
}
|
||||
}
|
||||
|
||||
public static Socket connect() throws IOException {
|
||||
@@ -58,14 +72,12 @@ public class ADB {
|
||||
return new Socket(host, port);
|
||||
}
|
||||
|
||||
private static boolean execCommandAsync(OutputStream outputStream,
|
||||
InputStream inputStream, String cmd) throws IOException {
|
||||
static boolean execCommandAsync(OutputStream outputStream, InputStream inputStream, String cmd) throws IOException {
|
||||
outputStream.write(cmd.getBytes());
|
||||
return isOkay(inputStream);
|
||||
}
|
||||
|
||||
private static byte[] execCommandSync(OutputStream outputStream,
|
||||
InputStream inputStream, String cmd) throws IOException {
|
||||
private static byte[] execCommandSync(OutputStream outputStream, InputStream inputStream, String cmd) throws IOException {
|
||||
outputStream.write(cmd.getBytes());
|
||||
if (isOkay(inputStream)) {
|
||||
return readServiceProtocol(inputStream);
|
||||
@@ -73,43 +85,46 @@ public class ADB {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static byte[] readServiceProtocol(InputStream stream) {
|
||||
byte[] bytes = null;
|
||||
byte[] buf = new byte[4];
|
||||
static byte[] readServiceProtocol(InputStream stream) {
|
||||
try {
|
||||
int len = stream.read(buf, 0, 4);
|
||||
if (len == 4) {
|
||||
len = unhex(buf);
|
||||
if (len == 0) {
|
||||
return new byte[0];
|
||||
}
|
||||
if (len != -1) {
|
||||
buf = new byte[len];
|
||||
if (stream.read(buf, 0, len) == len) {
|
||||
bytes = buf;
|
||||
}
|
||||
}
|
||||
byte[] buf = IOUtils.readNBytes(stream, 4);
|
||||
if (buf == null) {
|
||||
return null;
|
||||
}
|
||||
} catch (IOException ignore) {
|
||||
int len = unhex(buf);
|
||||
byte[] result;
|
||||
if (len == 0) {
|
||||
result = new byte[0];
|
||||
} else {
|
||||
result = IOUtils.readNBytes(stream, len);
|
||||
}
|
||||
if (LOG.isTraceEnabled()) {
|
||||
LOG.trace("readServiceProtocol result: {}", LogUtils.escape(result));
|
||||
}
|
||||
return result;
|
||||
} catch (SocketException e) {
|
||||
LOG.error("Aborting readServiceProtocol: socket closed");
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to read readServiceProtocol", e);
|
||||
}
|
||||
return bytes;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean setSerial(String serial, OutputStream outputStream, InputStream inputStream) throws IOException {
|
||||
static boolean setSerial(String serial, OutputStream outputStream, InputStream inputStream) throws IOException {
|
||||
String setSerialCmd = String.format("host:tport:serial:%s", serial);
|
||||
setSerialCmd = String.format("%04x%s", setSerialCmd.length(), setSerialCmd);
|
||||
outputStream.write(setSerialCmd.getBytes());
|
||||
boolean ok = isOkay(inputStream);
|
||||
if (ok) {
|
||||
// skip the shell-state-id returned by ADB server, it's not important for the following actions.
|
||||
ok = inputStream.skip(8) == 8;
|
||||
IOUtils.readNBytes(inputStream, 8);
|
||||
} else {
|
||||
LOG.error("setSerial command {} failed", LogUtils.escape(setSerialCmd));
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
private static byte[] execShellCommandRaw(String cmd,
|
||||
OutputStream outputStream, InputStream inputStream) throws IOException {
|
||||
|
||||
private static byte[] execShellCommandRaw(String cmd, OutputStream outputStream, InputStream inputStream) throws IOException {
|
||||
cmd = String.format("shell,v2,TERM=xterm-256color,raw:%s", cmd);
|
||||
cmd = String.format("%04x%s", cmd.length(), cmd);
|
||||
outputStream.write(cmd.getBytes());
|
||||
@@ -119,8 +134,7 @@ public class ADB {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static byte[] execShellCommandRaw(String serial, String cmd,
|
||||
OutputStream outputStream, InputStream inputStream) throws IOException {
|
||||
static byte[] execShellCommandRaw(String serial, String cmd, OutputStream outputStream, InputStream inputStream) throws IOException {
|
||||
if (setSerial(serial, outputStream, inputStream)) {
|
||||
return execShellCommandRaw(cmd, outputStream, inputStream);
|
||||
}
|
||||
@@ -137,28 +151,32 @@ public class ADB {
|
||||
|
||||
public static boolean startServer(String adbPath, int port) throws IOException {
|
||||
String tcpPort = String.format("tcp:%d", port);
|
||||
java.lang.Process proc = new ProcessBuilder(adbPath, "-L", tcpPort, "start-server")
|
||||
List<String> command = Arrays.asList(adbPath, "-L", tcpPort, "start-server");
|
||||
java.lang.Process proc = new ProcessBuilder(command)
|
||||
.redirectErrorStream(true)
|
||||
.start();
|
||||
try {
|
||||
proc.waitFor(3, TimeUnit.SECONDS); // for listening to a port, 3 sec should be more than enough.
|
||||
// Wait for the adb server to start. On Windows even on a fast system 6 seconds are not unusual.
|
||||
proc.waitFor(10, TimeUnit.SECONDS);
|
||||
proc.exitValue();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Start server error", e);
|
||||
LOG.error("ADB start server failed with command: {}", String.join(" ", command), e);
|
||||
proc.destroyForcibly();
|
||||
return false;
|
||||
}
|
||||
InputStream is = proc.getInputStream();
|
||||
int size = is.available();
|
||||
byte[] bytes = new byte[size];
|
||||
is.read(bytes, 0, size);
|
||||
return new String(bytes).contains(tcpPort);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
try (InputStream in = proc.getInputStream()) {
|
||||
int read;
|
||||
byte[] buf = new byte[1024];
|
||||
while ((read = in.read(buf)) >= 0) {
|
||||
out.write(buf, 0, read);
|
||||
}
|
||||
}
|
||||
return out.toString().contains(tcpPort);
|
||||
}
|
||||
|
||||
public static boolean isServerRunning(String host, int port) {
|
||||
try {
|
||||
Socket sock = new Socket(host, port);
|
||||
sock.close();
|
||||
try (Socket sock = new Socket(host, port)) {
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
@@ -178,22 +196,21 @@ public class ADB {
|
||||
}
|
||||
ExecutorService listenThread = Executors.newFixedThreadPool(1);
|
||||
listenThread.execute(() -> {
|
||||
for (;;) {
|
||||
while (true) {
|
||||
byte[] res = readServiceProtocol(inputStream);
|
||||
if (res != null) {
|
||||
if (listener != null) {
|
||||
String payload = new String(res);
|
||||
String[] deviceLines = payload.split("\n");
|
||||
List<DeviceInfo> deviceInfoList = new ArrayList<>(deviceLines.length);
|
||||
for (String deviceLine : deviceLines) {
|
||||
if (!deviceLine.trim().isEmpty()) {
|
||||
deviceInfoList.add(DeviceInfo.make(deviceLine, host, port));
|
||||
}
|
||||
if (res == null) {
|
||||
break; // socket disconnected
|
||||
}
|
||||
if (listener != null) {
|
||||
String payload = new String(res);
|
||||
String[] deviceLines = payload.split("\n");
|
||||
List<ADBDeviceInfo> deviceInfoList = new ArrayList<>(deviceLines.length);
|
||||
for (String deviceLine : deviceLines) {
|
||||
if (!deviceLine.trim().isEmpty()) {
|
||||
deviceInfoList.add(ADBDeviceInfo.make(deviceLine, host, port));
|
||||
}
|
||||
listener.onDeviceStatusChange(deviceInfoList);
|
||||
}
|
||||
} else { // socket disconnected
|
||||
break;
|
||||
listener.onDeviceStatusChange(deviceInfoList);
|
||||
}
|
||||
}
|
||||
if (listener != null) {
|
||||
@@ -204,46 +221,39 @@ public class ADB {
|
||||
}
|
||||
|
||||
public static List<String> listForward(String host, int port) throws IOException {
|
||||
Socket socket = connect(host, port);
|
||||
String cmd = "0011host:list-forward";
|
||||
InputStream inputStream = socket.getInputStream();
|
||||
OutputStream outputStream = socket.getOutputStream();
|
||||
outputStream.write(cmd.getBytes());
|
||||
if (isOkay(inputStream)) {
|
||||
byte[] bytes = readServiceProtocol(inputStream);
|
||||
if (bytes != null) {
|
||||
String[] forwards = new String(bytes).split("\n");
|
||||
List<String> forwardList = new ArrayList<>(forwards.length);
|
||||
for (String forward : forwards) {
|
||||
forwardList.add(forward.trim());
|
||||
try (Socket socket = connect(host, port)) {
|
||||
String cmd = "0011host:list-forward";
|
||||
InputStream inputStream = socket.getInputStream();
|
||||
OutputStream outputStream = socket.getOutputStream();
|
||||
outputStream.write(cmd.getBytes());
|
||||
if (isOkay(inputStream)) {
|
||||
byte[] bytes = readServiceProtocol(inputStream);
|
||||
if (bytes != null) {
|
||||
String[] forwards = new String(bytes).split("\n");
|
||||
return Stream.of(forwards).map(String::trim).collect(Collectors.toList());
|
||||
}
|
||||
socket.close();
|
||||
return forwardList;
|
||||
}
|
||||
}
|
||||
socket.close();
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public static boolean removeForward(String host, int port, String serial, String localPort) throws IOException {
|
||||
Socket socket = connect(host, port);
|
||||
String cmd = String.format("host:killforward:tcp:%s", localPort);
|
||||
cmd = String.format("%04x%s", cmd.length(), cmd);
|
||||
InputStream inputStream = socket.getInputStream();
|
||||
OutputStream outputStream = socket.getOutputStream();
|
||||
boolean ok = false;
|
||||
if (setSerial(serial, outputStream, inputStream)) {
|
||||
outputStream.write(cmd.getBytes());
|
||||
ok = isOkay(inputStream) && isOkay(inputStream);
|
||||
try (Socket socket = connect(host, port)) {
|
||||
String cmd = String.format("host:killforward:tcp:%s", localPort);
|
||||
cmd = String.format("%04x%s", cmd.length(), cmd);
|
||||
InputStream inputStream = socket.getInputStream();
|
||||
OutputStream outputStream = socket.getOutputStream();
|
||||
if (setSerial(serial, outputStream, inputStream)) {
|
||||
outputStream.write(cmd.getBytes());
|
||||
return isOkay(inputStream) && isOkay(inputStream);
|
||||
}
|
||||
}
|
||||
socket.close();
|
||||
return ok;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Little endian
|
||||
private static int readInt(byte[] bytes, int start) {
|
||||
int result = 0;
|
||||
result = (bytes[start] & 0xff);
|
||||
int result = (bytes[start] & 0xff);
|
||||
result += ((bytes[start + 1] & 0xff) << 8);
|
||||
result += ((bytes[start + 2] & 0xff) << 16);
|
||||
result += (bytes[start + 3] & 0xff) << 24;
|
||||
@@ -300,287 +310,17 @@ public class ADB {
|
||||
}
|
||||
|
||||
public interface JDWPProcessListener {
|
||||
void jdwpProcessOccurred(Device device, Set<String> id);
|
||||
void jdwpProcessOccurred(ADBDevice device, Set<String> id);
|
||||
|
||||
void jdwpListenerClosed(Device device);
|
||||
void jdwpListenerClosed(ADBDevice device);
|
||||
}
|
||||
|
||||
public interface DeviceStateListener {
|
||||
void onDeviceStatusChange(List<DeviceInfo> deviceInfoList);
|
||||
void onDeviceStatusChange(List<ADBDeviceInfo> deviceInfoList);
|
||||
|
||||
void adbDisconnected();
|
||||
}
|
||||
|
||||
public static class Device {
|
||||
DeviceInfo info;
|
||||
String androidReleaseVer;
|
||||
volatile Socket jdwpListenerSock;
|
||||
|
||||
public Device(DeviceInfo info) {
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
public DeviceInfo getDeviceInfo() {
|
||||
return info;
|
||||
}
|
||||
|
||||
public boolean updateDeviceInfo(DeviceInfo info) {
|
||||
boolean matched = this.info.serial.equals(info.serial);
|
||||
if (matched) {
|
||||
this.info = info;
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
public String getSerial() {
|
||||
return info.serial;
|
||||
}
|
||||
|
||||
public boolean removeForward(String localPort) throws IOException {
|
||||
return ADB.removeForward(info.adbHost, info.adbPort, info.serial, localPort);
|
||||
}
|
||||
|
||||
public ForwardResult forwardJDWP(String localPort, String jdwpPid) throws IOException {
|
||||
Socket socket = connect(info.adbHost, info.adbPort);
|
||||
String cmd = String.format("host:forward:tcp:%s;jdwp:%s", localPort, jdwpPid);
|
||||
cmd = String.format("%04x%s", cmd.length(), cmd);
|
||||
InputStream inputStream = socket.getInputStream();
|
||||
OutputStream outputStream = socket.getOutputStream();
|
||||
ForwardResult rst;
|
||||
if (setSerial(info.serial, outputStream, inputStream)) {
|
||||
outputStream.write(cmd.getBytes());
|
||||
if (!isOkay(inputStream)) {
|
||||
rst = new ForwardResult(1, readServiceProtocol(inputStream));
|
||||
} else if (!isOkay(inputStream)) {
|
||||
rst = new ForwardResult(2, readServiceProtocol(inputStream));
|
||||
} else {
|
||||
rst = new ForwardResult(0, null);
|
||||
}
|
||||
} else {
|
||||
rst = new ForwardResult(1, "Unknown error.".getBytes());
|
||||
}
|
||||
socket.close();
|
||||
return rst;
|
||||
}
|
||||
|
||||
public static class ForwardResult {
|
||||
/**
|
||||
* 0 for success, 1 for failed at binding to local tcp, 2 for failed at remote.
|
||||
*/
|
||||
public int state;
|
||||
public String desc;
|
||||
|
||||
public ForwardResult(int state, byte[] desc) {
|
||||
if (desc != null) {
|
||||
this.desc = new String(desc);
|
||||
} else {
|
||||
this.desc = "";
|
||||
}
|
||||
this.state = state;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return pid otherwise -1
|
||||
*/
|
||||
public int launchApp(String fullAppName) throws IOException, InterruptedException {
|
||||
Socket socket = connect(info.adbHost, info.adbPort);
|
||||
String cmd = "am start -D -n " + fullAppName;
|
||||
byte[] res = execShellCommandRaw(info.serial, cmd, socket.getOutputStream(), socket.getInputStream());
|
||||
socket.close();
|
||||
String rst = new String(res).trim();
|
||||
if (rst.startsWith("Starting: Intent {") && rst.endsWith(fullAppName + " }")) {
|
||||
Thread.sleep(40);
|
||||
String pkg = fullAppName.split("/")[0];
|
||||
for (Process process : getProcessByPkg(pkg)) {
|
||||
return Integer.parseInt(process.pid);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public String getAndroidReleaseVersion() {
|
||||
if (!StringUtils.isEmpty(androidReleaseVer)) {
|
||||
return androidReleaseVer;
|
||||
}
|
||||
try {
|
||||
List<String> list = getProp("ro.build.version.release");
|
||||
if (list.size() != 0) {
|
||||
androidReleaseVer = list.get(0);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to get android release version", e);
|
||||
androidReleaseVer = "";
|
||||
}
|
||||
return androidReleaseVer;
|
||||
}
|
||||
|
||||
public List<String> getProp(String entry) throws IOException {
|
||||
Socket socket = connect(info.adbHost, info.adbPort);
|
||||
List<String> props = Collections.emptyList();
|
||||
String cmd = "getprop";
|
||||
if (!StringUtils.isEmpty(entry)) {
|
||||
cmd += " " + entry;
|
||||
}
|
||||
byte[] payload = execShellCommandRaw(info.serial, cmd,
|
||||
socket.getOutputStream(), socket.getInputStream());
|
||||
if (payload != null) {
|
||||
props = new ArrayList<>();
|
||||
String[] lines = new String(payload).split("\n");
|
||||
for (String line : lines) {
|
||||
line = line.trim();
|
||||
if (!line.isEmpty()) {
|
||||
props.add(line.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
socket.close();
|
||||
return props;
|
||||
}
|
||||
|
||||
public List<Process> getProcessByPkg(String pkg) throws IOException {
|
||||
return getProcessList("ps | grep " + pkg, 0);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<Process> getProcessList() throws IOException {
|
||||
return getProcessList("ps", 1);
|
||||
}
|
||||
|
||||
private List<Process> getProcessList(String cmd, int index) throws IOException {
|
||||
Socket socket = connect(info.adbHost, info.adbPort);
|
||||
List<Process> procs = Collections.emptyList();
|
||||
byte[] payload = execShellCommandRaw(info.serial, cmd,
|
||||
socket.getOutputStream(), socket.getInputStream());
|
||||
if (payload != null) {
|
||||
String ps = new String(payload);
|
||||
String[] psLines = ps.split("\n");
|
||||
for (int i = index; i < psLines.length; i++) {
|
||||
Process proc = Process.make(psLines[i]);
|
||||
if (proc != null) {
|
||||
if (procs.isEmpty()) {
|
||||
procs = new ArrayList<>();
|
||||
}
|
||||
procs.add(proc);
|
||||
}
|
||||
}
|
||||
}
|
||||
socket.close();
|
||||
return procs;
|
||||
}
|
||||
|
||||
public boolean listenForJDWP(JDWPProcessListener listener) throws IOException {
|
||||
if (this.jdwpListenerSock != null) {
|
||||
return false;
|
||||
}
|
||||
jdwpListenerSock = connect(this.info.adbHost, this.info.adbPort);
|
||||
InputStream inputStream = jdwpListenerSock.getInputStream();
|
||||
OutputStream outputStream = jdwpListenerSock.getOutputStream();
|
||||
if (setSerial(info.serial, outputStream, inputStream)
|
||||
&& execCommandAsync(outputStream, inputStream, CMD_TRACK_JDWP)) {
|
||||
Executors.newFixedThreadPool(1).execute(() -> {
|
||||
for (;;) {
|
||||
byte[] res = readServiceProtocol(inputStream);
|
||||
if (res != null) {
|
||||
if (listener != null) {
|
||||
String payload = new String(res);
|
||||
String[] ids = payload.split("\n");
|
||||
Set<String> idList = new HashSet<>(ids.length);
|
||||
for (String id : ids) {
|
||||
if (!id.trim().isEmpty()) {
|
||||
idList.add(id);
|
||||
}
|
||||
}
|
||||
listener.jdwpProcessOccurred(this, idList);
|
||||
}
|
||||
} else { // socket disconnected
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (listener != null) {
|
||||
this.jdwpListenerSock = null;
|
||||
listener.jdwpListenerClosed(this);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
jdwpListenerSock.close();
|
||||
jdwpListenerSock = null;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void stopListenForJDWP() {
|
||||
if (jdwpListenerSock != null) {
|
||||
try {
|
||||
jdwpListenerSock.close();
|
||||
} catch (Exception e) {
|
||||
LOG.error("JDWP socket close failed", e);
|
||||
}
|
||||
}
|
||||
this.jdwpListenerSock = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return info.serial.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Device) {
|
||||
return ((Device) obj).getDeviceInfo().serial.equals(info.serial);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return info.allInfo;
|
||||
}
|
||||
}
|
||||
|
||||
public static class DeviceInfo {
|
||||
public String adbHost;
|
||||
public int adbPort;
|
||||
public String serial;
|
||||
public String state;
|
||||
public String model;
|
||||
public String allInfo;
|
||||
|
||||
public boolean isOnline() {
|
||||
return state.equals("device");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return allInfo;
|
||||
}
|
||||
|
||||
static DeviceInfo make(String info, String host, int port) {
|
||||
DeviceInfo deviceInfo = new DeviceInfo();
|
||||
String[] infoFields = info.trim().split("\\s+");
|
||||
deviceInfo.allInfo = String.join(" ", infoFields);
|
||||
if (infoFields.length > 2) {
|
||||
deviceInfo.serial = infoFields[0];
|
||||
deviceInfo.state = infoFields[1];
|
||||
}
|
||||
int pos = info.indexOf("model:");
|
||||
if (pos != -1) {
|
||||
int spacePos = info.indexOf(" ", pos);
|
||||
if (spacePos != -1) {
|
||||
deviceInfo.model = info.substring(pos + "model:".length(), spacePos);
|
||||
}
|
||||
}
|
||||
if (deviceInfo.model == null || deviceInfo.model.equals("")) {
|
||||
deviceInfo.model = deviceInfo.serial;
|
||||
}
|
||||
deviceInfo.adbHost = host;
|
||||
deviceInfo.adbPort = port;
|
||||
return deviceInfo;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Process {
|
||||
public String user;
|
||||
public String pid;
|
||||
@@ -619,23 +359,23 @@ public class ADB {
|
||||
|
||||
public static byte[] readStdout(InputStream inputStream) throws IOException {
|
||||
byte[] header = new byte[5];
|
||||
byte[] payload = new byte[0];
|
||||
byte[] tempBuf = new byte[0];
|
||||
ByteArrayOutputStream payload = new ByteArrayOutputStream();
|
||||
byte[] tempBuf = new byte[1024];
|
||||
for (boolean exit = false; !exit;) {
|
||||
if (inputStream.read(header, 0, 5) == 5) {
|
||||
exit = header[0] == ID_EXIT;
|
||||
int payloadSize = readInt(header, 1);
|
||||
if (tempBuf.length < payloadSize) {
|
||||
tempBuf = new byte[payloadSize];
|
||||
}
|
||||
int readSize = inputStream.read(tempBuf, 0, payloadSize);
|
||||
if (readSize != payloadSize) {
|
||||
return null; // we don't want corrupted data.
|
||||
}
|
||||
payload = appendBytes(payload, tempBuf, readSize);
|
||||
IOUtils.read(inputStream, header);
|
||||
exit = header[0] == ID_EXIT;
|
||||
int payloadSize = readInt(header, 1);
|
||||
if (tempBuf.length < payloadSize) {
|
||||
tempBuf = new byte[payloadSize];
|
||||
}
|
||||
int readSize = IOUtils.read(inputStream, tempBuf, 0, payloadSize);
|
||||
if (readSize != payloadSize) {
|
||||
LOG.error("Failed to read ShellProtocol data");
|
||||
return null; // we don't want corrupted data.
|
||||
}
|
||||
payload.write(tempBuf, 0, readSize);
|
||||
}
|
||||
return payload;
|
||||
return payload.toByteArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
package jadx.gui.device.protocol;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.reactivex.annotations.NonNull;
|
||||
|
||||
import jadx.core.utils.StringUtils;
|
||||
import jadx.core.utils.log.LogUtils;
|
||||
import jadx.gui.device.protocol.ADB.JDWPProcessListener;
|
||||
import jadx.gui.device.protocol.ADB.Process;
|
||||
|
||||
public class ADBDevice {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ADBDevice.class);
|
||||
|
||||
private static final String CMD_TRACK_JDWP = "000atrack-jdwp";
|
||||
|
||||
ADBDeviceInfo info;
|
||||
String androidReleaseVer;
|
||||
volatile Socket jdwpListenerSock;
|
||||
|
||||
public ADBDevice(ADBDeviceInfo info) {
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
public ADBDeviceInfo getDeviceInfo() {
|
||||
return info;
|
||||
}
|
||||
|
||||
public boolean updateDeviceInfo(ADBDeviceInfo info) {
|
||||
boolean matched = this.info.serial.equals(info.serial);
|
||||
if (matched) {
|
||||
this.info = info;
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
public String getSerial() {
|
||||
return info.serial;
|
||||
}
|
||||
|
||||
public boolean removeForward(String localPort) throws IOException {
|
||||
return ADB.removeForward(info.adbHost, info.adbPort, info.serial, localPort);
|
||||
}
|
||||
|
||||
public ForwardResult forwardJDWP(String localPort, String jdwpPid) throws IOException {
|
||||
try (Socket socket = ADB.connect(info.adbHost, info.adbPort)) {
|
||||
String cmd = String.format("host:forward:tcp:%s;jdwp:%s", localPort, jdwpPid);
|
||||
cmd = String.format("%04x%s", cmd.length(), cmd);
|
||||
InputStream inputStream = socket.getInputStream();
|
||||
OutputStream outputStream = socket.getOutputStream();
|
||||
ForwardResult rst;
|
||||
if (ADB.setSerial(info.serial, outputStream, inputStream)) {
|
||||
outputStream.write(cmd.getBytes());
|
||||
if (!ADB.isOkay(inputStream)) {
|
||||
rst = new ForwardResult(1, ADB.readServiceProtocol(inputStream));
|
||||
} else if (!ADB.isOkay(inputStream)) {
|
||||
rst = new ForwardResult(2, ADB.readServiceProtocol(inputStream));
|
||||
} else {
|
||||
rst = new ForwardResult(0, null);
|
||||
}
|
||||
} else {
|
||||
rst = new ForwardResult(1, "Unknown error.".getBytes());
|
||||
}
|
||||
return rst;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ForwardResult {
|
||||
/**
|
||||
* 0 for success, 1 for failed at binding to local tcp, 2 for failed at remote.
|
||||
*/
|
||||
public int state;
|
||||
public String desc;
|
||||
|
||||
public ForwardResult(int state, byte[] desc) {
|
||||
if (desc != null) {
|
||||
this.desc = new String(desc);
|
||||
} else {
|
||||
this.desc = "";
|
||||
}
|
||||
this.state = state;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return pid otherwise -1
|
||||
*/
|
||||
public int launchApp(String fullAppName) throws IOException, InterruptedException {
|
||||
byte[] res;
|
||||
try (Socket socket = ADB.connect(info.adbHost, info.adbPort)) {
|
||||
String cmd = "am start -D -n " + fullAppName;
|
||||
res = ADB.execShellCommandRaw(info.serial, cmd, socket.getOutputStream(), socket.getInputStream());
|
||||
if (res == null) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
String rst = new String(res).trim();
|
||||
if (rst.startsWith("Starting: Intent {") && rst.endsWith(fullAppName + " }")) {
|
||||
Thread.sleep(40);
|
||||
String pkg = fullAppName.split("/")[0];
|
||||
for (Process process : getProcessByPkg(pkg)) {
|
||||
return Integer.parseInt(process.pid);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public String getAndroidReleaseVersion() {
|
||||
if (!StringUtils.isEmpty(androidReleaseVer)) {
|
||||
return androidReleaseVer;
|
||||
}
|
||||
try {
|
||||
List<String> list = getProp("ro.build.version.release");
|
||||
if (list.size() != 0) {
|
||||
androidReleaseVer = list.get(0);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to get android release version", e);
|
||||
androidReleaseVer = "";
|
||||
}
|
||||
return androidReleaseVer;
|
||||
}
|
||||
|
||||
public List<String> getProp(String entry) throws IOException {
|
||||
try (Socket socket = ADB.connect(info.adbHost, info.adbPort)) {
|
||||
List<String> props = Collections.emptyList();
|
||||
String cmd = "getprop";
|
||||
if (!StringUtils.isEmpty(entry)) {
|
||||
cmd += " " + entry;
|
||||
}
|
||||
byte[] payload = ADB.execShellCommandRaw(info.serial, cmd,
|
||||
socket.getOutputStream(), socket.getInputStream());
|
||||
if (payload != null) {
|
||||
props = new ArrayList<>();
|
||||
String[] lines = new String(payload).split("\n");
|
||||
for (String line : lines) {
|
||||
line = line.trim();
|
||||
if (!line.isEmpty()) {
|
||||
props.add(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
return props;
|
||||
}
|
||||
}
|
||||
|
||||
public List<Process> getProcessByPkg(String pkg) throws IOException {
|
||||
return getProcessList("ps | grep " + pkg, 0);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<Process> getProcessList() throws IOException {
|
||||
return getProcessList("ps", 1);
|
||||
}
|
||||
|
||||
private List<Process> getProcessList(String cmd, int index) throws IOException {
|
||||
try (Socket socket = ADB.connect(info.adbHost, info.adbPort)) {
|
||||
List<Process> procs = new ArrayList<>();
|
||||
byte[] payload = ADB.execShellCommandRaw(info.serial, cmd,
|
||||
socket.getOutputStream(), socket.getInputStream());
|
||||
if (payload != null) {
|
||||
String ps = new String(payload);
|
||||
String[] psLines = ps.split("\n");
|
||||
for (String line : psLines) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
Process proc = Process.make(line);
|
||||
if (proc != null) {
|
||||
procs.add(proc);
|
||||
} else {
|
||||
LOG.error("Unexpected process info data received: \"{}\"", LogUtils.escape(line));
|
||||
}
|
||||
}
|
||||
}
|
||||
return procs;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean listenForJDWP(JDWPProcessListener listener) throws IOException {
|
||||
if (this.jdwpListenerSock != null) {
|
||||
return false;
|
||||
}
|
||||
jdwpListenerSock = ADB.connect(this.info.adbHost, this.info.adbPort);
|
||||
InputStream inputStream = jdwpListenerSock.getInputStream();
|
||||
OutputStream outputStream = jdwpListenerSock.getOutputStream();
|
||||
if (ADB.setSerial(info.serial, outputStream, inputStream)
|
||||
&& ADB.execCommandAsync(outputStream, inputStream, CMD_TRACK_JDWP)) {
|
||||
Executors.newFixedThreadPool(1).execute(() -> {
|
||||
for (;;) {
|
||||
byte[] res = ADB.readServiceProtocol(inputStream);
|
||||
if (res != null) {
|
||||
if (listener != null) {
|
||||
String payload = new String(res);
|
||||
String[] ids = payload.split("\n");
|
||||
Set<String> idList = new HashSet<>(ids.length);
|
||||
for (String id : ids) {
|
||||
if (!id.trim().isEmpty()) {
|
||||
idList.add(id);
|
||||
}
|
||||
}
|
||||
listener.jdwpProcessOccurred(this, idList);
|
||||
}
|
||||
} else { // socket disconnected
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (listener != null) {
|
||||
this.jdwpListenerSock = null;
|
||||
listener.jdwpListenerClosed(this);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
jdwpListenerSock.close();
|
||||
jdwpListenerSock = null;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void stopListenForJDWP() {
|
||||
if (jdwpListenerSock != null) {
|
||||
try {
|
||||
jdwpListenerSock.close();
|
||||
} catch (Exception e) {
|
||||
LOG.error("JDWP socket close failed", e);
|
||||
}
|
||||
}
|
||||
this.jdwpListenerSock = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return info.serial.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof ADBDevice) {
|
||||
return ((ADBDevice) obj).getDeviceInfo().serial.equals(info.serial);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return info.allInfo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package jadx.gui.device.protocol;
|
||||
|
||||
public class ADBDeviceInfo {
|
||||
public String adbHost;
|
||||
public int adbPort;
|
||||
public String serial;
|
||||
public String state;
|
||||
public String model;
|
||||
public String allInfo;
|
||||
|
||||
public boolean isOnline() {
|
||||
return state.equals("device");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return allInfo;
|
||||
}
|
||||
|
||||
static ADBDeviceInfo make(String info, String host, int port) {
|
||||
ADBDeviceInfo deviceInfo = new ADBDeviceInfo();
|
||||
String[] infoFields = info.trim().split("\\s+");
|
||||
deviceInfo.allInfo = String.join(" ", infoFields);
|
||||
if (infoFields.length > 2) {
|
||||
deviceInfo.serial = infoFields[0];
|
||||
deviceInfo.state = infoFields[1];
|
||||
}
|
||||
int pos = info.indexOf("model:");
|
||||
if (pos != -1) {
|
||||
int spacePos = info.indexOf(" ", pos);
|
||||
if (spacePos != -1) {
|
||||
deviceInfo.model = info.substring(pos + "model:".length(), spacePos);
|
||||
}
|
||||
}
|
||||
if (deviceInfo.model == null || deviceInfo.model.equals("")) {
|
||||
deviceInfo.model = deviceInfo.serial;
|
||||
}
|
||||
deviceInfo.adbHost = host;
|
||||
deviceInfo.adbPort = port;
|
||||
return deviceInfo;
|
||||
}
|
||||
}
|
||||
@@ -105,7 +105,6 @@ public class BackgroundExecutor {
|
||||
protected TaskStatus doInBackground() throws Exception {
|
||||
progressPane.changeLabel(this, task.getTitle() + "… ");
|
||||
progressPane.changeCancelBtnVisible(this, task.canBeCanceled());
|
||||
progressPane.changeVisibility(this, true);
|
||||
|
||||
runJobs();
|
||||
return status;
|
||||
@@ -116,6 +115,9 @@ public class BackgroundExecutor {
|
||||
jobsCount = jobs.size();
|
||||
LOG.debug("Starting background task '{}', jobs count: {}, time limit: {} ms, memory check: {}",
|
||||
task.getTitle(), jobsCount, task.timeLimit(), task.checkMemoryUsage());
|
||||
if (jobsCount != 1) {
|
||||
progressPane.changeVisibility(this, true);
|
||||
}
|
||||
status = TaskStatus.STARTED;
|
||||
int threadsCount = mainWindow.getSettings().getThreadsCount();
|
||||
executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(threadsCount);
|
||||
@@ -146,6 +148,10 @@ public class BackgroundExecutor {
|
||||
setProgress(calcProgress(executor.getCompletedTaskCount()));
|
||||
k++;
|
||||
Thread.sleep(k < 20 ? 100 : 1000); // faster update for short tasks
|
||||
if (jobsCount == 1 && k == 3) {
|
||||
// small delay before show progress to reduce blinking on short tasks
|
||||
progressPane.changeVisibility(this, true);
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
LOG.debug("Task wait interrupted");
|
||||
@@ -189,7 +195,11 @@ public class BackgroundExecutor {
|
||||
// reduce thread count and continue
|
||||
executor.setCorePoolSize(1);
|
||||
System.gc();
|
||||
UiUtils.sleep(500); // wait GC
|
||||
UiUtils.sleep(1000); // wait GC
|
||||
if (!UiUtils.isFreeMemoryAvailable()) {
|
||||
LOG.error("Task '{}' memory limit reached (after GC), force cancel", task.getTitle());
|
||||
return TaskStatus.CANCEL_BY_MEMORY;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -125,9 +125,12 @@ public class JadxProject {
|
||||
.map(TabStateViewAdapter::build)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
data.setOpenTabs(tabStateList);
|
||||
data.setActiveTab(activeTab);
|
||||
changed();
|
||||
boolean dataChanged;
|
||||
dataChanged = data.setOpenTabs(tabStateList);
|
||||
dataChanged |= data.setActiveTab(activeTab);
|
||||
if (dataChanged) {
|
||||
changed();
|
||||
}
|
||||
}
|
||||
|
||||
public List<EditorViewState> getOpenTabs(MainWindow mw) {
|
||||
|
||||
@@ -27,11 +27,11 @@ import org.slf4j.LoggerFactory;
|
||||
import com.beust.jcommander.Parameter;
|
||||
|
||||
import jadx.api.CommentsLevel;
|
||||
import jadx.api.DecompilationMode;
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.args.DeobfuscationMapFileMode;
|
||||
import jadx.cli.JadxCLIArgs;
|
||||
import jadx.cli.LogHelper;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.ui.codearea.EditorTheme;
|
||||
import jadx.gui.utils.FontUtils;
|
||||
@@ -44,7 +44,7 @@ public class JadxSettings extends JadxCLIArgs {
|
||||
|
||||
private static final Path USER_HOME = Paths.get(System.getProperty("user.home"));
|
||||
private static final int RECENT_PROJECTS_COUNT = 15;
|
||||
private static final int CURRENT_SETTINGS_VERSION = 16;
|
||||
private static final int CURRENT_SETTINGS_VERSION = 17;
|
||||
|
||||
private static final Font DEFAULT_FONT = new RSyntaxTextArea().getFont();
|
||||
|
||||
@@ -291,6 +291,10 @@ public class JadxSettings extends JadxCLIArgs {
|
||||
this.skipSources = skipSources;
|
||||
}
|
||||
|
||||
public void setDecompilationMode(DecompilationMode decompilationMode) {
|
||||
this.decompilationMode = decompilationMode;
|
||||
}
|
||||
|
||||
public void setShowInconsistentCode(boolean showInconsistentCode) {
|
||||
this.showInconsistentCode = showInconsistentCode;
|
||||
}
|
||||
@@ -672,8 +676,16 @@ public class JadxSettings extends JadxCLIArgs {
|
||||
}
|
||||
fromVersion++;
|
||||
}
|
||||
if (fromVersion == 16) {
|
||||
if (fallbackMode) {
|
||||
decompilationMode = DecompilationMode.FALLBACK;
|
||||
} else {
|
||||
decompilationMode = DecompilationMode.AUTO;
|
||||
}
|
||||
fromVersion++;
|
||||
}
|
||||
if (fromVersion != CURRENT_SETTINGS_VERSION) {
|
||||
throw new JadxRuntimeException("Incorrect settings upgrade");
|
||||
LOG.warn("Incorrect settings upgrade. Expected version: {}, got: {}", CURRENT_SETTINGS_VERSION, fromVersion);
|
||||
}
|
||||
settingsVersion = CURRENT_SETTINGS_VERSION;
|
||||
sync();
|
||||
|
||||
@@ -59,6 +59,7 @@ import com.google.gson.JsonObject;
|
||||
import say.swing.JFontChooser;
|
||||
|
||||
import jadx.api.CommentsLevel;
|
||||
import jadx.api.DecompilationMode;
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.JadxArgs.UseKotlinMethodsForVarNames;
|
||||
import jadx.api.args.DeobfuscationMapFileMode;
|
||||
@@ -123,10 +124,10 @@ public class JadxSettingsWindow extends JDialog {
|
||||
leftPanel.add(makeAppearanceGroup());
|
||||
leftPanel.add(makeOtherGroup());
|
||||
leftPanel.add(makeSearchResGroup());
|
||||
leftPanel.add(makePluginOptionsGroup());
|
||||
leftPanel.add(Box.createVerticalGlue());
|
||||
|
||||
rightPanel.add(makeDecompilationGroup());
|
||||
rightPanel.add(makePluginOptionsGroup());
|
||||
rightPanel.add(Box.createVerticalGlue());
|
||||
|
||||
JButton saveBtn = new JButton(NLS.str("preferences.save"));
|
||||
@@ -419,13 +420,6 @@ public class JadxSettingsWindow extends JDialog {
|
||||
}
|
||||
|
||||
private SettingsGroup makeDecompilationGroup() {
|
||||
JCheckBox fallback = new JCheckBox();
|
||||
fallback.setSelected(settings.isFallbackMode());
|
||||
fallback.addItemListener(e -> {
|
||||
settings.setFallbackMode(e.getStateChange() == ItemEvent.SELECTED);
|
||||
needReload();
|
||||
});
|
||||
|
||||
JCheckBox useDx = new JCheckBox();
|
||||
useDx.setSelected(settings.isUseDx());
|
||||
useDx.addItemListener(e -> {
|
||||
@@ -433,6 +427,13 @@ public class JadxSettingsWindow extends JDialog {
|
||||
needReload();
|
||||
});
|
||||
|
||||
JComboBox<DecompilationMode> decompilationModeComboBox = new JComboBox<>(DecompilationMode.values());
|
||||
decompilationModeComboBox.setSelectedItem(settings.getDecompilationMode());
|
||||
decompilationModeComboBox.addActionListener(e -> {
|
||||
settings.setDecompilationMode((DecompilationMode) decompilationModeComboBox.getSelectedItem());
|
||||
needReload();
|
||||
});
|
||||
|
||||
JCheckBox showInconsistentCode = new JCheckBox();
|
||||
showInconsistentCode.setSelected(settings.isShowInconsistentCode());
|
||||
showInconsistentCode.addItemListener(e -> {
|
||||
@@ -543,6 +544,7 @@ public class JadxSettingsWindow extends JDialog {
|
||||
other.addRow(NLS.str("preferences.excludedPackages"), NLS.str("preferences.excludedPackages.tooltip"),
|
||||
editExcludedPackages);
|
||||
other.addRow(NLS.str("preferences.start_jobs"), autoStartJobs);
|
||||
other.addRow(NLS.str("preferences.decompilationMode"), decompilationModeComboBox);
|
||||
other.addRow(NLS.str("preferences.showInconsistentCode"), showInconsistentCode);
|
||||
other.addRow(NLS.str("preferences.escapeUnicode"), escapeUnicode);
|
||||
other.addRow(NLS.str("preferences.replaceConsts"), replaceConsts);
|
||||
@@ -551,7 +553,6 @@ public class JadxSettingsWindow extends JDialog {
|
||||
other.addRow(NLS.str("preferences.inlineAnonymous"), inlineAnonymous);
|
||||
other.addRow(NLS.str("preferences.inlineMethods"), inlineMethods);
|
||||
other.addRow(NLS.str("preferences.fsCaseSensitive"), fsCaseSensitive);
|
||||
other.addRow(NLS.str("preferences.fallback"), fallback);
|
||||
other.addRow(NLS.str("preferences.useDx"), useDx);
|
||||
other.addRow(NLS.str("preferences.skipResourcesDecode"), resourceDecode);
|
||||
other.addRow(NLS.str("preferences.useKotlinMethodsForVarNames"), kotlinRenameVars);
|
||||
|
||||
@@ -14,7 +14,7 @@ public class ProjectData {
|
||||
private List<String[]> treeExpansions = new ArrayList<>();
|
||||
private JadxCodeData codeData = new JadxCodeData();
|
||||
private List<TabViewState> openTabs = Collections.emptyList();
|
||||
private int activeTab;
|
||||
private int activeTab = -1;
|
||||
|
||||
public List<Path> getFiles() {
|
||||
return files;
|
||||
@@ -52,15 +52,33 @@ public class ProjectData {
|
||||
return openTabs;
|
||||
}
|
||||
|
||||
public void setOpenTabs(List<TabViewState> openTabs) {
|
||||
/**
|
||||
*
|
||||
* @param openTabs
|
||||
* @return <code>true></code> if a change was saved
|
||||
*/
|
||||
public boolean setOpenTabs(List<TabViewState> openTabs) {
|
||||
if (this.openTabs.equals(openTabs)) {
|
||||
return false;
|
||||
}
|
||||
this.openTabs = openTabs;
|
||||
return true;
|
||||
}
|
||||
|
||||
public int getActiveTab() {
|
||||
return activeTab;
|
||||
}
|
||||
|
||||
public void setActiveTab(int activeTab) {
|
||||
/**
|
||||
*
|
||||
* @param activeTab
|
||||
* @return <code>true></code> if a change was saved
|
||||
*/
|
||||
public boolean setActiveTab(int activeTab) {
|
||||
if (this.activeTab == activeTab) {
|
||||
return false;
|
||||
}
|
||||
this.activeTab = activeTab;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ public class JClass extends JLoadableNode implements Comparable<JClass> {
|
||||
return !cls.getClassNode().contains(AFlag.DONT_RENAME);
|
||||
}
|
||||
|
||||
public synchronized void load() {
|
||||
private synchronized void load() {
|
||||
if (loaded) {
|
||||
return;
|
||||
}
|
||||
@@ -122,7 +122,6 @@ public class JClass extends JLoadableNode implements Comparable<JClass> {
|
||||
return new ClassCodeContentPanel(tabbedPane, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSmali() {
|
||||
return cls.getSmali();
|
||||
}
|
||||
|
||||
@@ -12,9 +12,6 @@ import jadx.api.JavaNode;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.info.AccessInfo;
|
||||
import jadx.core.dex.instructions.args.ArgType;
|
||||
import jadx.gui.ui.TabbedPane;
|
||||
import jadx.gui.ui.codearea.ClassCodeContentPanel;
|
||||
import jadx.gui.ui.panel.ContentPanel;
|
||||
import jadx.gui.utils.OverlayIcon;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
|
||||
@@ -65,11 +62,6 @@ public class JMethod extends JNode {
|
||||
return mth.getDecompiledLine();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentPanel getContentPanel(TabbedPane tabbedPane) {
|
||||
return new ClassCodeContentPanel(tabbedPane, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Icon getIcon() {
|
||||
AccessInfo accessFlags = mth.getAccessFlags();
|
||||
@@ -111,6 +103,9 @@ public class JMethod extends JNode {
|
||||
|
||||
@Override
|
||||
public boolean canRename() {
|
||||
if (mth.isClassInit()) {
|
||||
return false;
|
||||
}
|
||||
return !mth.getMethodNode().contains(AFlag.DONT_RENAME);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,10 +38,6 @@ public abstract class JNode extends DefaultMutableTreeNode {
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getSmali() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getSyntaxName() {
|
||||
return SyntaxConstants.SYNTAX_STYLE_NONE;
|
||||
}
|
||||
|
||||
@@ -403,15 +403,22 @@ public class MainWindow extends JFrame {
|
||||
}
|
||||
|
||||
void open(List<Path> paths, Runnable onFinish) {
|
||||
closeAll();
|
||||
if (paths.size() == 1) {
|
||||
Path singleFile = paths.get(0);
|
||||
String fileExtension = CommonFileUtils.getFileExtension(singleFile.getFileName().toString());
|
||||
if (fileExtension != null && fileExtension.equalsIgnoreCase(JadxProject.PROJECT_EXTENSION)) {
|
||||
openProject(singleFile, onFinish);
|
||||
List<Path> projectFiles = openProject(singleFile);
|
||||
if (!Utils.isEmpty(projectFiles)) {
|
||||
openFiles(projectFiles, onFinish);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
closeAll();
|
||||
openFiles(paths, onFinish);
|
||||
}
|
||||
|
||||
private void openFiles(List<Path> paths, Runnable onFinish) {
|
||||
project.setFilePath(paths);
|
||||
if (paths.isEmpty()) {
|
||||
return;
|
||||
@@ -491,9 +498,9 @@ public class MainWindow extends JFrame {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void openProject(Path path, Runnable onFinish) {
|
||||
private List<Path> openProject(Path path) {
|
||||
if (!ensureProjectIsSaved()) {
|
||||
return;
|
||||
return Collections.emptyList();
|
||||
}
|
||||
JadxProject jadxProject = JadxProject.from(path);
|
||||
if (jadxProject == null) {
|
||||
@@ -506,12 +513,7 @@ public class MainWindow extends JFrame {
|
||||
}
|
||||
updateProject(jadxProject);
|
||||
settings.addRecentProject(path);
|
||||
List<Path> filePaths = jadxProject.getFilePaths();
|
||||
if (filePaths == null) {
|
||||
closeAll();
|
||||
} else {
|
||||
open(filePaths, onFinish);
|
||||
}
|
||||
return jadxProject.getFilePaths();
|
||||
}
|
||||
|
||||
public void updateProject(JadxProject jadxProject) {
|
||||
|
||||
@@ -35,6 +35,7 @@ import jadx.gui.ui.panel.IViewStateSupport;
|
||||
import jadx.gui.ui.panel.ImagePanel;
|
||||
import jadx.gui.utils.JumpManager;
|
||||
import jadx.gui.utils.JumpPosition;
|
||||
import jadx.gui.utils.NLS;
|
||||
|
||||
public class TabbedPane extends JTabbedPane {
|
||||
private static final long serialVersionUID = -8833600618794570904L;
|
||||
@@ -165,42 +166,49 @@ public class TabbedPane extends JTabbedPane {
|
||||
JNode jumpNode = jumpPos.getNode();
|
||||
Objects.requireNonNull(jumpNode, "Null node in JumpPosition");
|
||||
|
||||
final AbstractCodeContentPanel contentPanel = (AbstractCodeContentPanel) getContentPanel(jumpNode);
|
||||
if (contentPanel == null) {
|
||||
return;
|
||||
}
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
selectTab(contentPanel);
|
||||
AbstractCodeArea codeArea = contentPanel.getCodeArea();
|
||||
int pos = jumpPos.getPos();
|
||||
if (pos > 0) {
|
||||
codeArea.scrollToPos(pos);
|
||||
} else {
|
||||
int line = jumpPos.getLine();
|
||||
if (line < 0) {
|
||||
try {
|
||||
line = 1 + codeArea.getLineOfOffset(-line);
|
||||
} catch (BadLocationException e) {
|
||||
LOG.error("Can't get line for: {}", jumpPos, e);
|
||||
line = jumpNode.getLine();
|
||||
mainWindow.getBackgroundExecutor().execute(
|
||||
NLS.str("progress.load"),
|
||||
jumpNode::getContent, // run heavy loading in background
|
||||
status -> {
|
||||
// show the code in UI thread
|
||||
AbstractCodeContentPanel contentPanel = (AbstractCodeContentPanel) getContentPanel(jumpNode);
|
||||
if (contentPanel != null) {
|
||||
scrollToPos(contentPanel, jumpPos);
|
||||
selectTab(contentPanel);
|
||||
}
|
||||
}
|
||||
int lineNum = Math.max(0, line - 1);
|
||||
});
|
||||
}
|
||||
|
||||
private void scrollToPos(AbstractCodeContentPanel contentPanel, JumpPosition jumpPos) {
|
||||
AbstractCodeArea codeArea = contentPanel.getCodeArea();
|
||||
int pos = jumpPos.getPos();
|
||||
if (pos > 0) {
|
||||
codeArea.scrollToPos(pos);
|
||||
} else {
|
||||
int line = jumpPos.getLine();
|
||||
if (line < 0) {
|
||||
try {
|
||||
int offs = codeArea.getLineStartOffset(lineNum);
|
||||
while (StringUtils.isWhite(codeArea.getText(offs, 1).charAt(0))) {
|
||||
offs += 1;
|
||||
}
|
||||
offs += pos;
|
||||
jumpPos.setPos(offs);
|
||||
codeArea.scrollToPos(offs);
|
||||
line = 1 + codeArea.getLineOfOffset(-line);
|
||||
} catch (BadLocationException e) {
|
||||
LOG.error("Failed to jump to position: {}", pos, e);
|
||||
codeArea.scrollToLine(line);
|
||||
LOG.error("Can't get line for: {}", jumpPos, e);
|
||||
line = jumpPos.getNode().getLine();
|
||||
}
|
||||
}
|
||||
codeArea.requestFocus();
|
||||
});
|
||||
int lineNum = Math.max(0, line - 1);
|
||||
try {
|
||||
int offs = codeArea.getLineStartOffset(lineNum);
|
||||
while (StringUtils.isWhite(codeArea.getText(offs, 1).charAt(0))) {
|
||||
offs += 1;
|
||||
}
|
||||
offs += pos;
|
||||
jumpPos.setPos(offs);
|
||||
codeArea.scrollToPos(offs);
|
||||
} catch (BadLocationException e) {
|
||||
LOG.error("Failed to jump to position: {}", pos, e);
|
||||
codeArea.scrollToLine(line);
|
||||
}
|
||||
}
|
||||
codeArea.requestFocus();
|
||||
}
|
||||
|
||||
public boolean showNode(JNode node) {
|
||||
|
||||
@@ -8,8 +8,6 @@ import java.awt.event.FocusEvent;
|
||||
import java.awt.event.FocusListener;
|
||||
import java.awt.event.KeyAdapter;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.JCheckBoxMenuItem;
|
||||
@@ -23,7 +21,11 @@ import javax.swing.text.BadLocationException;
|
||||
import javax.swing.text.Caret;
|
||||
import javax.swing.text.DefaultCaret;
|
||||
|
||||
import org.fife.ui.rsyntaxtextarea.AbstractTokenMakerFactory;
|
||||
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
|
||||
import org.fife.ui.rsyntaxtextarea.Token;
|
||||
import org.fife.ui.rsyntaxtextarea.TokenMakerFactory;
|
||||
import org.fife.ui.rsyntaxtextarea.TokenTypes;
|
||||
import org.fife.ui.rtextarea.SearchContext;
|
||||
import org.fife.ui.rtextarea.SearchEngine;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -31,6 +33,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.core.utils.StringUtils;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.gui.settings.JadxSettings;
|
||||
import jadx.gui.treemodel.JClass;
|
||||
import jadx.gui.treemodel.JNode;
|
||||
@@ -47,12 +50,24 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractCodeArea.class);
|
||||
|
||||
public static final String SYNTAX_STYLE_SMALI = "text/smali";
|
||||
|
||||
static {
|
||||
TokenMakerFactory tokenMakerFactory = TokenMakerFactory.getDefaultInstance();
|
||||
if (tokenMakerFactory instanceof AbstractTokenMakerFactory) {
|
||||
AbstractTokenMakerFactory atmf = (AbstractTokenMakerFactory) tokenMakerFactory;
|
||||
atmf.putMapping(SYNTAX_STYLE_SMALI, "jadx.gui.ui.codearea.SmaliTokenMaker");
|
||||
} else {
|
||||
throw new JadxRuntimeException("Unexpected TokenMakerFactory instance: " + tokenMakerFactory.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
protected final ContentPanel contentPanel;
|
||||
protected final JNode node;
|
||||
|
||||
public AbstractCodeArea(ContentPanel contentPanel) {
|
||||
public AbstractCodeArea(ContentPanel contentPanel, JNode node) {
|
||||
this.contentPanel = contentPanel;
|
||||
this.node = contentPanel.getNode();
|
||||
this.node = node;
|
||||
|
||||
setMarkOccurrences(false);
|
||||
setEditable(false);
|
||||
@@ -178,51 +193,41 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
|
||||
return getWordByPosition(getCaretPosition());
|
||||
}
|
||||
|
||||
public int getWordStart(int pos) {
|
||||
int start = Math.max(0, pos - 1);
|
||||
@Nullable
|
||||
public String getWordByPosition(int pos) {
|
||||
try {
|
||||
if (!StringUtils.isWordSeparator(getText(start, 1).charAt(0))) {
|
||||
do {
|
||||
start--;
|
||||
} while (start >= 0 && !StringUtils.isWordSeparator(getText(start, 1).charAt(0)));
|
||||
}
|
||||
start++;
|
||||
} catch (BadLocationException e) {
|
||||
LOG.error("Failed to find word start", e);
|
||||
start = -1;
|
||||
Token token = modelToToken(pos);
|
||||
return getWordFromToken(token);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to get word at pos: {}", pos, e);
|
||||
return null;
|
||||
}
|
||||
return start;
|
||||
}
|
||||
|
||||
public int getWordEnd(int pos, int max) {
|
||||
int end = pos;
|
||||
try {
|
||||
if (!StringUtils.isWordSeparator(getText(end, 1).charAt(0))) {
|
||||
do {
|
||||
end++;
|
||||
} while (end < max && !StringUtils.isWordSeparator(getText(end, 1).charAt(0)));
|
||||
}
|
||||
} catch (BadLocationException e) {
|
||||
LOG.error("Failed to find word end", e);
|
||||
end = max;
|
||||
}
|
||||
return end;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getWordByPosition(int pos) {
|
||||
int len = getDocument().getLength();
|
||||
int start = getWordStart(pos);
|
||||
int end = getWordEnd(pos, len);
|
||||
try {
|
||||
if (end <= start) {
|
||||
return null;
|
||||
}
|
||||
return getText(start, end - start);
|
||||
} catch (BadLocationException e) {
|
||||
LOG.error("Failed to get word at pos: {}, start: {}, end: {}", pos, start, end, e);
|
||||
private static String getWordFromToken(@Nullable Token token) {
|
||||
if (token == null) {
|
||||
return null;
|
||||
}
|
||||
switch (token.getType()) {
|
||||
case TokenTypes.NULL:
|
||||
case TokenTypes.WHITESPACE:
|
||||
case TokenTypes.SEPARATOR:
|
||||
case TokenTypes.OPERATOR:
|
||||
return null;
|
||||
|
||||
case TokenTypes.IDENTIFIER:
|
||||
if (token.length() == 1) {
|
||||
char ch = token.charAt(0);
|
||||
if (ch == ';' || ch == '.') {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return token.getLexeme();
|
||||
|
||||
default:
|
||||
return token.getLexeme();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -311,23 +316,6 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
|
||||
}
|
||||
}
|
||||
|
||||
private void registerWordHighlighter() {
|
||||
addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent evt) {
|
||||
if (evt.getClickCount() % 2 == 0 && !evt.isConsumed()) {
|
||||
evt.consume();
|
||||
String str = getSelectedText();
|
||||
if (str != null) {
|
||||
highlightAllMatches(str);
|
||||
}
|
||||
} else {
|
||||
highlightAllMatches(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param str - if null -> reset current highlights
|
||||
*/
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
package jadx.gui.ui.codearea;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Point;
|
||||
|
||||
import javax.swing.JCheckBox;
|
||||
import javax.swing.JSplitPane;
|
||||
import javax.swing.JTabbedPane;
|
||||
import javax.swing.JToolBar;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
|
||||
import org.fife.ui.rsyntaxtextarea.AbstractTokenMakerFactory;
|
||||
import org.fife.ui.rsyntaxtextarea.TokenMakerFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.gui.treemodel.JNode;
|
||||
import jadx.api.DecompilationMode;
|
||||
import jadx.gui.jobs.BackgroundExecutor;
|
||||
import jadx.gui.treemodel.JClass;
|
||||
import jadx.gui.ui.TabbedPane;
|
||||
import jadx.gui.ui.codearea.mode.JCodeMode;
|
||||
import jadx.gui.ui.panel.IViewStateSupport;
|
||||
import jadx.gui.utils.NLS;
|
||||
|
||||
import static com.formdev.flatlaf.FlatClientProperties.TABBED_PANE_TRAILING_COMPONENT;
|
||||
|
||||
/**
|
||||
* Displays one class with two different view:
|
||||
*
|
||||
@@ -32,32 +39,77 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel implem
|
||||
private final transient CodePanel smaliCodePanel;
|
||||
private final transient JTabbedPane areaTabbedPane;
|
||||
|
||||
public ClassCodeContentPanel(TabbedPane panel, JNode jnode) {
|
||||
super(panel, jnode);
|
||||
private boolean splitView = false;
|
||||
|
||||
// FIXME I don't know the project very well, so need to get the right place
|
||||
AbstractTokenMakerFactory atmf = (AbstractTokenMakerFactory) TokenMakerFactory.getDefaultInstance();
|
||||
atmf.putMapping("text/smali", "jadx.gui.ui.codearea.SmaliTokenMaker");
|
||||
public ClassCodeContentPanel(TabbedPane panel, JClass jCls) {
|
||||
super(panel, jCls);
|
||||
|
||||
javaCodePanel = new CodePanel(new CodeArea(this));
|
||||
smaliCodePanel = new CodePanel(new SmaliArea(this));
|
||||
javaCodePanel = new CodePanel(new CodeArea(this, jCls));
|
||||
smaliCodePanel = new CodePanel(new SmaliArea(this, jCls));
|
||||
areaTabbedPane = buildTabbedPane(jCls, false);
|
||||
addCustomControls(areaTabbedPane);
|
||||
|
||||
initView();
|
||||
javaCodePanel.load();
|
||||
}
|
||||
|
||||
private void initView() {
|
||||
removeAll();
|
||||
setLayout(new BorderLayout());
|
||||
setBorder(new EmptyBorder(0, 0, 0, 0));
|
||||
if (splitView) {
|
||||
JTabbedPane splitPaneView = buildTabbedPane(((JClass) node), true);
|
||||
JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, areaTabbedPane, splitPaneView);
|
||||
add(splitPane);
|
||||
splitPane.setDividerLocation(0.5);
|
||||
splitPaneView.setSelectedIndex(1);
|
||||
} else {
|
||||
add(areaTabbedPane);
|
||||
}
|
||||
invalidate();
|
||||
}
|
||||
|
||||
areaTabbedPane = new JTabbedPane(JTabbedPane.BOTTOM);
|
||||
private JTabbedPane buildTabbedPane(JClass jCls, boolean split) {
|
||||
JTabbedPane areaTabbedPane = new JTabbedPane(JTabbedPane.BOTTOM);
|
||||
areaTabbedPane.setBorder(new EmptyBorder(0, 0, 0, 0));
|
||||
areaTabbedPane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
|
||||
areaTabbedPane.add(javaCodePanel, NLS.str("tabs.code"));
|
||||
areaTabbedPane.add(smaliCodePanel, NLS.str("tabs.smali"));
|
||||
add(areaTabbedPane);
|
||||
|
||||
javaCodePanel.load();
|
||||
|
||||
if (split) {
|
||||
areaTabbedPane.add(new CodePanel(new CodeArea(this, jCls)), NLS.str("tabs.code"));
|
||||
areaTabbedPane.add(new CodePanel(new SmaliArea(this, jCls)), NLS.str("tabs.smali"));
|
||||
} else {
|
||||
areaTabbedPane.add(javaCodePanel, NLS.str("tabs.code"));
|
||||
areaTabbedPane.add(smaliCodePanel, NLS.str("tabs.smali"));
|
||||
}
|
||||
areaTabbedPane.add(new CodePanel(new CodeArea(this, new JCodeMode(jCls, DecompilationMode.SIMPLE))), "Simple");
|
||||
areaTabbedPane.add(new CodePanel(new CodeArea(this, new JCodeMode(jCls, DecompilationMode.FALLBACK))), "Fallback");
|
||||
areaTabbedPane.addChangeListener(e -> {
|
||||
CodePanel selectedPanel = (CodePanel) areaTabbedPane.getSelectedComponent();
|
||||
// TODO: to run background load extract ui update to other method
|
||||
selectedPanel.load();
|
||||
// execInBackground(selectedPanel::load);
|
||||
});
|
||||
return areaTabbedPane;
|
||||
}
|
||||
|
||||
private void addCustomControls(JTabbedPane tabbedPane) {
|
||||
JCheckBox splitCheckBox = new JCheckBox("Split view", splitView);
|
||||
splitCheckBox.addItemListener(e -> {
|
||||
splitView = splitCheckBox.isSelected();
|
||||
this.initView();
|
||||
});
|
||||
|
||||
JToolBar trailing = new JToolBar();
|
||||
trailing.setFloatable(false);
|
||||
trailing.setBorder(null);
|
||||
// trailing.add(Box.createHorizontalGlue());
|
||||
trailing.addSeparator(new Dimension(50, 1));
|
||||
trailing.add(splitCheckBox);
|
||||
tabbedPane.putClientProperty(TABBED_PANE_TRAILING_COMPONENT, trailing);
|
||||
}
|
||||
|
||||
private void execInBackground(Runnable runnable) {
|
||||
BackgroundExecutor bgExec = this.tabbedPane.getMainWindow().getBackgroundExecutor();
|
||||
bgExec.execute("Loading", runnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -5,9 +5,7 @@ import java.awt.event.InputEvent;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
|
||||
import javax.swing.JPopupMenu;
|
||||
import javax.swing.event.PopupMenuEvent;
|
||||
import javax.swing.text.BadLocationException;
|
||||
|
||||
import org.fife.ui.rsyntaxtextarea.RSyntaxDocument;
|
||||
import org.fife.ui.rsyntaxtextarea.Token;
|
||||
@@ -39,10 +37,9 @@ public final class CodeArea extends AbstractCodeArea {
|
||||
|
||||
private static final long serialVersionUID = 6312736869579635796L;
|
||||
|
||||
CodeArea(ContentPanel contentPanel) {
|
||||
super(contentPanel);
|
||||
CodeArea(ContentPanel contentPanel, JNode node) {
|
||||
super(contentPanel, node);
|
||||
setSyntaxEditingStyle(node.getSyntaxName());
|
||||
|
||||
boolean isJavaCode = node instanceof JClass;
|
||||
if (isJavaCode) {
|
||||
((RSyntaxDocument) getDocument()).setSyntaxStyle(new JadxTokenMaker(this));
|
||||
@@ -90,33 +87,19 @@ public final class CodeArea extends AbstractCodeArea {
|
||||
}
|
||||
|
||||
private void addMenuItems() {
|
||||
FindUsageAction findUsage = new FindUsageAction(this);
|
||||
GoToDeclarationAction goToDeclaration = new GoToDeclarationAction(this);
|
||||
RenameAction rename = new RenameAction(this);
|
||||
CommentAction comment = new CommentAction(this);
|
||||
FridaAction frida = new FridaAction(this);
|
||||
XposedAction xposed = new XposedAction(this);
|
||||
|
||||
JPopupMenu popup = getPopupMenu();
|
||||
JNodePopupBuilder popup = new JNodePopupBuilder(this, getPopupMenu());
|
||||
popup.addSeparator();
|
||||
popup.add(findUsage);
|
||||
popup.add(goToDeclaration);
|
||||
popup.add(comment);
|
||||
popup.add(new FindUsageAction(this));
|
||||
popup.add(new GoToDeclarationAction(this));
|
||||
popup.add(new CommentAction(this));
|
||||
popup.add(new CommentSearchAction(this));
|
||||
popup.add(rename);
|
||||
popup.add(new RenameAction(this));
|
||||
popup.addSeparator();
|
||||
popup.add(frida);
|
||||
popup.add(xposed);
|
||||
|
||||
popup.addPopupMenuListener(findUsage);
|
||||
popup.addPopupMenuListener(goToDeclaration);
|
||||
popup.addPopupMenuListener(comment);
|
||||
popup.addPopupMenuListener(rename);
|
||||
popup.addPopupMenuListener(frida);
|
||||
popup.addPopupMenuListener(xposed);
|
||||
popup.add(new FridaAction(this));
|
||||
popup.add(new XposedAction(this));
|
||||
|
||||
// move caret on mouse right button click
|
||||
popup.addPopupMenuListener(new DefaultPopupMenuListener() {
|
||||
popup.getMenu().addPopupMenuListener(new DefaultPopupMenuListener() {
|
||||
@Override
|
||||
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
|
||||
CodeArea codeArea = CodeArea.this;
|
||||
@@ -187,43 +170,25 @@ public final class CodeArea extends AbstractCodeArea {
|
||||
return nodeCache.makeFrom(javaNode);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public CodePosition getMouseCodePos() {
|
||||
try {
|
||||
Point mousePos = UiUtils.getMousePosition(this);
|
||||
return buildCodePosFromOffset(this.viewToModel(mousePos));
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to get offset at mouse position", e);
|
||||
@Nullable
|
||||
public JNode getNodeUnderCaret() {
|
||||
int caretPos = getCaretPosition();
|
||||
Token token = modelToToken(caretPos);
|
||||
if (token == null) {
|
||||
return null;
|
||||
}
|
||||
int start = adjustOffsetForToken(token);
|
||||
if (start == -1) {
|
||||
start = caretPos;
|
||||
}
|
||||
return getJNodeAtOffset(start);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public CodePosition getCaretCodePos() {
|
||||
try {
|
||||
return buildCodePosFromOffset(getCaretPosition());
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Failed to get caret position", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private CodePosition buildCodePosFromOffset(int offset) throws BadLocationException {
|
||||
int start = getWordStart(offset);
|
||||
if (start == -1) {
|
||||
start = offset;
|
||||
}
|
||||
int line = getLineOfOffset(start);
|
||||
int lineOffset = start - getLineStartOffset(line);
|
||||
return new CodePosition(line + 1, lineOffset + 1, start);
|
||||
}
|
||||
|
||||
public JNode getNodeUnderCaret() {
|
||||
int start = getWordStart(getCaretPosition());
|
||||
if (start == -1) {
|
||||
start = getCaretPosition();
|
||||
}
|
||||
return getJNodeAtOffset(start);
|
||||
public JNode getNodeUnderMouse() {
|
||||
Point pos = UiUtils.getMousePosition(this);
|
||||
int offset = adjustOffsetForToken(viewToToken(pos));
|
||||
return getJNodeAtOffset(offset);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -15,7 +15,7 @@ public final class CodeContentPanel extends AbstractCodeContentPanel implements
|
||||
public CodeContentPanel(TabbedPane panel, JNode jnode) {
|
||||
super(panel, jnode);
|
||||
setLayout(new BorderLayout());
|
||||
codePanel = new CodePanel(new CodeArea(this));
|
||||
codePanel = new CodePanel(new CodeArea(this, jnode));
|
||||
add(codePanel, BorderLayout.CENTER);
|
||||
codePanel.load();
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import java.awt.event.ActionEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.KeyStroke;
|
||||
import javax.swing.event.PopupMenuEvent;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -48,16 +47,9 @@ public class CommentAction extends AbstractAction implements DefaultPopupMenuLis
|
||||
} else {
|
||||
this.topCls = null;
|
||||
}
|
||||
|
||||
KeyStroke key = getKeyStroke(KeyEvent.VK_SEMICOLON, 0);
|
||||
codeArea.getInputMap().put(key, "popup.add_comment");
|
||||
codeArea.getActionMap().put("popup.add_comment", new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
int line = codeArea.getCaretLineNumber() + 1;
|
||||
ICodeComment codeComment = getCommentRef(line);
|
||||
showCommentDialog(codeComment);
|
||||
}
|
||||
UiUtils.addKeyBinding(codeArea, getKeyStroke(KeyEvent.VK_SEMICOLON, 0), "popup.add_comment", () -> {
|
||||
int line = codeArea.getCaretLineNumber() + 1;
|
||||
showCommentDialog(getCommentRef(line));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import java.awt.event.ActionEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.Action;
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import jadx.gui.ui.dialog.SearchDialog;
|
||||
@@ -19,18 +18,11 @@ public class CommentSearchAction extends AbstractAction {
|
||||
private final CodeArea codeArea;
|
||||
|
||||
public CommentSearchAction(CodeArea codeArea) {
|
||||
super(NLS.str("popup.search_comment") + " (Ctrl + ;)");
|
||||
this.codeArea = codeArea;
|
||||
|
||||
KeyStroke key = getKeyStroke(KeyEvent.VK_SEMICOLON, UiUtils.ctrlButton());
|
||||
putValue(Action.NAME, NLS.str("popup.search_comment") + " (Ctrl + ;)");
|
||||
|
||||
codeArea.getInputMap().put(key, "popup.search_comment");
|
||||
codeArea.getActionMap().put("popup.search_comment", new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
startSearch();
|
||||
}
|
||||
});
|
||||
UiUtils.addKeyBinding(codeArea, key, "popup.search_comment", this::startSearch);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,51 +1,24 @@
|
||||
package jadx.gui.ui.codearea;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.gui.treemodel.JNode;
|
||||
import jadx.gui.ui.dialog.UsageDialog;
|
||||
import jadx.gui.utils.NLS;
|
||||
|
||||
import static javax.swing.KeyStroke.getKeyStroke;
|
||||
|
||||
public final class FindUsageAction extends JNodeMenuAction<JNode> {
|
||||
public final class FindUsageAction extends JNodeAction {
|
||||
private static final long serialVersionUID = 4692546569977976384L;
|
||||
|
||||
public FindUsageAction(CodeArea codeArea) {
|
||||
super(NLS.str("popup.find_usage") + " (x)", codeArea);
|
||||
KeyStroke key = getKeyStroke(KeyEvent.VK_X, 0);
|
||||
codeArea.getInputMap().put(key, "trigger usage");
|
||||
codeArea.getActionMap().put("trigger usage", new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
node = codeArea.getNodeUnderCaret();
|
||||
showUsageDialog();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void showUsageDialog() {
|
||||
if (node != null) {
|
||||
UsageDialog usageDialog = new UsageDialog(codeArea.getMainWindow(), node);
|
||||
usageDialog.setVisible(true);
|
||||
node = null;
|
||||
}
|
||||
addKeyBinding(getKeyStroke(KeyEvent.VK_X, 0), "trigger usage");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
showUsageDialog();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public JNode getNodeByOffset(int offset) {
|
||||
return codeArea.getJNodeAtOffset(offset);
|
||||
public void runAction(JNode node) {
|
||||
UsageDialog usageDialog = new UsageDialog(getCodeArea().getMainWindow(), node);
|
||||
usageDialog.setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package jadx.gui.ui.codearea;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.util.*;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.JOptionPane;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -30,50 +30,41 @@ import jadx.gui.utils.UiUtils;
|
||||
|
||||
import static javax.swing.KeyStroke.getKeyStroke;
|
||||
|
||||
public final class FridaAction extends JNodeMenuAction<JNode> {
|
||||
public final class FridaAction extends JNodeAction {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FridaAction.class);
|
||||
private static final long serialVersionUID = -3084073927621269039L;
|
||||
private final Map<String, Boolean> isInitial = new HashMap<>();
|
||||
|
||||
public FridaAction(CodeArea codeArea) {
|
||||
|
||||
super(NLS.str("popup.frida") + " (f)", codeArea);
|
||||
KeyStroke key = getKeyStroke(KeyEvent.VK_F, 0);
|
||||
codeArea.getInputMap().put(key, "trigger frida");
|
||||
codeArea.getActionMap().put("trigger frida", new AbstractAction() {
|
||||
@Override
|
||||
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
node = getNodeByOffset(codeArea.getWordStart(codeArea.getCaretPosition()));
|
||||
copyFridaSnippet();
|
||||
}
|
||||
});
|
||||
addKeyBinding(getKeyStroke(KeyEvent.VK_F, 0), "trigger frida");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
node = codeArea.getNodeUnderCaret();
|
||||
copyFridaSnippet();
|
||||
}
|
||||
|
||||
private void copyFridaSnippet() {
|
||||
public void runAction(JNode node) {
|
||||
try {
|
||||
String fridaSnippet = generateFridaSnippet();
|
||||
String fridaSnippet = generateFridaSnippet(node);
|
||||
LOG.info("Frida snippet:\n{}", fridaSnippet);
|
||||
UiUtils.copyToClipboard(fridaSnippet);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to generate Frida code snippet", e);
|
||||
JOptionPane.showMessageDialog(codeArea.getMainWindow(), e.getLocalizedMessage(), NLS.str("error_dialog.title"),
|
||||
JOptionPane.showMessageDialog(getCodeArea().getMainWindow(), e.getLocalizedMessage(), NLS.str("error_dialog.title"),
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
private String generateFridaSnippet() {
|
||||
@Override
|
||||
public boolean isActionEnabled(JNode node) {
|
||||
return node instanceof JMethod || node instanceof JClass || node instanceof JField;
|
||||
}
|
||||
|
||||
private String generateFridaSnippet(JNode node) {
|
||||
if (node instanceof JMethod) {
|
||||
return generateMethodSnippet((JMethod) node);
|
||||
} else if (node instanceof JClass) {
|
||||
}
|
||||
if (node instanceof JClass) {
|
||||
return generateClassSnippet((JClass) node);
|
||||
} else if (node instanceof JField) {
|
||||
}
|
||||
if (node instanceof JField) {
|
||||
return generateFieldSnippet((JField) node);
|
||||
}
|
||||
throw new JadxRuntimeException("Unsupported node type: " + (node != null ? node.getClass() : "null"));
|
||||
@@ -86,7 +77,6 @@ public final class FridaAction extends JNodeMenuAction<JNode> {
|
||||
if (methodInfo.isConstructor()) {
|
||||
methodName = "$init";
|
||||
}
|
||||
String rawClassName = javaMethod.getDeclaringClass().getRawName();
|
||||
String shortClassName = javaMethod.getDeclaringClass().getName();
|
||||
|
||||
String functionUntilImplementation;
|
||||
@@ -114,23 +104,14 @@ public final class FridaAction extends JNodeMenuAction<JNode> {
|
||||
+ "};",
|
||||
functionUntilImplementation, functionParametersString, methodName, methodName, functionParametersString, methodName);
|
||||
|
||||
String finalFridaCode;
|
||||
if (isInitial.getOrDefault(rawClassName, true)) {
|
||||
String classSnippet = generateClassSnippet(jMth.getJParent());
|
||||
finalFridaCode = classSnippet + "\n" + functionParameterAndBody;
|
||||
} else {
|
||||
finalFridaCode = functionParameterAndBody;
|
||||
}
|
||||
return finalFridaCode;
|
||||
return generateClassSnippet(jMth.getJParent()) + "\n" + functionParameterAndBody;
|
||||
}
|
||||
|
||||
private String generateClassSnippet(JClass jc) {
|
||||
JavaClass javaClass = jc.getCls();
|
||||
String rawClassName = javaClass.getRawName();
|
||||
String shortClassName = javaClass.getName();
|
||||
String finalFridaCode = String.format("let %s = Java.use(\"%s\");", shortClassName, rawClassName);
|
||||
isInitial.put(rawClassName, false);
|
||||
return finalFridaCode;
|
||||
return String.format("let %s = Java.use(\"%s\");", shortClassName, rawClassName);
|
||||
}
|
||||
|
||||
private String generateFieldSnippet(JField jf) {
|
||||
@@ -168,10 +149,4 @@ public final class FridaAction extends JNodeMenuAction<JNode> {
|
||||
}
|
||||
return parsedArgType.append("'").toString();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public JNode getNodeByOffset(int offset) {
|
||||
return codeArea.getJNodeAtOffset(offset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,44 @@
|
||||
package jadx.gui.ui.codearea;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.api.CodePosition;
|
||||
import jadx.gui.treemodel.JNode;
|
||||
import jadx.gui.utils.JumpPosition;
|
||||
import jadx.gui.utils.NLS;
|
||||
|
||||
import static javax.swing.KeyStroke.getKeyStroke;
|
||||
|
||||
public final class GoToDeclarationAction extends JNodeMenuAction<JumpPosition> {
|
||||
public final class GoToDeclarationAction extends JNodeAction {
|
||||
private static final long serialVersionUID = -1186470538894941301L;
|
||||
|
||||
private transient @Nullable JumpPosition declPos;
|
||||
|
||||
public GoToDeclarationAction(CodeArea codeArea) {
|
||||
super(NLS.str("popup.go_to_declaration") + " (d)", codeArea);
|
||||
KeyStroke key = getKeyStroke(KeyEvent.VK_D, 0);
|
||||
codeArea.getInputMap().put(key, "trigger goto decl");
|
||||
codeArea.getActionMap().put("trigger goto decl", new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
node = getNodeByOffset(codeArea.getWordStart(codeArea.getCaretPosition()));
|
||||
doJump();
|
||||
}
|
||||
});
|
||||
addKeyBinding(getKeyStroke(KeyEvent.VK_D, 0), "trigger goto decl");
|
||||
}
|
||||
|
||||
private void doJump() {
|
||||
if (node != null) {
|
||||
codeArea.getContentPanel().getTabbedPane().codeJump(node);
|
||||
node = null;
|
||||
@Override
|
||||
public boolean isActionEnabled(JNode node) {
|
||||
declPos = null;
|
||||
if (node == null) {
|
||||
return false;
|
||||
}
|
||||
CodePosition defPos = getCodeArea().getDecompiler().getDefinitionPosition(node.getJavaNode());
|
||||
if (defPos == null) {
|
||||
return false;
|
||||
}
|
||||
declPos = new JumpPosition(node.getRootClass(), defPos);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void runAction(JNode node) {
|
||||
if (declPos != null) {
|
||||
getCodeArea().getContentPanel().getTabbedPane().codeJump(declPos);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
doJump();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public JumpPosition getNodeByOffset(int offset) {
|
||||
return codeArea.getDefPosForNodeAtOffset(offset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package jadx.gui.ui.codearea;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.gui.treemodel.JNode;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
|
||||
/**
|
||||
* Add menu and key binding actions for JNode in code area
|
||||
*/
|
||||
public abstract class JNodeAction extends AbstractAction {
|
||||
private static final long serialVersionUID = -2600154727884853550L;
|
||||
|
||||
private final transient CodeArea codeArea;
|
||||
private transient @Nullable JNode node;
|
||||
|
||||
public JNodeAction(String name, CodeArea codeArea) {
|
||||
super(name);
|
||||
this.codeArea = codeArea;
|
||||
}
|
||||
|
||||
public abstract void runAction(JNode node);
|
||||
|
||||
public boolean isActionEnabled(JNode node) {
|
||||
return node != null;
|
||||
}
|
||||
|
||||
public void addKeyBinding(KeyStroke key, String id) {
|
||||
UiUtils.addKeyBinding(codeArea, key, id, new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
node = codeArea.getNodeUnderCaret();
|
||||
if (isActionEnabled(node)) {
|
||||
runAction(node);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
runAction(node);
|
||||
}
|
||||
|
||||
public void changeNode(JNode node) {
|
||||
this.node = node;
|
||||
setEnabled(isActionEnabled(node));
|
||||
}
|
||||
|
||||
public CodeArea getCodeArea() {
|
||||
return codeArea;
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package jadx.gui.ui.codearea;
|
||||
|
||||
import java.awt.Point;
|
||||
import java.awt.event.ActionEvent;
|
||||
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.event.PopupMenuEvent;
|
||||
import javax.swing.event.PopupMenuListener;
|
||||
|
||||
import org.fife.ui.rsyntaxtextarea.Token;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.gui.utils.UiUtils;
|
||||
|
||||
public abstract class JNodeMenuAction<T> extends AbstractAction implements PopupMenuListener {
|
||||
private static final long serialVersionUID = -2600154727884853550L;
|
||||
|
||||
protected final transient CodeArea codeArea;
|
||||
@Nullable
|
||||
protected transient T node;
|
||||
|
||||
public JNodeMenuAction(String name, CodeArea codeArea) {
|
||||
super(name);
|
||||
this.codeArea = codeArea;
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract void actionPerformed(ActionEvent e);
|
||||
|
||||
@Nullable
|
||||
public abstract T getNodeByOffset(int offset);
|
||||
|
||||
@Override
|
||||
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
|
||||
node = getNode();
|
||||
setEnabled(node != null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private T getNode() {
|
||||
Point pos = UiUtils.getMousePosition(codeArea);
|
||||
Token token = codeArea.viewToToken(pos);
|
||||
int offset = codeArea.adjustOffsetForToken(token);
|
||||
return getNodeByOffset(offset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void popupMenuCanceled(PopupMenuEvent e) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package jadx.gui.ui.codearea;
|
||||
|
||||
import javax.swing.Action;
|
||||
import javax.swing.JPopupMenu;
|
||||
import javax.swing.event.PopupMenuListener;
|
||||
|
||||
public class JNodePopupBuilder {
|
||||
private final JPopupMenu menu;
|
||||
private final JNodePopupListener popupListener;
|
||||
|
||||
public JNodePopupBuilder(CodeArea codeArea, JPopupMenu popupMenu) {
|
||||
menu = popupMenu;
|
||||
popupListener = new JNodePopupListener(codeArea);
|
||||
popupMenu.addPopupMenuListener(popupListener);
|
||||
}
|
||||
|
||||
public void addSeparator() {
|
||||
menu.addSeparator();
|
||||
}
|
||||
|
||||
public void add(JNodeAction nodeAction) {
|
||||
menu.add(nodeAction);
|
||||
popupListener.addActions(nodeAction);
|
||||
}
|
||||
|
||||
public void add(Action action) {
|
||||
menu.add(action);
|
||||
if (action instanceof PopupMenuListener) {
|
||||
menu.addPopupMenuListener((PopupMenuListener) action);
|
||||
}
|
||||
}
|
||||
|
||||
public JPopupMenu getMenu() {
|
||||
return menu;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package jadx.gui.ui.codearea;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.event.PopupMenuEvent;
|
||||
|
||||
import jadx.gui.treemodel.JNode;
|
||||
import jadx.gui.utils.DefaultPopupMenuListener;
|
||||
|
||||
public final class JNodePopupListener implements DefaultPopupMenuListener {
|
||||
private final CodeArea codeArea;
|
||||
private final List<JNodeAction> actions = new ArrayList<>();
|
||||
|
||||
public JNodePopupListener(CodeArea codeArea) {
|
||||
this.codeArea = codeArea;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
|
||||
JNode node = codeArea.getNodeUnderMouse();
|
||||
actions.forEach(action -> action.changeNode(node));
|
||||
}
|
||||
|
||||
public void addActions(JNodeAction action) {
|
||||
actions.add(action);
|
||||
}
|
||||
}
|
||||
@@ -1,72 +1,27 @@
|
||||
package jadx.gui.ui.codearea;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.KeyStroke;
|
||||
import javax.swing.event.PopupMenuEvent;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.gui.treemodel.JNode;
|
||||
import jadx.gui.ui.dialog.RenameDialog;
|
||||
import jadx.gui.utils.NLS;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
|
||||
import static java.awt.event.KeyEvent.VK_N;
|
||||
import static javax.swing.KeyStroke.getKeyStroke;
|
||||
|
||||
public final class RenameAction extends JNodeMenuAction<JNode> {
|
||||
public final class RenameAction extends JNodeAction {
|
||||
private static final long serialVersionUID = -4680872086148463289L;
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(RenameAction.class);
|
||||
|
||||
public RenameAction(CodeArea codeArea) {
|
||||
super(NLS.str("popup.rename") + " (n)", codeArea);
|
||||
KeyStroke key = getKeyStroke(VK_N, 0);
|
||||
String renameActionId = "trigger rename";
|
||||
codeArea.getInputMap().put(key, renameActionId);
|
||||
codeArea.getActionMap().put(renameActionId, new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
node = codeArea.getNodeUnderCaret();
|
||||
showRenameDialog();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void showRenameDialog() {
|
||||
if (node == null) {
|
||||
LOG.info("node == null!");
|
||||
UiUtils.showMessageBox(codeArea.getMainWindow(), NLS.str("msg.rename_node_disabled"));
|
||||
return;
|
||||
}
|
||||
if (!node.canRename()) {
|
||||
UiUtils.showMessageBox(codeArea.getMainWindow(),
|
||||
NLS.str("msg.rename_node_failed", node.getJavaNode().getFullName()));
|
||||
LOG.warn("Can't rename node: {}", node);
|
||||
return;
|
||||
}
|
||||
RenameDialog.rename(codeArea.getMainWindow(), codeArea.getNode(), node);
|
||||
node = null;
|
||||
addKeyBinding(getKeyStroke(VK_N, 0), "trigger rename");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
|
||||
super.popupMenuWillBecomeVisible(e);
|
||||
setEnabled(node != null && node.canRename());
|
||||
public boolean isActionEnabled(JNode node) {
|
||||
return node != null && node.canRename();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
showRenameDialog();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public JNode getNodeByOffset(int offset) {
|
||||
return codeArea.getJNodeAtOffset(offset);
|
||||
public void runAction(JNode node) {
|
||||
RenameDialog.rename(getCodeArea().getMainWindow(), getCodeArea().getNode(), node);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,8 +60,8 @@ public final class SmaliArea extends AbstractCodeArea {
|
||||
private boolean curVersion = false;
|
||||
private SmaliModel model;
|
||||
|
||||
SmaliArea(ContentPanel contentPanel) {
|
||||
super(contentPanel);
|
||||
SmaliArea(ContentPanel contentPanel, JClass node) {
|
||||
super(contentPanel, node);
|
||||
this.textNode = new TextNode(node.getName());
|
||||
|
||||
cbUseSmaliV2 = new JCheckBoxMenuItem(NLS.str("popup.bytecode_col"),
|
||||
@@ -106,6 +106,10 @@ public final class SmaliArea extends AbstractCodeArea {
|
||||
return textNode;
|
||||
}
|
||||
|
||||
public JClass getJClass() {
|
||||
return ((JClass) node);
|
||||
}
|
||||
|
||||
private void switchModel() {
|
||||
if (model != null) {
|
||||
model.unload();
|
||||
@@ -166,12 +170,12 @@ public final class SmaliArea extends AbstractCodeArea {
|
||||
public NormalModel() {
|
||||
Theme theme = getContentPanel().getTabbedPane().getMainWindow().getEditorTheme();
|
||||
setSyntaxScheme(theme.scheme);
|
||||
setSyntaxEditingStyle("text/smali");
|
||||
setSyntaxEditingStyle(SYNTAX_STYLE_SMALI);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
setText(node.getSmali());
|
||||
setText(getJClass().getSmali());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
package jadx.gui.ui.codearea;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -26,42 +22,34 @@ import jadx.gui.utils.UiUtils;
|
||||
|
||||
import static javax.swing.KeyStroke.getKeyStroke;
|
||||
|
||||
public class XposedAction extends JNodeMenuAction<JNode> {
|
||||
public class XposedAction extends JNodeAction {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(XposedAction.class);
|
||||
private static final long serialVersionUID = 2641585141624592578L;
|
||||
|
||||
public XposedAction(CodeArea codeArea) {
|
||||
super(NLS.str("popup.xposed") + " (y)", codeArea);
|
||||
KeyStroke key = getKeyStroke(KeyEvent.VK_Y, 0);
|
||||
codeArea.getInputMap().put(key, "trigger xposed");
|
||||
codeArea.getActionMap().put("trigger xposed", new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
node = getNodeByOffset(codeArea.getWordStart(codeArea.getCaretPosition()));
|
||||
copyXposedSnippet();
|
||||
}
|
||||
});
|
||||
addKeyBinding(getKeyStroke(KeyEvent.VK_Y, 0), "trigger xposed");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
node = codeArea.getNodeUnderCaret();
|
||||
copyXposedSnippet();
|
||||
}
|
||||
|
||||
private void copyXposedSnippet() {
|
||||
public void runAction(JNode node) {
|
||||
try {
|
||||
String xposedSnippet = generateXposedSnippet();
|
||||
String xposedSnippet = generateXposedSnippet(node);
|
||||
LOG.info("Xposed snippet:\n{}", xposedSnippet);
|
||||
UiUtils.copyToClipboard(xposedSnippet);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to generate Xposed code snippet", e);
|
||||
JOptionPane.showMessageDialog(codeArea.getMainWindow(), e.getLocalizedMessage(), NLS.str("error_dialog.title"),
|
||||
JOptionPane.showMessageDialog(getCodeArea().getMainWindow(), e.getLocalizedMessage(), NLS.str("error_dialog.title"),
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
private String generateXposedSnippet() {
|
||||
@Override
|
||||
public boolean isActionEnabled(JNode node) {
|
||||
return node instanceof JMethod || node instanceof JClass;
|
||||
}
|
||||
|
||||
private String generateXposedSnippet(JNode node) {
|
||||
if (node instanceof JMethod) {
|
||||
return generateMethodSnippet((JMethod) node);
|
||||
}
|
||||
@@ -111,10 +99,4 @@ public class XposedAction extends JNodeMenuAction<JNode> {
|
||||
+ "Class %sClass=classLoader.loadClass(\"%s\");",
|
||||
shortClassName, rawClassName);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public JNode getNodeByOffset(int offset) {
|
||||
return codeArea.getJNodeAtOffset(offset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package jadx.gui.ui.codearea.mode;
|
||||
|
||||
import javax.swing.Icon;
|
||||
|
||||
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.api.DecompilationMode;
|
||||
import jadx.api.ICodeInfo;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.gui.treemodel.JClass;
|
||||
import jadx.gui.treemodel.JNode;
|
||||
|
||||
public class JCodeMode extends JNode {
|
||||
|
||||
private final JClass jCls;
|
||||
private final DecompilationMode mode;
|
||||
|
||||
private @Nullable ICodeInfo codeInfo;
|
||||
|
||||
public JCodeMode(JClass jClass, DecompilationMode mode) {
|
||||
this.jCls = jClass;
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JClass getJParent() {
|
||||
return jCls.getJParent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Icon getIcon() {
|
||||
return jCls.getIcon();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String makeString() {
|
||||
return jCls.makeString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull ICodeInfo getCodeInfo() {
|
||||
if (codeInfo != null) {
|
||||
return codeInfo;
|
||||
}
|
||||
ClassNode cls = jCls.getCls().getClassNode();
|
||||
codeInfo = cls.decompileWithMode(mode);
|
||||
return codeInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContent() {
|
||||
return getCodeInfo().getCodeStr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSyntaxName() {
|
||||
return SyntaxConstants.SYNTAX_STYLE_JAVA;
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,8 @@ import jadx.core.utils.StringUtils;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.gui.device.debugger.DbgUtils;
|
||||
import jadx.gui.device.protocol.ADB;
|
||||
import jadx.gui.device.protocol.ADBDevice;
|
||||
import jadx.gui.device.protocol.ADBDeviceInfo;
|
||||
import jadx.gui.treemodel.JClass;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.ui.panel.IDebugController;
|
||||
@@ -49,7 +51,7 @@ import jadx.gui.utils.NLS;
|
||||
import jadx.gui.utils.SystemInfo;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
|
||||
import static jadx.gui.device.protocol.ADB.Device.ForwardResult;
|
||||
import static jadx.gui.device.protocol.ADBDevice.ForwardResult;
|
||||
|
||||
public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.JDWPProcessListener {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ADBDialog.class);
|
||||
@@ -254,8 +256,8 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
|
||||
private void connectToADB() {
|
||||
String tip;
|
||||
try {
|
||||
String host = hostTextField.getText();
|
||||
String port = portTextField.getText();
|
||||
String host = hostTextField.getText().trim();
|
||||
String port = portTextField.getText().trim();
|
||||
tipLabel.setText(NLS.str("adb_dialog.connecting", host, port));
|
||||
deviceSocket = ADB.listenForDeviceState(this, host, Integer.parseInt(port));
|
||||
if (deviceSocket != null) {
|
||||
@@ -273,9 +275,9 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeviceStatusChange(List<ADB.DeviceInfo> deviceInfoList) {
|
||||
public void onDeviceStatusChange(List<ADBDeviceInfo> deviceInfoList) {
|
||||
List<DeviceNode> nodes = new ArrayList<>(deviceInfoList.size());
|
||||
info_loop: for (ADB.DeviceInfo info : deviceInfoList) {
|
||||
info_loop: for (ADBDeviceInfo info : deviceInfoList) {
|
||||
for (DeviceNode deviceNode : deviceNodes) {
|
||||
if (deviceNode.device.updateDeviceInfo(info)) {
|
||||
deviceNode.refresh();
|
||||
@@ -283,7 +285,7 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
|
||||
continue info_loop;
|
||||
}
|
||||
}
|
||||
ADB.Device device = new ADB.Device(info);
|
||||
ADBDevice device = new ADBDevice(info);
|
||||
device.getAndroidReleaseVersion();
|
||||
nodes.add(new DeviceNode(device));
|
||||
listenJDWP(device);
|
||||
@@ -402,7 +404,7 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
|
||||
return null;
|
||||
}
|
||||
|
||||
private DeviceNode getDeviceNode(ADB.Device device) {
|
||||
private DeviceNode getDeviceNode(ADBDevice device) {
|
||||
for (DeviceNode deviceNode : deviceNodes) {
|
||||
if (deviceNode.device.equals(device)) {
|
||||
return deviceNode;
|
||||
@@ -411,7 +413,7 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
|
||||
throw new JadxRuntimeException("Unexpected device: " + device);
|
||||
}
|
||||
|
||||
private void listenJDWP(ADB.Device device) {
|
||||
private void listenJDWP(ADBDevice device) {
|
||||
try {
|
||||
device.listenForJDWP(this);
|
||||
} catch (Exception e) {
|
||||
@@ -441,7 +443,7 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
|
||||
}
|
||||
|
||||
@Override
|
||||
public void jdwpProcessOccurred(ADB.Device device, Set<String> id) {
|
||||
public void jdwpProcessOccurred(ADBDevice device, Set<String> id) {
|
||||
List<ADB.Process> procs;
|
||||
try {
|
||||
Thread.sleep(40); /*
|
||||
@@ -514,7 +516,7 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
|
||||
return;
|
||||
}
|
||||
String fullName = pkg + "/" + cls.getCls().getClassNode().getClassInfo().getFullName();
|
||||
ADB.Device device = deviceNodes.get(0).device; // TODO: if multiple devices presented should let user select the one they desire.
|
||||
ADBDevice device = deviceNodes.get(0).device; // TODO: if multiple devices presented should let user select the one they desire.
|
||||
if (device != null) {
|
||||
try {
|
||||
device.launchApp(fullName);
|
||||
@@ -547,7 +549,7 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
|
||||
}
|
||||
|
||||
@Override
|
||||
public void jdwpListenerClosed(ADB.Device device) {
|
||||
public void jdwpListenerClosed(ADBDevice device) {
|
||||
|
||||
}
|
||||
|
||||
@@ -556,27 +558,29 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
|
||||
}
|
||||
|
||||
private static class DeviceNode {
|
||||
ADB.Device device;
|
||||
ADBDevice device;
|
||||
DeviceTreeNode tNode;
|
||||
|
||||
DeviceNode(ADB.Device adbDevice) {
|
||||
DeviceNode(ADBDevice adbDevice) {
|
||||
this.device = adbDevice;
|
||||
tNode = new DeviceTreeNode();
|
||||
refresh();
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
ADB.DeviceInfo info = device.getDeviceInfo();
|
||||
ADBDeviceInfo info = device.getDeviceInfo();
|
||||
String text = info.model;
|
||||
if (!text.equals(info.serial)) {
|
||||
text += String.format(" [serial: %s]", info.serial);
|
||||
if (text != null) {
|
||||
if (!text.equals(info.serial)) {
|
||||
text += String.format(" [serial: %s]", info.serial);
|
||||
}
|
||||
text += String.format(" [state: %s]", info.isOnline() ? "online" : "offline");
|
||||
tNode.setUserObject(text);
|
||||
}
|
||||
text += String.format(" [state: %s]", info.isOnline() ? "online" : "offline");
|
||||
tNode.setUserObject(text);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean setupArgs(ADB.Device device, String pid, String name) {
|
||||
private boolean setupArgs(ADBDevice device, String pid, String name) {
|
||||
String ver = device.getAndroidReleaseVersion();
|
||||
if (StringUtils.isEmpty(ver)) {
|
||||
if (JOptionPane.showConfirmDialog(mainWindow,
|
||||
@@ -588,7 +592,7 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
|
||||
ver = "8";
|
||||
}
|
||||
ver = getMajorVer(ver);
|
||||
debugSetter.set(device, ver, pid, name);
|
||||
debugSetter.set(device, Integer.parseInt(ver), pid, name);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -602,15 +606,15 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
|
||||
|
||||
private class DebugSetting {
|
||||
private static final int FORWARD_TCP_PORT = 33233;
|
||||
private String ver;
|
||||
private int ver;
|
||||
private String pid;
|
||||
private String name;
|
||||
private ADB.Device device;
|
||||
private ADBDevice device;
|
||||
private int forwardTcpPort = FORWARD_TCP_PORT;
|
||||
private String expectPkg = "";
|
||||
private boolean autoAttachPkg = false;
|
||||
|
||||
private void set(ADB.Device device, String ver, String pid, String name) {
|
||||
private void set(ADBDevice device, int ver, String pid, String name) {
|
||||
this.ver = ver;
|
||||
this.pid = pid;
|
||||
this.name = name;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user