fix: improve plugins data handling
This commit is contained in:
@@ -17,12 +17,13 @@ import com.beust.jcommander.ParameterDescription;
|
||||
import com.beust.jcommander.ParameterException;
|
||||
import com.beust.jcommander.Parameterized;
|
||||
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.JadxDecompiler;
|
||||
import jadx.api.impl.plugins.PluginsContext;
|
||||
import jadx.api.plugins.JadxPlugin;
|
||||
import jadx.api.plugins.JadxPluginInfo;
|
||||
import jadx.api.plugins.options.JadxPluginOptions;
|
||||
import jadx.api.plugins.options.OptionDescription;
|
||||
import jadx.core.plugins.JadxPluginManager;
|
||||
import jadx.core.plugins.PluginContext;
|
||||
import jadx.core.utils.Utils;
|
||||
|
||||
public class JCommanderWrapper<T> {
|
||||
@@ -175,12 +176,15 @@ public class JCommanderWrapper<T> {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int k = 1;
|
||||
// load and init all options plugins to print all options
|
||||
try (JadxDecompiler decompiler = new JadxDecompiler(argsObj.toJadxArgs())) {
|
||||
PluginsContext context = new PluginsContext(decompiler);
|
||||
decompiler.getPluginManager().initAll(context);
|
||||
for (Map.Entry<JadxPlugin, JadxPluginOptions> entry : context.getOptionsMap().entrySet()) {
|
||||
if (appendPlugin(entry.getKey(), entry.getValue(), sb, maxNamesLen, k)) {
|
||||
k++;
|
||||
try (JadxDecompiler decompiler = new JadxDecompiler(new JadxArgs())) {
|
||||
JadxPluginManager pluginManager = decompiler.getPluginManager();
|
||||
pluginManager.initAll();
|
||||
for (PluginContext context : pluginManager.getAllPluginContexts()) {
|
||||
JadxPluginOptions options = context.getOptions();
|
||||
if (options != null) {
|
||||
if (appendPlugin(context.getPluginInfo(), context.getOptions(), sb, maxNamesLen, k)) {
|
||||
k++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,12 +194,11 @@ public class JCommanderWrapper<T> {
|
||||
return "\nPlugin options (-P<name>=<value>):" + sb;
|
||||
}
|
||||
|
||||
private boolean appendPlugin(JadxPlugin plugin, JadxPluginOptions options, StringBuilder out, int maxNamesLen, int k) {
|
||||
private boolean appendPlugin(JadxPluginInfo pluginInfo, JadxPluginOptions options, StringBuilder out, int maxNamesLen, int k) {
|
||||
List<OptionDescription> descs = options.getOptionsDescriptions();
|
||||
if (descs.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
JadxPluginInfo pluginInfo = plugin.getPluginInfo();
|
||||
out.append("\n ").append(k).append(") ");
|
||||
out.append(pluginInfo.getPluginId()).append(": ").append(pluginInfo.getDescription());
|
||||
for (OptionDescription desc : descs) {
|
||||
|
||||
@@ -13,14 +13,13 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.JadxDecompiler;
|
||||
import jadx.api.impl.plugins.PluginsContext;
|
||||
import jadx.api.plugins.JadxPluginManager;
|
||||
import jadx.api.plugins.input.ICodeLoader;
|
||||
import jadx.api.plugins.input.JadxCodeInput;
|
||||
import jadx.core.clsp.ClsSet;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.dex.visitors.SignatureProcessor;
|
||||
import jadx.core.plugins.JadxPluginManager;
|
||||
|
||||
/**
|
||||
* Utility class for convert dex or jar to jadx classes set (.jcst)
|
||||
@@ -40,32 +39,34 @@ public class ConvertToClsSet {
|
||||
List<Path> inputPaths = Stream.of(args).map(Paths::get).collect(Collectors.toList());
|
||||
Path output = inputPaths.remove(0);
|
||||
|
||||
PluginsContext pluginsContext = new PluginsContext(new JadxDecompiler());
|
||||
JadxPluginManager pluginManager = new JadxPluginManager();
|
||||
pluginManager.load();
|
||||
pluginManager.initResolved(pluginsContext);
|
||||
List<ICodeLoader> loadedInputs = new ArrayList<>();
|
||||
for (JadxCodeInput inputPlugin : pluginsContext.getCodeInputs()) {
|
||||
loadedInputs.add(inputPlugin.loadFiles(inputPaths));
|
||||
}
|
||||
|
||||
JadxArgs jadxArgs = new JadxArgs();
|
||||
jadxArgs.setRenameFlags(EnumSet.noneOf(JadxArgs.RenameEnum.class));
|
||||
RootNode root = new RootNode(jadxArgs);
|
||||
root.loadClasses(loadedInputs);
|
||||
try (JadxDecompiler decompiler = new JadxDecompiler(jadxArgs)) {
|
||||
JadxPluginManager pluginManager = decompiler.getPluginManager();
|
||||
pluginManager.load();
|
||||
pluginManager.initResolved();
|
||||
List<ICodeLoader> loadedInputs = new ArrayList<>();
|
||||
for (JadxCodeInput inputPlugin : pluginManager.getCodeInputs()) {
|
||||
loadedInputs.add(inputPlugin.loadFiles(inputPaths));
|
||||
}
|
||||
RootNode root = decompiler.getRoot();
|
||||
root.loadClasses(loadedInputs);
|
||||
|
||||
// from pre-decompilation stage run only SignatureProcessor
|
||||
SignatureProcessor signatureProcessor = new SignatureProcessor();
|
||||
signatureProcessor.init(root);
|
||||
for (ClassNode classNode : root.getClasses()) {
|
||||
signatureProcessor.visit(classNode);
|
||||
// from pre-decompilation stage run only SignatureProcessor
|
||||
SignatureProcessor signatureProcessor = new SignatureProcessor();
|
||||
signatureProcessor.init(root);
|
||||
for (ClassNode classNode : root.getClasses()) {
|
||||
signatureProcessor.visit(classNode);
|
||||
}
|
||||
|
||||
ClsSet set = new ClsSet(root);
|
||||
set.loadFrom(root);
|
||||
set.save(output);
|
||||
|
||||
LOG.info("Output: {}", output);
|
||||
LOG.info("done");
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed with error", e);
|
||||
}
|
||||
|
||||
ClsSet set = new ClsSet(root);
|
||||
set.loadFrom(root);
|
||||
set.save(output);
|
||||
|
||||
LOG.info("Output: {}", output);
|
||||
LOG.info("done");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -138,7 +138,7 @@ public class JadxWrapper {
|
||||
|
||||
private void initGuiPluginsContext() {
|
||||
guiPluginsContext = new GuiPluginsContext(mainWindow);
|
||||
decompiler.getPluginsContext().setGuiContext(guiPluginsContext);
|
||||
decompiler.getPluginManager().setGuiContext(guiPluginsContext);
|
||||
}
|
||||
|
||||
public GuiPluginsContext getGuiPluginsContext() {
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
@@ -28,10 +29,11 @@ import org.slf4j.LoggerFactory;
|
||||
import jadx.api.ICodeCache;
|
||||
import jadx.api.ICodeInfo;
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.args.UserRenamesMappingsMode;
|
||||
import jadx.api.JadxDecompiler;
|
||||
import jadx.core.Jadx;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.plugins.PluginContext;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
@@ -64,7 +66,7 @@ public class DiskCodeCache implements ICodeCache {
|
||||
codeVersionFile = baseDir.resolve("code-version");
|
||||
namesMapFile = baseDir.resolve("names-map");
|
||||
JadxArgs args = root.getArgs();
|
||||
codeVersion = buildCodeVersion(args);
|
||||
codeVersion = buildCodeVersion(args, root.getDecompiler());
|
||||
writePool = Executors.newFixedThreadPool(args.getThreadsCount());
|
||||
codeMetadataAdapter = new CodeMetadataAdapter(root);
|
||||
allClsIds = buildClassIdsMap(root.getClasses());
|
||||
@@ -193,24 +195,28 @@ public class DiskCodeCache implements ICodeCache {
|
||||
}
|
||||
}
|
||||
|
||||
private String buildCodeVersion(JadxArgs args) {
|
||||
private String buildCodeVersion(JadxArgs args, @Nullable JadxDecompiler decompiler) {
|
||||
List<File> inputFiles = new ArrayList<>(args.getInputFiles());
|
||||
Path userMappingPath = args.getUserRenamesMappingsPath();
|
||||
if (args.getUserRenamesMappingsMode() != UserRenamesMappingsMode.IGNORE
|
||||
&& userMappingPath != null
|
||||
&& Files.exists(userMappingPath)) {
|
||||
inputFiles.add(userMappingPath.toFile());
|
||||
}
|
||||
File generatedMappingFile = args.getGeneratedRenamesMappingFile();
|
||||
if (args.getGeneratedRenamesMappingFileMode().shouldRead()
|
||||
&& generatedMappingFile != null
|
||||
&& generatedMappingFile.exists()) {
|
||||
inputFiles.add(generatedMappingFile);
|
||||
&& args.getGeneratedRenamesMappingFile() != null
|
||||
&& args.getGeneratedRenamesMappingFile().exists()) {
|
||||
inputFiles.add(args.getGeneratedRenamesMappingFile());
|
||||
}
|
||||
return DATA_FORMAT_VERSION
|
||||
+ ":" + Jadx.getVersion()
|
||||
+ ":" + args.makeCodeArgsHash()
|
||||
+ ":" + FileUtils.buildInputsHash(Utils.collectionMap(inputFiles, File::toPath));
|
||||
+ ":" + FileUtils.buildInputsHash(Utils.collectionMap(inputFiles, File::toPath))
|
||||
+ ":" + FileUtils.md5Sum(buildPluginsHash(decompiler));
|
||||
}
|
||||
|
||||
private String buildPluginsHash(JadxDecompiler decompiler) {
|
||||
if (decompiler == null) {
|
||||
return "";
|
||||
}
|
||||
return decompiler.getPluginManager().getResolvedPluginContexts()
|
||||
.stream()
|
||||
.map(PluginContext::getInputsHash)
|
||||
.collect(Collectors.joining());
|
||||
}
|
||||
|
||||
private int getClsId(String clsFullName) {
|
||||
|
||||
@@ -7,7 +7,9 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -65,6 +67,7 @@ public class JadxProject {
|
||||
jadxArgs.setInputFiles(FileUtils.toFiles(getFilePaths()));
|
||||
jadxArgs.setUserRenamesMappingsPath(getMappingsPath());
|
||||
jadxArgs.setCodeData(getCodeData());
|
||||
jadxArgs.getPluginOptions().putAll(data.getPluginOptions());
|
||||
}
|
||||
|
||||
public @Nullable Path getWorkingDir() {
|
||||
@@ -176,6 +179,18 @@ public class JadxProject {
|
||||
changed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not expose options map directly to be able to intercept changes
|
||||
*/
|
||||
public void updatePluginOptions(Consumer<Map<String, String>> update) {
|
||||
update.accept(data.getPluginOptions());
|
||||
changed();
|
||||
}
|
||||
|
||||
public @Nullable String getPluginOption(String key) {
|
||||
return data.getPluginOptions().get(key);
|
||||
}
|
||||
|
||||
public @NotNull Path getCacheDir() {
|
||||
Path cacheDir = data.getCacheDir();
|
||||
if (cacheDir != null) {
|
||||
|
||||
@@ -39,7 +39,7 @@ public class JadxSettingsStorage {
|
||||
|
||||
private static Path initConfigFile() {
|
||||
ProjectDirectories jadxDirs = ProjectDirectories.from("io.github", "skylot", "jadx");
|
||||
Path confPath = Paths.get(jadxDirs.configDir, "config.json");
|
||||
Path confPath = Paths.get(jadxDirs.configDir, "gui.json");
|
||||
if (!Files.exists(confPath)) {
|
||||
copyFromPreferences(confPath);
|
||||
}
|
||||
|
||||
@@ -22,8 +22,10 @@ import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.BorderFactory;
|
||||
@@ -62,9 +64,10 @@ import jadx.api.JadxArgs;
|
||||
import jadx.api.JadxArgs.UseKotlinMethodsForVarNames;
|
||||
import jadx.api.args.GeneratedRenamesMappingFileMode;
|
||||
import jadx.api.args.ResourceNameSource;
|
||||
import jadx.api.plugins.JadxPlugin;
|
||||
import jadx.api.plugins.options.JadxPluginOptions;
|
||||
import jadx.api.plugins.options.OptionDescription;
|
||||
import jadx.api.plugins.options.OptionDescription.OptionFlag;
|
||||
import jadx.core.plugins.PluginContext;
|
||||
import jadx.gui.cache.code.CodeCacheMode;
|
||||
import jadx.gui.cache.usage.UsageCacheMode;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
@@ -75,7 +78,6 @@ import jadx.gui.utils.LangLocale;
|
||||
import jadx.gui.utils.NLS;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
import jadx.gui.utils.plugins.CollectPluginOptions;
|
||||
import jadx.gui.utils.plugins.PluginWithOptions;
|
||||
import jadx.gui.utils.ui.DocumentUpdateListener;
|
||||
|
||||
public class JadxSettingsWindow extends JDialog {
|
||||
@@ -613,34 +615,54 @@ public class JadxSettingsWindow extends JDialog {
|
||||
|
||||
private SettingsGroup makePluginOptionsGroup() {
|
||||
SettingsGroup pluginsGroup = new SettingsGroup(NLS.str("preferences.plugins"));
|
||||
List<PluginWithOptions> list = new CollectPluginOptions(mainWindow.getWrapper()).build();
|
||||
for (PluginWithOptions data : list) {
|
||||
addPluginOptions(pluginsGroup, data.getPlugin(), data.getOptions());
|
||||
List<PluginContext> list = new CollectPluginOptions(mainWindow.getWrapper()).build();
|
||||
for (PluginContext context : list) {
|
||||
addPluginOptions(pluginsGroup, context);
|
||||
}
|
||||
return pluginsGroup;
|
||||
}
|
||||
|
||||
private void addPluginOptions(SettingsGroup pluginsGroup, JadxPlugin plugin, JadxPluginOptions options) {
|
||||
String pluginId = plugin.getPluginInfo().getPluginId();
|
||||
private void addPluginOptions(SettingsGroup pluginsGroup, PluginContext context) {
|
||||
JadxPluginOptions options = context.getOptions();
|
||||
if (options == null) {
|
||||
return;
|
||||
}
|
||||
String pluginId = context.getPluginId();
|
||||
for (OptionDescription opt : options.getOptionsDescriptions()) {
|
||||
if (opt.getFlags().contains(OptionFlag.HIDE_IN_GUI)) {
|
||||
continue;
|
||||
}
|
||||
String optName = opt.name();
|
||||
String title;
|
||||
if (pluginId.equals("jadx-script")) {
|
||||
title = '[' + opt.name().replace("jadx-script.", "script:") + "] " + opt.description();
|
||||
title = '[' + optName.replace("jadx-script.", "script:") + "] " + opt.description();
|
||||
} else {
|
||||
title = '[' + pluginId + "] " + opt.description();
|
||||
}
|
||||
Consumer<String> updateFunc;
|
||||
String curValue;
|
||||
if (opt.getFlags().contains(OptionFlag.PER_PROJECT)) {
|
||||
JadxProject project = mainWindow.getProject();
|
||||
updateFunc = value -> project.updatePluginOptions(m -> m.put(optName, value));
|
||||
curValue = project.getPluginOption(optName);
|
||||
} else {
|
||||
Map<String, String> optionsMap = settings.getPluginOptions();
|
||||
updateFunc = value -> optionsMap.put(optName, value);
|
||||
curValue = optionsMap.get(optName);
|
||||
}
|
||||
String value = curValue != null ? curValue : opt.defaultValue();
|
||||
|
||||
if (opt.values().isEmpty() || opt.getType() == OptionDescription.OptionType.BOOLEAN) {
|
||||
try {
|
||||
pluginsGroup.addRow(title, getPluginOptionEditor(opt));
|
||||
pluginsGroup.addRow(title, getPluginOptionEditor(opt, value, updateFunc));
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to add editor for plugin option: {}", opt.name(), e);
|
||||
LOG.error("Failed to add editor for plugin option: {}", optName, e);
|
||||
}
|
||||
} else {
|
||||
String curValue = settings.getPluginOptions().get(opt.name());
|
||||
JComboBox<String> combo = new JComboBox<>(opt.values().toArray(new String[0]));
|
||||
combo.setSelectedItem(curValue != null ? curValue : opt.defaultValue());
|
||||
combo.setSelectedItem(value);
|
||||
combo.addActionListener(e -> {
|
||||
settings.getPluginOptions().put(opt.name(), ((String) combo.getSelectedItem()));
|
||||
updateFunc.accept((String) combo.getSelectedItem());
|
||||
needReload();
|
||||
});
|
||||
pluginsGroup.addRow(title, combo);
|
||||
@@ -648,16 +670,13 @@ public class JadxSettingsWindow extends JDialog {
|
||||
}
|
||||
}
|
||||
|
||||
private JComponent getPluginOptionEditor(OptionDescription opt) {
|
||||
String curValue = settings.getPluginOptions().get(opt.name());
|
||||
String value = curValue == null ? opt.defaultValue() : curValue;
|
||||
|
||||
private JComponent getPluginOptionEditor(OptionDescription opt, String value, Consumer<String> updateFunc) {
|
||||
switch (opt.getType()) {
|
||||
case STRING:
|
||||
JTextField textField = new JTextField();
|
||||
textField.setText(value == null ? "" : value);
|
||||
textField.getDocument().addDocumentListener(new DocumentUpdateListener(event -> {
|
||||
settings.getPluginOptions().put(opt.name(), textField.getText());
|
||||
updateFunc.accept(textField.getText());
|
||||
needReload();
|
||||
}));
|
||||
return textField;
|
||||
@@ -666,7 +685,7 @@ public class JadxSettingsWindow extends JDialog {
|
||||
JSpinner numberField = new JSpinner();
|
||||
numberField.setValue(safeStringToInt(value, 0));
|
||||
numberField.addChangeListener(e -> {
|
||||
settings.getPluginOptions().put(opt.name(), numberField.getValue().toString());
|
||||
updateFunc.accept(numberField.getValue().toString());
|
||||
needReload();
|
||||
});
|
||||
return numberField;
|
||||
@@ -676,7 +695,7 @@ public class JadxSettingsWindow extends JDialog {
|
||||
boolField.setSelected(Objects.equals(value, "yes") || Objects.equals(value, "true"));
|
||||
boolField.addItemListener(e -> {
|
||||
boolean editorValue = e.getStateChange() == ItemEvent.SELECTED;
|
||||
settings.getPluginOptions().put(opt.name(), editorValue ? "yes" : "no");
|
||||
updateFunc.accept(editorValue ? "yes" : "no");
|
||||
needReload();
|
||||
});
|
||||
return boolField;
|
||||
|
||||
@@ -8,6 +8,7 @@ import jadx.api.JavaClass;
|
||||
import jadx.gui.settings.data.TabViewState;
|
||||
import jadx.gui.settings.data.ViewPoint;
|
||||
import jadx.gui.treemodel.JClass;
|
||||
import jadx.gui.treemodel.JInputMapping;
|
||||
import jadx.gui.treemodel.JInputScript;
|
||||
import jadx.gui.treemodel.JNode;
|
||||
import jadx.gui.treemodel.JResource;
|
||||
@@ -62,6 +63,9 @@ public class TabStateViewAdapter {
|
||||
return mw.getTreeRoot()
|
||||
.followStaticPath("JInputs", "JInputScripts")
|
||||
.searchNode(node -> node instanceof JInputScript && node.getName().equals(tvs.getTabPath()));
|
||||
|
||||
case "mapping":
|
||||
return mw.getTreeRoot().followStaticPath("JInputs", "JInputMapping");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -82,6 +86,10 @@ public class TabStateViewAdapter {
|
||||
tvs.setTabPath(node.getName());
|
||||
return true;
|
||||
}
|
||||
if (node instanceof JInputMapping) {
|
||||
tvs.setType("mapping");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ package jadx.gui.settings.data;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -22,6 +24,7 @@ public class ProjectData {
|
||||
private @Nullable Path cacheDir;
|
||||
private boolean enableLiveReload = false;
|
||||
private List<String> searchHistory = new ArrayList<>();
|
||||
protected Map<String, String> pluginOptions = new HashMap<>();
|
||||
|
||||
public List<Path> getFiles() {
|
||||
return files;
|
||||
@@ -122,4 +125,8 @@ public class ProjectData {
|
||||
public void setSearchHistory(List<String> searchHistory) {
|
||||
this.searchHistory = searchHistory;
|
||||
}
|
||||
|
||||
public Map<String, String> getPluginOptions() {
|
||||
return pluginOptions;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +145,7 @@ import jadx.gui.utils.UiUtils;
|
||||
import jadx.gui.utils.fileswatcher.LiveReloadWorker;
|
||||
import jadx.gui.utils.ui.ActionHandler;
|
||||
import jadx.gui.utils.ui.NodeLabel;
|
||||
import jadx.plugins.mappings.RenameMappingsOptions;
|
||||
import jadx.plugins.mappings.save.MappingExporter;
|
||||
|
||||
import static io.reactivex.internal.functions.Functions.EMPTY_RUNNABLE;
|
||||
@@ -389,7 +390,7 @@ public class MainWindow extends JFrame {
|
||||
update();
|
||||
}
|
||||
|
||||
private void openMappings(MappingFormat mappingFormat) {
|
||||
private void openMappings(MappingFormat mappingFormat, boolean inverted) {
|
||||
FileDialogWrapper fileDialog = new FileDialogWrapper(this, FileOpenMode.CUSTOM_OPEN);
|
||||
fileDialog.setTitle(NLS.str("file.open_mappings"));
|
||||
if (mappingFormat.hasSingleFile()) {
|
||||
@@ -407,12 +408,17 @@ public class MainWindow extends JFrame {
|
||||
LOG.info("Loading mappings from: {}", filePath.toAbsolutePath());
|
||||
project.setMappingsPath(filePath);
|
||||
currentMappingFormat = mappingFormat;
|
||||
project.updatePluginOptions(options -> {
|
||||
options.put(RenameMappingsOptions.FORMAT_OPT, mappingFormat.name());
|
||||
options.put(RenameMappingsOptions.INVERT_OPT, inverted ? "yes" : "no");
|
||||
});
|
||||
reopen();
|
||||
}
|
||||
|
||||
public void closeMappingsAndRemoveFromProject() {
|
||||
project.setMappingsPath(null);
|
||||
currentMappingFormat = null;
|
||||
reopen();
|
||||
}
|
||||
|
||||
private void saveMappings() {
|
||||
@@ -1012,88 +1018,25 @@ public class MainWindow extends JFrame {
|
||||
liveReloadMenuItem = new JCheckBoxMenuItem(liveReload);
|
||||
liveReloadMenuItem.setState(project.isEnableLiveReload());
|
||||
|
||||
ActionHandler openProGuardMappings = new ActionHandler(ev -> openMappings(MappingFormat.PROGUARD));
|
||||
openProGuardMappings.setNameAndDesc("Proguard");
|
||||
|
||||
Action openTiny2Mappings = new AbstractAction("Tiny v2 file") {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
openMappings(MappingFormat.TINY_2);
|
||||
}
|
||||
};
|
||||
openTiny2Mappings.putValue(Action.SHORT_DESCRIPTION, "Tiny v2 file");
|
||||
|
||||
Action openEnigmaMappings = new AbstractAction("Enigma file") {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
openMappings(MappingFormat.ENIGMA);
|
||||
}
|
||||
};
|
||||
openEnigmaMappings.putValue(Action.SHORT_DESCRIPTION, "Enigma file");
|
||||
|
||||
Action openEnigmaDirMappings = new AbstractAction("Enigma directory") {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
openMappings(MappingFormat.ENIGMA_DIR);
|
||||
}
|
||||
};
|
||||
openEnigmaDirMappings.putValue(Action.SHORT_DESCRIPTION, "Enigma directory");
|
||||
|
||||
openMappingsMenu = new JMenu(NLS.str("file.open_mappings"));
|
||||
openMappingsMenu.add(openProGuardMappings);
|
||||
openMappingsMenu.add(openTiny2Mappings);
|
||||
openMappingsMenu.add(openEnigmaMappings);
|
||||
openMappingsMenu.add(openEnigmaDirMappings);
|
||||
openMappingsMenu.add(new ActionHandler(ev -> openMappings(MappingFormat.PROGUARD, true)).withNameAndDesc("Proguard (inverted)"));
|
||||
openMappingsMenu.add(new ActionHandler(ev -> openMappings(MappingFormat.PROGUARD, false)).withNameAndDesc("Proguard"));
|
||||
|
||||
saveMappingsAction = new AbstractAction(NLS.str("file.save_mappings")) {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
saveMappings();
|
||||
}
|
||||
};
|
||||
saveMappingsAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.save_mappings"));
|
||||
|
||||
ActionHandler saveProGuardMappings = new ActionHandler(ev -> saveMappingsAs(MappingFormat.PROGUARD));
|
||||
saveProGuardMappings.setNameAndDesc("Proguard");
|
||||
|
||||
Action saveMappingsAsTiny2 = new AbstractAction("Tiny v2 file") {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
saveMappingsAs(MappingFormat.TINY_2);
|
||||
}
|
||||
};
|
||||
saveMappingsAsTiny2.putValue(Action.SHORT_DESCRIPTION, "Tiny v2 file");
|
||||
|
||||
Action saveMappingsAsEnigma = new AbstractAction("Enigma file") {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
saveMappingsAs(MappingFormat.ENIGMA);
|
||||
}
|
||||
};
|
||||
saveMappingsAsEnigma.putValue(Action.SHORT_DESCRIPTION, "Enigma file");
|
||||
|
||||
Action saveMappingsAsEnigmaDir = new AbstractAction("Enigma directory") {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
saveMappingsAs(MappingFormat.ENIGMA_DIR);
|
||||
}
|
||||
};
|
||||
saveMappingsAsEnigmaDir.putValue(Action.SHORT_DESCRIPTION, "Enigma directory");
|
||||
saveMappingsAction = new ActionHandler(this::saveMappings).withNameAndDesc(NLS.str("file.save_mappings"));
|
||||
|
||||
saveMappingsAsMenu = new JMenu(NLS.str("file.save_mappings_as"));
|
||||
saveMappingsAsMenu.add(saveProGuardMappings);
|
||||
saveMappingsAsMenu.add(saveMappingsAsTiny2);
|
||||
saveMappingsAsMenu.add(saveMappingsAsEnigma);
|
||||
saveMappingsAsMenu.add(saveMappingsAsEnigmaDir);
|
||||
|
||||
closeMappingsAction = new AbstractAction(NLS.str("file.close_mappings")) {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
closeMappingsAndRemoveFromProject();
|
||||
reopen();
|
||||
for (MappingFormat mappingFormat : MappingFormat.values()) {
|
||||
if (mappingFormat != MappingFormat.PROGUARD) {
|
||||
openMappingsMenu.add(new ActionHandler(ev -> openMappings(mappingFormat, false))
|
||||
.withNameAndDesc(mappingFormat.name));
|
||||
}
|
||||
};
|
||||
closeMappingsAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.close_mappings"));
|
||||
saveMappingsAsMenu.add(new ActionHandler(ev -> saveMappingsAs(mappingFormat))
|
||||
.withNameAndDesc(mappingFormat.name));
|
||||
}
|
||||
|
||||
closeMappingsAction = new ActionHandler(ev -> closeMappingsAndRemoveFromProject())
|
||||
.withNameAndDesc(NLS.str("file.close_mappings"));
|
||||
|
||||
Action saveAllAction = new AbstractAction(NLS.str("file.save_all"), Icons.SAVE_ALL) {
|
||||
@Override
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
package jadx.gui.utils.plugins;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.JadxDecompiler;
|
||||
import jadx.api.plugins.JadxPlugin;
|
||||
import jadx.api.plugins.JadxPluginManager;
|
||||
import jadx.api.plugins.options.JadxPluginOptions;
|
||||
import jadx.core.plugins.JadxPluginManager;
|
||||
import jadx.core.plugins.PluginContext;
|
||||
import jadx.gui.JadxWrapper;
|
||||
|
||||
/**
|
||||
@@ -21,47 +19,32 @@ import jadx.gui.JadxWrapper;
|
||||
public class CollectPluginOptions {
|
||||
|
||||
private final JadxWrapper wrapper;
|
||||
private final Map<Class<?>, PluginWithOptions> plugins;
|
||||
|
||||
public CollectPluginOptions(JadxWrapper wrapper) {
|
||||
this.wrapper = wrapper;
|
||||
this.plugins = new HashMap<>();
|
||||
}
|
||||
|
||||
public List<PluginWithOptions> build() {
|
||||
wrapper.getCurrentDecompiler().ifPresent(decompiler -> {
|
||||
List<JadxPlugin> loadedPlugins = decompiler.getPluginManager().getResolvedPlugins();
|
||||
addOptions(decompiler, loadedPlugins);
|
||||
});
|
||||
public List<PluginContext> build() {
|
||||
SortedSet<PluginContext> allPlugins = new TreeSet<>();
|
||||
wrapper.getCurrentDecompiler()
|
||||
.ifPresent(decompiler -> allPlugins.addAll(decompiler.getPluginManager().getResolvedPluginContexts()));
|
||||
|
||||
// collect and init not loaded plugins in new context
|
||||
try (JadxDecompiler decompiler = new JadxDecompiler(new JadxArgs())) {
|
||||
JadxPluginManager pluginManager = decompiler.getPluginManager();
|
||||
List<JadxPlugin> missingPlugins = new ArrayList<>();
|
||||
for (JadxPlugin plugin : pluginManager.getAllPlugins()) {
|
||||
if (!plugins.containsKey(plugin.getClass())) {
|
||||
missingPlugins.add(plugin);
|
||||
pluginManager.load();
|
||||
SortedSet<PluginContext> missingPlugins = new TreeSet<>();
|
||||
for (PluginContext context : pluginManager.getAllPluginContexts()) {
|
||||
if (!allPlugins.contains(context)) {
|
||||
missingPlugins.add(context);
|
||||
}
|
||||
}
|
||||
pluginManager.init(decompiler.getPluginsContext(), missingPlugins);
|
||||
addOptions(decompiler, missingPlugins);
|
||||
pluginManager.init(missingPlugins);
|
||||
allPlugins.addAll(missingPlugins);
|
||||
}
|
||||
return plugins.values().stream()
|
||||
.filter(data -> data != PluginWithOptions.NULL)
|
||||
return allPlugins.stream()
|
||||
.filter(context -> context.getOptions() != null)
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private void addOptions(JadxDecompiler decompiler, List<JadxPlugin> loadedPlugins) {
|
||||
Map<JadxPlugin, JadxPluginOptions> optionsMap = decompiler.getPluginsContext().getOptionsMap();
|
||||
for (JadxPlugin loadedPlugin : loadedPlugins) {
|
||||
JadxPluginOptions pluginOptions = optionsMap.get(loadedPlugin);
|
||||
PluginWithOptions options;
|
||||
if (pluginOptions != null) {
|
||||
options = new PluginWithOptions(loadedPlugin, pluginOptions);
|
||||
} else {
|
||||
options = PluginWithOptions.NULL;
|
||||
}
|
||||
plugins.put(loadedPlugin.getClass(), options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,11 @@ public class ActionHandler extends AbstractAction {
|
||||
putValue(NAME, name);
|
||||
}
|
||||
|
||||
public ActionHandler withNameAndDesc(String name) {
|
||||
setNameAndDesc(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
public void setNameAndDesc(String name) {
|
||||
setName(name);
|
||||
setShortDescription(name);
|
||||
|
||||
+2
-4
@@ -1,6 +1,5 @@
|
||||
package jadx.plugins.input.dex;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@@ -21,11 +20,10 @@ public class DexInputOptions extends BaseOptionsParser {
|
||||
|
||||
public List<OptionDescription> getOptionsDescriptions() {
|
||||
return Collections.singletonList(
|
||||
new JadxOptionDescription(
|
||||
JadxOptionDescription.booleanOption(
|
||||
VERIFY_CHECKSUM_OPT,
|
||||
"verify dex file checksum before load",
|
||||
"yes",
|
||||
Arrays.asList("yes", "no")));
|
||||
true));
|
||||
}
|
||||
|
||||
public boolean isVerifyChecksum() {
|
||||
|
||||
+7
-3
@@ -7,6 +7,7 @@ import java.util.Locale;
|
||||
import jadx.api.plugins.options.OptionDescription;
|
||||
import jadx.api.plugins.options.impl.BaseOptionsParser;
|
||||
import jadx.api.plugins.options.impl.JadxOptionDescription;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
|
||||
public class JavaConvertOptions extends BaseOptionsParser {
|
||||
|
||||
@@ -34,11 +35,10 @@ public class JavaConvertOptions extends BaseOptionsParser {
|
||||
"convert mode",
|
||||
"both",
|
||||
Arrays.asList("dx", "d8", "both")),
|
||||
new JadxOptionDescription(
|
||||
JadxOptionDescription.booleanOption(
|
||||
D8_DESUGAR_OPT,
|
||||
"use desugar in d8",
|
||||
"no",
|
||||
Arrays.asList("yes", "no")));
|
||||
false));
|
||||
}
|
||||
|
||||
public Mode getMode() {
|
||||
@@ -48,4 +48,8 @@ public class JavaConvertOptions extends BaseOptionsParser {
|
||||
public boolean isD8Desugar() {
|
||||
return d8Desugar;
|
||||
}
|
||||
|
||||
public String getOptionsHash() {
|
||||
return FileUtils.md5Sum(mode + ":" + d8Desugar);
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -32,6 +32,7 @@ public class JavaConvertPlugin implements JadxPlugin, JadxCodeInput {
|
||||
public void init(JadxPluginContext context) {
|
||||
context.registerOptions(options);
|
||||
context.addCodeInput(this);
|
||||
context.registerInputsHashSupplier(options::getOptionsHash);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package jadx.plugins.mappings;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import net.fabricmc.mappingio.format.MappingFormat;
|
||||
|
||||
import jadx.api.plugins.options.OptionDescription;
|
||||
import jadx.api.plugins.options.OptionDescription.OptionFlag;
|
||||
import jadx.api.plugins.options.impl.BaseOptionsParser;
|
||||
import jadx.api.plugins.options.impl.JadxOptionDescription;
|
||||
|
||||
import static jadx.plugins.mappings.RenameMappingsPlugin.PLUGIN_ID;
|
||||
|
||||
public class RenameMappingsOptions extends BaseOptionsParser {
|
||||
|
||||
public static final String INVERT_OPT = PLUGIN_ID + ".invert";
|
||||
public static final String FORMAT_OPT = PLUGIN_ID + ".format";
|
||||
|
||||
private boolean invert = false;
|
||||
|
||||
/**
|
||||
* null value - used for 'auto' option
|
||||
*/
|
||||
private @Nullable MappingFormat format = null;
|
||||
|
||||
@Override
|
||||
public void parseOptions() {
|
||||
format = getOption(FORMAT_OPT, RenameMappingsOptions::parseMappingFormat, null);
|
||||
invert = getBooleanOption(INVERT_OPT, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OptionDescription> getOptionsDescriptions() {
|
||||
return Arrays.asList(
|
||||
new JadxOptionDescription(FORMAT_OPT, "mapping format", "auto", getMappingFormats())
|
||||
.withFlag(OptionFlag.PER_PROJECT),
|
||||
JadxOptionDescription.booleanOption(INVERT_OPT, "invert mapping", false)
|
||||
.withFlag(OptionFlag.PER_PROJECT));
|
||||
}
|
||||
|
||||
private static MappingFormat parseMappingFormat(String name) {
|
||||
String upName = name.toUpperCase(Locale.ROOT);
|
||||
if (upName.equals("AUTO")) {
|
||||
return null;
|
||||
}
|
||||
return MappingFormat.valueOf(upName);
|
||||
}
|
||||
|
||||
private static List<String> getMappingFormats() {
|
||||
List<String> list = new ArrayList<>();
|
||||
list.add("auto");
|
||||
for (MappingFormat value : MappingFormat.values()) {
|
||||
list.add(value.name());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public MappingFormat getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
public boolean isInvert() {
|
||||
return invert;
|
||||
}
|
||||
|
||||
public String getOptionsHashString() {
|
||||
return format + ":" + invert;
|
||||
}
|
||||
}
|
||||
+35
-35
@@ -1,58 +1,58 @@
|
||||
package jadx.plugins.mappings;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import net.fabricmc.mappingio.MappingReader;
|
||||
import net.fabricmc.mappingio.MappingUtil;
|
||||
import net.fabricmc.mappingio.tree.MappingTree;
|
||||
import net.fabricmc.mappingio.tree.MemoryMappingTree;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.args.UserRenamesMappingsMode;
|
||||
import jadx.api.plugins.JadxPlugin;
|
||||
import jadx.api.plugins.JadxPluginContext;
|
||||
import jadx.api.plugins.JadxPluginInfo;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.plugins.mappings.load.CodeMappingsVisitor;
|
||||
import jadx.plugins.mappings.load.MappingsVisitor;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
import jadx.plugins.mappings.load.ApplyMappingsPass;
|
||||
import jadx.plugins.mappings.load.CodeMappingsPass;
|
||||
import jadx.plugins.mappings.load.LoadMappingsPass;
|
||||
|
||||
public class RenameMappingsPlugin implements JadxPlugin {
|
||||
public static final String PLUGIN_ID = "rename-mappings";
|
||||
|
||||
private final RenameMappingsOptions options = new RenameMappingsOptions();
|
||||
|
||||
@Override
|
||||
public JadxPluginInfo getPluginInfo() {
|
||||
return new JadxPluginInfo("jadx-rename-mappings", "Rename Mappings", "various mappings support");
|
||||
return new JadxPluginInfo(PLUGIN_ID, "Rename Mappings", "various mappings support");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(JadxPluginContext context) {
|
||||
MappingTree mappingTree = openMapping(context.getArgs());
|
||||
if (mappingTree != null) {
|
||||
context.addPass(new MappingsVisitor(mappingTree));
|
||||
context.addPass(new CodeMappingsVisitor(mappingTree));
|
||||
context.registerOptions(options);
|
||||
JadxArgs args = context.getArgs();
|
||||
if (args.getUserRenamesMappingsMode() == UserRenamesMappingsMode.IGNORE) {
|
||||
return;
|
||||
}
|
||||
Path mappingsPath = args.getUserRenamesMappingsPath();
|
||||
if (mappingsPath == null || !Files.isReadable(mappingsPath)) {
|
||||
return;
|
||||
}
|
||||
LoadMappingsPass loadPass = new LoadMappingsPass(options);
|
||||
context.addPass(loadPass);
|
||||
context.addPass(new ApplyMappingsPass(loadPass));
|
||||
context.addPass(new CodeMappingsPass(loadPass));
|
||||
|
||||
// use mapping file time modification to check for changes
|
||||
context.registerInputsHashSupplier(() -> FileUtils.md5Sum(getInputsHashString(mappingsPath)));
|
||||
}
|
||||
|
||||
public MappingTree openMapping(JadxArgs args) {
|
||||
if (args.getUserRenamesMappingsMode() != UserRenamesMappingsMode.IGNORE
|
||||
&& args.getUserRenamesMappingsPath() != null) {
|
||||
try {
|
||||
MemoryMappingTree mappingTree = new MemoryMappingTree();
|
||||
MappingReader.read(args.getUserRenamesMappingsPath(), mappingTree);
|
||||
if (mappingTree.getSrcNamespace() == null) {
|
||||
mappingTree.setSrcNamespace(MappingUtil.NS_SOURCE_FALLBACK);
|
||||
}
|
||||
if (mappingTree.getDstNamespaces() == null || mappingTree.getDstNamespaces().isEmpty()) {
|
||||
mappingTree.setDstNamespaces(Collections.singletonList(MappingUtil.NS_TARGET_FALLBACK));
|
||||
} else if (mappingTree.getDstNamespaces().size() > 1) {
|
||||
throw new JadxRuntimeException(
|
||||
String.format("JADX only supports mappings with just one destination namespace! The provided ones have %s.",
|
||||
mappingTree.getDstNamespaces().size()));
|
||||
}
|
||||
return mappingTree;
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Failed to load mappings", e);
|
||||
}
|
||||
private String getInputsHashString(Path mappingsPath) {
|
||||
return getFileHashString(mappingsPath) + ':' + options.getOptionsHashString();
|
||||
}
|
||||
|
||||
private static String getFileHashString(Path mappingsPath) {
|
||||
try {
|
||||
return mappingsPath.toAbsolutePath().normalize()
|
||||
+ ":" + Files.getLastModifiedTime(mappingsPath).toMillis();
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+15
-10
@@ -16,31 +16,36 @@ import jadx.core.dex.nodes.FieldNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
|
||||
public class MappingsVisitor implements JadxPreparePass {
|
||||
public class ApplyMappingsPass implements JadxPreparePass {
|
||||
|
||||
private final MappingTree mappingTree;
|
||||
private final LoadMappingsPass loadPass;
|
||||
|
||||
public MappingsVisitor(MappingTree mappingTree) {
|
||||
this.mappingTree = mappingTree;
|
||||
public ApplyMappingsPass(LoadMappingsPass loadPass) {
|
||||
this.loadPass = loadPass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JadxPassInfo getInfo() {
|
||||
return new OrderedJadxPassInfo(
|
||||
"MappingVisitor",
|
||||
"ApplyMappings",
|
||||
"Apply mappings to classes, fields and methods")
|
||||
.after("LoadMappings")
|
||||
.before("RenameVisitor");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(RootNode root) {
|
||||
process(root);
|
||||
root.registerCodeDataUpdateListener(codeData -> process(root));
|
||||
MappingTree mappingTree = loadPass.getMappings();
|
||||
if (mappingTree == null) {
|
||||
return;
|
||||
}
|
||||
process(root, mappingTree);
|
||||
root.registerCodeDataUpdateListener(codeData -> process(root, mappingTree));
|
||||
}
|
||||
|
||||
private void process(RootNode root) {
|
||||
private void process(RootNode root, MappingTree mappingTree) {
|
||||
for (ClassNode cls : root.getClasses()) {
|
||||
String clsRawName = cls.getClassInfo().makeRawFullName().replace('.', '/');
|
||||
String clsRawName = cls.getClassInfo().getRawName().replace('.', '/');
|
||||
ClassMapping mapping = mappingTree.getClass(clsRawName);
|
||||
if (mapping != null) {
|
||||
processClass(cls, mapping);
|
||||
@@ -95,6 +100,6 @@ public class MappingsVisitor implements JadxPreparePass {
|
||||
if (comment != null) {
|
||||
method.addCodeComment(comment);
|
||||
}
|
||||
// Method args & vars are handled in CodeMappingsVisitor
|
||||
// Method args & vars are handled in CodeMappingsPass
|
||||
}
|
||||
}
|
||||
+13
-9
@@ -18,26 +18,30 @@ import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.plugins.mappings.utils.DalvikToJavaBytecodeUtils;
|
||||
|
||||
public class CodeMappingsVisitor implements JadxDecompilePass {
|
||||
private final MappingTree mappingTree;
|
||||
public class CodeMappingsPass implements JadxDecompilePass {
|
||||
private final LoadMappingsPass loadPass;
|
||||
private Map<String, ClassMapping> clsRenamesMap;
|
||||
|
||||
public CodeMappingsVisitor(MappingTree mappingTree) {
|
||||
this.mappingTree = mappingTree;
|
||||
public CodeMappingsPass(LoadMappingsPass loadPass) {
|
||||
this.loadPass = loadPass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JadxPassInfo getInfo() {
|
||||
return new OrderedJadxPassInfo(
|
||||
"ApplyCodeMappings",
|
||||
"CodeMappings",
|
||||
"Apply mappings to method args and vars")
|
||||
.before("CodeRenameVisitor");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(RootNode root) {
|
||||
updateMappingsMap();
|
||||
root.registerCodeDataUpdateListener(codeData -> updateMappingsMap());
|
||||
MappingTree mappingTree = loadPass.getMappings();
|
||||
if (mappingTree == null) {
|
||||
return;
|
||||
}
|
||||
updateMappingsMap(mappingTree);
|
||||
root.registerCodeDataUpdateListener(codeData -> updateMappingsMap(mappingTree));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -89,9 +93,9 @@ public class CodeMappingsVisitor implements JadxDecompilePass {
|
||||
return clsRenamesMap.get(classPath);
|
||||
}
|
||||
|
||||
private void updateMappingsMap() {
|
||||
private void updateMappingsMap(MappingTree mappings) {
|
||||
clsRenamesMap = new HashMap<>();
|
||||
for (ClassMapping cls : mappingTree.getClasses()) {
|
||||
for (ClassMapping cls : mappings.getClasses()) {
|
||||
for (MethodMapping mth : cls.getMethods()) {
|
||||
if (!mth.getArgs().isEmpty() || !mth.getVars().isEmpty()) {
|
||||
clsRenamesMap.put(cls.getSrcName(), cls);
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package jadx.plugins.mappings.load;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import net.fabricmc.mappingio.MappingReader;
|
||||
import net.fabricmc.mappingio.MappingUtil;
|
||||
import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch;
|
||||
import net.fabricmc.mappingio.tree.MappingTree;
|
||||
import net.fabricmc.mappingio.tree.MemoryMappingTree;
|
||||
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.plugins.pass.JadxPassInfo;
|
||||
import jadx.api.plugins.pass.impl.SimpleJadxPassInfo;
|
||||
import jadx.api.plugins.pass.types.JadxPreparePass;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.plugins.mappings.RenameMappingsOptions;
|
||||
|
||||
public class LoadMappingsPass implements JadxPreparePass {
|
||||
|
||||
private final RenameMappingsOptions options;
|
||||
private MappingTree mappings;
|
||||
|
||||
public LoadMappingsPass(RenameMappingsOptions options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JadxPassInfo getInfo() {
|
||||
return new SimpleJadxPassInfo("LoadMappings", "Load mappings file");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(RootNode root) {
|
||||
mappings = loadMapping(root.getArgs());
|
||||
}
|
||||
|
||||
public @Nullable MappingTree getMappings() {
|
||||
return mappings;
|
||||
}
|
||||
|
||||
private MappingTree loadMapping(JadxArgs args) {
|
||||
try {
|
||||
Path mappingsPath = args.getUserRenamesMappingsPath();
|
||||
MemoryMappingTree mappingTree = new MemoryMappingTree();
|
||||
MappingReader.read(mappingsPath, options.getFormat(), mappingTree);
|
||||
if (mappingTree.getSrcNamespace() == null) {
|
||||
mappingTree.setSrcNamespace(MappingUtil.NS_SOURCE_FALLBACK);
|
||||
}
|
||||
if (mappingTree.getDstNamespaces() == null || mappingTree.getDstNamespaces().isEmpty()) {
|
||||
mappingTree.setDstNamespaces(Collections.singletonList(MappingUtil.NS_TARGET_FALLBACK));
|
||||
} else if (mappingTree.getDstNamespaces().size() > 1) {
|
||||
throw new JadxRuntimeException(
|
||||
String.format("JADX only supports mappings with just one destination namespace! The provided ones have %s.",
|
||||
mappingTree.getDstNamespaces().size()));
|
||||
}
|
||||
if (options.isInvert()) {
|
||||
MemoryMappingTree invertedMappingTree = new MemoryMappingTree();
|
||||
String dstNamespace = mappingTree.getDstNamespaces().get(0);
|
||||
mappingTree.accept(new MappingSourceNsSwitch(invertedMappingTree, dstNamespace));
|
||||
return invertedMappingTree;
|
||||
}
|
||||
return mappingTree;
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Failed to load mappings", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user