fix: improve plugins data handling

This commit is contained in:
Skylot
2023-04-01 21:06:05 +01:00
parent a992c93198
commit 7a309ca367
35 changed files with 786 additions and 562 deletions
@@ -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);
@@ -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,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);
}
}
@@ -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
@@ -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;
}
}
@@ -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;
}
}
@@ -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
}
}
@@ -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);
@@ -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);
}
}
}