fix: improve plugins data handling
This commit is contained in:
@@ -2,7 +2,6 @@ package jadx.api;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -636,8 +635,8 @@ public class JadxArgs implements Closeable {
|
||||
+ insertDebugLines + extractFinally
|
||||
+ debugInfo + useSourceNameAsClassAlias + escapeUnicode + replaceConsts
|
||||
+ respectBytecodeAccModifiers + fsCaseSensitive + renameFlags
|
||||
+ commentsLevel + useDxInput + pluginOptions;
|
||||
return FileUtils.md5Sum(argStr.getBytes(StandardCharsets.US_ASCII));
|
||||
+ commentsLevel + useDxInput;
|
||||
return FileUtils.md5Sum(argStr);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -23,14 +23,12 @@ import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.impl.plugins.PluginsContext;
|
||||
import jadx.api.metadata.ICodeAnnotation;
|
||||
import jadx.api.metadata.ICodeNodeRef;
|
||||
import jadx.api.metadata.annotations.NodeDeclareRef;
|
||||
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.input.ICodeLoader;
|
||||
import jadx.api.plugins.input.JadxCodeInput;
|
||||
import jadx.api.plugins.pass.JadxPass;
|
||||
@@ -45,6 +43,7 @@ import jadx.core.dex.nodes.PackageNode;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.dex.visitors.SaveCode;
|
||||
import jadx.core.export.ExportGradleProject;
|
||||
import jadx.core.plugins.JadxPluginManager;
|
||||
import jadx.core.utils.DecompilerScheduler;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
@@ -85,7 +84,7 @@ public final class JadxDecompiler implements Closeable {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JadxDecompiler.class);
|
||||
|
||||
private final JadxArgs args;
|
||||
private final JadxPluginManager pluginManager = new JadxPluginManager();
|
||||
private final JadxPluginManager pluginManager = new JadxPluginManager(this);
|
||||
private final List<ICodeLoader> loadedInputs = new ArrayList<>();
|
||||
|
||||
private RootNode root;
|
||||
@@ -97,7 +96,6 @@ public final class JadxDecompiler implements Closeable {
|
||||
|
||||
private final IDecompileScheduler decompileScheduler = new DecompilerScheduler();
|
||||
|
||||
private final PluginsContext pluginsContext = new PluginsContext(this);
|
||||
private final List<ICodeLoader> customCodeLoaders = new ArrayList<>();
|
||||
private final Map<JadxPassType, List<JadxPass>> customPasses = new HashMap<>();
|
||||
|
||||
@@ -144,7 +142,7 @@ public final class JadxDecompiler implements Closeable {
|
||||
List<Path> inputPaths = Utils.collectionMap(args.getInputFiles(), File::toPath);
|
||||
List<Path> inputFiles = FileUtils.expandDirs(inputPaths);
|
||||
long start = System.currentTimeMillis();
|
||||
for (JadxCodeInput codeLoader : pluginsContext.getCodeInputs()) {
|
||||
for (JadxCodeInput codeLoader : pluginManager.getCodeInputs()) {
|
||||
ICodeLoader loader = codeLoader.loadFiles(inputFiles);
|
||||
if (loader != null && !loader.isEmpty()) {
|
||||
loadedInputs.add(loader);
|
||||
@@ -186,10 +184,9 @@ public final class JadxDecompiler implements Closeable {
|
||||
pluginManager.providesSuggestion("java-input", args.isUseDxInput() ? "java-convert" : "java-input");
|
||||
pluginManager.load();
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Resolved plugins: {}", Utils.collectionMap(pluginManager.getResolvedPlugins(),
|
||||
p -> p.getPluginInfo().getPluginId()));
|
||||
LOG.debug("Resolved plugins: {}", pluginManager.getResolvedPluginContexts());
|
||||
}
|
||||
pluginManager.initResolved(pluginsContext);
|
||||
pluginManager.initResolved();
|
||||
if (LOG.isDebugEnabled()) {
|
||||
List<String> passes = customPasses.values().stream().flatMap(Collection::stream)
|
||||
.map(p -> p.getInfo().getName()).collect(Collectors.toList());
|
||||
@@ -673,10 +670,6 @@ public final class JadxDecompiler implements Closeable {
|
||||
customPasses.computeIfAbsent(pass.getPassType(), l -> new ArrayList<>()).add(pass);
|
||||
}
|
||||
|
||||
public PluginsContext getPluginsContext() {
|
||||
return pluginsContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "jadx decompiler " + getVersion();
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
package jadx.api.impl.plugins;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.JadxDecompiler;
|
||||
import jadx.api.plugins.JadxPlugin;
|
||||
import jadx.api.plugins.JadxPluginContext;
|
||||
import jadx.api.plugins.gui.JadxGuiContext;
|
||||
import jadx.api.plugins.input.JadxCodeInput;
|
||||
import jadx.api.plugins.options.JadxPluginOptions;
|
||||
import jadx.api.plugins.pass.JadxPass;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
|
||||
public class PluginsContext implements JadxPluginContext {
|
||||
|
||||
private final JadxDecompiler decompiler;
|
||||
private final List<JadxCodeInput> codeInputs = new ArrayList<>();
|
||||
private final Map<JadxPlugin, JadxPluginOptions> optionsMap = new IdentityHashMap<>();
|
||||
private @Nullable JadxGuiContext guiContext;
|
||||
|
||||
private @Nullable JadxPlugin currentPlugin;
|
||||
|
||||
public PluginsContext(JadxDecompiler decompiler) {
|
||||
this.decompiler = decompiler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JadxArgs getArgs() {
|
||||
return decompiler.getArgs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public JadxDecompiler getDecompiler() {
|
||||
return decompiler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPass(JadxPass pass) {
|
||||
decompiler.addCustomPass(pass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCodeInput(JadxCodeInput codeInput) {
|
||||
codeInputs.add(codeInput);
|
||||
}
|
||||
|
||||
public List<JadxCodeInput> getCodeInputs() {
|
||||
return codeInputs;
|
||||
}
|
||||
|
||||
public void setCurrentPlugin(@Nullable JadxPlugin currentPlugin) {
|
||||
this.currentPlugin = currentPlugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerOptions(JadxPluginOptions options) {
|
||||
Objects.requireNonNull(currentPlugin);
|
||||
try {
|
||||
options.setOptions(decompiler.getArgs().getPluginOptions());
|
||||
optionsMap.put(currentPlugin, options);
|
||||
} catch (Exception e) {
|
||||
String pluginId = currentPlugin.getPluginInfo().getPluginId();
|
||||
throw new JadxRuntimeException("Failed to apply options for plugin: " + pluginId, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Map<JadxPlugin, JadxPluginOptions> getOptionsMap() {
|
||||
return optionsMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable JadxGuiContext getGuiContext() {
|
||||
return guiContext;
|
||||
}
|
||||
|
||||
public void setGuiContext(JadxGuiContext guiContext) {
|
||||
this.guiContext = guiContext;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package jadx.api.plugins;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.api.JadxArgs;
|
||||
@@ -21,6 +23,13 @@ public interface JadxPluginContext {
|
||||
|
||||
void registerOptions(JadxPluginOptions options);
|
||||
|
||||
/**
|
||||
* Function to calculate hash of all options which can change output code.
|
||||
* Hash for input files ({@link JadxArgs#getInputFiles()}) already calculated,
|
||||
* so this method can omit these files.
|
||||
*/
|
||||
void registerInputsHashSupplier(Supplier<String> supplier);
|
||||
|
||||
@Nullable
|
||||
JadxGuiContext getGuiContext();
|
||||
}
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
package jadx.api.plugins;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.impl.plugins.PluginsContext;
|
||||
import jadx.api.plugins.options.JadxPluginOptions;
|
||||
import jadx.api.plugins.options.OptionDescription;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
|
||||
public class JadxPluginManager {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JadxPluginManager.class);
|
||||
|
||||
private final Set<PluginData> allPlugins = new TreeSet<>();
|
||||
private final Map<String, String> provideSuggestions = new TreeMap<>();
|
||||
|
||||
private List<JadxPlugin> resolvedPlugins = Collections.emptyList();
|
||||
|
||||
public JadxPluginManager() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add suggestion how to resolve conflicting plugins
|
||||
*/
|
||||
public void providesSuggestion(String provides, String pluginId) {
|
||||
provideSuggestions.put(provides, pluginId);
|
||||
}
|
||||
|
||||
public void load() {
|
||||
allPlugins.clear();
|
||||
ServiceLoader<JadxPlugin> jadxPlugins = ServiceLoader.load(JadxPlugin.class);
|
||||
for (JadxPlugin plugin : jadxPlugins) {
|
||||
addPlugin(plugin);
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
|
||||
public void register(JadxPlugin plugin) {
|
||||
Objects.requireNonNull(plugin);
|
||||
PluginData addedPlugin = addPlugin(plugin);
|
||||
LOG.debug("Register plugin: {}", addedPlugin.getPluginId());
|
||||
resolve();
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
return pluginData;
|
||||
}
|
||||
|
||||
public boolean unload(String pluginId) {
|
||||
boolean result = allPlugins.removeIf(pd -> {
|
||||
String id = pd.getPluginId();
|
||||
boolean match = id.equals(pluginId);
|
||||
if (match) {
|
||||
LOG.debug("Unload plugin: {}", id);
|
||||
}
|
||||
return match;
|
||||
});
|
||||
resolve();
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<JadxPlugin> getAllPlugins() {
|
||||
if (allPlugins.isEmpty()) {
|
||||
load();
|
||||
}
|
||||
return allPlugins.stream().map(PluginData::getPlugin).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<JadxPlugin> getResolvedPlugins() {
|
||||
return Collections.unmodifiableList(resolvedPlugins);
|
||||
}
|
||||
|
||||
private synchronized void resolve() {
|
||||
Map<String, List<PluginData>> provides = allPlugins.stream()
|
||||
.collect(Collectors.groupingBy(p -> p.getInfo().getProvides()));
|
||||
List<PluginData> result = new ArrayList<>(provides.size());
|
||||
provides.forEach((provide, list) -> {
|
||||
if (list.size() == 1) {
|
||||
result.add(list.get(0));
|
||||
} else {
|
||||
String suggestion = provideSuggestions.get(provide);
|
||||
if (suggestion != null) {
|
||||
list.stream().filter(p -> p.getPluginId().equals(suggestion))
|
||||
.findFirst()
|
||||
.ifPresent(result::add);
|
||||
} else {
|
||||
PluginData selected = list.get(0);
|
||||
result.add(selected);
|
||||
LOG.debug("Select providing '{}' plugin '{}', candidates: {}", provide, selected, list);
|
||||
}
|
||||
}
|
||||
});
|
||||
Collections.sort(result);
|
||||
resolvedPlugins = result.stream().map(PluginData::getPlugin).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public void initAll(PluginsContext context) {
|
||||
init(context, getAllPlugins());
|
||||
}
|
||||
|
||||
public void initResolved(PluginsContext context) {
|
||||
init(context, resolvedPlugins);
|
||||
}
|
||||
|
||||
public void init(PluginsContext context, List<JadxPlugin> plugins) {
|
||||
for (JadxPlugin plugin : plugins) {
|
||||
try {
|
||||
context.setCurrentPlugin(plugin);
|
||||
plugin.init(context);
|
||||
context.setCurrentPlugin(null);
|
||||
} catch (Exception e) {
|
||||
String pluginId = plugin.getPluginInfo().getPluginId();
|
||||
throw new JadxRuntimeException("Failed to init plugin: " + pluginId, e);
|
||||
}
|
||||
}
|
||||
for (Map.Entry<JadxPlugin, JadxPluginOptions> entry : context.getOptionsMap().entrySet()) {
|
||||
verifyOptions(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyOptions(JadxPlugin plugin, JadxPluginOptions options) {
|
||||
String pluginId = plugin.getPluginInfo().getPluginId();
|
||||
List<OptionDescription> descriptions = options.getOptionsDescriptions();
|
||||
if (descriptions == null) {
|
||||
throw new IllegalArgumentException("Null option descriptions in plugin id: " + pluginId);
|
||||
}
|
||||
String prefix = pluginId + '.';
|
||||
descriptions.forEach(descObj -> {
|
||||
String optName = descObj.name();
|
||||
if (optName == null || !optName.startsWith(prefix)) {
|
||||
throw new IllegalArgumentException("Plugin option name should start with plugin id: '" + prefix + "', option: " + optName);
|
||||
}
|
||||
String desc = descObj.description();
|
||||
if (desc == null || desc.isEmpty()) {
|
||||
throw new IllegalArgumentException("Plugin option description not set, plugin: " + pluginId);
|
||||
}
|
||||
List<String> values = descObj.values();
|
||||
if (values == null) {
|
||||
throw new IllegalArgumentException("Plugin option values is null, option: " + optName + ", plugin: " + pluginId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static final class PluginData implements Comparable<PluginData> {
|
||||
private final JadxPlugin plugin;
|
||||
private final JadxPluginInfo info;
|
||||
|
||||
private PluginData(JadxPlugin plugin, JadxPluginInfo info) {
|
||||
this.plugin = plugin;
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
public JadxPlugin getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
public JadxPluginInfo getInfo() {
|
||||
return info;
|
||||
}
|
||||
|
||||
public String getPluginId() {
|
||||
return info.getPluginId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull JadxPluginManager.PluginData o) {
|
||||
return this.info.getPluginId().compareTo(o.info.getPluginId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof PluginData)) {
|
||||
return false;
|
||||
}
|
||||
PluginData that = (PluginData) o;
|
||||
return getInfo().getPluginId().equals(that.getInfo().getPluginId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return info.getPluginId().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return info.getPluginId();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package jadx.api.plugins.options;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -30,4 +32,13 @@ public interface OptionDescription {
|
||||
default OptionType getType() {
|
||||
return OptionType.STRING;
|
||||
}
|
||||
|
||||
enum OptionFlag {
|
||||
PER_PROJECT, // store in project settings instead global (for jadx-gui)
|
||||
HIDE_IN_GUI, // do not show this option in jadx-gui (useful if option is configured with custom ui)
|
||||
}
|
||||
|
||||
default Set<OptionFlag> getFlags() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package jadx.api.plugins.options.impl;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -8,11 +12,19 @@ import jadx.api.plugins.options.OptionDescription;
|
||||
|
||||
public class JadxOptionDescription implements OptionDescription {
|
||||
|
||||
public static JadxOptionDescription booleanOption(String name, String desc, boolean defaultValue) {
|
||||
return new JadxOptionDescription(name, desc,
|
||||
defaultValue ? "yes" : "no",
|
||||
Arrays.asList("yes", "no"),
|
||||
OptionType.BOOLEAN);
|
||||
}
|
||||
|
||||
private final String name;
|
||||
private final String desc;
|
||||
private final String defaultValue;
|
||||
private final List<String> values;
|
||||
private final OptionType type;
|
||||
private final Set<OptionFlag> flags = EnumSet.noneOf(OptionFlag.class);
|
||||
|
||||
public JadxOptionDescription(String name, String desc, @Nullable String defaultValue, List<String> values) {
|
||||
this(name, desc, defaultValue, values, OptionType.STRING);
|
||||
@@ -51,6 +63,21 @@ public class JadxOptionDescription implements OptionDescription {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<OptionFlag> getFlags() {
|
||||
return flags;
|
||||
}
|
||||
|
||||
public JadxOptionDescription withFlag(OptionFlag flag) {
|
||||
this.flags.add(flag);
|
||||
return this;
|
||||
}
|
||||
|
||||
public JadxOptionDescription withFlags(OptionFlag... flags) {
|
||||
Collections.addAll(this.flags, flags);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "OptionDescription{" + desc + ", values=" + values + '}';
|
||||
|
||||
@@ -4,7 +4,7 @@ import jadx.api.JadxDecompiler;
|
||||
import jadx.api.plugins.pass.JadxPass;
|
||||
|
||||
public interface JadxAfterLoadPass extends JadxPass {
|
||||
JadxPassType TYPE = new JadxPassType(JadxAfterLoadPass.class);
|
||||
JadxPassType TYPE = new JadxPassType("AfterLoadPass");
|
||||
|
||||
void init(JadxDecompiler decompiler);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
|
||||
public interface JadxDecompilePass extends JadxPass {
|
||||
JadxPassType TYPE = new JadxPassType(JadxDecompilePass.class);
|
||||
JadxPassType TYPE = new JadxPassType("DecompilePass");
|
||||
|
||||
void init(RootNode root);
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
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();
|
||||
public JadxPassType(String clsName) {
|
||||
this.cls = clsName;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -27,6 +25,6 @@ public class JadxPassType {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "JadxPassType{" + cls + '}';
|
||||
return cls;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import jadx.api.plugins.pass.JadxPass;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
|
||||
public interface JadxPreparePass extends JadxPass {
|
||||
JadxPassType TYPE = new JadxPassType(JadxPreparePass.class);
|
||||
JadxPassType TYPE = new JadxPassType("PreparePass");
|
||||
|
||||
void init(RootNode root);
|
||||
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
package jadx.core.plugins;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.JadxDecompiler;
|
||||
import jadx.api.plugins.JadxPlugin;
|
||||
import jadx.api.plugins.gui.JadxGuiContext;
|
||||
import jadx.api.plugins.input.JadxCodeInput;
|
||||
import jadx.api.plugins.options.JadxPluginOptions;
|
||||
import jadx.api.plugins.options.OptionDescription;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
|
||||
public class JadxPluginManager {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JadxPluginManager.class);
|
||||
|
||||
private final JadxDecompiler decompiler;
|
||||
private final SortedSet<PluginContext> allPlugins = new TreeSet<>();
|
||||
private final SortedSet<PluginContext> resolvedPlugins = new TreeSet<>();
|
||||
private final Map<String, String> provideSuggestions = new TreeMap<>();
|
||||
|
||||
private @Nullable JadxGuiContext guiContext;
|
||||
|
||||
public JadxPluginManager(JadxDecompiler decompiler) {
|
||||
this.decompiler = decompiler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add suggestion how to resolve conflicting plugins
|
||||
*/
|
||||
public void providesSuggestion(String provides, String pluginId) {
|
||||
provideSuggestions.put(provides, pluginId);
|
||||
}
|
||||
|
||||
public void load() {
|
||||
allPlugins.clear();
|
||||
ServiceLoader<JadxPlugin> jadxPlugins = ServiceLoader.load(JadxPlugin.class);
|
||||
for (JadxPlugin plugin : jadxPlugins) {
|
||||
addPlugin(plugin);
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
|
||||
public void register(JadxPlugin plugin) {
|
||||
Objects.requireNonNull(plugin);
|
||||
PluginContext addedPlugin = addPlugin(plugin);
|
||||
LOG.debug("Register plugin: {}", addedPlugin.getPluginId());
|
||||
resolve();
|
||||
}
|
||||
|
||||
private PluginContext addPlugin(JadxPlugin plugin) {
|
||||
PluginContext pluginContext = new PluginContext(decompiler, plugin);
|
||||
LOG.debug("Loading plugin: {}", pluginContext);
|
||||
if (!allPlugins.add(pluginContext)) {
|
||||
throw new IllegalArgumentException("Duplicate plugin id: " + pluginContext + ", class " + plugin.getClass());
|
||||
}
|
||||
pluginContext.setGuiContext(guiContext);
|
||||
return pluginContext;
|
||||
}
|
||||
|
||||
public boolean unload(String pluginId) {
|
||||
boolean result = allPlugins.removeIf(context -> {
|
||||
if (context.getPluginId().equals(pluginId)) {
|
||||
LOG.debug("Unload plugin: {}", pluginId);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
resolve();
|
||||
return result;
|
||||
}
|
||||
|
||||
public SortedSet<PluginContext> getAllPluginContexts() {
|
||||
return allPlugins;
|
||||
}
|
||||
|
||||
public SortedSet<PluginContext> getResolvedPluginContexts() {
|
||||
return resolvedPlugins;
|
||||
}
|
||||
|
||||
private synchronized void resolve() {
|
||||
Map<String, List<PluginContext>> provides = allPlugins.stream()
|
||||
.collect(Collectors.groupingBy(p -> p.getPluginInfo().getProvides()));
|
||||
List<PluginContext> resolved = new ArrayList<>(provides.size());
|
||||
provides.forEach((provide, list) -> {
|
||||
if (list.size() == 1) {
|
||||
resolved.add(list.get(0));
|
||||
} else {
|
||||
String suggestion = provideSuggestions.get(provide);
|
||||
if (suggestion != null) {
|
||||
list.stream().filter(p -> p.getPluginId().equals(suggestion))
|
||||
.findFirst()
|
||||
.ifPresent(resolved::add);
|
||||
} else {
|
||||
PluginContext selected = list.get(0);
|
||||
resolved.add(selected);
|
||||
LOG.debug("Select providing '{}' plugin '{}', candidates: {}", provide, selected, list);
|
||||
}
|
||||
}
|
||||
});
|
||||
resolvedPlugins.clear();
|
||||
resolvedPlugins.addAll(resolved);
|
||||
}
|
||||
|
||||
public void initAll() {
|
||||
init(allPlugins);
|
||||
}
|
||||
|
||||
public void initResolved() {
|
||||
init(resolvedPlugins);
|
||||
}
|
||||
|
||||
public void init(SortedSet<PluginContext> pluginContexts) {
|
||||
for (PluginContext context : pluginContexts) {
|
||||
try {
|
||||
context.init();
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Failed to init plugin: " + context.getPluginId(), e);
|
||||
}
|
||||
}
|
||||
for (PluginContext context : pluginContexts) {
|
||||
JadxPluginOptions options = context.getOptions();
|
||||
if (options != null) {
|
||||
verifyOptions(context, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyOptions(PluginContext pluginContext, JadxPluginOptions options) {
|
||||
String pluginId = pluginContext.getPluginId();
|
||||
List<OptionDescription> descriptions = options.getOptionsDescriptions();
|
||||
if (descriptions == null) {
|
||||
throw new IllegalArgumentException("Null option descriptions in plugin id: " + pluginId);
|
||||
}
|
||||
String prefix = pluginId + '.';
|
||||
descriptions.forEach(descObj -> {
|
||||
String optName = descObj.name();
|
||||
if (optName == null || !optName.startsWith(prefix)) {
|
||||
throw new IllegalArgumentException("Plugin option name should start with plugin id: '" + prefix + "', option: " + optName);
|
||||
}
|
||||
String desc = descObj.description();
|
||||
if (desc == null || desc.isEmpty()) {
|
||||
throw new IllegalArgumentException("Plugin option description not set, plugin: " + pluginId);
|
||||
}
|
||||
List<String> values = descObj.values();
|
||||
if (values == null) {
|
||||
throw new IllegalArgumentException("Plugin option values is null, option: " + optName + ", plugin: " + pluginId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public List<JadxCodeInput> getCodeInputs() {
|
||||
return getResolvedPluginContexts()
|
||||
.stream()
|
||||
.flatMap(p -> p.getCodeInputs().stream())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public void setGuiContext(JadxGuiContext guiContext) {
|
||||
this.guiContext = guiContext;
|
||||
for (PluginContext context : getAllPluginContexts()) {
|
||||
context.setGuiContext(guiContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package jadx.core.plugins;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.JadxDecompiler;
|
||||
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.input.JadxCodeInput;
|
||||
import jadx.api.plugins.options.JadxPluginOptions;
|
||||
import jadx.api.plugins.pass.JadxPass;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
|
||||
public class PluginContext implements JadxPluginContext, Comparable<PluginContext> {
|
||||
private final JadxDecompiler decompiler;
|
||||
private final JadxPlugin plugin;
|
||||
private final JadxPluginInfo pluginInfo;
|
||||
private @Nullable JadxGuiContext guiContext;
|
||||
|
||||
private final List<JadxCodeInput> codeInputs = new ArrayList<>();
|
||||
private @Nullable JadxPluginOptions options;
|
||||
private @Nullable Supplier<String> inputsHashSupplier;
|
||||
|
||||
private boolean initialized;
|
||||
|
||||
PluginContext(JadxDecompiler decompiler, JadxPlugin plugin) {
|
||||
this.decompiler = decompiler;
|
||||
this.plugin = plugin;
|
||||
this.pluginInfo = plugin.getPluginInfo();
|
||||
}
|
||||
|
||||
void init() {
|
||||
plugin.init(this);
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
public boolean isInitialized() {
|
||||
return initialized;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JadxArgs getArgs() {
|
||||
return decompiler.getArgs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public JadxDecompiler getDecompiler() {
|
||||
return decompiler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPass(JadxPass pass) {
|
||||
decompiler.addCustomPass(pass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCodeInput(JadxCodeInput codeInput) {
|
||||
this.codeInputs.add(codeInput);
|
||||
}
|
||||
|
||||
public List<JadxCodeInput> getCodeInputs() {
|
||||
return codeInputs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerOptions(JadxPluginOptions options) {
|
||||
try {
|
||||
this.options = Objects.requireNonNull(options);
|
||||
options.setOptions(getArgs().getPluginOptions());
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Failed to apply options for plugin: " + getPluginId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerInputsHashSupplier(Supplier<String> supplier) {
|
||||
this.inputsHashSupplier = supplier;
|
||||
}
|
||||
|
||||
public String getInputsHash() {
|
||||
if (inputsHashSupplier != null) {
|
||||
try {
|
||||
return inputsHashSupplier.get();
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Failed to get inputs hash for plugin: " + getPluginId(), e);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable JadxGuiContext getGuiContext() {
|
||||
return guiContext;
|
||||
}
|
||||
|
||||
public void setGuiContext(JadxGuiContext guiContext) {
|
||||
this.guiContext = guiContext;
|
||||
}
|
||||
|
||||
public JadxPlugin getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
public JadxPluginInfo getPluginInfo() {
|
||||
return pluginInfo;
|
||||
}
|
||||
|
||||
public String getPluginId() {
|
||||
return pluginInfo.getPluginId();
|
||||
}
|
||||
|
||||
public JadxPluginOptions getOptions() {
|
||||
return options;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof PluginContext)) {
|
||||
return false;
|
||||
}
|
||||
return this.getPluginId().equals(((PluginContext) other).getPluginId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getPluginId().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(PluginContext other) {
|
||||
return this.getPluginId().compareTo(other.getPluginId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getPluginId();
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,13 @@ public class Utils {
|
||||
return obj;
|
||||
}
|
||||
|
||||
public static String cutObject(String obj) {
|
||||
if (obj.charAt(0) == 'L') {
|
||||
return obj.substring(1, obj.length() - 1);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
public static String makeQualifiedObjectName(String obj) {
|
||||
return 'L' + obj.replace('.', '/') + ';';
|
||||
}
|
||||
|
||||
@@ -351,6 +351,10 @@ public class FileUtils {
|
||||
return paths.stream().map(Path::toFile).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static String md5Sum(String str) {
|
||||
return md5Sum(str.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public static String md5Sum(byte[] data) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
|
||||
Reference in New Issue
Block a user