diff --git a/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java b/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java index 6912572b..04114f5d 100644 --- a/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java +++ b/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java @@ -4,7 +4,7 @@ import java.io.PrintStream; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; -import java.util.LinkedHashMap; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -13,6 +13,7 @@ import java.util.function.Supplier; import org.jetbrains.annotations.Nullable; import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; import com.beust.jcommander.ParameterDescription; import com.beust.jcommander.ParameterException; import com.beust.jcommander.Parameterized; @@ -133,14 +134,16 @@ public class JCommanderWrapper { out.println("options:"); List params = jc.getParameters(); - Map paramsMap = new LinkedHashMap<>(params.size()); + Map paramsMap = new HashMap<>(params.size()); int maxNamesLen = 0; for (ParameterDescription p : params) { paramsMap.put(p.getParameterized().getName(), p); int len = p.getNames().length(); - if (len > maxNamesLen) { - maxNamesLen = len; + String valueDesc = getValueDesc(p); + if (valueDesc != null) { + len += 1 + valueDesc.length(); } + maxNamesLen = Math.max(maxNamesLen, len); } maxNamesLen += 3; @@ -153,8 +156,12 @@ public class JCommanderWrapper { } StringBuilder opt = new StringBuilder(); opt.append(" ").append(p.getNames()); - String description = p.getDescription(); + String valueDesc = getValueDesc(p); + if (valueDesc != null) { + opt.append(' ').append(valueDesc); + } addSpaces(opt, maxNamesLen - opt.length()); + String description = p.getDescription(); if (description.contains("\n")) { String[] lines = description.split("\n"); opt.append("- ").append(lines[0]); @@ -177,6 +184,11 @@ public class JCommanderWrapper { return maxNamesLen; } + private static @Nullable String getValueDesc(ParameterDescription p) { + Parameter parameterAnnotation = p.getParameterAnnotation(); + return parameterAnnotation == null ? null : parameterAnnotation.defaultValueDescription(); + } + /** * Get all declared fields of the specified class and all super classes */ diff --git a/jadx-cli/src/main/java/jadx/cli/commands/CommandPlugins.java b/jadx-cli/src/main/java/jadx/cli/commands/CommandPlugins.java index 5ee12ec6..88f1b072 100644 --- a/jadx-cli/src/main/java/jadx/cli/commands/CommandPlugins.java +++ b/jadx-cli/src/main/java/jadx/cli/commands/CommandPlugins.java @@ -12,6 +12,7 @@ import com.beust.jcommander.Parameters; import jadx.api.plugins.JadxPluginInfo; import jadx.cli.JCommanderWrapper; import jadx.cli.LogHelper; +import jadx.core.utils.StringUtils; import jadx.plugins.tools.JadxPluginsList; import jadx.plugins.tools.JadxPluginsTools; import jadx.plugins.tools.data.JadxPluginMetadata; @@ -20,33 +21,40 @@ import jadx.plugins.tools.data.JadxPluginUpdate; @Parameters(commandDescription = "manage jadx plugins") public class CommandPlugins implements ICommand { - @Parameter(names = { "-i", "--install" }, description = "install plugin with locationId") + @Parameter(names = { "-i", "--install" }, description = "install plugin with locationId", defaultValueDescription = "") protected String install; - @Parameter(names = { "-j", "--install-jar" }, description = "install plugin from jar file") + @Parameter(names = { "-j", "--install-jar" }, description = "install plugin from jar file", defaultValueDescription = "") protected String installJar; @Parameter(names = { "-l", "--list" }, description = "list installed plugins") protected boolean list; - @Parameter(names = { "--list-all" }, description = "list all plugins including bundled and dropins") - protected boolean listAll; - - @Parameter(names = { "-a", "--available" }, description = "list available plugins") + @Parameter(names = { "-a", "--available" }, description = "list available plugins from jadx-plugins-list (aka marketplace)") protected boolean available; @Parameter(names = { "-u", "--update" }, description = "update installed plugins") protected boolean update; - @Parameter(names = { "--uninstall" }, description = "uninstall plugin with pluginId") + @Parameter(names = { "--uninstall" }, description = "uninstall plugin with pluginId", defaultValueDescription = "") protected String uninstall; - @Parameter(names = { "--disable" }, description = "disable plugin with pluginId") + @Parameter(names = { "--disable" }, description = "disable plugin with pluginId", defaultValueDescription = "") protected String disable; - @Parameter(names = { "--enable" }, description = "enable plugin with pluginId") + @Parameter(names = { "--enable" }, description = "enable plugin with pluginId", defaultValueDescription = "") protected String enable; + @Parameter(names = { "--list-all" }, description = "list all plugins including bundled and dropins") + protected boolean listAll; + + @Parameter( + names = { "--list-versions" }, + description = "fetch latest versions of plugin from locationId (will download all artefacts, limited to 10)", + defaultValueDescription = "" + ) + protected String listVersions; + @Parameter(names = { "-h", "--help" }, description = "print this help", help = true) protected boolean printHelp = false; @@ -55,6 +63,7 @@ public class CommandPlugins implements ICommand { return "plugins"; } + @SuppressWarnings("UnnecessaryReturnStatement") @Override public void process(JCommanderWrapper jcw, JCommander subCommander) { if (printHelp) { @@ -71,13 +80,16 @@ public class CommandPlugins implements ICommand { if (install != null) { installPlugin(install); + return; } if (installJar != null) { installPlugin("file:" + installJar); + return; } if (uninstall != null) { boolean uninstalled = JadxPluginsTools.getInstance().uninstall(uninstall); System.out.println(uninstalled ? "Uninstalled" : "Plugin not found"); + return; } if (update) { List updates = JadxPluginsTools.getInstance().updateAll(); @@ -89,14 +101,20 @@ public class CommandPlugins implements ICommand { System.out.println(" " + update.getPluginId() + ": " + update.getOldVersion() + " -> " + update.getNewVersion()); } } + return; } if (list) { printPlugins(JadxPluginsTools.getInstance().getInstalled()); + return; } if (listAll) { printAllPlugins(); + return; + } + if (listVersions != null) { + printVersions(listVersions, 10); + return; } - if (available) { List availableList = JadxPluginsList.getInstance().get(); System.out.println("Available plugins: " + availableList.size()); @@ -104,6 +122,7 @@ public class CommandPlugins implements ICommand { System.out.println(" - " + plugin.getName() + ": " + plugin.getDescription() + " (" + plugin.getLocationId() + ")"); } + return; } if (disable != null) { @@ -112,6 +131,7 @@ public class CommandPlugins implements ICommand { } else { System.out.println("Plugin '" + disable + "' already disabled."); } + return; } if (enable != null) { if (JadxPluginsTools.getInstance().changeDisabledStatus(enable, false)) { @@ -119,6 +139,7 @@ public class CommandPlugins implements ICommand { } else { System.out.println("Plugin '" + enable + "' already enabled."); } + return; } } @@ -140,6 +161,26 @@ public class CommandPlugins implements ICommand { } } + private void printVersions(String locationId, int limit) { + System.out.println("Loading ..."); + List versions = JadxPluginsTools.getInstance().getVersionsByLocation(locationId, 1, limit); + if (versions.isEmpty()) { + System.out.println("No versions found"); + return; + } + JadxPluginMetadata plugin = versions.get(0); + System.out.println("Versions for plugin id: " + plugin.getPluginId()); + for (JadxPluginMetadata version : versions) { + StringBuilder sb = new StringBuilder(); + sb.append(" - ").append(version.getVersion()); + String reqVer = version.getRequiredJadxVersion(); + if (StringUtils.notBlank(reqVer)) { + sb.append(", require jadx: ").append(reqVer); + } + System.out.println(sb); + } + } + private static void printAllPlugins() { List installed = JadxPluginsTools.getInstance().getInstalled(); printPlugins(installed); diff --git a/jadx-core/src/main/java/jadx/api/plugins/JadxPluginInfo.java b/jadx-core/src/main/java/jadx/api/plugins/JadxPluginInfo.java index 08aa34ed..c092cc50 100644 --- a/jadx-core/src/main/java/jadx/api/plugins/JadxPluginInfo.java +++ b/jadx-core/src/main/java/jadx/api/plugins/JadxPluginInfo.java @@ -1,15 +1,29 @@ package jadx.api.plugins; +import org.jetbrains.annotations.Nullable; + public class JadxPluginInfo { private final String pluginId; private final String name; private final String description; - private final String homepage; + private String homepage; /** * Conflicting plugins should have the same 'provides' property; only one will be loaded */ - private final String provides; + private String provides; + + /** + * Minimum required jadx version to run this plugin. + *
+ * Format: ", r". + * Example: "1.5.1, r2305" + * + * @see wiki + * page + * for details. + */ + private @Nullable String requiredJadxVersion; public JadxPluginInfo(String id, String name, String description) { this(id, name, description, "", id); @@ -43,10 +57,26 @@ public class JadxPluginInfo { return homepage; } + public void setHomepage(String homepage) { + this.homepage = homepage; + } + public String getProvides() { return provides; } + public void setProvides(String provides) { + this.provides = provides; + } + + public @Nullable String getRequiredJadxVersion() { + return requiredJadxVersion; + } + + public void setRequiredJadxVersion(@Nullable String requiredJadxVersion) { + this.requiredJadxVersion = requiredJadxVersion; + } + @Override public String toString() { return pluginId + ": " + name + " - '" + description + '\''; diff --git a/jadx-core/src/main/java/jadx/api/plugins/JadxPluginInfoBuilder.java b/jadx-core/src/main/java/jadx/api/plugins/JadxPluginInfoBuilder.java index 2f036fb1..cfa84346 100644 --- a/jadx-core/src/main/java/jadx/api/plugins/JadxPluginInfoBuilder.java +++ b/jadx-core/src/main/java/jadx/api/plugins/JadxPluginInfoBuilder.java @@ -4,11 +4,14 @@ import java.util.Objects; import org.jetbrains.annotations.Nullable; +import jadx.core.plugins.versions.VerifyRequiredVersion; + public class JadxPluginInfoBuilder { private String pluginId; private String name; private String description; private String homepage = ""; + private @Nullable String requiredJadxVersion; private @Nullable String provides; /** @@ -43,6 +46,11 @@ public class JadxPluginInfoBuilder { return this; } + public JadxPluginInfoBuilder requiredJadxVersion(String versions) { + this.requiredJadxVersion = versions; + return this; + } + public JadxPluginInfo build() { Objects.requireNonNull(pluginId, "PluginId is required"); Objects.requireNonNull(name, "Name is required"); @@ -50,6 +58,11 @@ public class JadxPluginInfoBuilder { if (provides == null) { provides = pluginId; } - return new JadxPluginInfo(pluginId, name, description, homepage, provides); + if (requiredJadxVersion != null) { + VerifyRequiredVersion.verify(requiredJadxVersion); + } + JadxPluginInfo pluginInfo = new JadxPluginInfo(pluginId, name, description, homepage, provides); + pluginInfo.setRequiredJadxVersion(requiredJadxVersion); + return pluginInfo; } } diff --git a/jadx-core/src/main/java/jadx/core/plugins/JadxPluginManager.java b/jadx-core/src/main/java/jadx/core/plugins/JadxPluginManager.java index 4cd235a2..eb0d204a 100644 --- a/jadx-core/src/main/java/jadx/core/plugins/JadxPluginManager.java +++ b/jadx-core/src/main/java/jadx/core/plugins/JadxPluginManager.java @@ -21,6 +21,7 @@ import jadx.api.plugins.input.JadxCodeInput; import jadx.api.plugins.loader.JadxPluginLoader; import jadx.api.plugins.options.JadxPluginOptions; import jadx.api.plugins.options.OptionDescription; +import jadx.core.plugins.versions.VerifyRequiredVersion; import jadx.core.utils.exceptions.JadxRuntimeException; public class JadxPluginManager { @@ -50,15 +51,16 @@ public class JadxPluginManager { public void load(JadxPluginLoader pluginLoader) { allPlugins.clear(); + VerifyRequiredVersion verifyRequiredVersion = new VerifyRequiredVersion(); for (JadxPlugin plugin : pluginLoader.load()) { - addPlugin(plugin); + addPlugin(plugin, verifyRequiredVersion); } resolve(); } public void register(JadxPlugin plugin) { Objects.requireNonNull(plugin); - PluginContext addedPlugin = addPlugin(plugin); + PluginContext addedPlugin = addPlugin(plugin, new VerifyRequiredVersion()); if (addedPlugin == null) { LOG.debug("Can't register plugin, it was disabled: {}", plugin.getPluginInfo().getPluginId()); return; @@ -67,11 +69,17 @@ public class JadxPluginManager { resolve(); } - private @Nullable PluginContext addPlugin(JadxPlugin plugin) { + private @Nullable PluginContext addPlugin(JadxPlugin plugin, VerifyRequiredVersion verifyRequiredVersion) { PluginContext pluginContext = new PluginContext(decompiler, pluginsData, plugin); if (disabledPlugins.contains(pluginContext.getPluginId())) { return null; } + String requiredJadxVersion = pluginContext.getPluginInfo().getRequiredJadxVersion(); + if (!verifyRequiredVersion.isCompatible(requiredJadxVersion)) { + LOG.warn("Plugin '{}' not loaded: requires '{}' jadx version which it is not compatible with current: {}", + pluginContext, requiredJadxVersion, verifyRequiredVersion.getJadxVersion()); + return null; + } LOG.debug("Loading plugin: {}", pluginContext); if (!allPlugins.add(pluginContext)) { throw new IllegalArgumentException("Duplicate plugin id: " + pluginContext + ", class " + plugin.getClass()); diff --git a/jadx-core/src/main/java/jadx/core/plugins/versions/VerifyRequiredVersion.java b/jadx-core/src/main/java/jadx/core/plugins/versions/VerifyRequiredVersion.java new file mode 100644 index 00000000..94807c69 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/plugins/versions/VerifyRequiredVersion.java @@ -0,0 +1,85 @@ +package jadx.core.plugins.versions; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.jetbrains.annotations.Nullable; + +import jadx.core.Jadx; + +public class VerifyRequiredVersion { + + public static boolean isJadxCompatible(@Nullable String reqVersionStr) { + return new VerifyRequiredVersion().isCompatible(reqVersionStr); + } + + public static void verify(String requiredJadxVersion) { + try { + parse(requiredJadxVersion); + } catch (Exception e) { + throw new IllegalArgumentException("Malformed 'requiredJadxVersion': " + e.getMessage(), e); + } + } + + private final String jadxVersion; + private final boolean unstable; + + private final boolean dev; + + public VerifyRequiredVersion() { + this(Jadx.getVersion()); + } + + public VerifyRequiredVersion(String jadxVersion) { + this.jadxVersion = jadxVersion; + this.unstable = jadxVersion.startsWith("r"); + this.dev = jadxVersion.equals(Jadx.VERSION_DEV); + } + + public boolean isCompatible(@Nullable String reqVersionStr) { + if (reqVersionStr == null || reqVersionStr.isEmpty()) { + return true; + } + RequiredVersionData reqVer = parse(reqVersionStr); + if (dev) { + // keep version str parsing for verification + return true; + } + if (unstable) { + return VersionComparator.checkAndCompare(jadxVersion, reqVer.getUnstableRev()) >= 0; + } + return VersionComparator.checkAndCompare(jadxVersion, reqVer.getReleaseVer()) >= 0; + } + + public String getJadxVersion() { + return jadxVersion; + } + + private static final Pattern REQ_VER_FORMAT = Pattern.compile("(\\d+\\.\\d+\\.\\d+),\\s+(r\\d+)"); + + private static RequiredVersionData parse(String reqVersionStr) { + Matcher matcher = REQ_VER_FORMAT.matcher(reqVersionStr); + if (!matcher.matches()) { + throw new RuntimeException("Expect format: " + REQ_VER_FORMAT + ", got: " + reqVersionStr); + } + return new RequiredVersionData(matcher.group(1), matcher.group(2)); + } + + private static final class RequiredVersionData { + private final String releaseVer; + private final String unstableRev; + + private RequiredVersionData(String releaseVer, String unstableRev) { + this.releaseVer = releaseVer; + this.unstableRev = unstableRev; + } + + public String getReleaseVer() { + return releaseVer; + } + + public String getUnstableRev() { + return unstableRev; + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/update/VersionComparator.java b/jadx-core/src/main/java/jadx/core/plugins/versions/VersionComparator.java similarity index 97% rename from jadx-gui/src/main/java/jadx/gui/update/VersionComparator.java rename to jadx-core/src/main/java/jadx/core/plugins/versions/VersionComparator.java index c0f886ea..2f475738 100644 --- a/jadx-gui/src/main/java/jadx/gui/update/VersionComparator.java +++ b/jadx-core/src/main/java/jadx/core/plugins/versions/VersionComparator.java @@ -1,4 +1,4 @@ -package jadx.gui.update; +package jadx.core.plugins.versions; public class VersionComparator { diff --git a/jadx-core/src/test/java/jadx/core/plugins/versions/VerifyRequiredVersionTest.java b/jadx-core/src/test/java/jadx/core/plugins/versions/VerifyRequiredVersionTest.java new file mode 100644 index 00000000..21bd2a5b --- /dev/null +++ b/jadx-core/src/test/java/jadx/core/plugins/versions/VerifyRequiredVersionTest.java @@ -0,0 +1,27 @@ +package jadx.core.plugins.versions; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class VerifyRequiredVersionTest { + @Test + public void test() { + isCompatible("1.5.0, r2000", "1.5.1", true); + isCompatible("1.5.1, r3000", "1.5.1", true); + isCompatible("1.5.1, r3000", "1.6.0", true); + isCompatible("1.5.1, r3000", "1.5.0", false); + + isCompatible("1.5.1, r3000", "r3001.417bb7a", true); + isCompatible("1.5.1, r3000", "r4000", true); + isCompatible("1.5.1, r3000", "r3000", true); + isCompatible("1.5.1, r3000", "r2000", false); + } + + private static void isCompatible(String requiredVersion, String jadxVersion, boolean result) { + assertThat(new VerifyRequiredVersion(jadxVersion).isCompatible(requiredVersion)) + .as("Expect plugin with required version %s is%s compatible with jadx %s", + requiredVersion, result ? "" : " not", jadxVersion) + .isEqualTo(result); + } +} diff --git a/jadx-gui/src/test/java/jadx/gui/update/VersionComparatorTest.java b/jadx-core/src/test/java/jadx/core/plugins/versions/VersionComparatorTest.java similarity index 97% rename from jadx-gui/src/test/java/jadx/gui/update/VersionComparatorTest.java rename to jadx-core/src/test/java/jadx/core/plugins/versions/VersionComparatorTest.java index df07ae68..282996f1 100644 --- a/jadx-gui/src/test/java/jadx/gui/update/VersionComparatorTest.java +++ b/jadx-core/src/test/java/jadx/core/plugins/versions/VersionComparatorTest.java @@ -1,4 +1,4 @@ -package jadx.gui.update; +package jadx.core.plugins.versions; import org.junit.jupiter.api.Test; diff --git a/jadx-gui/src/main/java/jadx/gui/update/JadxUpdate.kt b/jadx-gui/src/main/java/jadx/gui/update/JadxUpdate.kt index 2e0fcd98..113fdaeb 100644 --- a/jadx-gui/src/main/java/jadx/gui/update/JadxUpdate.kt +++ b/jadx-gui/src/main/java/jadx/gui/update/JadxUpdate.kt @@ -5,6 +5,7 @@ import com.google.gson.annotations.SerializedName import io.github.oshai.kotlinlogging.KotlinLogging import jadx.api.JadxDecompiler import jadx.core.Jadx +import jadx.core.plugins.versions.VersionComparator import jadx.gui.settings.JadxUpdateChannel import org.jetbrains.kotlin.konan.file.use import java.io.InputStreamReader diff --git a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxExternalPluginsLoader.java b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxExternalPluginsLoader.java index 5338084d..d608d259 100644 --- a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxExternalPluginsLoader.java +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxExternalPluginsLoader.java @@ -1,24 +1,23 @@ package jadx.plugins.tools; import java.io.File; -import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Path; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.ServiceLoader; -import java.util.Set; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.JadxPlugin; import jadx.api.plugins.loader.JadxPluginLoader; +import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; public class JadxExternalPluginsLoader implements JadxPluginLoader { @@ -31,9 +30,8 @@ public class JadxExternalPluginsLoader implements JadxPluginLoader { close(); long start = System.currentTimeMillis(); Map, JadxPlugin> map = new HashMap<>(); - ClassLoader classLoader = JadxPluginsTools.class.getClassLoader(); - loadFromClsLoader(map, classLoader); - loadInstalledPlugins(map, classLoader); + loadFromClsLoader(map, thisClassLoader()); + loadInstalledPlugins(map); List list = new ArrayList<>(map.size()); list.addAll(map.values()); @@ -44,52 +42,53 @@ public class JadxExternalPluginsLoader implements JadxPluginLoader { return list; } - /** - * TODO: find a better way to load only plugin from single jar without plugins from parent - * classloader - */ public JadxPlugin loadFromJar(Path jar) { Map, JadxPlugin> map = new HashMap<>(); - ClassLoader classLoader = JadxPluginsTools.class.getClassLoader(); - loadFromClsLoader(map, classLoader); - Set> clspPlugins = new HashSet<>(map.keySet()); - try (URLClassLoader pluginClassLoader = loadFromJar(map, classLoader, jar)) { - return map.entrySet().stream() - .filter(entry -> !clspPlugins.contains(entry.getKey())) - .findFirst() - .map(Map.Entry::getValue) - .orElseThrow(() -> new RuntimeException("No plugin found in jar: " + jar)); - } catch (IOException e) { - throw new RuntimeException("Failed to load plugin jar: " + jar, e); + loadFromJar(map, jar); + int loaded = map.size(); + if (loaded == 0) { + throw new JadxRuntimeException("No plugin found in jar: " + jar); } + if (loaded > 1) { + String plugins = map.values().stream().map(p -> p.getPluginInfo().getPluginId()).collect(Collectors.joining(", ")); + throw new JadxRuntimeException("Expect only one plugin per jar: " + jar + ", but found: " + loaded + " - " + plugins); + } + return Utils.first(map.values()); + } private void loadFromClsLoader(Map, JadxPlugin> map, ClassLoader classLoader) { ServiceLoader.load(JadxPlugin.class, classLoader) .stream() + .filter(p -> p.type().getClassLoader() == classLoader) .filter(p -> !map.containsKey(p.type())) .forEach(p -> map.put(p.type(), p.get())); } - private void loadInstalledPlugins(Map, JadxPlugin> map, ClassLoader classLoader) { + private void loadInstalledPlugins(Map, JadxPlugin> map) { List jars = JadxPluginsTools.getInstance().getEnabledPluginJars(); for (Path jar : jars) { - classLoaders.add(loadFromJar(map, classLoader, jar)); + loadFromJar(map, jar); } } - private URLClassLoader loadFromJar(Map, JadxPlugin> map, ClassLoader classLoader, Path jar) { + private void loadFromJar(Map, JadxPlugin> map, Path jar) { try { File jarFile = jar.toFile(); + String clsLoaderName = "jadx-plugin:" + jarFile.getName(); URL[] urls = new URL[] { jarFile.toURI().toURL() }; - URLClassLoader pluginClsLoader = new URLClassLoader("jadx-plugin:" + jarFile.getName(), urls, classLoader); + URLClassLoader pluginClsLoader = new URLClassLoader(clsLoaderName, urls, thisClassLoader()); + classLoaders.add(pluginClsLoader); loadFromClsLoader(map, pluginClsLoader); - return pluginClsLoader; } catch (Exception e) { - throw new JadxRuntimeException("Failed to load plugins, jar: " + jar, e); + throw new JadxRuntimeException("Failed to load plugins from jar: " + jar, e); } } + private static ClassLoader thisClassLoader() { + return JadxExternalPluginsLoader.class.getClassLoader(); + } + @Override public void close() { try { diff --git a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxPluginsTools.java b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxPluginsTools.java index f70d6085..90425015 100644 --- a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxPluginsTools.java +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxPluginsTools.java @@ -16,12 +16,18 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import jadx.api.plugins.JadxPlugin; import jadx.api.plugins.JadxPluginInfo; +import jadx.core.Jadx; +import jadx.core.plugins.versions.VerifyRequiredVersion; +import jadx.core.utils.StringUtils; +import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; import jadx.plugins.tools.data.JadxInstalledPlugins; import jadx.plugins.tools.data.JadxPluginMetadata; @@ -35,6 +41,8 @@ import static jadx.plugins.tools.utils.PluginFiles.INSTALLED_DIR; import static jadx.plugins.tools.utils.PluginFiles.PLUGINS_JSON; public class JadxPluginsTools { + private static final Logger LOG = LoggerFactory.getLogger(JadxPluginsTools.class); + private static final JadxPluginsTools INSTANCE = new JadxPluginsTools(); public static JadxPluginsTools getInstance() { @@ -45,25 +53,62 @@ public class JadxPluginsTools { } public JadxPluginMetadata install(String locationId) { - JadxPluginMetadata pluginMetadata = resolveMetadata(locationId); - install(pluginMetadata); - return pluginMetadata; + IJadxPluginResolver resolver = ResolversRegistry.getResolver(locationId); + boolean hasVersion = resolver.hasVersion(locationId); + if (hasVersion) { + JadxPluginMetadata pluginMetadata = resolver.resolve(locationId) + .orElseThrow(() -> new JadxRuntimeException("Failed to resolve plugin location: " + locationId)); + fillMetadata(pluginMetadata); + install(pluginMetadata); + return pluginMetadata; + } + // try other versions in case latest is not compatible with current jadx + VerifyRequiredVersion verifyRequiredVersion = new VerifyRequiredVersion(); + for (int i = 1; i <= 5; i++) { + try { + for (JadxPluginMetadata pluginMetadata : resolver.resolveVersions(locationId, i, 1)) { + fillMetadata(pluginMetadata); + if (verifyRequiredVersion.isCompatible(pluginMetadata.getRequiredJadxVersion())) { + install(pluginMetadata); + return pluginMetadata; + } + } + } catch (Exception e) { + LOG.warn("Failed to fetch plugin ({} version before latest)", i, e); + } + } + throw new JadxRuntimeException("Can't find compatible version to install"); } public JadxPluginMetadata resolveMetadata(String locationId) { - JadxPluginMetadata pluginMetadata = ResolversRegistry.resolve(locationId) + IJadxPluginResolver resolver = ResolversRegistry.getResolver(locationId); + JadxPluginMetadata pluginMetadata = resolver.resolve(locationId) .orElseThrow(() -> new RuntimeException("Failed to resolve locationId: " + locationId)); fillMetadata(pluginMetadata); return pluginMetadata; } + public List getVersionsByLocation(String locationId, int page, int perPage) { + IJadxPluginResolver resolver = ResolversRegistry.getResolver(locationId); + List list = resolver.resolveVersions(locationId, page, perPage); + for (JadxPluginMetadata pluginMetadata : list) { + fillMetadata(pluginMetadata); + } + return list; + } + public List updateAll() { JadxInstalledPlugins plugins = loadPluginsJson(); int size = plugins.getInstalled().size(); List updates = new ArrayList<>(size); List newList = new ArrayList<>(size); for (JadxPluginMetadata plugin : plugins.getInstalled()) { - JadxPluginMetadata newVersion = update(plugin); + JadxPluginMetadata newVersion = null; + try { + newVersion = update(plugin); + } catch (Exception e) { + LOG.warn("Failed to update plugin: {}", plugin.getPluginId(), e); + } if (newVersion != null) { updates.add(new JadxPluginUpdate(plugin, newVersion)); newList.add(newVersion); @@ -135,7 +180,7 @@ public class JadxPluginsTools { for (JadxPluginMetadata pluginMetadata : loadPluginsJson().getInstalled()) { list.add(INSTALLED_DIR.resolve(pluginMetadata.getJar())); } - collectFromDir(list, DROPINS_DIR); + collectJarsFromDir(list, DROPINS_DIR); return list; } @@ -147,7 +192,7 @@ public class JadxPluginsTools { } list.add(INSTALLED_DIR.resolve(pluginMetadata.getJar())); } - collectFromDir(list, DROPINS_DIR); + collectJarsFromDir(list, DROPINS_DIR); return list; } @@ -172,7 +217,7 @@ public class JadxPluginsTools { } private @Nullable JadxPluginMetadata update(JadxPluginMetadata plugin) { - IJadxPluginResolver resolver = ResolversRegistry.getById(plugin.getResolverId()); + IJadxPluginResolver resolver = ResolversRegistry.getResolver(plugin.getLocationId()); if (!resolver.isUpdateSupported()) { return null; } @@ -189,19 +234,28 @@ public class JadxPluginsTools { return update; } - public void install(JadxPluginMetadata metadata) { + private void install(JadxPluginMetadata metadata) { + String reqVersionStr = metadata.getRequiredJadxVersion(); + if (!VerifyRequiredVersion.isJadxCompatible(reqVersionStr)) { + throw new JadxRuntimeException("Can't install plugin, required version: \"" + reqVersionStr + '\"' + + " is not compatible with current jadx version: " + Jadx.getVersion()); + } + String version = metadata.getVersion(); - String fileName = metadata.getPluginId() + (version != null ? '-' + version : "") + ".jar"; + String fileName = metadata.getPluginId() + (StringUtils.notBlank(version) ? '-' + version : "") + ".jar"; Path pluginJar = INSTALLED_DIR.resolve(fileName); copyJar(Paths.get(metadata.getJar()), pluginJar); metadata.setJar(INSTALLED_DIR.relativize(pluginJar).toString()); JadxInstalledPlugins plugins = loadPluginsJson(); // remove previous version jar - plugins.getInstalled().stream() - .filter(p -> p.getPluginId().equals(metadata.getPluginId())) - .forEach(this::deletePluginJar); - plugins.getInstalled().remove(metadata); + plugins.getInstalled().removeIf(p -> { + if (p.getPluginId().equals(metadata.getPluginId())) { + deletePluginJar(p); + return true; + } + return false; + }); plugins.getInstalled().add(metadata); plugins.setUpdated(System.currentTimeMillis()); savePluginsJson(plugins); @@ -227,6 +281,9 @@ public class JadxPluginsTools { metadata.setName(pluginInfo.getName()); metadata.setDescription(pluginInfo.getDescription()); metadata.setHomepage(pluginInfo.getHomepage()); + metadata.setRequiredJadxVersion(pluginInfo.getRequiredJadxVersion()); + } catch (NoSuchMethodError e) { + throw new RuntimeException("Looks like plugin uses unknown API, try to update jadx version", e); } } @@ -258,10 +315,14 @@ public class JadxPluginsTools { private JadxInstalledPlugins loadPluginsJson() { if (!Files.isRegularFile(PLUGINS_JSON)) { - return new JadxInstalledPlugins(); + JadxInstalledPlugins plugins = new JadxInstalledPlugins(); + plugins.setVersion(1); + return plugins; } try (Reader reader = Files.newBufferedReader(PLUGINS_JSON, StandardCharsets.UTF_8)) { - return buildGson().fromJson(reader, JadxInstalledPlugins.class); + JadxInstalledPlugins data = buildGson().fromJson(reader, JadxInstalledPlugins.class); + upgradePluginsData(data); + return data; } catch (Exception e) { throw new RuntimeException("Failed to read file: " + PLUGINS_JSON); } @@ -284,7 +345,13 @@ public class JadxPluginsTools { } } - private static void collectFromDir(List list, Path dir) { + private void upgradePluginsData(JadxInstalledPlugins data) { + if (data.getVersion() == 0) { + data.setVersion(1); + } + } + + private static void collectJarsFromDir(List list, Path dir) { try (Stream files = Files.list(dir)) { files.filter(p -> p.getFileName().toString().endsWith(".jar")).forEach(list::add); } catch (IOException e) { diff --git a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/data/JadxInstalledPlugins.java b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/data/JadxInstalledPlugins.java index 5310f357..bb413185 100644 --- a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/data/JadxInstalledPlugins.java +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/data/JadxInstalledPlugins.java @@ -4,11 +4,18 @@ import java.util.ArrayList; import java.util.List; public class JadxInstalledPlugins { - + private int version; private long updated; - private List installed = new ArrayList<>(); + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } + public long getUpdated() { return updated; } diff --git a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/data/JadxPluginMetadata.java b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/data/JadxPluginMetadata.java index 5e122b89..1965a434 100644 --- a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/data/JadxPluginMetadata.java +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/data/JadxPluginMetadata.java @@ -8,9 +8,10 @@ public class JadxPluginMetadata implements Comparable { private String name; private String description; private String homepage; + private @Nullable String requiredJadxVersion; + private @Nullable String version; private String locationId; - private String resolverId; private String jar; private boolean disabled; @@ -34,7 +35,7 @@ public class JadxPluginMetadata implements Comparable { return version; } - public void setVersion(String version) { + public void setVersion(@Nullable String version) { this.version = version; } @@ -54,6 +55,14 @@ public class JadxPluginMetadata implements Comparable { this.homepage = homepage; } + public @Nullable String getRequiredJadxVersion() { + return requiredJadxVersion; + } + + public void setRequiredJadxVersion(@Nullable String requiredJadxVersion) { + this.requiredJadxVersion = requiredJadxVersion; + } + public String getLocationId() { return locationId; } @@ -62,14 +71,6 @@ public class JadxPluginMetadata implements Comparable { this.locationId = locationId; } - public String getResolverId() { - return resolverId; - } - - public void setResolverId(String resolverId) { - this.resolverId = resolverId; - } - public String getJar() { return jar; } diff --git a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/IJadxPluginResolver.java b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/IJadxPluginResolver.java index c72757da..daeb1acd 100644 --- a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/IJadxPluginResolver.java +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/IJadxPluginResolver.java @@ -1,14 +1,37 @@ package jadx.plugins.tools.resolvers; +import java.util.List; import java.util.Optional; import jadx.plugins.tools.data.JadxPluginMetadata; public interface IJadxPluginResolver { + /** + * Unique resolver identifier, should be same as locationId prefix + */ String id(); + /** + * This resolver support updates and can fetch the latest version. + */ boolean isUpdateSupported(); + /** + * Fetch the latest version plugin metadata by location + */ Optional resolve(String locationId); + + /** + * Fetch several latest versions (pageable) of plugin by locationId. + * + * @param page page number, starts with 1 + * @param perPage result's count limit + */ + List resolveVersions(String locationId, int page, int perPage); + + /** + * Check if locationId has a specified version number + */ + boolean hasVersion(String locationId); } diff --git a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/ResolversRegistry.java b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/ResolversRegistry.java index 0a109215..aecc328f 100644 --- a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/ResolversRegistry.java +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/ResolversRegistry.java @@ -1,16 +1,15 @@ package jadx.plugins.tools.resolvers; +import java.util.HashMap; import java.util.Map; -import java.util.Optional; -import java.util.TreeMap; +import java.util.Objects; -import jadx.plugins.tools.data.JadxPluginMetadata; import jadx.plugins.tools.resolvers.file.LocalFileResolver; import jadx.plugins.tools.resolvers.github.GithubReleaseResolver; public class ResolversRegistry { - private static final Map RESOLVERS_MAP = new TreeMap<>(); + private static final Map RESOLVERS_MAP = new HashMap<>(); static { register(new LocalFileResolver()); @@ -21,14 +20,13 @@ public class ResolversRegistry { RESOLVERS_MAP.put(resolver.id(), resolver); } - public static Optional resolve(String locationId) { - for (IJadxPluginResolver resolver : RESOLVERS_MAP.values()) { - Optional result = resolver.resolve(locationId); - if (result.isPresent()) { - return result; - } + public static IJadxPluginResolver getResolver(String locationId) { + Objects.requireNonNull(locationId); + int sep = locationId.indexOf(':'); + if (sep <= 0) { + throw new IllegalArgumentException("Malformed locationId: " + locationId); } - return Optional.empty(); + return getById(locationId.substring(0, sep)); } public static IJadxPluginResolver getById(String resolverId) { diff --git a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/file/LocalFileResolver.java b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/file/LocalFileResolver.java index 51807a36..dee2c612 100644 --- a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/file/LocalFileResolver.java +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/file/LocalFileResolver.java @@ -1,6 +1,7 @@ package jadx.plugins.tools.resolvers.file; import java.io.File; +import java.util.List; import java.util.Optional; import jadx.plugins.tools.data.JadxPluginMetadata; @@ -30,8 +31,23 @@ public class LocalFileResolver implements IJadxPluginResolver { } JadxPluginMetadata metadata = new JadxPluginMetadata(); metadata.setLocationId(locationId); - metadata.setResolverId(id()); metadata.setJar(jarFile.getAbsolutePath()); return Optional.of(metadata); } + + @Override + public List resolveVersions(String locationId, int page, int perPage) { + if (page > 1) { + // no other versions + return List.of(); + } + // return only the current file + return resolve(locationId).map(List::of).orElseGet(List::of); + } + + @Override + public boolean hasVersion(String locationId) { + // no supported + return false; + } } diff --git a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/GithubReleaseResolver.java b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/GithubReleaseResolver.java index b45f128f..b7472a4c 100644 --- a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/GithubReleaseResolver.java +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/GithubReleaseResolver.java @@ -3,6 +3,7 @@ package jadx.plugins.tools.resolvers.github; import java.util.List; import java.util.Optional; import java.util.regex.Pattern; +import java.util.stream.Collectors; import jadx.core.utils.ListUtils; import jadx.plugins.tools.data.JadxPluginMetadata; @@ -22,16 +23,38 @@ public class GithubReleaseResolver implements IJadxPluginResolver { return Optional.empty(); } Release release = GithubTools.fetchRelease(info); + JadxPluginMetadata metadata = buildMetadata(release, info); + return Optional.of(metadata); + } + + @Override + public List resolveVersions(String locationId, int page, int perPage) { + LocationInfo info = parseLocation(locationId); + if (info == null) { + return List.of(); + } + return GithubTools.fetchReleases(info, page, perPage) + .stream() + .map(r -> buildMetadata(r, info)) + .collect(Collectors.toList()); + } + + @Override + public boolean hasVersion(String locationId) { + LocationInfo locationInfo = parseLocation(locationId); + return locationInfo != null && locationInfo.getVersion() != null; + } + + private JadxPluginMetadata buildMetadata(Release release, LocationInfo info) { List assets = release.getAssets(); String releaseVersion = removePrefix(release.getName(), "v"); Asset asset = searchPluginAsset(assets, info.getArtifactPrefix(), releaseVersion); JadxPluginMetadata metadata = new JadxPluginMetadata(); - metadata.setResolverId(id()); metadata.setVersion(releaseVersion); metadata.setLocationId(buildLocationIdWithoutVersion(info)); // exclude version for later updates metadata.setJar(asset.getDownloadUrl()); - return Optional.of(metadata); + return metadata; } private static LocationInfo parseLocation(String locationId) { @@ -87,7 +110,7 @@ public class GithubReleaseResolver implements IJadxPluginResolver { @Override public String id() { - return "github-release"; + return "github"; } @Override diff --git a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/GithubTools.java b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/GithubTools.java index 235b091b..4346f3bb 100644 --- a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/GithubTools.java +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/GithubTools.java @@ -30,8 +30,8 @@ public class GithubTools { // get latest version return get(projectUrl + "/releases/latest", RELEASE_TYPE); } - // search version among all releases (by name) - List releases = get(projectUrl + "/releases", RELEASE_LIST_TYPE); + // search version in other releases (by name) + List releases = fetchReleases(info, 1, 50); return releases.stream() .filter(r -> r.getName().equals(version)) .findFirst() @@ -39,6 +39,12 @@ public class GithubTools { + " Available versions: " + releases.stream().map(Release::getName).collect(Collectors.joining(", ")))); } + public static List fetchReleases(LocationInfo info, int page, int perPage) { + String projectUrl = GITHUB_API_URL + "repos/" + info.getOwner() + "/" + info.getProject(); + String requestUrl = projectUrl + "/releases?page=" + page + "&per_page=" + perPage; + return get(requestUrl, RELEASE_LIST_TYPE); + } + private static T get(String url, Type type) { HttpURLConnection con; try {