feat: add base scripting support

This commit is contained in:
Skylot 2022-07-09 17:57:44 +01:00
parent fdf170529f
commit e5e64365fc
No known key found for this signature in database
GPG Key ID: 1E23F5B52567AA39
64 changed files with 1488 additions and 53 deletions

View File

@ -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) {

View File

@ -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)

View File

@ -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'

View File

@ -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;
* </code>
* </pre>
*/
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<ILoadResult> customLoads = new ArrayList<>();
private final Map<JadxPassType, List<JadxPass>> 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<ILoadResult> 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<String, String> 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<JadxPlugin> 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<String> 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<JadxPass> 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<ILoadResult> 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();

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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<ClassNode> {
public class ClassNode extends NotificationAttrNode implements IClassNode, ILoadable, ICodeNode, Comparable<ClassNode> {
private final RootNode root;
private final IClassData clsData;

View File

@ -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<MethodNode> {
public class MethodNode extends NotificationAttrNode implements IMethodNode,
IMethodDetails, ILoadable, ICodeNode, Comparable<MethodNode> {
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) {

View File

@ -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<JadxPassType, List<JadxPass>> 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;
}

View File

@ -460,4 +460,9 @@ public class OverrideMethodVisitor extends AbstractVisitor {
k++;
}
}
@Override
public String toString() {
return "OverrideMethodVisitor";
}
}

View File

@ -360,4 +360,9 @@ public class ProcessAnonymous extends AbstractVisitor {
}
return null;
}
@Override
public String toString() {
return "ProcessAnonymous";
}
}

View File

@ -71,4 +71,9 @@ public class ProcessMethodsForInline extends AbstractVisitor {
}
}
}
@Override
public String toString() {
return "ProcessMethodsForInline";
}
}

View File

@ -276,4 +276,9 @@ public class SignatureProcessor extends AbstractVisitor {
}
return validateInnerType(innerType);
}
@Override
public String toString() {
return "SignatureProcessor";
}
}

View File

@ -266,4 +266,9 @@ public class RenameVisitor extends AbstractVisitor {
}
return pkg.substring(0, dotPos);
}
@Override
public String toString() {
return "RenameVisitor";
}
}

View File

@ -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<ICodeRename> renames) {
List<ClassNode> classes = root.getClasses(false);

View File

@ -145,4 +145,9 @@ public class UsageInfoVisitor extends AbstractVisitor {
mergeIntoMth.setUseIn(mergedUsage);
sourceMth.setUseIn(Collections.emptyList());
}
@Override
public String toString() {
return "UsageInfoVisitor";
}
}

View File

@ -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<IDexTreeVisitor> passes, List<JadxPass> customPasses, Function<JadxPass, IDexTreeVisitor> 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<IDexTreeVisitor> passes, JadxPassInfo info) {
List<String> runAfter = info.runAfter();
List<String> 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<String, Integer> 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<String, Integer> buildNamesMap(List<IDexTreeVisitor> passes) {
int size = passes.size();
Map<String, Integer> 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();
}
}

View File

@ -0,0 +1,4 @@
package jadx.api.core.nodes;
public interface IClassNode {
}

View File

@ -0,0 +1,4 @@
package jadx.api.core.nodes;
public interface IJadxDecompiler {
}

View File

@ -0,0 +1,4 @@
package jadx.api.core.nodes;
public interface IMethodNode {
}

View File

@ -0,0 +1,4 @@
package jadx.api.core.nodes;
public interface IRootNode {
}

View File

@ -2,4 +2,8 @@ package jadx.api.plugins;
public interface JadxPlugin {
JadxPluginInfo getPluginInfo();
default void init(JadxPluginContext context) {
// default to no-op
}
}

View File

@ -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();
}

View File

@ -42,7 +42,6 @@ public class JadxPluginManager {
ServiceLoader<JadxPlugin> 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<JadxInputPlugin> getInputPlugins() {
return resolvedPlugins.stream()
.filter(JadxInputPlugin.class::isInstance)
.map(JadxInputPlugin.class::cast)
.collect(Collectors.toList());
return getPluginsWithType(JadxInputPlugin.class);
}
public List<JadxPluginOptions> getPluginsWithOptions() {
return getPluginsWithType(JadxPluginOptions.class);
}
@SuppressWarnings("unchecked")
public <T extends JadxPlugin> List<T> getPluginsWithType(Class<T> 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());
}

View File

@ -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);
}

View File

@ -0,0 +1,9 @@
package jadx.api.plugins.pass;
import jadx.api.plugins.pass.types.JadxPassType;
public interface JadxPass {
JadxPassInfo getInfo();
JadxPassType getPassType();
}

View File

@ -0,0 +1,6 @@
package jadx.api.plugins.pass;
public interface JadxPassContext {
void addPass(JadxPass pass);
}

View File

@ -0,0 +1,14 @@
package jadx.api.plugins.pass;
import java.util.List;
public interface JadxPassInfo {
String getName();
String getDescription();
List<String> runAfter();
List<String> runBefore();
}

View File

@ -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<String> runAfter;
private final List<String> 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<String> runAfter, List<String> 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<String> runAfter() {
return runAfter;
}
@Override
public List<String> runBefore() {
return runBefore;
}
}

View File

@ -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<String> runAfter() {
return Collections.emptyList();
}
@Override
public List<String> runBefore() {
return Collections.emptyList();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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<? extends JadxPass> 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 + '}';
}
}

View File

@ -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;
}
}

View File

@ -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")
}
}

View File

@ -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<String> = [])
@Target(AnnotationTarget.FILE)
@Repeatable
@Retention(AnnotationRetention.SOURCE)
annotation class Repository(vararg val repositoriesCoordinates: String, val options: Array<String> = [])

View File

@ -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")
}

View File

@ -0,0 +1,9 @@
// customize jadx-gui
val jadx = getJadxInstance()
jadx.gui.ifAvailable {
addMenuAction("Decompile All") {
jadx.decompile.all()
}
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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")
}

View File

@ -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))
}
}

View File

@ -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}" }
}
}
}
}

View File

@ -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<EvaluationResult> {
val compilationConf = createJvmCompilationConfigurationFromTemplate<JadxScript>()
val evalConf = createJvmEvaluationConfigurationFromTemplate<JadxScript> {
constructorArgs(scriptData)
}
return BasicJvmScriptingHost().eval(scriptFile.toScriptSource(), compilationConf, evalConf)
}
private fun processEvalResult(res: ResultWithDiagnostics<EvaluationResult>, 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) }
}
}
}
}
}

View File

@ -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<ScriptStateData> = ArrayList()
fun add(scriptFile: File, scriptData: JadxScriptData) {
data.add(ScriptStateData(scriptFile, scriptData))
}
fun getScripts() = data
}

View File

@ -0,0 +1 @@
jadx.plugins.script.JadxScriptPlugin

View File

@ -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"))
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}

View File

@ -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
}
}
}
}
})
}
}

View File

@ -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))
}
}
}
}
}

View File

@ -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<ClassNode> {
return dec.root.searchClassByShortName(fullName)
}
}

View File

@ -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<InsnNode?>) -> 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<BlockNode>) -> Unit) {
mthBlocks(beforePass = "SSATransform", block)
}
fun mthBlocks(
beforePass: String = "RegionMakerVisitor",
block: (MethodNode, List<BlockNode>) -> 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)
}
}
})
}
}

View File

@ -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<String> = listOf(),
private val runBefore: List<String> = 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<String> = listOf(), runBefore: List<String> = listOf()
) : ScriptOrderedPass(jadx, name, runAfter, runBefore), JadxPreparePass {}
abstract class ScriptOrderedDecompilePass(
jadx: JadxScriptInstance, name: String, runAfter: List<String> = listOf(), runBefore: List<String> = 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) {
}
}

View File

@ -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<JavaClass>
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
}

View File

@ -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<ScriptCompilationConfiguration> {
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()
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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'

17
settings.gradle.kts Normal file
View File

@ -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")