diff --git a/README.md b/README.md index 12440b4fc1f20dcc5dbbcc62619224adea54fb65..030bae1b792c4c34c708638b14fbd348b7969ff5 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 0f92086b5d9042177d5c824df9e7637d7b1a873d..7676203d4c1639afc32b4c8cc28090673641455b 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 ae4f2a5950b50fb93a0e4beb5d86d404684bda66..7cbf5441a689a17ae4898ba0781d8b86a8376d0f 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 aef39c9a5c2c4c454e6655911c161dbb768db73e..dad0546a71b8db032c29747a0ba5a3505065e074 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 03fc58817948f8325a70b6fe1a07ae4606dfad1c..5fb4c2a0d8e0eabed3dc373fcd004c07b074fa9e 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 a118a0deeee42ddf2e535777067759827ceff2cc..f18243cf08541bbbe3df02df853b9efc5e17ffb4 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 5d2a160c25532026b199c24e21704ad75010c810..05f4a2e783a4125573db777f4d958bb2f86ef7ec 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 cd345359b750ed72cdb1a974c0c84c9f1ef5b5e8..85f178197cacfc0e8ce9c330ec4f9c817b49ff64 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 dde3436ee334e51dcd6cf3d67cd6d5266eba4b37..5376fdf9a87ae97251e8f4912c95625318d9a6d5 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 e6f4a60756deddc016dfd736d6052915fb52d9df..f28cf50b6449e8cdf59d1896bdb5915d06405e2b 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 635d46e3ed9c6f90ed424e77c0d99b2b195ec87c..2d3d8715ae2932d6a259244535e59d3b2ee826c0 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 8b44e993e40058d441a4583e07f560cbc922d785..67716a6323655d26d088350696b62f153175e7e7 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 467b8813c7fb3a2c09f713f02a61c26690045813..65f0134e8b4c4e6a9846156006b46d8165ce919c 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 de352c91bbefe41b351907ffa68ff4ac6e366351..1591a4151a5ec4641afd6d3ebd1cf1f98a8cea34 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 22468a3b69c5690ec51dddf5b3e4752909683334..6fdb62979e4cc95d295eb741bcf4fc51344ba0a4 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 ef937d3ea3cf30542ba3498d6c205e3bbff32eda..d9faee563853913b66a5f3f3135065aa7ff80d92 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 6e468ce74ae2cd6aa46286331cdb21131d0b9fd2..0000000000000000000000000000000000000000 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 27fa338916b8096d03d447c0e6a78c6518073a97..d84faa8b12e17d6db87670f5cad08f8e8ddf5cf0 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 7933d60211db99b18e789301494603ffc0241477..71ab330bf6f01f0797ff4a80efbf117d0b41b295 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 e9670f72bad534c61f4ae1d94dbe4206a7abad6d..e1d1b37a2b40697d35934b7e2a629fa8b0218c5f 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 97e669471316179fa088e531a6ba62dd82819214..576302e9c68162bff4bcff16e1e6cde583ac74fb 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(); + } + } }