From 7a309ca36797c0d6a3ceaed7f8490dec57798151 Mon Sep 17 00:00:00 2001 From: Skylot Date: Sat, 1 Apr 2023 21:06:05 +0100 Subject: [PATCH] fix: improve plugins data handling --- .../main/java/jadx/cli/JCommanderWrapper.java | 23 +- .../java/jadx/cli/clst/ConvertToClsSet.java | 51 ++--- .../src/main/java/jadx/api/JadxArgs.java | 5 +- .../main/java/jadx/api/JadxDecompiler.java | 17 +- .../jadx/api/impl/plugins/PluginsContext.java | 86 ------- .../jadx/api/plugins/JadxPluginContext.java | 9 + .../jadx/api/plugins/JadxPluginManager.java | 209 ------------------ .../plugins/options/OptionDescription.java | 11 + .../options/impl/JadxOptionDescription.java | 27 +++ .../plugins/pass/types/JadxAfterLoadPass.java | 2 +- .../plugins/pass/types/JadxDecompilePass.java | 2 +- .../api/plugins/pass/types/JadxPassType.java | 8 +- .../plugins/pass/types/JadxPreparePass.java | 2 +- .../jadx/core/plugins/JadxPluginManager.java | 176 +++++++++++++++ .../java/jadx/core/plugins/PluginContext.java | 148 +++++++++++++ .../src/main/java/jadx/core/utils/Utils.java | 7 + .../java/jadx/core/utils/files/FileUtils.java | 4 + .../src/main/java/jadx/gui/JadxWrapper.java | 2 +- .../gui/cache/code/disk/DiskCodeCache.java | 34 +-- .../java/jadx/gui/settings/JadxProject.java | 15 ++ .../gui/settings/JadxSettingsStorage.java | 2 +- .../jadx/gui/settings/JadxSettingsWindow.java | 59 +++-- .../gui/settings/TabStateViewAdapter.java | 8 + .../jadx/gui/settings/data/ProjectData.java | 7 + .../src/main/java/jadx/gui/ui/MainWindow.java | 97 ++------ .../utils/plugins/CollectPluginOptions.java | 53 ++--- .../java/jadx/gui/utils/ui/ActionHandler.java | 5 + .../plugins/input/dex/DexInputOptions.java | 6 +- .../input/javaconvert/JavaConvertOptions.java | 10 +- .../input/javaconvert/JavaConvertPlugin.java | 1 + .../mappings/RenameMappingsOptions.java | 74 +++++++ .../mappings/RenameMappingsPlugin.java | 70 +++--- ...ngsVisitor.java => ApplyMappingsPass.java} | 25 ++- ...ingsVisitor.java => CodeMappingsPass.java} | 22 +- .../mappings/load/LoadMappingsPass.java | 71 ++++++ 35 files changed, 786 insertions(+), 562 deletions(-) delete mode 100644 jadx-core/src/main/java/jadx/api/impl/plugins/PluginsContext.java delete mode 100644 jadx-core/src/main/java/jadx/api/plugins/JadxPluginManager.java create mode 100644 jadx-core/src/main/java/jadx/core/plugins/JadxPluginManager.java create mode 100644 jadx-core/src/main/java/jadx/core/plugins/PluginContext.java create mode 100644 jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/RenameMappingsOptions.java rename jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/load/{MappingsVisitor.java => ApplyMappingsPass.java} (80%) rename jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/load/{CodeMappingsVisitor.java => CodeMappingsPass.java} (85%) create mode 100644 jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/load/LoadMappingsPass.java diff --git a/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java b/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java index 3377c48f9..f133e3cf0 100644 --- a/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java +++ b/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java @@ -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 { @@ -175,12 +176,15 @@ public class JCommanderWrapper { 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 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 { return "\nPlugin options (-P=):" + 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 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) { diff --git a/jadx-cli/src/main/java/jadx/cli/clst/ConvertToClsSet.java b/jadx-cli/src/main/java/jadx/cli/clst/ConvertToClsSet.java index 439307b6e..0e44a86a9 100644 --- a/jadx-cli/src/main/java/jadx/cli/clst/ConvertToClsSet.java +++ b/jadx-cli/src/main/java/jadx/cli/clst/ConvertToClsSet.java @@ -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 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 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 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"); } } diff --git a/jadx-core/src/main/java/jadx/api/JadxArgs.java b/jadx-core/src/main/java/jadx/api/JadxArgs.java index 839938888..c3ac36a41 100644 --- a/jadx-core/src/main/java/jadx/api/JadxArgs.java +++ b/jadx-core/src/main/java/jadx/api/JadxArgs.java @@ -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 diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index 370368200..5965a0cfc 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -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 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 customCodeLoaders = new ArrayList<>(); private final Map> customPasses = new HashMap<>(); @@ -144,7 +142,7 @@ public final class JadxDecompiler implements Closeable { List inputPaths = Utils.collectionMap(args.getInputFiles(), File::toPath); List 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 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(); diff --git a/jadx-core/src/main/java/jadx/api/impl/plugins/PluginsContext.java b/jadx-core/src/main/java/jadx/api/impl/plugins/PluginsContext.java deleted file mode 100644 index 444569d81..000000000 --- a/jadx-core/src/main/java/jadx/api/impl/plugins/PluginsContext.java +++ /dev/null @@ -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 codeInputs = new ArrayList<>(); - private final Map 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 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 getOptionsMap() { - return optionsMap; - } - - @Override - public @Nullable JadxGuiContext getGuiContext() { - return guiContext; - } - - public void setGuiContext(JadxGuiContext guiContext) { - this.guiContext = guiContext; - } -} diff --git a/jadx-core/src/main/java/jadx/api/plugins/JadxPluginContext.java b/jadx-core/src/main/java/jadx/api/plugins/JadxPluginContext.java index ead7d6a81..a650eba85 100644 --- a/jadx-core/src/main/java/jadx/api/plugins/JadxPluginContext.java +++ b/jadx-core/src/main/java/jadx/api/plugins/JadxPluginContext.java @@ -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 supplier); + @Nullable JadxGuiContext getGuiContext(); } diff --git a/jadx-core/src/main/java/jadx/api/plugins/JadxPluginManager.java b/jadx-core/src/main/java/jadx/api/plugins/JadxPluginManager.java deleted file mode 100644 index d50fd79fb..000000000 --- a/jadx-core/src/main/java/jadx/api/plugins/JadxPluginManager.java +++ /dev/null @@ -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 allPlugins = new TreeSet<>(); - private final Map provideSuggestions = new TreeMap<>(); - - private List 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 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 getAllPlugins() { - if (allPlugins.isEmpty()) { - load(); - } - return allPlugins.stream().map(PluginData::getPlugin).collect(Collectors.toList()); - } - - public List getResolvedPlugins() { - return Collections.unmodifiableList(resolvedPlugins); - } - - private synchronized void resolve() { - Map> provides = allPlugins.stream() - .collect(Collectors.groupingBy(p -> p.getInfo().getProvides())); - List 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 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 entry : context.getOptionsMap().entrySet()) { - verifyOptions(entry.getKey(), entry.getValue()); - } - } - - private void verifyOptions(JadxPlugin plugin, JadxPluginOptions options) { - String pluginId = plugin.getPluginInfo().getPluginId(); - List 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 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 { - 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(); - } - } -} diff --git a/jadx-core/src/main/java/jadx/api/plugins/options/OptionDescription.java b/jadx-core/src/main/java/jadx/api/plugins/options/OptionDescription.java index 62355c137..7d1c822ca 100644 --- a/jadx-core/src/main/java/jadx/api/plugins/options/OptionDescription.java +++ b/jadx-core/src/main/java/jadx/api/plugins/options/OptionDescription.java @@ -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 getFlags() { + return Collections.emptySet(); + } } diff --git a/jadx-core/src/main/java/jadx/api/plugins/options/impl/JadxOptionDescription.java b/jadx-core/src/main/java/jadx/api/plugins/options/impl/JadxOptionDescription.java index b913c1dad..12b892543 100644 --- a/jadx-core/src/main/java/jadx/api/plugins/options/impl/JadxOptionDescription.java +++ b/jadx-core/src/main/java/jadx/api/plugins/options/impl/JadxOptionDescription.java @@ -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 values; private final OptionType type; + private final Set flags = EnumSet.noneOf(OptionFlag.class); public JadxOptionDescription(String name, String desc, @Nullable String defaultValue, List values) { this(name, desc, defaultValue, values, OptionType.STRING); @@ -51,6 +63,21 @@ public class JadxOptionDescription implements OptionDescription { return type; } + @Override + public Set 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 + '}'; diff --git a/jadx-core/src/main/java/jadx/api/plugins/pass/types/JadxAfterLoadPass.java b/jadx-core/src/main/java/jadx/api/plugins/pass/types/JadxAfterLoadPass.java index d1814d3c5..409f8b41c 100644 --- a/jadx-core/src/main/java/jadx/api/plugins/pass/types/JadxAfterLoadPass.java +++ b/jadx-core/src/main/java/jadx/api/plugins/pass/types/JadxAfterLoadPass.java @@ -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); diff --git a/jadx-core/src/main/java/jadx/api/plugins/pass/types/JadxDecompilePass.java b/jadx-core/src/main/java/jadx/api/plugins/pass/types/JadxDecompilePass.java index 7b74d7198..b4dc217c7 100644 --- a/jadx-core/src/main/java/jadx/api/plugins/pass/types/JadxDecompilePass.java +++ b/jadx-core/src/main/java/jadx/api/plugins/pass/types/JadxDecompilePass.java @@ -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); diff --git a/jadx-core/src/main/java/jadx/api/plugins/pass/types/JadxPassType.java b/jadx-core/src/main/java/jadx/api/plugins/pass/types/JadxPassType.java index cbfac39bc..a1aea70c3 100644 --- a/jadx-core/src/main/java/jadx/api/plugins/pass/types/JadxPassType.java +++ b/jadx-core/src/main/java/jadx/api/plugins/pass/types/JadxPassType.java @@ -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 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; } } diff --git a/jadx-core/src/main/java/jadx/api/plugins/pass/types/JadxPreparePass.java b/jadx-core/src/main/java/jadx/api/plugins/pass/types/JadxPreparePass.java index 6c338c2c8..9ac75cd47 100644 --- a/jadx-core/src/main/java/jadx/api/plugins/pass/types/JadxPreparePass.java +++ b/jadx-core/src/main/java/jadx/api/plugins/pass/types/JadxPreparePass.java @@ -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); diff --git a/jadx-core/src/main/java/jadx/core/plugins/JadxPluginManager.java b/jadx-core/src/main/java/jadx/core/plugins/JadxPluginManager.java new file mode 100644 index 000000000..b18cec1ec --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/plugins/JadxPluginManager.java @@ -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 allPlugins = new TreeSet<>(); + private final SortedSet resolvedPlugins = new TreeSet<>(); + private final Map 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 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 getAllPluginContexts() { + return allPlugins; + } + + public SortedSet getResolvedPluginContexts() { + return resolvedPlugins; + } + + private synchronized void resolve() { + Map> provides = allPlugins.stream() + .collect(Collectors.groupingBy(p -> p.getPluginInfo().getProvides())); + List 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 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 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 values = descObj.values(); + if (values == null) { + throw new IllegalArgumentException("Plugin option values is null, option: " + optName + ", plugin: " + pluginId); + } + }); + } + + public List 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); + } + } +} diff --git a/jadx-core/src/main/java/jadx/core/plugins/PluginContext.java b/jadx-core/src/main/java/jadx/core/plugins/PluginContext.java new file mode 100644 index 000000000..cfc8539be --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/plugins/PluginContext.java @@ -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 { + private final JadxDecompiler decompiler; + private final JadxPlugin plugin; + private final JadxPluginInfo pluginInfo; + private @Nullable JadxGuiContext guiContext; + + private final List codeInputs = new ArrayList<>(); + private @Nullable JadxPluginOptions options; + private @Nullable Supplier 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 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 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(); + } +} diff --git a/jadx-core/src/main/java/jadx/core/utils/Utils.java b/jadx-core/src/main/java/jadx/core/utils/Utils.java index 7f3eb40b3..ff02e6fc0 100644 --- a/jadx-core/src/main/java/jadx/core/utils/Utils.java +++ b/jadx-core/src/main/java/jadx/core/utils/Utils.java @@ -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('.', '/') + ';'; } diff --git a/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java b/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java index 78ef49551..401e949df 100644 --- a/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java @@ -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"); diff --git a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java index d07e28826..07396ee33 100644 --- a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java +++ b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java @@ -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() { diff --git a/jadx-gui/src/main/java/jadx/gui/cache/code/disk/DiskCodeCache.java b/jadx-gui/src/main/java/jadx/gui/cache/code/disk/DiskCodeCache.java index 9b6a1c1d7..96fea2112 100644 --- a/jadx-gui/src/main/java/jadx/gui/cache/code/disk/DiskCodeCache.java +++ b/jadx-gui/src/main/java/jadx/gui/cache/code/disk/DiskCodeCache.java @@ -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 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) { diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxProject.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxProject.java index b4874ede6..8f0080c8b 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxProject.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxProject.java @@ -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> 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) { diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsStorage.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsStorage.java index d19813c28..7340ccda3 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsStorage.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsStorage.java @@ -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); } diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java index 013212915..4216a9cc1 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java @@ -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 list = new CollectPluginOptions(mainWindow.getWrapper()).build(); - for (PluginWithOptions data : list) { - addPluginOptions(pluginsGroup, data.getPlugin(), data.getOptions()); + List 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 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 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 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 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; diff --git a/jadx-gui/src/main/java/jadx/gui/settings/TabStateViewAdapter.java b/jadx-gui/src/main/java/jadx/gui/settings/TabStateViewAdapter.java index 05275756f..d88b594ab 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/TabStateViewAdapter.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/TabStateViewAdapter.java @@ -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; } } diff --git a/jadx-gui/src/main/java/jadx/gui/settings/data/ProjectData.java b/jadx-gui/src/main/java/jadx/gui/settings/data/ProjectData.java index c1fb3050b..6706cf479 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/data/ProjectData.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/data/ProjectData.java @@ -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 searchHistory = new ArrayList<>(); + protected Map pluginOptions = new HashMap<>(); public List getFiles() { return files; @@ -122,4 +125,8 @@ public class ProjectData { public void setSearchHistory(List searchHistory) { this.searchHistory = searchHistory; } + + public Map getPluginOptions() { + return pluginOptions; + } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index 17ca39675..a769d7096 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -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 diff --git a/jadx-gui/src/main/java/jadx/gui/utils/plugins/CollectPluginOptions.java b/jadx-gui/src/main/java/jadx/gui/utils/plugins/CollectPluginOptions.java index e61077119..7bbf61871 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/plugins/CollectPluginOptions.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/plugins/CollectPluginOptions.java @@ -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, PluginWithOptions> plugins; public CollectPluginOptions(JadxWrapper wrapper) { this.wrapper = wrapper; - this.plugins = new HashMap<>(); } - public List build() { - wrapper.getCurrentDecompiler().ifPresent(decompiler -> { - List loadedPlugins = decompiler.getPluginManager().getResolvedPlugins(); - addOptions(decompiler, loadedPlugins); - }); + public List build() { + SortedSet 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 missingPlugins = new ArrayList<>(); - for (JadxPlugin plugin : pluginManager.getAllPlugins()) { - if (!plugins.containsKey(plugin.getClass())) { - missingPlugins.add(plugin); + pluginManager.load(); + SortedSet 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 loadedPlugins) { - Map 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); - } - } } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/ui/ActionHandler.java b/jadx-gui/src/main/java/jadx/gui/utils/ui/ActionHandler.java index 9faed3a0f..67b354599 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/ui/ActionHandler.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/ui/ActionHandler.java @@ -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); diff --git a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputOptions.java b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputOptions.java index 17d49273d..12da31ab1 100644 --- a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputOptions.java +++ b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputOptions.java @@ -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 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() { diff --git a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertOptions.java b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertOptions.java index 1ac71abd4..516728795 100644 --- a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertOptions.java +++ b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertOptions.java @@ -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); + } } diff --git a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java index 188ddfbb2..474c177d2 100644 --- a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java +++ b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java @@ -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 diff --git a/jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/RenameMappingsOptions.java b/jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/RenameMappingsOptions.java new file mode 100644 index 000000000..10cb386c8 --- /dev/null +++ b/jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/RenameMappingsOptions.java @@ -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 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 getMappingFormats() { + List 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; + } +} diff --git a/jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/RenameMappingsPlugin.java b/jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/RenameMappingsPlugin.java index 7da01ecf1..02d8377cb 100644 --- a/jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/RenameMappingsPlugin.java +++ b/jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/RenameMappingsPlugin.java @@ -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; } } diff --git a/jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/load/MappingsVisitor.java b/jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/load/ApplyMappingsPass.java similarity index 80% rename from jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/load/MappingsVisitor.java rename to jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/load/ApplyMappingsPass.java index e419c8ba9..06559ce94 100644 --- a/jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/load/MappingsVisitor.java +++ b/jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/load/ApplyMappingsPass.java @@ -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 } } diff --git a/jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/load/CodeMappingsVisitor.java b/jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/load/CodeMappingsPass.java similarity index 85% rename from jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/load/CodeMappingsVisitor.java rename to jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/load/CodeMappingsPass.java index 397f03a62..7ac2c80ca 100644 --- a/jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/load/CodeMappingsVisitor.java +++ b/jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/load/CodeMappingsPass.java @@ -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 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); diff --git a/jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/load/LoadMappingsPass.java b/jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/load/LoadMappingsPass.java new file mode 100644 index 000000000..a4b62e493 --- /dev/null +++ b/jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/load/LoadMappingsPass.java @@ -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); + } + } +}