diff --git a/README.md b/README.md index 12440b4f..030bae1b 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ options: --cfg - save methods control flow graph to dot file --raw-cfg - save methods control flow graph (use raw instructions) -f, --fallback - make simple dump (using goto instead of 'if', 'for', etc) + --use-dx - use dx/d8 to convert java bytecode --comments-level - set code comments level, values: none, user_only, error, warn, info, debug, default: info --log-level - set log level, values: quiet, progress, error, warn, info, debug, default: progress -v, --verbose - verbose output (set --log-level to DEBUG) diff --git a/jadx-cli/build.gradle b/jadx-cli/build.gradle index 0f92086b..7676203d 100644 --- a/jadx-cli/build.gradle +++ b/jadx-cli/build.gradle @@ -7,6 +7,7 @@ dependencies { runtimeOnly(project(':jadx-plugins:jadx-dex-input')) runtimeOnly(project(':jadx-plugins:jadx-java-input')) + runtimeOnly(project(':jadx-plugins:jadx-java-convert')) runtimeOnly(project(':jadx-plugins:jadx-smali-input')) implementation 'com.beust:jcommander:1.81' diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java index ae4f2a59..7cbf5441 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java @@ -125,6 +125,9 @@ public class JadxCLIArgs { @Parameter(names = { "-f", "--fallback" }, description = "make simple dump (using goto instead of 'if', 'for', etc)") protected boolean fallbackMode = false; + @Parameter(names = { "--use-dx" }, description = "use dx/d8 to convert java bytecode") + protected boolean useDx = false; + @Parameter( names = { "--comments-level" }, description = "set code comments level, values: error, warn, info, debug, user_only, none", @@ -231,6 +234,7 @@ public class JadxCLIArgs { args.setRenameFlags(renameFlags); args.setFsCaseSensitive(fsCaseSensitive); args.setCommentsLevel(commentsLevel); + args.setUseDxInput(useDx); return args; } @@ -266,6 +270,10 @@ public class JadxCLIArgs { return fallbackMode; } + public boolean isUseDx() { + return useDx; + } + public boolean isShowInconsistentCode() { return showInconsistentCode; } 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 aef39c9a..dad0546a 100644 --- a/jadx-cli/src/main/java/jadx/cli/clst/ConvertToClsSet.java +++ b/jadx-cli/src/main/java/jadx/cli/clst/ConvertToClsSet.java @@ -39,6 +39,7 @@ public class ConvertToClsSet { Path output = inputPaths.remove(0); JadxPluginManager pluginManager = new JadxPluginManager(); + pluginManager.load(); List loadedInputs = new ArrayList<>(); for (JadxInputPlugin inputPlugin : pluginManager.getInputPlugins()) { loadedInputs.add(inputPlugin.loadFiles(inputPaths)); diff --git a/jadx-core/src/main/java/jadx/api/JadxArgs.java b/jadx-core/src/main/java/jadx/api/JadxArgs.java index 03fc5881..5fb4c2a0 100644 --- a/jadx-core/src/main/java/jadx/api/JadxArgs.java +++ b/jadx-core/src/main/java/jadx/api/JadxArgs.java @@ -85,6 +85,8 @@ public class JadxArgs { private CommentsLevel commentsLevel = CommentsLevel.INFO; + private boolean useDxInput = false; + public JadxArgs() { // use default options } @@ -423,6 +425,14 @@ public class JadxArgs { this.commentsLevel = commentsLevel; } + public boolean isUseDxInput() { + return useDxInput; + } + + public void setUseDxInput(boolean useDxInput) { + this.useDxInput = useDxInput; + } + @Override public String toString() { return "JadxArgs{" + "inputFiles=" + inputFiles @@ -454,6 +464,7 @@ public class JadxArgs { + ", commentsLevel=" + commentsLevel + ", codeCache=" + codeCache + ", codeWriter=" + codeWriterProvider.apply(this).getClass().getSimpleName() + + ", useDxInput=" + useDxInput + '}'; } } diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index a118a0de..f18243cf 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -107,6 +107,7 @@ public final class JadxDecompiler implements Closeable { reset(); JadxArgsValidator.validate(args); LOG.info("loading ..."); + loadPlugins(args); loadInputFiles(); root = new RootNode(args); @@ -159,6 +160,15 @@ public final class JadxDecompiler implements Closeable { reset(); } + private void loadPlugins(JadxArgs args) { + 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())); + } + } + public void registerPlugin(JadxPlugin plugin) { pluginManager.register(plugin); } diff --git a/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java b/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java index 5d2a160c..05f4a2e7 100644 --- a/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java +++ b/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java @@ -185,14 +185,11 @@ public abstract class IntegrationTest extends TestUtils { protected JadxDecompiler loadFiles(List inputFiles) { args.setInputFiles(inputFiles); + boolean useDx = !isJavaInput(); + LOG.info(useDx ? "Using dex input" : "Using java input"); + args.setUseDxInput(useDx); + JadxDecompiler d = new JadxDecompiler(args); - if (isJavaInput()) { - d.getPluginManager().unload("java-convert"); - LOG.info("Using java input"); - } else { - d.getPluginManager().unload("java-input"); - LOG.info("Using dex input"); - } try { d.load(); } catch (Exception e) { diff --git a/jadx-core/src/test/java/jadx/tests/external/BaseExternalTest.java b/jadx-core/src/test/java/jadx/tests/external/BaseExternalTest.java index cd345359..85f17819 100644 --- a/jadx-core/src/test/java/jadx/tests/external/BaseExternalTest.java +++ b/jadx-core/src/test/java/jadx/tests/external/BaseExternalTest.java @@ -51,7 +51,6 @@ public abstract class BaseExternalTest extends IntegrationTest { protected JadxDecompiler decompile(JadxArgs jadxArgs, @Nullable String clsPatternStr, @Nullable String mthPatternStr) { JadxDecompiler jadx = new JadxDecompiler(jadxArgs); - jadx.getPluginManager().unload("java-convert"); jadx.load(); if (clsPatternStr == null) { diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java index dde3436e..5376fdf9 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java @@ -278,6 +278,10 @@ public class JadxSettings extends JadxCLIArgs { this.fallbackMode = fallbackMode; } + public void setUseDx(boolean useDx) { + this.useDx = useDx; + } + public void setSkipResources(boolean skipResources) { this.skipResources = skipResources; } 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 e6f4a607..f28cf50b 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java @@ -412,6 +412,13 @@ public class JadxSettingsWindow extends JDialog { needReload(); }); + JCheckBox useDx = new JCheckBox(); + useDx.setSelected(settings.isUseDx()); + useDx.addItemListener(e -> { + settings.setUseDx(e.getStateChange() == ItemEvent.SELECTED); + needReload(); + }); + JCheckBox showInconsistentCode = new JCheckBox(); showInconsistentCode.setSelected(settings.isShowInconsistentCode()); showInconsistentCode.addItemListener(e -> { @@ -522,6 +529,7 @@ public class JadxSettingsWindow extends JDialog { other.addRow(NLS.str("preferences.inlineMethods"), inlineMethods); other.addRow(NLS.str("preferences.fsCaseSensitive"), fsCaseSensitive); other.addRow(NLS.str("preferences.fallback"), fallback); + other.addRow(NLS.str("preferences.useDx"), useDx); other.addRow(NLS.str("preferences.skipResourcesDecode"), resourceDecode); other.addRow(NLS.str("preferences.commentsLevel"), commentsLevel); return other; diff --git a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties index 635d46e3..2d3d8715 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -124,6 +124,7 @@ preferences.language=Sprache preferences.lineNumbersMode=Editor Zeilennummern-Modus preferences.check_for_updates=Nach Updates beim Start suchen preferences.fallback=Zwischencode ausgeben (einfacher Speicherauszug) +#preferences.useDx=Use dx/d8 to convert java bytecode preferences.showInconsistentCode=Inkonsistenten Code anzeigen preferences.escapeUnicode=Unicodezeichen escapen preferences.replaceConsts=Konstanten ersetzen diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index 8b44e993..67716a63 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -124,6 +124,7 @@ preferences.language=Language preferences.lineNumbersMode=Editor line numbers mode preferences.check_for_updates=Check for updates on startup preferences.fallback=Fallback mode (simple dump) +preferences.useDx=Use dx/d8 to convert java bytecode preferences.showInconsistentCode=Show inconsistent code preferences.escapeUnicode=Escape unicode preferences.replaceConsts=Replace constants diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties index 467b8813..65f0134e 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -124,6 +124,7 @@ preferences.language=Idioma #preferences.lineNumbersMode=Editor line numbers mode preferences.check_for_updates=Buscar actualizaciones al iniciar preferences.fallback=Modo fallback (simple dump) +#preferences.useDx=Use dx/d8 to convert java bytecode preferences.showInconsistentCode=Mostrar código inconsistente preferences.escapeUnicode=Escape unicode preferences.replaceConsts=Reemplazar constantes diff --git a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties index de352c91..1591a415 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -124,6 +124,7 @@ preferences.language=언어 preferences.lineNumbersMode=편집기 줄 번호 모드 preferences.check_for_updates=시작시 업데이트 확인 preferences.fallback=대체 모드 (단순 덤프) +#preferences.useDx=Use dx/d8 to convert java bytecode preferences.showInconsistentCode=디컴파일 안된 코드 표시 preferences.escapeUnicode=유니코드 이스케이프 preferences.replaceConsts=상수 바꾸기 diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties index 22468a3b..6fdb6297 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -124,6 +124,7 @@ preferences.language=语言 preferences.lineNumbersMode=编辑器行号模式 preferences.check_for_updates=启动时检查更新 preferences.fallback=输出中间代码 +#preferences.useDx=Use dx/d8 to convert java bytecode preferences.showInconsistentCode=显示不一致的代码 preferences.escapeUnicode=将 Unicode 字符转义 preferences.replaceConsts=替换常量 diff --git a/jadx-plugins/jadx-java-convert/build.gradle b/jadx-plugins/jadx-java-convert/build.gradle index ef937d3e..d9faee56 100644 --- a/jadx-plugins/jadx-java-convert/build.gradle +++ b/jadx-plugins/jadx-java-convert/build.gradle @@ -6,7 +6,7 @@ dependencies { api(project(":jadx-plugins:jadx-plugins-api")) implementation(project(":jadx-plugins:jadx-dex-input")) - implementation(files('lib/dx-1.16.jar')) + implementation('com.jakewharton.android.repackaged:dalvik-dx:11.0.0_r3') implementation('com.android.tools:r8:3.0.73') implementation 'org.ow2.asm:asm:9.2' diff --git a/jadx-plugins/jadx-java-convert/lib/dx-1.16.jar b/jadx-plugins/jadx-java-convert/lib/dx-1.16.jar deleted file mode 100644 index 6e468ce7..00000000 Binary files a/jadx-plugins/jadx-java-convert/lib/dx-1.16.jar and /dev/null differ diff --git a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertLoader.java b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertLoader.java index 27fa3389..d84faa8b 100644 --- a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertLoader.java +++ b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertLoader.java @@ -159,7 +159,7 @@ public class JavaConvertLoader { try { DxConverter.run(path, tempDirectory); } catch (Exception e) { - LOG.warn("DX convert failed, trying D8"); + LOG.warn("DX convert failed, trying D8, path: {}", path); D8Converter.run(path, tempDirectory); } 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 7933d602..71ab330b 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 @@ -13,7 +13,11 @@ public class JavaConvertPlugin implements JadxInputPlugin { @Override public JadxPluginInfo getPluginInfo() { - return new JadxPluginInfo("java-convert", "JavaConvert", "Convert .jar and .class files to dex"); + return new JadxPluginInfo( + "java-convert", + "JavaConvert", + "Convert .jar and .class files to dex", + "java-input"); } @Override diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/JadxPluginInfo.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/JadxPluginInfo.java index e9670f72..e1d1b37a 100644 --- a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/JadxPluginInfo.java +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/JadxPluginInfo.java @@ -5,10 +5,20 @@ public class JadxPluginInfo { private final String name; private final String description; + /** + * Conflicting plugins should have same 'provides' property, only one will be loaded + */ + private final String provides; + public JadxPluginInfo(String id, String name, String description) { - this.pluginId = id; + this(id, name, description, id); + } + + public JadxPluginInfo(String pluginId, String name, String description, String provides) { + this.pluginId = pluginId; this.name = name; this.description = description; + this.provides = provides; } public String getPluginId() { @@ -23,8 +33,12 @@ public class JadxPluginInfo { return description; } + public String getProvides() { + return provides; + } + @Override public String toString() { - return name + " - '" + description + '\''; + return pluginId + ": " + name + " - '" + description + '\''; } } diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/JadxPluginManager.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/JadxPluginManager.java index 97e66947..576302e9 100644 --- a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/JadxPluginManager.java +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/JadxPluginManager.java @@ -1,13 +1,17 @@ package jadx.api.plugins; import java.util.ArrayList; -import java.util.HashMap; +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; @@ -16,40 +20,142 @@ import jadx.api.plugins.input.JadxInputPlugin; public class JadxPluginManager { private static final Logger LOG = LoggerFactory.getLogger(JadxPluginManager.class); - private final Map, JadxPlugin> allPlugins = new HashMap<>(); + 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() { ServiceLoader jadxPlugins = ServiceLoader.load(JadxPlugin.class); - for (JadxPlugin jadxPlugin : jadxPlugins) { - register(jadxPlugin); + for (JadxPlugin plugin : jadxPlugins) { + addPlugin(plugin); } + resolve(); } public void register(JadxPlugin plugin) { Objects.requireNonNull(plugin); - LOG.debug("Register plugin: {}", plugin.getPluginInfo().getPluginId()); - allPlugins.put(plugin.getClass(), plugin); + PluginData addedPlugin = addPlugin(plugin); + LOG.debug("Register plugin: {}", addedPlugin.getPluginId()); + resolve(); + } + + private PluginData addPlugin(JadxPlugin plugin) { + PluginData pluginData = new PluginData(plugin, plugin.getPluginInfo()); + if (!allPlugins.add(pluginData)) { + throw new IllegalArgumentException("Duplicate plugin id: " + pluginData + ", class " + plugin.getClass()); + } + return pluginData; } public boolean unload(String pluginId) { - return allPlugins.values().removeIf(p -> { - String id = p.getPluginInfo().getPluginId(); + 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() { - return new ArrayList<>(allPlugins.values()); + return allPlugins.stream().map(PluginData::getPlugin).collect(Collectors.toList()); + } + + public List getResolvedPlugins() { + return Collections.unmodifiableList(resolvedPlugins); } public List getInputPlugins() { - return allPlugins.values().stream() + return resolvedPlugins.stream() .filter(JadxInputPlugin.class::isInstance) .map(JadxInputPlugin.class::cast) .collect(Collectors.toList()); } + + 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()); + } + + 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(); + } + } }