diff --git a/build.gradle b/build.gradle index c3942c75..0e752250 100644 --- a/build.gradle +++ b/build.gradle @@ -90,7 +90,7 @@ dependencyUpdates { resolutionStrategy { componentSelection { rules -> rules.all { ComponentSelection selection -> - boolean rejected = ['alpha', 'beta', 'rc', 'cr', 'm', 'atlassian'].any { qualifier -> + boolean rejected = ['alpha', 'beta', 'dev', 'rc', 'cr', 'm', 'atlassian'].any { qualifier -> selection.candidate.version ==~ /(?i).*[.-]${qualifier}[.\d-]*/ } if (rejected) { diff --git a/gradle.properties b/gradle.properties index 15b925af..238ba233 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,6 @@ org.gradle.warning.mode=all org.gradle.parallel=true +org.gradle.caching=true # Flags for google-java-format (optimize imports by spotless) for Java >= 16. # Java < 9 will ignore unsupported flags (thanks to -XX:+IgnoreUnrecognizedVMOptions) diff --git a/jadx-cli/build.gradle b/jadx-cli/build.gradle index 477c11f9..89dee873 100644 --- a/jadx-cli/build.gradle +++ b/jadx-cli/build.gradle @@ -9,6 +9,7 @@ dependencies { runtimeOnly(project(':jadx-plugins:jadx-java-input')) runtimeOnly(project(':jadx-plugins:jadx-java-convert')) runtimeOnly(project(':jadx-plugins:jadx-smali-input')) + runtimeOnly(project(':jadx-plugins:jadx-script:jadx-script-plugin')) implementation 'com.beust:jcommander:1.82' implementation 'ch.qos.logback:logback-classic:1.3.5' diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index 671188ea..909aa6bb 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -24,6 +24,8 @@ import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import jadx.api.core.nodes.IJadxDecompiler; +import jadx.api.impl.plugins.SimplePluginContext; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeNodeRef; import jadx.api.metadata.annotations.NodeDeclareRef; @@ -31,9 +33,13 @@ import jadx.api.metadata.annotations.VarNode; import jadx.api.metadata.annotations.VarRef; import jadx.api.plugins.JadxPlugin; import jadx.api.plugins.JadxPluginManager; +import jadx.api.plugins.gui.JadxGuiContext; import jadx.api.plugins.input.JadxInputPlugin; import jadx.api.plugins.input.data.ILoadResult; import jadx.api.plugins.options.JadxPluginOptions; +import jadx.api.plugins.pass.JadxPass; +import jadx.api.plugins.pass.types.JadxAfterLoadPass; +import jadx.api.plugins.pass.types.JadxPassType; import jadx.core.Jadx; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.nodes.ClassNode; @@ -78,7 +84,7 @@ import jadx.core.xmlgen.ResourcesSaver; * * */ -public final class JadxDecompiler implements Closeable { +public final class JadxDecompiler implements IJadxDecompiler, Closeable { private static final Logger LOG = LoggerFactory.getLogger(JadxDecompiler.class); private final JadxArgs args; @@ -95,6 +101,8 @@ public final class JadxDecompiler implements Closeable { private final IDecompileScheduler decompileScheduler = new DecompilerScheduler(); private final List customLoads = new ArrayList<>(); + private final Map> customPasses = new HashMap<>(); + private @Nullable JadxGuiContext guiContext; public JadxDecompiler() { this(new JadxArgs()); @@ -112,11 +120,14 @@ public final class JadxDecompiler implements Closeable { loadInputFiles(); root = new RootNode(args); + root.setDecompilerRef(this); + root.mergePasses(customPasses); root.loadClasses(loadedInputs); root.initClassPath(); root.loadResources(getResources()); root.runPreDecompileStage(); root.initPasses(); + loadFinished(); } private void loadInputFiles() { @@ -136,14 +147,6 @@ public final class JadxDecompiler implements Closeable { } } - public void addCustomLoad(ILoadResult customLoad) { - customLoads.add(customLoad); - } - - public List getCustomLoads() { - return customLoads; - } - private void reset() { root = null; classes = null; @@ -177,6 +180,11 @@ public final class JadxDecompiler implements Closeable { LOG.debug("Resolved plugins: {}", Utils.collectionMap(pluginManager.getResolvedPlugins(), p -> p.getPluginInfo().getPluginId())); } + applyPluginOptions(args); + initPlugins(); + } + + private void applyPluginOptions(JadxArgs args) { Map pluginOptions = args.getPluginOptions(); if (!pluginOptions.isEmpty()) { LOG.debug("Applying plugin options: {}", pluginOptions); @@ -191,6 +199,36 @@ public final class JadxDecompiler implements Closeable { } } + private void initPlugins() { + customPasses.clear(); + + List plugins = pluginManager.getResolvedPlugins(); + SimplePluginContext context = new SimplePluginContext(this); + context.setGuiContext(guiContext); + for (JadxPlugin passPlugin : plugins) { + try { + passPlugin.init(context); + } catch (Exception e) { + String pluginId = passPlugin.getPluginInfo().getPluginId(); + throw new JadxRuntimeException("Failed to pass plugin: " + pluginId, e); + } + } + if (LOG.isDebugEnabled()) { + List passes = customPasses.values().stream().flatMap(Collection::stream) + .map(p -> p.getInfo().getName()).collect(Collectors.toList()); + LOG.debug("Loaded custom passes: {} {}", passes.size(), passes); + } + } + + private void loadFinished() { + List list = customPasses.get(JadxAfterLoadPass.TYPE); + if (list != null) { + for (JadxPass pass : list) { + ((JadxAfterLoadPass) pass).init(this); + } + } + } + @SuppressWarnings("unused") public void registerPlugin(JadxPlugin plugin) { pluginManager.register(plugin); @@ -644,6 +682,22 @@ public final class JadxDecompiler implements Closeable { return decompileScheduler; } + public void addCustomLoad(ILoadResult customLoad) { + customLoads.add(customLoad); + } + + public List getCustomLoads() { + return customLoads; + } + + public void addCustomPass(JadxPass pass) { + customPasses.computeIfAbsent(pass.getPassType(), l -> new ArrayList<>()).add(pass); + } + + public void setJadxGuiContext(JadxGuiContext guiContext) { + this.guiContext = guiContext; + } + @Override public String toString() { return "jadx decompiler " + getVersion(); diff --git a/jadx-core/src/main/java/jadx/api/impl/passes/DecompilePassWrapper.java b/jadx-core/src/main/java/jadx/api/impl/passes/DecompilePassWrapper.java new file mode 100644 index 00000000..e56724e1 --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/impl/passes/DecompilePassWrapper.java @@ -0,0 +1,54 @@ +package jadx.api.impl.passes; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.api.plugins.pass.types.JadxDecompilePass; +import jadx.core.dex.nodes.ClassNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.nodes.RootNode; +import jadx.core.dex.visitors.AbstractVisitor; +import jadx.core.utils.exceptions.JadxException; + +public class DecompilePassWrapper extends AbstractVisitor { + private static final Logger LOG = LoggerFactory.getLogger(DecompilePassWrapper.class); + + private final JadxDecompilePass decompilePass; + + public DecompilePassWrapper(JadxDecompilePass decompilePass) { + this.decompilePass = decompilePass; + } + + @Override + public void init(RootNode root) throws JadxException { + try { + decompilePass.init(root); + } catch (Throwable e) { + LOG.error("Error in decompile pass init: {}", this, e); + } + } + + @Override + public boolean visit(ClassNode cls) throws JadxException { + try { + return decompilePass.visit(cls); + } catch (Throwable e) { + LOG.error("Error in decompile pass init: {}", this, e); + return false; + } + } + + @Override + public void visit(MethodNode mth) throws JadxException { + try { + decompilePass.visit(mth); + } catch (Throwable e) { + LOG.error("Error in decompile pass: {}", this, e); + } + } + + @Override + public String toString() { + return decompilePass.getInfo().getName(); + } +} diff --git a/jadx-core/src/main/java/jadx/api/impl/passes/PreparePassWrapper.java b/jadx-core/src/main/java/jadx/api/impl/passes/PreparePassWrapper.java new file mode 100644 index 00000000..57a91f01 --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/impl/passes/PreparePassWrapper.java @@ -0,0 +1,33 @@ +package jadx.api.impl.passes; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.api.plugins.pass.types.JadxPreparePass; +import jadx.core.dex.nodes.RootNode; +import jadx.core.dex.visitors.AbstractVisitor; +import jadx.core.utils.exceptions.JadxException; + +public class PreparePassWrapper extends AbstractVisitor { + private static final Logger LOG = LoggerFactory.getLogger(PreparePassWrapper.class); + + private final JadxPreparePass preparePass; + + public PreparePassWrapper(JadxPreparePass preparePass) { + this.preparePass = preparePass; + } + + @Override + public void init(RootNode root) throws JadxException { + try { + preparePass.init(root); + } catch (Exception e) { + LOG.error("Error in prepare pass init: {}", this, e); + } + } + + @Override + public String toString() { + return preparePass.getInfo().getName(); + } +} diff --git a/jadx-core/src/main/java/jadx/api/impl/plugins/SimplePassContext.java b/jadx-core/src/main/java/jadx/api/impl/plugins/SimplePassContext.java new file mode 100644 index 00000000..ae60ce53 --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/impl/plugins/SimplePassContext.java @@ -0,0 +1,19 @@ +package jadx.api.impl.plugins; + +import jadx.api.JadxDecompiler; +import jadx.api.plugins.pass.JadxPass; +import jadx.api.plugins.pass.JadxPassContext; + +public class SimplePassContext implements JadxPassContext { + + private final JadxDecompiler jadxDecompiler; + + public SimplePassContext(JadxDecompiler jadxDecompiler) { + this.jadxDecompiler = jadxDecompiler; + } + + @Override + public void addPass(JadxPass pass) { + jadxDecompiler.addCustomPass(pass); + } +} diff --git a/jadx-core/src/main/java/jadx/api/impl/plugins/SimplePluginContext.java b/jadx-core/src/main/java/jadx/api/impl/plugins/SimplePluginContext.java new file mode 100644 index 00000000..a11c6645 --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/impl/plugins/SimplePluginContext.java @@ -0,0 +1,39 @@ +package jadx.api.impl.plugins; + +import org.jetbrains.annotations.Nullable; + +import jadx.api.JadxDecompiler; +import jadx.api.plugins.JadxPluginContext; +import jadx.api.plugins.gui.JadxGuiContext; +import jadx.api.plugins.pass.JadxPassContext; + +public class SimplePluginContext implements JadxPluginContext { + + private final JadxDecompiler decompiler; + private final JadxPassContext passContext; + private @Nullable JadxGuiContext guiContext; + + public SimplePluginContext(JadxDecompiler decompiler) { + this.decompiler = decompiler; + this.passContext = new SimplePassContext(decompiler); + } + + @Override + public JadxDecompiler getDecompiler() { + return decompiler; + } + + @Override + public JadxPassContext getPassContext() { + return passContext; + } + + @Override + public @Nullable JadxGuiContext getGuiContext() { + return guiContext; + } + + public void setGuiContext(JadxGuiContext guiContext) { + this.guiContext = guiContext; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java index 5d413042..d3e33b7a 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java @@ -20,6 +20,7 @@ import jadx.api.ICodeInfo; import jadx.api.ICodeWriter; import jadx.api.JadxArgs; import jadx.api.JavaClass; +import jadx.api.core.nodes.IClassNode; import jadx.api.impl.SimpleCodeInfo; import jadx.api.plugins.input.data.IClassData; import jadx.api.plugins.input.data.IFieldData; @@ -53,7 +54,7 @@ import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.dex.nodes.ProcessState.LOADED; import static jadx.core.dex.nodes.ProcessState.NOT_LOADED; -public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeNode, Comparable { +public class ClassNode extends NotificationAttrNode implements IClassNode, ILoadable, ICodeNode, Comparable { private final RootNode root; private final IClassData clsData; diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java index 0512dd46..b5f87f33 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java @@ -11,13 +11,16 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JavaMethod; +import jadx.api.core.nodes.IMethodNode; import jadx.api.plugins.input.data.ICodeReader; import jadx.api.plugins.input.data.IDebugInfo; import jadx.api.plugins.input.data.IMethodData; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.types.ExceptionsAttr; import jadx.core.dex.attributes.AFlag; +import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.LoopInfo; +import jadx.core.dex.attributes.nodes.MethodOverrideAttr; import jadx.core.dex.attributes.nodes.NotificationAttrNode; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.info.AccessInfo.AFType; @@ -36,7 +39,8 @@ import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.utils.Utils.lockList; -public class MethodNode extends NotificationAttrNode implements IMethodDetails, ILoadable, ICodeNode, Comparable { +public class MethodNode extends NotificationAttrNode implements IMethodNode, + IMethodDetails, ILoadable, ICodeNode, Comparable { private static final Logger LOG = LoggerFactory.getLogger(MethodNode.class); private final MethodInfo mthInfo; @@ -572,8 +576,19 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails, noCode = true; } + public void rename(String newName) { + MethodOverrideAttr overrideAttr = get(AType.METHOD_OVERRIDE); + if (overrideAttr != null) { + for (MethodNode relatedMth : overrideAttr.getRelatedMthNodes()) { + relatedMth.getMethodInfo().setAlias(newName); + } + } else { + mthInfo.setAlias(newName); + } + } + /** - * Calculate instructions count at currect stage + * Calculate instructions count at current stage */ public long countInsns() { if (instructions != null) { diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java index c176bf7c..d0519617 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java @@ -15,12 +15,20 @@ import org.slf4j.LoggerFactory; import jadx.api.ICodeCache; import jadx.api.ICodeWriter; import jadx.api.JadxArgs; +import jadx.api.JadxDecompiler; import jadx.api.ResourceFile; import jadx.api.ResourceType; import jadx.api.ResourcesLoader; +import jadx.api.core.nodes.IRootNode; import jadx.api.data.ICodeData; +import jadx.api.impl.passes.DecompilePassWrapper; +import jadx.api.impl.passes.PreparePassWrapper; import jadx.api.plugins.input.data.IClassData; import jadx.api.plugins.input.data.ILoadResult; +import jadx.api.plugins.pass.JadxPass; +import jadx.api.plugins.pass.types.JadxDecompilePass; +import jadx.api.plugins.pass.types.JadxPassType; +import jadx.api.plugins.pass.types.JadxPreparePass; import jadx.core.Jadx; import jadx.core.ProcessClass; import jadx.core.clsp.ClspGraph; @@ -38,6 +46,7 @@ import jadx.core.dex.visitors.typeinference.TypeCompare; import jadx.core.dex.visitors.typeinference.TypeUpdate; import jadx.core.utils.CacheStorage; import jadx.core.utils.ErrorsCounter; +import jadx.core.utils.PassMerge; import jadx.core.utils.StringUtils; import jadx.core.utils.Utils; import jadx.core.utils.android.AndroidResourcesUtils; @@ -49,7 +58,7 @@ import jadx.core.xmlgen.ResourceStorage; import jadx.core.xmlgen.entry.ResourceEntry; import jadx.core.xmlgen.entry.ValuesParser; -public class RootNode { +public class RootNode implements IRootNode { private static final Logger LOG = LoggerFactory.getLogger(RootNode.class); private final JadxArgs args; @@ -76,10 +85,15 @@ public class RootNode { private ClassNode appResClass; private boolean isProto; + /** + * Optional decompiler reference + */ + private @Nullable JadxDecompiler decompiler; + public RootNode(JadxArgs args) { this.args = args; this.preDecompilePasses = Jadx.getPreDecompilePassesList(); - this.processClasses = new ProcessClass(this.getArgs()); + this.processClasses = new ProcessClass(args); this.stringUtils = new StringUtils(args); this.constValues = new ConstStorage(args); this.typeUpdate = new TypeUpdate(this); @@ -273,6 +287,15 @@ public class RootNode { classes.forEach(ClassNode::updateParentClass); } + public void mergePasses(Map> customPasses) { + PassMerge.run(preDecompilePasses, + customPasses.get(JadxPreparePass.TYPE), + p -> new PreparePassWrapper((JadxPreparePass) p)); + PassMerge.run(processClasses.getPasses(), + customPasses.get(JadxDecompilePass.TYPE), + p -> new DecompilePassWrapper((JadxDecompilePass) p)); + } + public void runPreDecompileStage() { boolean debugEnabled = LOG.isDebugEnabled(); for (IDexTreeVisitor pass : preDecompilePasses) { @@ -290,7 +313,7 @@ public class RootNode { DepthTraversal.visit(pass, cls); } if (debugEnabled) { - LOG.debug("{} time: {}ms", pass.getClass().getSimpleName(), System.currentTimeMillis() - start); + LOG.debug("Prepare pass: '{}' - {}ms", pass, System.currentTimeMillis() - start); } } } @@ -537,6 +560,14 @@ public class RootNode { return args; } + public void setDecompilerRef(JadxDecompiler jadxDecompiler) { + this.decompiler = jadxDecompiler; + } + + public @Nullable JadxDecompiler getDecompiler() { + return decompiler; + } + public TypeUpdate getTypeUpdate() { return typeUpdate; } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/OverrideMethodVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/OverrideMethodVisitor.java index 18a74258..ee439bf0 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/OverrideMethodVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/OverrideMethodVisitor.java @@ -460,4 +460,9 @@ public class OverrideMethodVisitor extends AbstractVisitor { k++; } } + + @Override + public String toString() { + return "OverrideMethodVisitor"; + } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java index cb5350f3..1872a874 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java @@ -360,4 +360,9 @@ public class ProcessAnonymous extends AbstractVisitor { } return null; } + + @Override + public String toString() { + return "ProcessAnonymous"; + } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessMethodsForInline.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessMethodsForInline.java index 04fd3e22..0851f2e3 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessMethodsForInline.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessMethodsForInline.java @@ -71,4 +71,9 @@ public class ProcessMethodsForInline extends AbstractVisitor { } } } + + @Override + public String toString() { + return "ProcessMethodsForInline"; + } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/SignatureProcessor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/SignatureProcessor.java index 3944ee87..5f0d6fb5 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/SignatureProcessor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/SignatureProcessor.java @@ -276,4 +276,9 @@ public class SignatureProcessor extends AbstractVisitor { } return validateInnerType(innerType); } + + @Override + public String toString() { + return "SignatureProcessor"; + } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/rename/RenameVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/rename/RenameVisitor.java index 9e8706dd..a359e87b 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/rename/RenameVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/rename/RenameVisitor.java @@ -266,4 +266,9 @@ public class RenameVisitor extends AbstractVisitor { } return pkg.substring(0, dotPos); } + + @Override + public String toString() { + return "RenameVisitor"; + } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/rename/UserRenames.java b/jadx-core/src/main/java/jadx/core/dex/visitors/rename/UserRenames.java index bce60be6..2b5cb028 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/rename/UserRenames.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/rename/UserRenames.java @@ -11,8 +11,6 @@ import jadx.api.data.ICodeData; import jadx.api.data.ICodeRename; import jadx.api.data.IJavaCodeRef; import jadx.api.data.IJavaNodeRef; -import jadx.core.dex.attributes.AType; -import jadx.core.dex.attributes.nodes.MethodOverrideAttr; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.InfoStorage; import jadx.core.dex.instructions.args.ArgType; @@ -72,28 +70,13 @@ public class UserRenames { } else { IJavaCodeRef codeRef = rename.getCodeRef(); if (codeRef == null) { - applyMethodRename(mth, rename); + mth.rename(rename.getNewName()); } } break; } } - private static void applyMethodRename(MethodNode mth, ICodeRename rename) { - MethodOverrideAttr overrideAttr = mth.get(AType.METHOD_OVERRIDE); - if (overrideAttr != null) { - for (MethodNode relatedMth : overrideAttr.getRelatedMthNodes()) { - renameMethod(relatedMth, rename); - } - } else { - renameMethod(mth, rename); - } - } - - private static void renameMethod(MethodNode mth, ICodeRename rename) { - mth.getMethodInfo().setAlias(rename.getNewName()); - } - // TODO: Very inefficient!!! Add PackageInfo class to build package hierarchy private static void applyPkgRenames(RootNode root, List renames) { List classes = root.getClasses(false); diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfoVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfoVisitor.java index 0278129f..40de47f6 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfoVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfoVisitor.java @@ -145,4 +145,9 @@ public class UsageInfoVisitor extends AbstractVisitor { mergeIntoMth.setUseIn(mergedUsage); sourceMth.setUseIn(Collections.emptyList()); } + + @Override + public String toString() { + return "UsageInfoVisitor"; + } } diff --git a/jadx-core/src/main/java/jadx/core/utils/PassMerge.java b/jadx-core/src/main/java/jadx/core/utils/PassMerge.java new file mode 100644 index 00000000..dbd36317 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/utils/PassMerge.java @@ -0,0 +1,82 @@ +package jadx.core.utils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import jadx.api.plugins.pass.JadxPass; +import jadx.api.plugins.pass.JadxPassInfo; +import jadx.core.dex.visitors.IDexTreeVisitor; +import jadx.core.utils.exceptions.JadxRuntimeException; + +public class PassMerge { + + public static void run(List passes, List customPasses, Function wrap) { + if (Utils.isEmpty(customPasses)) { + return; + } + for (JadxPass customPass : customPasses) { + IDexTreeVisitor pass = wrap.apply(customPass); + int pos = searchInsertPos(passes, customPass.getInfo()); + if (pos == -1) { + passes.add(pass); + } else { + passes.add(pos, pass); + } + } + } + + private static int searchInsertPos(List passes, JadxPassInfo info) { + List runAfter = info.runAfter(); + List runBefore = info.runBefore(); + if (runAfter.isEmpty() && runBefore.isEmpty()) { + return -1; // last + } + if (ListUtils.isSingleElement(runAfter, "start")) { + return 0; + } + if (ListUtils.isSingleElement(runBefore, "end")) { + return -1; + } + Map namesMap = buildNamesMap(passes); + int after = 0; + for (String name : runAfter) { + Integer pos = namesMap.get(name); + if (pos != null) { + after = Math.max(after, pos); + } + } + int before = Integer.MAX_VALUE; + for (String name : runBefore) { + Integer pos = namesMap.get(name); + if (pos != null) { + before = Math.min(before, pos); + } + } + if (before <= after) { + throw new JadxRuntimeException("Conflict pass order requirements: " + info.getName() + + "\n run after: " + runAfter + + "\n run before: " + runBefore + + "\n passes: " + ListUtils.map(passes, PassMerge::getPassName)); + } + if (after == 0) { + return before; + } + int pos = after + 1; + return pos >= passes.size() ? -1 : pos; + } + + private static Map buildNamesMap(List passes) { + int size = passes.size(); + Map namesMap = new HashMap<>(size); + for (int i = 0; i < size; i++) { + namesMap.put(getPassName(passes.get(i)), i); + } + return namesMap; + } + + private static String getPassName(IDexTreeVisitor pass) { + return pass.getClass().getSimpleName(); + } +} diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/core/nodes/IClassNode.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/core/nodes/IClassNode.java new file mode 100644 index 00000000..b8982307 --- /dev/null +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/core/nodes/IClassNode.java @@ -0,0 +1,4 @@ +package jadx.api.core.nodes; + +public interface IClassNode { +} diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/core/nodes/IJadxDecompiler.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/core/nodes/IJadxDecompiler.java new file mode 100644 index 00000000..3a7a865f --- /dev/null +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/core/nodes/IJadxDecompiler.java @@ -0,0 +1,4 @@ +package jadx.api.core.nodes; + +public interface IJadxDecompiler { +} diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/core/nodes/IMethodNode.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/core/nodes/IMethodNode.java new file mode 100644 index 00000000..8fd7141c --- /dev/null +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/core/nodes/IMethodNode.java @@ -0,0 +1,4 @@ +package jadx.api.core.nodes; + +public interface IMethodNode { +} diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/core/nodes/IRootNode.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/core/nodes/IRootNode.java new file mode 100644 index 00000000..1448bd35 --- /dev/null +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/core/nodes/IRootNode.java @@ -0,0 +1,4 @@ +package jadx.api.core.nodes; + +public interface IRootNode { +} diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/JadxPlugin.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/JadxPlugin.java index 51337098..52536a68 100644 --- a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/JadxPlugin.java +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/JadxPlugin.java @@ -2,4 +2,8 @@ package jadx.api.plugins; public interface JadxPlugin { JadxPluginInfo getPluginInfo(); + + default void init(JadxPluginContext context) { + // default to no-op + } } diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/JadxPluginContext.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/JadxPluginContext.java new file mode 100644 index 00000000..83564428 --- /dev/null +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/JadxPluginContext.java @@ -0,0 +1,17 @@ +package jadx.api.plugins; + +import org.jetbrains.annotations.Nullable; + +import jadx.api.core.nodes.IJadxDecompiler; +import jadx.api.plugins.gui.JadxGuiContext; +import jadx.api.plugins.pass.JadxPassContext; + +public interface JadxPluginContext { + + IJadxDecompiler getDecompiler(); + + JadxPassContext getPassContext(); + + @Nullable + JadxGuiContext getGuiContext(); +} diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/JadxPluginManager.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/JadxPluginManager.java index 24d4f667..23a983ed 100644 --- a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/JadxPluginManager.java +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/JadxPluginManager.java @@ -42,7 +42,6 @@ public class JadxPluginManager { ServiceLoader jadxPlugins = ServiceLoader.load(JadxPlugin.class); for (JadxPlugin plugin : jadxPlugins) { addPlugin(plugin); - LOG.debug("Loading plugin: {}", plugin.getPluginInfo().getPluginId()); } resolve(); } @@ -56,6 +55,7 @@ public class JadxPluginManager { private PluginData addPlugin(JadxPlugin plugin) { PluginData pluginData = new PluginData(plugin, plugin.getPluginInfo()); + LOG.debug("Loading plugin: {}", pluginData.getPluginId()); if (!allPlugins.add(pluginData)) { throw new IllegalArgumentException("Duplicate plugin id: " + pluginData + ", class " + plugin.getClass()); } @@ -112,16 +112,18 @@ public class JadxPluginManager { } public List getInputPlugins() { - return resolvedPlugins.stream() - .filter(JadxInputPlugin.class::isInstance) - .map(JadxInputPlugin.class::cast) - .collect(Collectors.toList()); + return getPluginsWithType(JadxInputPlugin.class); } public List getPluginsWithOptions() { + return getPluginsWithType(JadxPluginOptions.class); + } + + @SuppressWarnings("unchecked") + public List getPluginsWithType(Class type) { return resolvedPlugins.stream() - .filter(JadxPluginOptions.class::isInstance) - .map(JadxPluginOptions.class::cast) + .filter(p -> type.isAssignableFrom(p.getClass())) + .map(p -> (T) p) .collect(Collectors.toList()); } diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/gui/JadxGuiContext.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/gui/JadxGuiContext.java new file mode 100644 index 00000000..7c810d23 --- /dev/null +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/gui/JadxGuiContext.java @@ -0,0 +1,11 @@ +package jadx.api.plugins.gui; + +public interface JadxGuiContext { + + /** + * Run code in UI Thread + */ + void uiRun(Runnable runnable); + + void addMenuAction(String name, Runnable action); +} diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/JadxPass.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/JadxPass.java new file mode 100644 index 00000000..8bfc8aa2 --- /dev/null +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/JadxPass.java @@ -0,0 +1,9 @@ +package jadx.api.plugins.pass; + +import jadx.api.plugins.pass.types.JadxPassType; + +public interface JadxPass { + JadxPassInfo getInfo(); + + JadxPassType getPassType(); +} diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/JadxPassContext.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/JadxPassContext.java new file mode 100644 index 00000000..fd8a7dba --- /dev/null +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/JadxPassContext.java @@ -0,0 +1,6 @@ +package jadx.api.plugins.pass; + +public interface JadxPassContext { + + void addPass(JadxPass pass); +} diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/JadxPassInfo.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/JadxPassInfo.java new file mode 100644 index 00000000..3f638cb2 --- /dev/null +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/JadxPassInfo.java @@ -0,0 +1,14 @@ +package jadx.api.plugins.pass; + +import java.util.List; + +public interface JadxPassInfo { + + String getName(); + + String getDescription(); + + List runAfter(); + + List runBefore(); +} diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/impl/OrderedJadxPassInfo.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/impl/OrderedJadxPassInfo.java new file mode 100644 index 00000000..d9725978 --- /dev/null +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/impl/OrderedJadxPassInfo.java @@ -0,0 +1,49 @@ +package jadx.api.plugins.pass.impl; + +import java.util.ArrayList; +import java.util.List; + +import jadx.api.plugins.pass.JadxPassInfo; + +public class OrderedJadxPassInfo implements JadxPassInfo { + + private final String name; + private final String desc; + private final List runAfter; + private final List runBefore; + + public OrderedJadxPassInfo(String name) { + this(name, name); + } + + public OrderedJadxPassInfo(String name, String desc) { + this(name, desc, new ArrayList<>(), new ArrayList<>()); + } + + public OrderedJadxPassInfo(String name, String desc, List runAfter, List runBefore) { + this.name = name; + this.desc = desc; + this.runAfter = runAfter; + this.runBefore = runBefore; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getDescription() { + return desc; + } + + @Override + public List runAfter() { + return runAfter; + } + + @Override + public List runBefore() { + return runBefore; + } +} diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/impl/SimpleJadxPassInfo.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/impl/SimpleJadxPassInfo.java new file mode 100644 index 00000000..9c942ca8 --- /dev/null +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/impl/SimpleJadxPassInfo.java @@ -0,0 +1,41 @@ +package jadx.api.plugins.pass.impl; + +import java.util.Collections; +import java.util.List; + +import jadx.api.plugins.pass.JadxPassInfo; + +public class SimpleJadxPassInfo implements JadxPassInfo { + + private final String name; + private final String desc; + + public SimpleJadxPassInfo(String name) { + this(name, name); + } + + public SimpleJadxPassInfo(String name, String desc) { + this.name = name; + this.desc = desc; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getDescription() { + return desc; + } + + @Override + public List runAfter() { + return Collections.emptyList(); + } + + @Override + public List runBefore() { + return Collections.emptyList(); + } +} diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/types/JadxAfterLoadPass.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/types/JadxAfterLoadPass.java new file mode 100644 index 00000000..fbeae446 --- /dev/null +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/types/JadxAfterLoadPass.java @@ -0,0 +1,15 @@ +package jadx.api.plugins.pass.types; + +import jadx.api.core.nodes.IJadxDecompiler; +import jadx.api.plugins.pass.JadxPass; + +public interface JadxAfterLoadPass extends JadxPass { + JadxPassType TYPE = new JadxPassType(JadxAfterLoadPass.class); + + void init(IJadxDecompiler decompiler); + + @Override + default JadxPassType getPassType() { + return TYPE; + } +} diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/types/JadxDecompilePass.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/types/JadxDecompilePass.java new file mode 100644 index 00000000..4bf0fb26 --- /dev/null +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/types/JadxDecompilePass.java @@ -0,0 +1,29 @@ +package jadx.api.plugins.pass.types; + +import jadx.api.core.nodes.IClassNode; +import jadx.api.core.nodes.IMethodNode; +import jadx.api.core.nodes.IRootNode; +import jadx.api.plugins.pass.JadxPass; + +public interface JadxDecompilePass extends JadxPass { + JadxPassType TYPE = new JadxPassType(JadxDecompilePass.class); + + void init(IRootNode root); + + /** + * Visit class + * + * @return false for disable child methods and inner classes traversal + */ + boolean visit(IClassNode cls); + + /** + * Visit method + */ + void visit(IMethodNode mth); + + @Override + default JadxPassType getPassType() { + return TYPE; + } +} diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/types/JadxPassType.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/types/JadxPassType.java new file mode 100644 index 00000000..cbfac39b --- /dev/null +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/types/JadxPassType.java @@ -0,0 +1,32 @@ +package jadx.api.plugins.pass.types; + +import jadx.api.plugins.pass.JadxPass; + +public class JadxPassType { + private final String cls; + + public JadxPassType(Class cls) { + this.cls = cls.getSimpleName(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof JadxPassType)) { + return false; + } + return cls.equals(((JadxPassType) o).cls); + } + + @Override + public int hashCode() { + return cls.hashCode(); + } + + @Override + public String toString() { + return "JadxPassType{" + cls + '}'; + } +} diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/types/JadxPreparePass.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/types/JadxPreparePass.java new file mode 100644 index 00000000..bc197fc3 --- /dev/null +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/pass/types/JadxPreparePass.java @@ -0,0 +1,15 @@ +package jadx.api.plugins.pass.types; + +import jadx.api.core.nodes.IRootNode; +import jadx.api.plugins.pass.JadxPass; + +public interface JadxPreparePass extends JadxPass { + JadxPassType TYPE = new JadxPassType(JadxPreparePass.class); + + void init(IRootNode root); + + @Override + default JadxPassType getPassType() { + return TYPE; + } +} diff --git a/jadx-plugins/jadx-script/examples/build.gradle.kts b/jadx-plugins/jadx-script/examples/build.gradle.kts new file mode 100644 index 00000000..8219f693 --- /dev/null +++ b/jadx-plugins/jadx-script/examples/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + kotlin("jvm") version "1.7.20" +} + +dependencies { + implementation(project(":jadx-plugins:jadx-script:jadx-script-runtime")) + + implementation("org.jetbrains.kotlin:kotlin-stdlib-common") + implementation("org.jetbrains.kotlin:kotlin-script-runtime") + + implementation("io.github.microutils:kotlin-logging-jvm:3.0.2") +} + +sourceSets { + main { + java.srcDirs("scripts", "context") + } +} diff --git a/jadx-plugins/jadx-script/examples/context/stubs.kt b/jadx-plugins/jadx-script/examples/context/stubs.kt new file mode 100644 index 00000000..310aca10 --- /dev/null +++ b/jadx-plugins/jadx-script/examples/context/stubs.kt @@ -0,0 +1,28 @@ +@file:Suppress("MayBeConstant", "unused") + +import jadx.plugins.script.runtime.JadxScriptInstance +import mu.KotlinLogging + +/** + * Stubs for JadxScriptBaseClass script super class + */ + +val log = KotlinLogging.logger("JadxScript") +val scriptName = "script" + +fun getJadxInstance(): JadxScriptInstance { + throw IllegalStateException("Stub method!") +} + +/** + * Annotations for maven imports + */ +@Target(AnnotationTarget.FILE) +@Repeatable +@Retention(AnnotationRetention.SOURCE) +annotation class DependsOn(vararg val artifactsCoordinates: String, val options: Array = []) + +@Target(AnnotationTarget.FILE) +@Repeatable +@Retention(AnnotationRetention.SOURCE) +annotation class Repository(vararg val repositoriesCoordinates: String, val options: Array = []) diff --git a/jadx-plugins/jadx-script/examples/scripts/deobf.jadx.kts b/jadx-plugins/jadx-script/examples/scripts/deobf.jadx.kts new file mode 100644 index 00000000..febcdb81 --- /dev/null +++ b/jadx-plugins/jadx-script/examples/scripts/deobf.jadx.kts @@ -0,0 +1,21 @@ +// custom deobfuscator example + +val jadx = getJadxInstance() +jadx.args.isDeobfuscationOn = false +jadx.args.renameFlags = emptySet() + +val regex = """[Oo0]+""".toRegex() +var n = 0 +jadx.rename.all { name, node -> + when { + name matches regex -> { + val newName = "${node.typeName()}${n++}" + println("renaming ${node.typeName()} '$node' to '$newName'") + newName + } + else -> null + } +} +jadx.afterLoad { + println("Renames count: $n") +} diff --git a/jadx-plugins/jadx-script/examples/scripts/gui.jadx.kts b/jadx-plugins/jadx-script/examples/scripts/gui.jadx.kts new file mode 100644 index 00000000..e04640d4 --- /dev/null +++ b/jadx-plugins/jadx-script/examples/scripts/gui.jadx.kts @@ -0,0 +1,9 @@ +// customize jadx-gui + +val jadx = getJadxInstance() + +jadx.gui.ifAvailable { + addMenuAction("Decompile All") { + jadx.decompile.all() + } +} diff --git a/jadx-plugins/jadx-script/examples/scripts/hello.jadx.kts b/jadx-plugins/jadx-script/examples/scripts/hello.jadx.kts new file mode 100644 index 00000000..5ec7a376 --- /dev/null +++ b/jadx-plugins/jadx-script/examples/scripts/hello.jadx.kts @@ -0,0 +1,29 @@ +// logger is preferred for output +log.info { "Hello from jadx script!" } + +// println will also work (will be redirected to logger) +println("println from script '$scriptName'") + +// get jadx decompiler script instance +val jadx = getJadxInstance() + +// adjust options if needed +jadx.args.isDeobfuscationOn = false + +// change names +jadx.rename.all { name -> + when (name) { + "HelloWorld" -> "HelloJadx" + else -> null + } +} + +// run some code after loading is finished +jadx.afterLoad { + println("Loaded classes: ${jadx.classes.size}") + // print first class code + jadx.classes.firstOrNull()?.let { cls -> + println("Class: '${cls.name}'") + println(cls.code) + } +} diff --git a/jadx-plugins/jadx-script/examples/scripts/replace.jadx.kts b/jadx-plugins/jadx-script/examples/scripts/replace.jadx.kts new file mode 100644 index 00000000..18908db8 --- /dev/null +++ b/jadx-plugins/jadx-script/examples/scripts/replace.jadx.kts @@ -0,0 +1,19 @@ +// instructions modification example + +import jadx.core.dex.instructions.ConstStringNode +import jadx.core.dex.instructions.InvokeNode +import jadx.core.dex.instructions.args.InsnArg + +val jadx = getJadxInstance() + +jadx.replace.insns { mth, insn -> + if (insn is InvokeNode) { + if (insn.callMth.shortId == "println(Ljava/lang/String;)V") { + val arg = insn.getArg(1) + val newArg = InsnArg.wrapInsnIntoArg(ConstStringNode("Jadx!")) + insn.setArg(1, newArg) + log.info { "Replace '$arg' with '$newArg' in $mth" } + } + } + null +} diff --git a/jadx-plugins/jadx-script/examples/scripts/stages.jadx.kts b/jadx-plugins/jadx-script/examples/scripts/stages.jadx.kts new file mode 100644 index 00000000..7120d7d3 --- /dev/null +++ b/jadx-plugins/jadx-script/examples/scripts/stages.jadx.kts @@ -0,0 +1,64 @@ +// insert processing passes for different decompilation stages + +import jadx.core.dex.instructions.InsnType +import jadx.core.dex.nodes.IRegion +import java.lang.Integer.max + +val jadx = getJadxInstance() + +// print raw instructions +jadx.stages.rawInsns { mth, insns -> + log.info { "Instructions for method: $mth" } + for ((offset, insn) in insns.withIndex()) { + insn?.let { + log.info { " 0x${offset.hex()}: $insn" } + } + } +} + +// access method basic blocks +jadx.stages.mthBlocks { mth, blocks -> + // count invoke instructions + var invCount = 0 + for (block in blocks) { + for (insn in block.instructions) { + if (insn.type == InsnType.INVOKE) { + invCount++ + } + } + } + log.info { "Invokes count in method $mth = $invCount" } +} + +// access method regions +jadx.stages.mthRegions { mth, region -> + // recursively count max depth of nested regions + fun countRegionsDepth(region: IRegion): Int { + val subBlocks = region.subBlocks + if (subBlocks.isEmpty()) { + return 0 + } + var depth = 1 + for (block in subBlocks) { + if (block is IRegion) { + depth = max(depth, 1 + countRegionsDepth(block)) + } + } + return depth + } + + val depth = countRegionsDepth(region) + log.info { "Max region depth in method $mth = $depth" } + if (depth > 5) { + jadx.debug.printMethodRegions(mth, printInsns = true) + } +} + +jadx.afterLoad { + /* + Start full decompilation (optional): + 1. jadx-cli start decompilation automatically + 2. jadx-gui start decompilation only on class open or search, so you might need to force it + */ + // jadx.decompile.all() +} diff --git a/jadx-plugins/jadx-script/jadx-script-plugin/build.gradle.kts b/jadx-plugins/jadx-script/jadx-script-plugin/build.gradle.kts new file mode 100644 index 00000000..297802d4 --- /dev/null +++ b/jadx-plugins/jadx-script/jadx-script-plugin/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("jadx-library") + + kotlin("jvm") version "1.7.20" +} + +dependencies { + implementation("org.jetbrains.kotlin:kotlin-scripting-common") + implementation("org.jetbrains.kotlin:kotlin-scripting-jvm") + implementation("org.jetbrains.kotlin:kotlin-scripting-jvm-host") + + implementation(project(":jadx-plugins:jadx-script:jadx-script-runtime")) + + implementation("io.github.microutils:kotlin-logging-jvm:3.0.2") +} diff --git a/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/JadxScriptPlugin.kt b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/JadxScriptPlugin.kt new file mode 100644 index 00000000..be3c3733 --- /dev/null +++ b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/JadxScriptPlugin.kt @@ -0,0 +1,19 @@ +package jadx.plugins.script + +import jadx.api.plugins.JadxPlugin +import jadx.api.plugins.JadxPluginContext +import jadx.api.plugins.JadxPluginInfo +import jadx.api.plugins.gui.JadxGuiContext +import jadx.api.plugins.pass.JadxPassContext +import jadx.plugins.script.passes.JadxScriptAfterLoadPass +import jadx.plugins.script.runner.ScriptEval + +class JadxScriptPlugin : JadxPlugin { + + override fun getPluginInfo() = JadxPluginInfo("jadx-script", "Jadx Script", "Scripting support for jadx") + + override fun init(init: JadxPluginContext) { + val scriptStates = ScriptEval().process(init) ?: return + init.passContext.addPass(JadxScriptAfterLoadPass(scriptStates)) + } +} diff --git a/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/passes/JadxScriptAfterLoadPass.kt b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/passes/JadxScriptAfterLoadPass.kt new file mode 100644 index 00000000..4e43aa7c --- /dev/null +++ b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/passes/JadxScriptAfterLoadPass.kt @@ -0,0 +1,30 @@ +package jadx.plugins.script.passes + +import jadx.api.core.nodes.IJadxDecompiler +import jadx.api.plugins.pass.impl.SimpleJadxPassInfo +import jadx.api.plugins.pass.types.JadxAfterLoadPass +import jadx.plugins.script.runner.ScriptStates +import mu.KotlinLogging + +private val LOG = KotlinLogging.logger {} + +class JadxScriptAfterLoadPass(private val scriptStates: ScriptStates) : JadxAfterLoadPass { + + override fun getInfo() = SimpleJadxPassInfo("JadxScriptAfterLoad", "Execute scripts 'afterLoad' block") + + override fun init(decompiler: IJadxDecompiler) { + for (script in scriptStates.getScripts()) { + if (script.error) { + continue + } + try { + for (b in script.scriptData.afterLoad) { + b.invoke() + } + } catch (e: Throwable) { + script.error = true + LOG.error(e) { "Error executing 'afterLoad' block in script: ${script.scriptFile.name}" } + } + } + } +} diff --git a/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/runner/ScriptEval.kt b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/runner/ScriptEval.kt new file mode 100644 index 00000000..2eabefaa --- /dev/null +++ b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/runner/ScriptEval.kt @@ -0,0 +1,67 @@ +package jadx.plugins.script.runner + +import jadx.api.JadxDecompiler +import jadx.api.plugins.JadxPluginContext +import jadx.api.plugins.pass.JadxPassContext +import jadx.plugins.script.runtime.JadxScript +import jadx.plugins.script.runtime.JadxScriptData +import mu.KotlinLogging +import java.io.File +import kotlin.script.experimental.api.* +import kotlin.script.experimental.host.toScriptSource +import kotlin.script.experimental.jvmhost.BasicJvmScriptingHost +import kotlin.script.experimental.jvmhost.createJvmCompilationConfigurationFromTemplate +import kotlin.script.experimental.jvmhost.createJvmEvaluationConfigurationFromTemplate + +private val LOG = KotlinLogging.logger {} + +class ScriptEval { + + fun process(init: JadxPluginContext): ScriptStates? { + val jadx = init.decompiler as JadxDecompiler + val scripts = jadx.args.inputFiles.filter { f -> f.name.endsWith(".jadx.kts") } + if (scripts.isEmpty()) { + return null + } + val scriptStates = ScriptStates() + for (scriptFile in scripts) { + val scriptData = JadxScriptData(jadx, init, scriptFile) + load(scriptFile, scriptData) + scriptStates.add(scriptFile, scriptData) + } + return scriptStates + } + + private fun load(scriptFile: File, scriptData: JadxScriptData) { + LOG.debug { "Loading script: ${scriptFile.absolutePath}" } + val result = eval(scriptFile, scriptData) + processEvalResult(result, scriptFile) + } + + private fun eval(scriptFile: File, scriptData: JadxScriptData): ResultWithDiagnostics { + val compilationConf = createJvmCompilationConfigurationFromTemplate() + val evalConf = createJvmEvaluationConfigurationFromTemplate { + constructorArgs(scriptData) + } + return BasicJvmScriptingHost().eval(scriptFile.toScriptSource(), compilationConf, evalConf) + } + + private fun processEvalResult(res: ResultWithDiagnostics, scriptFile: File) { + when (res) { + is ResultWithDiagnostics.Success -> { + val result = res.value.returnValue + if (result is ResultValue.Error) { + result.error.printStackTrace() + } + } + is ResultWithDiagnostics.Failure -> { + LOG.error { "Script execution failed: ${scriptFile.name}" } + res.reports + .filter { it.severity >= ScriptDiagnostic.Severity.ERROR } + .forEach { r -> + LOG.error(r.exception) { r.render(withSeverity = false) } + } + } + } + } +} diff --git a/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/runner/ScriptStates.kt b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/runner/ScriptStates.kt new file mode 100644 index 00000000..8e5d5237 --- /dev/null +++ b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/runner/ScriptStates.kt @@ -0,0 +1,21 @@ +package jadx.plugins.script.runner + +import jadx.plugins.script.runtime.JadxScriptData +import java.io.File + +data class ScriptStateData( + val scriptFile: File, + val scriptData: JadxScriptData, + var error: Boolean = false +) + +class ScriptStates { + + private val data: MutableList = ArrayList() + + fun add(scriptFile: File, scriptData: JadxScriptData) { + data.add(ScriptStateData(scriptFile, scriptData)) + } + + fun getScripts() = data +} diff --git a/jadx-plugins/jadx-script/jadx-script-plugin/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin new file mode 100644 index 00000000..d354b7a6 --- /dev/null +++ b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin @@ -0,0 +1 @@ +jadx.plugins.script.JadxScriptPlugin diff --git a/jadx-plugins/jadx-script/jadx-script-runtime/build.gradle.kts b/jadx-plugins/jadx-script/jadx-script-runtime/build.gradle.kts new file mode 100644 index 00000000..4f6f7954 --- /dev/null +++ b/jadx-plugins/jadx-script/jadx-script-runtime/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + id("jadx-library") + + kotlin("jvm") version "1.7.20" +} + +group = "jadx-script-context" + +dependencies { + implementation("org.jetbrains.kotlin:kotlin-scripting-common") + implementation("org.jetbrains.kotlin:kotlin-scripting-jvm") + + // allow to use maven dependencies in scripts + implementation("org.jetbrains.kotlin:kotlin-scripting-dependencies") + implementation("org.jetbrains.kotlin:kotlin-scripting-dependencies-maven") + + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("io.github.microutils:kotlin-logging-jvm:3.0.2") + + api(project(":jadx-plugins:jadx-plugins-api")) + api(project(":jadx-core")) // TODO: workaround + + runtimeOnly(project(":jadx-plugins:jadx-dex-input")) + runtimeOnly(project(":jadx-plugins:jadx-smali-input")) + runtimeOnly(project(":jadx-plugins:jadx-java-convert")) + runtimeOnly(project(":jadx-plugins:jadx-java-input")) + runtimeOnly(project(":jadx-plugins:jadx-raung-input")) + +} diff --git a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/debug.kt b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/debug.kt new file mode 100644 index 00000000..5a0cce6e --- /dev/null +++ b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/debug.kt @@ -0,0 +1,19 @@ +package jadx.plugins.script.runtime.data + +import jadx.api.core.nodes.IMethodNode +import jadx.core.dex.nodes.MethodNode +import jadx.core.dex.visitors.DotGraphVisitor +import jadx.core.utils.DebugUtils +import jadx.plugins.script.runtime.JadxScriptInstance +import java.io.File + +class Debug(private val jadx: JadxScriptInstance) { + + fun printMethodRegions(mth: IMethodNode, printInsns: Boolean = false) { + DebugUtils.printRegions(mth as MethodNode, printInsns) + } + + fun saveCFG(mth: IMethodNode, file: File = File("dump-mth-raw")) { + DotGraphVisitor.dumpRaw().save(file, mth as MethodNode) + } +} diff --git a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/decompile.kt b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/decompile.kt new file mode 100644 index 00000000..323cce28 --- /dev/null +++ b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/decompile.kt @@ -0,0 +1,24 @@ +package jadx.plugins.script.runtime.data + +import jadx.api.JadxArgs +import jadx.api.JavaClass +import jadx.plugins.script.runtime.JadxScriptInstance +import java.util.concurrent.Executors + +class Decompile(private val jadx: JadxScriptInstance) { + + fun all() { + jadx.classes.forEach(JavaClass::decompile) + } + + fun allThreaded(threadsCount: Int = JadxArgs.DEFAULT_THREADS_COUNT) { + val executor = Executors.newFixedThreadPool(threadsCount) + val dec = jadx.internalDecompiler + val batches = dec.decompileScheduler.buildBatches(jadx.classes) + for (batch in batches) { + executor.submit { + batch.forEach(JavaClass::decompile) + } + } + } +} diff --git a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/gui.kt b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/gui.kt new file mode 100644 index 00000000..9a8c4978 --- /dev/null +++ b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/gui.kt @@ -0,0 +1,24 @@ +package jadx.plugins.script.runtime.data + +import jadx.api.plugins.gui.JadxGuiContext +import jadx.plugins.script.runtime.JadxScriptInstance + +class Gui( + private val jadx: JadxScriptInstance, + private val guiContext: JadxGuiContext? +) { + + fun isAvailable() = guiContext != null + + fun ifAvailable(block: Gui.() -> Unit) { + guiContext?.let { this.apply(block) } + } + + fun ui(block: () -> Unit) { + guiContext?.uiRun(block) + } + + fun addMenuAction(name: String, action: () -> Unit) { + guiContext?.addMenuAction(name, action) + } +} diff --git a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/rename.kt b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/rename.kt new file mode 100644 index 00000000..5816e7eb --- /dev/null +++ b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/rename.kt @@ -0,0 +1,39 @@ +package jadx.plugins.script.runtime.data + +import jadx.api.core.nodes.IRootNode +import jadx.core.dex.nodes.IDexNode +import jadx.core.dex.nodes.RootNode +import jadx.plugins.script.runtime.JadxScriptInstance + +class RenamePass(private val jadx: JadxScriptInstance) { + + fun all(makeNewName: (String) -> String?) { + all { name, _ -> makeNewName.invoke(name) } + } + + fun all(makeNewName: (String, IDexNode) -> String?) { + jadx.addPass(object : ScriptPreparePass(jadx, "RenameAll") { + override fun init(root: IRootNode) { + val rootNode = root as RootNode + for (cls in rootNode.classes) { + makeNewName.invoke(cls.classInfo.shortName, cls)?.let { + cls.classInfo.changeShortName(it) + } + for (mth in cls.methods) { + if (mth.isConstructor) { + continue + } + makeNewName.invoke(mth.name, mth)?.let { + mth.rename(it) + } + } + for (fld in cls.fields) { + makeNewName.invoke(fld.name, fld)?.let { + fld.fieldInfo.alias = it + } + } + } + } + }) + } +} diff --git a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/replace.kt b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/replace.kt new file mode 100644 index 00000000..774bb008 --- /dev/null +++ b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/replace.kt @@ -0,0 +1,43 @@ +package jadx.plugins.script.runtime.data + +import jadx.core.dex.instructions.args.InsnArg +import jadx.core.dex.instructions.args.InsnWrapArg +import jadx.core.dex.nodes.InsnNode +import jadx.core.dex.nodes.MethodNode +import jadx.core.utils.InsnRemover +import jadx.plugins.script.runtime.JadxScriptInstance + +class Replace(private val jadx: JadxScriptInstance) { + + fun insns(replace: (MethodNode, InsnNode) -> InsnNode?) { + jadx.stages.mthBlocks { mth, blocks -> + for (block in blocks) { + val insns = block.instructions + for ((i, insn) in insns.withIndex()) { + replaceSubInsns(mth, insn, replace) + replace.invoke(mth, insn)?.let { + insns[i] = it + } + } + } + } + } + + private fun replaceSubInsns(mth: MethodNode, insn: InsnNode, replace: (MethodNode, InsnNode) -> InsnNode?) { + val argsCount = insn.argsCount + if (argsCount == 0) { + return + } + for (i in 0 until argsCount) { + val arg = insn.getArg(i) + if (arg is InsnWrapArg) { + val wrapInsn = arg.wrapInsn + replaceSubInsns(mth, wrapInsn, replace) + replace.invoke(mth, wrapInsn)?.let { + InsnRemover.unbindArgUsage(mth, arg) + insn.setArg(i, InsnArg.wrapInsnIntoArg(it)) + } + } + } + } +} diff --git a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/search.kt b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/search.kt new file mode 100644 index 00000000..f8404bd7 --- /dev/null +++ b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/search.kt @@ -0,0 +1,16 @@ +package jadx.plugins.script.runtime.data + +import jadx.core.dex.nodes.ClassNode +import jadx.plugins.script.runtime.JadxScriptInstance + +class Search(private val jadx: JadxScriptInstance) { + private val dec = jadx.internalDecompiler + + fun classByFullName(fullName: String): ClassNode? { + return dec.searchClassNodeByOrigFullName(fullName) + } + + fun classesByShortName(fullName: String): List { + return dec.root.searchClassByShortName(fullName) + } +} diff --git a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/stages.kt b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/stages.kt new file mode 100644 index 00000000..850a75ec --- /dev/null +++ b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/stages.kt @@ -0,0 +1,63 @@ +package jadx.plugins.script.runtime.data + +import jadx.api.core.nodes.IMethodNode +import jadx.core.dex.nodes.BlockNode +import jadx.core.dex.nodes.InsnNode +import jadx.core.dex.nodes.MethodNode +import jadx.core.dex.regions.Region +import jadx.plugins.script.runtime.JadxScriptInstance + +class Stages(private val jadx: JadxScriptInstance) { + + fun rawInsns(block: (MethodNode, Array) -> Unit) { + jadx.addPass(object : ScriptOrderedDecompilePass( + jadx, + "StageRawInsns", + runAfter = listOf("start") + ) { + override fun visit(mth: IMethodNode) { + val mthNode = mth as MethodNode + mthNode.instructions?.let { + block.invoke(mthNode, it) + } + } + }) + } + + fun mthEarlyBlocks(block: (MethodNode, List) -> Unit) { + mthBlocks(beforePass = "SSATransform", block) + } + + fun mthBlocks( + beforePass: String = "RegionMakerVisitor", + block: (MethodNode, List) -> Unit + ) { + jadx.addPass(object : ScriptOrderedDecompilePass( + jadx, + "StageMthBlocks", + runBefore = listOf(beforePass) + ) { + override fun visit(mth: IMethodNode) { + val mthNode = mth as MethodNode + mthNode.basicBlocks?.let { + block.invoke(mthNode, it) + } + } + }) + } + + fun mthRegions(block: (MethodNode, Region) -> Unit) { + jadx.addPass(object : ScriptOrderedDecompilePass( + jadx, + "StageMthRegions", + runBefore = listOf("PrepareForCodeGen") + ) { + override fun visit(mth: IMethodNode) { + val mthNode = mth as MethodNode + mthNode.region?.let { + block.invoke(mthNode, it) + } + } + }) + } +} diff --git a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/wrappers.kt b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/wrappers.kt new file mode 100644 index 00000000..4f6b990d --- /dev/null +++ b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/wrappers.kt @@ -0,0 +1,69 @@ +package jadx.plugins.script.runtime.data + +import jadx.api.core.nodes.IClassNode +import jadx.api.core.nodes.IMethodNode +import jadx.api.core.nodes.IRootNode +import jadx.api.plugins.pass.JadxPass +import jadx.api.plugins.pass.impl.OrderedJadxPassInfo +import jadx.api.plugins.pass.impl.SimpleJadxPassInfo +import jadx.api.plugins.pass.types.JadxDecompilePass +import jadx.api.plugins.pass.types.JadxPreparePass +import jadx.plugins.script.runtime.JadxScriptInstance + +private fun buildScriptName(jadx: JadxScriptInstance, name: String) = "JadxScript${name}(${jadx.scriptName})" + +private fun buildSimplePassInfo(jadx: JadxScriptInstance, name: String) = + SimpleJadxPassInfo(buildScriptName(jadx, name)) + +abstract class ScriptPreparePass( + private val jadx: JadxScriptInstance, private val name: String +) : JadxPreparePass { + override fun getInfo() = buildSimplePassInfo(jadx, name) +} + +abstract class ScriptDecompilePass( + private val jadx: JadxScriptInstance, private val name: String +) : JadxDecompilePass { + override fun getInfo() = buildSimplePassInfo(jadx, name) + + override fun init(root: IRootNode) { + } + + override fun visit(cls: IClassNode): Boolean { + return true + } + + override fun visit(mth: IMethodNode) { + } +} + +abstract class ScriptOrderedPass( + private val jadx: JadxScriptInstance, + private val name: String, + private val runAfter: List = listOf(), + private val runBefore: List = listOf() +) : JadxPass { + override fun getInfo(): OrderedJadxPassInfo { + val scriptName = buildScriptName(jadx, name) + return OrderedJadxPassInfo(scriptName, scriptName, runAfter, runBefore) + } +} + +abstract class ScriptOrderedPreparePass( + jadx: JadxScriptInstance, name: String, runAfter: List = listOf(), runBefore: List = listOf() +) : ScriptOrderedPass(jadx, name, runAfter, runBefore), JadxPreparePass {} + +abstract class ScriptOrderedDecompilePass( + jadx: JadxScriptInstance, name: String, runAfter: List = listOf(), runBefore: List = listOf() +) : ScriptOrderedPass(jadx, name, runAfter, runBefore), JadxDecompilePass { + + override fun init(root: IRootNode) { + } + + override fun visit(cls: IClassNode): Boolean { + return true + } + + override fun visit(mth: IMethodNode) { + } +} diff --git a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/runtime.kt b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/runtime.kt new file mode 100644 index 00000000..56202f10 --- /dev/null +++ b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/runtime.kt @@ -0,0 +1,75 @@ +@file:Suppress("unused", "MemberVisibilityCanBePrivate") + +package jadx.plugins.script.runtime + +import jadx.api.JadxArgs +import jadx.api.JadxDecompiler +import jadx.api.JavaClass +import jadx.api.plugins.JadxPluginContext +import jadx.api.plugins.pass.JadxPass +import jadx.plugins.script.runtime.data.* +import mu.KLogger +import mu.KotlinLogging +import java.io.File + + +open class JadxScriptBaseClass(private val scriptData: JadxScriptData) { + val scriptName = scriptData.scriptName + val log = KotlinLogging.logger("JadxScript:${scriptName}") + + fun getJadxInstance() = JadxScriptInstance(scriptData, log) + + fun println(message: Any?) { + log.info(message?.toString()) + } + + fun print(message: Any?) { + log.info(message?.toString()) + } +} + +class JadxScriptData( + val jadxInstance: JadxDecompiler, + val pluginContext: JadxPluginContext, + val scriptFile: File +) { + val afterLoad: MutableList<() -> Unit> = ArrayList() + + val scriptName get() = scriptFile.name.removeSuffix(".jadx.kts") +} + +class JadxScriptInstance( + private val scriptData: JadxScriptData, + val log: KLogger +) { + private val decompiler = scriptData.jadxInstance + + val rename: RenamePass by lazy { RenamePass(this) } + val stages: Stages by lazy { Stages(this) } + val replace: Replace by lazy { Replace(this) } + val decompile: Decompile by lazy { Decompile(this) } + val search: Search by lazy { Search(this) } + val gui: Gui by lazy { Gui(this, scriptData.pluginContext.guiContext) } + val debug: Debug by lazy { Debug(this) } + + val args: JadxArgs + get() = decompiler.args + + val classes: List + get() = decompiler.classes + + val scriptFile get() = scriptData.scriptFile + + val scriptName get() = scriptData.scriptName + + fun afterLoad(block: () -> Unit) { + scriptData.afterLoad.add(block) + } + + fun addPass(pass: JadxPass) { + scriptData.pluginContext.passContext.addPass(pass) + } + + val internalDecompiler: JadxDecompiler + get() = decompiler +} diff --git a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/script.kt b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/script.kt new file mode 100644 index 00000000..cdf68ce3 --- /dev/null +++ b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/script.kt @@ -0,0 +1,46 @@ +package jadx.plugins.script.runtime + +import kotlinx.coroutines.runBlocking +import kotlin.script.experimental.annotations.KotlinScript +import kotlin.script.experimental.api.* +import kotlin.script.experimental.dependencies.* +import kotlin.script.experimental.dependencies.maven.MavenDependenciesResolver +import kotlin.script.experimental.jvm.JvmDependency +import kotlin.script.experimental.jvm.dependenciesFromCurrentContext +import kotlin.script.experimental.jvm.jvm + +@KotlinScript( + fileExtension = "jadx.kts", + compilationConfiguration = JadxScriptConfiguration::class +) +abstract class JadxScript + +object JadxScriptConfiguration : ScriptCompilationConfiguration({ + defaultImports(DependsOn::class, Repository::class) + + jvm { + dependenciesFromCurrentContext( + wholeClasspath = true + ) + } + + baseClass(JadxScriptBaseClass::class) + + refineConfiguration { + onAnnotations(DependsOn::class, Repository::class, handler = ::configureMavenDepsOnAnnotations) + } +}) + +private val resolver = CompoundDependenciesResolver(FileSystemDependenciesResolver(), MavenDependenciesResolver()) + +fun configureMavenDepsOnAnnotations(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics { + val annotations = context.collectedData?.get(ScriptCollectedData.collectedAnnotations) + ?.takeIf { it.isNotEmpty() } + ?: return context.compilationConfiguration.asSuccess() + return runBlocking { resolver.resolveFromScriptSourceAnnotations(annotations) } + .onSuccess { + context.compilationConfiguration.with { + dependencies.append(JvmDependency(it)) + }.asSuccess() + } +} diff --git a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/utils.kt b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/utils.kt new file mode 100644 index 00000000..ed8c9bfb --- /dev/null +++ b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/utils.kt @@ -0,0 +1,10 @@ +/** + * Utils for use in scripts. + * Located in default package to reduce imports. + */ + +import java.io.File + +fun String.asFile(): File = File(this) + +fun Int.hex(): String = Integer.toHexString(this) diff --git a/jadx-plugins/jadx-script/readme.md b/jadx-plugins/jadx-script/readme.md new file mode 100644 index 00000000..bb10561d --- /dev/null +++ b/jadx-plugins/jadx-script/readme.md @@ -0,0 +1,29 @@ +## JADX scripting support + +NOTE: work still in progress! + +### Examples + +Check script examples in `examples/scripts/` (start with `hello`) + +### Script usage + +#### In jadx-cli + +Just add script file as input + +#### In jadx-gui + +1. Add script file to the project +2. Script will appear in `Inputs/Scripts` section +3. After script change you need to reload project (`Reload` button in toolbar or `F5`) +4. You can enable `Live reload` option in `File` menu to reload project automatically on scripts change + +### Script development + +Jadx-gui for now don't support autocompletion, errors highlighting, docs and code navigation +so best approach for script editing is to open jadx project in IntelliJ Idea and write your script in `examples/scripts/` folder. +Also, this will allow to debug your scripts: for that you need to create run configuration for jadx-cli or jadx-gui +add breakpoints and next run it in debug mode (jadx-gui is preferred because of faster script reload). + +Script logs and compilation errors will appear in `Log viewer` (separate script log will be added later) diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 1187a16e..00000000 --- a/settings.gradle +++ /dev/null @@ -1,12 +0,0 @@ -rootProject.name = 'jadx' - -include 'jadx-core' -include 'jadx-cli' -include 'jadx-gui' -include 'jadx-plugins' -include 'jadx-plugins:jadx-plugins-api' -include 'jadx-plugins:jadx-dex-input' -include 'jadx-plugins:jadx-java-input' -include 'jadx-plugins:jadx-raung-input' -include 'jadx-plugins:jadx-smali-input' -include 'jadx-plugins:jadx-java-convert' diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000..671852d3 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,17 @@ +rootProject.name = "jadx" + +include("jadx-core") +include("jadx-cli") +include("jadx-gui") + +include("jadx-plugins") +include("jadx-plugins:jadx-plugins-api") +include("jadx-plugins:jadx-dex-input") +include("jadx-plugins:jadx-java-input") +include("jadx-plugins:jadx-raung-input") +include("jadx-plugins:jadx-smali-input") +include("jadx-plugins:jadx-java-convert") + +include("jadx-plugins:jadx-script:jadx-script-plugin") +include("jadx-plugins:jadx-script:jadx-script-runtime") +include("jadx-plugins:jadx-script:examples")