diff --git a/README.md b/README.md index e2f61911b..be142cc45 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,12 @@ and also packed to `build/jadx-.zip` ### Usage ``` -jadx[-gui] [options] (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab) +jadx[-gui] [command] [options] (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab) +commands (use ' --help' for command options): + plugins - manage jadx plugins + +options: + -d, --output-dir - output directory options: -d, --output-dir - output directory -ds, --output-dir-src - output directory for sources diff --git a/jadx-cli/build.gradle b/jadx-cli/build.gradle index 9ddf18932..925cbe427 100644 --- a/jadx-cli/build.gradle +++ b/jadx-cli/build.gradle @@ -7,6 +7,7 @@ plugins { dependencies { implementation(project(':jadx-core')) + implementation(project(':jadx-plugins-tools')) runtimeOnly(project(':jadx-plugins:jadx-dex-input')) runtimeOnly(project(':jadx-plugins:jadx-java-input')) diff --git a/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java b/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java index 6194c2dde..a275611fd 100644 --- a/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java +++ b/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java @@ -25,13 +25,17 @@ import jadx.api.plugins.options.OptionDescription; import jadx.core.plugins.JadxPluginManager; import jadx.core.plugins.PluginContext; import jadx.core.utils.Utils; +import jadx.plugins.tools.JadxExternalPluginsLoader; public class JCommanderWrapper { private final JCommander jc; private final JadxCLIArgs argsObj; public JCommanderWrapper(JadxCLIArgs argsObj) { - this.jc = JCommander.newBuilder().addObject(argsObj).build(); + JCommander.Builder builder = JCommander.newBuilder().addObject(argsObj); + builder.acceptUnknownOptions(true); // workaround for "default" command + JadxCLICommands.append(builder); + this.jc = builder.build(); this.argsObj = argsObj; } @@ -46,6 +50,14 @@ public class JCommanderWrapper { } } + public boolean processCommands() { + String parsedCommand = jc.getParsedCommand(); + if (parsedCommand == null) { + return false; + } + return JadxCLICommands.process(this, jc, parsedCommand); + } + public void overrideProvided(JadxCLIArgs obj) { List fieldsParams = jc.getParameters(); List parameters = new ArrayList<>(1 + fieldsParams.size()); @@ -73,6 +85,10 @@ public class JCommanderWrapper { return value; } + public List getUnknownOptions() { + return jc.getUnknownOptions(); + } + public void printUsage() { LogHelper.setLogLevel(LogHelper.LogLevelEnum.ERROR); // mute logger while printing help @@ -81,7 +97,32 @@ public class JCommanderWrapper { out.println(); out.println("jadx - dex to java decompiler, version: " + JadxDecompiler.getVersion()); out.println(); - out.println("usage: jadx [options] " + jc.getMainParameterDescription()); + out.println("usage: jadx [command] [options] " + jc.getMainParameterDescription()); + + out.println("commands (use ' --help' for command options):"); + for (String command : jc.getCommands().keySet()) { + out.println(" " + command + "\t - " + jc.getUsageFormatter().getCommandDescription(command)); + } + out.println(); + + int maxNamesLen = printOptions(jc, out, true); + out.println(appendPluginOptions(maxNamesLen)); + out.println(); + out.println("Examples:"); + out.println(" jadx -d out classes.dex"); + out.println(" jadx --rename-flags \"none\" classes.dex"); + out.println(" jadx --rename-flags \"valid, printable\" classes.dex"); + out.println(" jadx --log-level ERROR app.apk"); + out.println(" jadx -Pdex-input.verify-checksum=no app.apk"); + } + + public void printUsage(JCommander subCommander) { + PrintStream out = System.out; + out.println("usage: " + subCommander.getProgramName() + " [options]"); + printOptions(subCommander, out, false); + } + + private static int printOptions(JCommander jc, PrintStream out, boolean addDefaults) { out.println("options:"); List params = jc.getParameters(); @@ -96,7 +137,7 @@ public class JCommanderWrapper { } maxNamesLen += 3; - JadxCLIArgs args = (JadxCLIArgs) jc.getObjects().get(0); + Object args = jc.getObjects().get(0); for (Field f : getFields(args.getClass())) { String name = f.getName(); ParameterDescription p = paramsMap.get(name); @@ -118,26 +159,21 @@ public class JCommanderWrapper { } else { opt.append("- ").append(description); } - String defaultValue = getDefaultValue(args, f, opt); - if (defaultValue != null && !description.contains("(default)")) { - opt.append(", default: ").append(defaultValue); + if (addDefaults) { + String defaultValue = getDefaultValue(args, f, opt); + if (defaultValue != null && !description.contains("(default)")) { + opt.append(", default: ").append(defaultValue); + } } out.println(opt); } - out.println(appendPluginOptions(maxNamesLen)); - out.println(); - out.println("Examples:"); - out.println(" jadx -d out classes.dex"); - out.println(" jadx --rename-flags \"none\" classes.dex"); - out.println(" jadx --rename-flags \"valid, printable\" classes.dex"); - out.println(" jadx --log-level ERROR app.apk"); - out.println(" jadx -Pdex-input.verify-checksum=no app.apk"); + return maxNamesLen; } /** * Get all declared fields of the specified class and all super classes */ - private List getFields(Class clazz) { + private static List getFields(Class clazz) { List fieldList = new ArrayList<>(); while (clazz != null) { fieldList.addAll(Arrays.asList(clazz.getDeclaredFields())); @@ -147,7 +183,7 @@ public class JCommanderWrapper { } @Nullable - private String getDefaultValue(JadxCLIArgs args, Field f, StringBuilder opt) { + private static String getDefaultValue(Object args, Field f, StringBuilder opt) { try { Class fieldType = f.getType(); if (fieldType == int.class) { @@ -180,7 +216,7 @@ public class JCommanderWrapper { // load and init all options plugins to print all options try (JadxDecompiler decompiler = new JadxDecompiler(new JadxArgs())) { JadxPluginManager pluginManager = decompiler.getPluginManager(); - pluginManager.load(); + pluginManager.load(new JadxExternalPluginsLoader()); pluginManager.initAll(); for (PluginContext context : pluginManager.getAllPluginContexts()) { JadxPluginOptions options = context.getOptions(); diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLI.java b/jadx-cli/src/main/java/jadx/cli/JadxCLI.java index fb0882f65..e76285a79 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLI.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLI.java @@ -10,6 +10,7 @@ import jadx.api.impl.SimpleCodeWriter; import jadx.cli.LogHelper.LogLevelEnum; import jadx.core.utils.exceptions.JadxArgsValidateException; import jadx.core.utils.files.FileUtils; +import jadx.plugins.tools.JadxExternalPluginsLoader; public class JadxCLI { private static final Logger LOG = LoggerFactory.getLogger(JadxCLI.class); @@ -44,6 +45,7 @@ public class JadxCLI { JadxArgs jadxArgs = cliArgs.toJadxArgs(); jadxArgs.setCodeCache(new NoOpCodeCache()); jadxArgs.setCodeWriterProvider(SimpleCodeWriter::new); + jadxArgs.setPluginLoader(new JadxExternalPluginsLoader()); try (JadxDecompiler jadx = new JadxDecompiler(jadxArgs)) { jadx.load(); if (checkForErrors(jadx)) { diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java index 1fe530a0f..b98c4e840 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java @@ -264,6 +264,10 @@ public class JadxCLIArgs { } private boolean process(JCommanderWrapper jcw) { + files.addAll(jcw.getUnknownOptions()); + if (jcw.processCommands()) { + return false; + } if (printHelp) { jcw.printUsage(); return false; diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLICommands.java b/jadx-cli/src/main/java/jadx/cli/JadxCLICommands.java new file mode 100644 index 000000000..69a6a06e0 --- /dev/null +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLICommands.java @@ -0,0 +1,35 @@ +package jadx.cli; + +import java.util.Map; +import java.util.TreeMap; + +import com.beust.jcommander.JCommander; + +import jadx.cli.commands.CommandPlugins; +import jadx.cli.commands.ICommand; + +public class JadxCLICommands { + private static final Map COMMANDS_MAP = new TreeMap<>(); + + static { + JadxCLICommands.register(new CommandPlugins()); + } + + public static void register(ICommand command) { + COMMANDS_MAP.put(command.name(), command); + } + + public static void append(JCommander.Builder builder) { + COMMANDS_MAP.forEach(builder::addCommand); + } + + public static boolean process(JCommanderWrapper jcw, JCommander jc, String parsedCommand) { + ICommand command = COMMANDS_MAP.get(parsedCommand); + if (command == null) { + throw new IllegalArgumentException("Unknown command: " + parsedCommand); + } + JCommander subCommander = jc.getCommands().get(parsedCommand); + command.process(jcw, subCommander); + return true; + } +} 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 0e44a86a9..7c9b6745c 100644 --- a/jadx-cli/src/main/java/jadx/cli/clst/ConvertToClsSet.java +++ b/jadx-cli/src/main/java/jadx/cli/clst/ConvertToClsSet.java @@ -15,6 +15,7 @@ import jadx.api.JadxArgs; import jadx.api.JadxDecompiler; import jadx.api.plugins.input.ICodeLoader; import jadx.api.plugins.input.JadxCodeInput; +import jadx.api.plugins.loader.JadxBasePluginLoader; import jadx.core.clsp.ClsSet; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.RootNode; @@ -43,7 +44,7 @@ public class ConvertToClsSet { jadxArgs.setRenameFlags(EnumSet.noneOf(JadxArgs.RenameEnum.class)); try (JadxDecompiler decompiler = new JadxDecompiler(jadxArgs)) { JadxPluginManager pluginManager = decompiler.getPluginManager(); - pluginManager.load(); + pluginManager.load(new JadxBasePluginLoader()); pluginManager.initResolved(); List loadedInputs = new ArrayList<>(); for (JadxCodeInput inputPlugin : pluginManager.getCodeInputs()) { diff --git a/jadx-cli/src/main/java/jadx/cli/commands/CommandPlugins.java b/jadx-cli/src/main/java/jadx/cli/commands/CommandPlugins.java new file mode 100644 index 000000000..7a4332f6d --- /dev/null +++ b/jadx-cli/src/main/java/jadx/cli/commands/CommandPlugins.java @@ -0,0 +1,81 @@ +package jadx.cli.commands; + +import java.util.List; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; + +import jadx.cli.JCommanderWrapper; +import jadx.plugins.tools.JadxPluginsTools; +import jadx.plugins.tools.data.JadxPluginMetadata; +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") + protected String install; + + @Parameter(names = { "-j", "--install-jar" }, description = "install plugin from jar file") + protected String installJar; + + @Parameter(names = { "-l", "--list" }, description = "list installed plugins") + protected boolean list; + + @Parameter(names = { "-u", "--update" }, description = "update installed plugins") + protected boolean update; + + @Parameter(names = { "--uninstall" }, description = "uninstall plugin with pluginId") + protected String uninstall; + + @Parameter(names = { "-h", "--help" }, description = "print this help", help = true) + protected boolean printHelp = false; + + @Override + public String name() { + return "plugins"; + } + + @Override + public void process(JCommanderWrapper jcw, JCommander subCommander) { + if (printHelp) { + jcw.printUsage(subCommander); + return; + } + if (install != null) { + installPlugin(install); + } + if (installJar != null) { + installPlugin("file:" + installJar); + } + if (uninstall != null) { + boolean uninstalled = JadxPluginsTools.getInstance().uninstall(uninstall); + System.out.println(uninstalled ? "Uninstalled" : "Plugin not found"); + } + if (update) { + List updates = JadxPluginsTools.getInstance().updateAll(); + if (updates.isEmpty()) { + System.out.println("No updates"); + } else { + System.out.println("Installed updates: " + updates.size()); + for (JadxPluginUpdate update : updates) { + System.out.println(" " + update.getPluginId() + ": " + update.getOldVersion() + " -> " + update.getNewVersion()); + } + } + } + if (list) { + List installed = JadxPluginsTools.getInstance().getInstalled(); + System.out.println("Installed plugins: " + installed.size()); + for (JadxPluginMetadata plugin : installed) { + System.out.println(" " + plugin.getPluginId() + ":" + plugin.getVersion() + + " - " + plugin.getName() + ": " + plugin.getDescription()); + } + } + } + + private void installPlugin(String locationId) { + JadxPluginMetadata plugin = JadxPluginsTools.getInstance().install(locationId); + System.out.println("Plugin installed: " + plugin.getPluginId() + ":" + plugin.getVersion()); + } +} diff --git a/jadx-cli/src/main/java/jadx/cli/commands/ICommand.java b/jadx-cli/src/main/java/jadx/cli/commands/ICommand.java new file mode 100644 index 000000000..3cfd0f273 --- /dev/null +++ b/jadx-cli/src/main/java/jadx/cli/commands/ICommand.java @@ -0,0 +1,11 @@ +package jadx.cli.commands; + +import com.beust.jcommander.JCommander; + +import jadx.cli.JCommanderWrapper; + +public interface ICommand { + String name(); + + void process(JCommanderWrapper jcw, JCommander subCommander); +} diff --git a/jadx-core/src/main/java/jadx/api/JadxArgs.java b/jadx-core/src/main/java/jadx/api/JadxArgs.java index 5641f14da..5e7f47984 100644 --- a/jadx-core/src/main/java/jadx/api/JadxArgs.java +++ b/jadx-core/src/main/java/jadx/api/JadxArgs.java @@ -25,6 +25,8 @@ import jadx.api.deobf.IAliasProvider; import jadx.api.deobf.IRenameCondition; import jadx.api.impl.AnnotatedCodeWriter; import jadx.api.impl.InMemoryCodeCache; +import jadx.api.plugins.loader.JadxBasePluginLoader; +import jadx.api.plugins.loader.JadxPluginLoader; import jadx.api.usage.IUsageInfoCache; import jadx.api.usage.impl.InMemoryUsageInfoCache; import jadx.core.deobf.DeobfAliasProvider; @@ -150,6 +152,8 @@ public class JadxArgs implements Closeable { private Map pluginOptions = new HashMap<>(); + private JadxPluginLoader pluginLoader = new JadxBasePluginLoader(); + public JadxArgs() { // use default options } @@ -170,6 +174,9 @@ public class JadxArgs implements Closeable { if (usageInfoCache != null) { usageInfoCache.close(); } + if (pluginLoader != null) { + pluginLoader.close(); + } } catch (Exception e) { LOG.error("Failed to close JadxArgs", e); } finally { @@ -634,6 +641,14 @@ public class JadxArgs implements Closeable { this.pluginOptions = pluginOptions; } + public JadxPluginLoader getPluginLoader() { + return pluginLoader; + } + + public void setPluginLoader(JadxPluginLoader pluginLoader) { + this.pluginLoader = pluginLoader; + } + /** * Hash of all options that can change result code */ diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index 5965a0cfc..7f64165e6 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -182,7 +182,7 @@ public final class JadxDecompiler implements Closeable { private void loadPlugins() { pluginManager.providesSuggestion("java-input", args.isUseDxInput() ? "java-convert" : "java-input"); - pluginManager.load(); + pluginManager.load(args.getPluginLoader()); if (LOG.isDebugEnabled()) { LOG.debug("Resolved plugins: {}", pluginManager.getResolvedPluginContexts()); } diff --git a/jadx-core/src/main/java/jadx/api/plugins/loader/JadxBasePluginLoader.java b/jadx-core/src/main/java/jadx/api/plugins/loader/JadxBasePluginLoader.java new file mode 100644 index 000000000..6ca571136 --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/plugins/loader/JadxBasePluginLoader.java @@ -0,0 +1,29 @@ +package jadx.api.plugins.loader; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.ServiceLoader; + +import jadx.api.plugins.JadxPlugin; + +/** + * Loading plugins from current classpath + */ +public class JadxBasePluginLoader implements JadxPluginLoader { + + @Override + public List load() { + List list = new ArrayList<>(); + ServiceLoader plugins = ServiceLoader.load(JadxPlugin.class); + for (JadxPlugin plugin : plugins) { + list.add(plugin); + } + return list; + } + + @Override + public void close() throws IOException { + // nothing to close + } +} diff --git a/jadx-core/src/main/java/jadx/api/plugins/loader/JadxPluginLoader.java b/jadx-core/src/main/java/jadx/api/plugins/loader/JadxPluginLoader.java new file mode 100644 index 000000000..9835311c5 --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/plugins/loader/JadxPluginLoader.java @@ -0,0 +1,11 @@ +package jadx.api.plugins.loader; + +import java.io.Closeable; +import java.util.List; + +import jadx.api.plugins.JadxPlugin; + +public interface JadxPluginLoader extends Closeable { + + List load(); +} 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 b18cec1ec..6728bf54d 100644 --- a/jadx-core/src/main/java/jadx/core/plugins/JadxPluginManager.java +++ b/jadx-core/src/main/java/jadx/core/plugins/JadxPluginManager.java @@ -4,7 +4,6 @@ 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; @@ -18,6 +17,7 @@ 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.loader.JadxPluginLoader; import jadx.api.plugins.options.JadxPluginOptions; import jadx.api.plugins.options.OptionDescription; import jadx.core.utils.exceptions.JadxRuntimeException; @@ -43,10 +43,9 @@ public class JadxPluginManager { provideSuggestions.put(provides, pluginId); } - public void load() { + public void load(JadxPluginLoader pluginLoader) { allPlugins.clear(); - ServiceLoader jadxPlugins = ServiceLoader.load(JadxPlugin.class); - for (JadxPlugin plugin : jadxPlugins) { + for (JadxPlugin plugin : pluginLoader.load()) { addPlugin(plugin); } resolve(); diff --git a/jadx-gui/build.gradle b/jadx-gui/build.gradle index 0aac0b28a..18f0b1d59 100644 --- a/jadx-gui/build.gradle +++ b/jadx-gui/build.gradle @@ -9,6 +9,7 @@ plugins { dependencies { implementation(project(':jadx-core')) implementation(project(':jadx-cli')) + implementation(project(':jadx-plugins-tools')) // import mappings implementation project(':jadx-plugins:jadx-rename-mappings') diff --git a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java index 07396ee33..2569c4eaa 100644 --- a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java +++ b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java @@ -36,6 +36,7 @@ import jadx.gui.settings.JadxProject; import jadx.gui.settings.JadxSettings; import jadx.gui.ui.MainWindow; import jadx.gui.utils.CacheObject; +import jadx.plugins.tools.JadxExternalPluginsLoader; import static jadx.core.dex.nodes.ProcessState.GENERATED_AND_UNLOADED; import static jadx.core.dex.nodes.ProcessState.NOT_LOADED; @@ -61,6 +62,7 @@ public class JadxWrapper { synchronized (DECOMPILER_UPDATE_SYNC) { JadxProject project = getProject(); JadxArgs jadxArgs = getSettings().toJadxArgs(); + jadxArgs.setPluginLoader(new JadxExternalPluginsLoader()); project.fillJadxArgs(jadxArgs); decompiler = new JadxDecompiler(jadxArgs); 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 7bbf61871..d295ca27c 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 @@ -10,6 +10,7 @@ import jadx.api.JadxDecompiler; import jadx.core.plugins.JadxPluginManager; import jadx.core.plugins.PluginContext; import jadx.gui.JadxWrapper; +import jadx.plugins.tools.JadxExternalPluginsLoader; /** * Collect options from all plugins. @@ -32,7 +33,7 @@ public class CollectPluginOptions { // collect and init not loaded plugins in new context try (JadxDecompiler decompiler = new JadxDecompiler(new JadxArgs())) { JadxPluginManager pluginManager = decompiler.getPluginManager(); - pluginManager.load(); + pluginManager.load(new JadxExternalPluginsLoader()); SortedSet missingPlugins = new TreeSet<>(); for (PluginContext context : pluginManager.getAllPluginContexts()) { if (!allPlugins.contains(context)) { diff --git a/jadx-plugins-tools/build.gradle.kts b/jadx-plugins-tools/build.gradle.kts new file mode 100644 index 000000000..419263499 --- /dev/null +++ b/jadx-plugins-tools/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("jadx-library") +} + +dependencies { + api(project(":jadx-core")) + + implementation("dev.dirs:directories:26") + implementation("com.google.code.gson:gson:2.10.1") +} 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 new file mode 100644 index 000000000..9581df99c --- /dev/null +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxExternalPluginsLoader.java @@ -0,0 +1,107 @@ +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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.api.plugins.JadxPlugin; +import jadx.api.plugins.loader.JadxPluginLoader; +import jadx.core.utils.exceptions.JadxRuntimeException; + +public class JadxExternalPluginsLoader implements JadxPluginLoader { + private static final Logger LOG = LoggerFactory.getLogger(JadxExternalPluginsLoader.class); + + private final List classLoaders = new ArrayList<>(); + + @Override + public List load() { + close(); + long start = System.currentTimeMillis(); + Map, JadxPlugin> map = new HashMap<>(); + ClassLoader classLoader = JadxPluginsTools.class.getClassLoader(); + loadFromClsLoader(map, classLoader); + loadInstalledPlugins(map, classLoader); + + List list = new ArrayList<>(map.size()); + list.addAll(map.values()); + list.sort(Comparator.comparing(p -> p.getClass().getSimpleName())); + if (LOG.isDebugEnabled()) { + LOG.debug("Collected {} plugins in {}ms", list.size(), System.currentTimeMillis() - start); + } + 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); + } + } + + private void loadFromClsLoader(Map, JadxPlugin> map, ClassLoader classLoader) { + ServiceLoader.load(JadxPlugin.class, classLoader) + .stream() + .filter(p -> !map.containsKey(p.type())) + .forEach(p -> map.put(p.type(), p.get())); + } + + private void loadInstalledPlugins(Map, JadxPlugin> map, ClassLoader classLoader) { + List jars = JadxPluginsTools.getInstance().getAllPluginJars(); + for (Path jar : jars) { + classLoaders.add(loadFromJar(map, classLoader, jar)); + } + } + + private URLClassLoader loadFromJar(Map, JadxPlugin> map, ClassLoader classLoader, Path jar) { + try { + File jarFile = jar.toFile(); + URL[] urls = new URL[] { jarFile.toURI().toURL() }; + URLClassLoader pluginClsLoader = new URLClassLoader("jadx-plugin:" + jarFile.getName(), urls, classLoader); + loadFromClsLoader(map, pluginClsLoader); + return pluginClsLoader; + } catch (Exception e) { + throw new JadxRuntimeException("Failed to load plugins, jar: " + jar, e); + } + } + + @Override + public void close() { + try { + for (URLClassLoader classLoader : classLoaders) { + try { + classLoader.close(); + } catch (Exception e) { + // ignore + } + } + } finally { + classLoaders.clear(); + } + } +} 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 new file mode 100644 index 000000000..ede725434 --- /dev/null +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxPluginsTools.java @@ -0,0 +1,252 @@ +package jadx.plugins.tools; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.Writer; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import org.jetbrains.annotations.Nullable; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import dev.dirs.ProjectDirectories; + +import jadx.api.plugins.JadxPlugin; +import jadx.api.plugins.JadxPluginInfo; +import jadx.core.utils.files.FileUtils; +import jadx.plugins.tools.data.JadxInstalledPlugins; +import jadx.plugins.tools.data.JadxPluginMetadata; +import jadx.plugins.tools.data.JadxPluginUpdate; +import jadx.plugins.tools.resolvers.IJadxPluginResolver; +import jadx.plugins.tools.resolvers.ResolversRegistry; + +import static jadx.core.utils.files.FileUtils.makeDirs; + +public class JadxPluginsTools { + private static final JadxPluginsTools INSTANCE = new JadxPluginsTools(); + + public static JadxPluginsTools getInstance() { + return INSTANCE; + } + + private final Path pluginsJson; + private final Path dropins; + private final Path installed; + + private JadxPluginsTools() { + ProjectDirectories jadxDirs = ProjectDirectories.from("io.github", "skylot", "jadx"); + Path plugins = Paths.get(jadxDirs.configDir, "plugins"); // TODO: use dataDir? + makeDirs(plugins); + pluginsJson = plugins.resolve("plugins.json"); + dropins = plugins.resolve("dropins"); + makeDirs(dropins); + installed = plugins.resolve("installed"); + makeDirs(installed); + } + + public JadxPluginMetadata install(String locationId) { + JadxPluginMetadata pluginMetadata = ResolversRegistry.resolve(locationId) + .orElseThrow(() -> new RuntimeException("Failed to resolve locationId: " + locationId)); + install(pluginMetadata); + return pluginMetadata; + } + + 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); + if (newVersion != null) { + updates.add(new JadxPluginUpdate(plugin, newVersion)); + newList.add(newVersion); + } else { + newList.add(plugin); + } + } + if (!updates.isEmpty()) { + plugins.setUpdated(System.currentTimeMillis()); + plugins.setInstalled(newList); + savePluginsJson(plugins); + } + return updates; + } + + public Optional update(String pluginId) { + JadxInstalledPlugins plugins = loadPluginsJson(); + JadxPluginMetadata plugin = plugins.getInstalled().stream() + .filter(p -> p.getPluginId().equals(pluginId)) + .findFirst() + .orElseThrow(() -> new RuntimeException("Plugin not found: " + pluginId)); + + JadxPluginMetadata newVersion = update(plugin); + if (newVersion == null) { + return Optional.empty(); + } + plugins.setUpdated(System.currentTimeMillis()); + plugins.getInstalled().remove(plugin); + plugins.getInstalled().add(newVersion); + savePluginsJson(plugins); + return Optional.of(new JadxPluginUpdate(plugin, newVersion)); + } + + public boolean uninstall(String pluginId) { + JadxInstalledPlugins plugins = loadPluginsJson(); + Optional found = plugins.getInstalled().stream() + .filter(p -> p.getPluginId().equals(pluginId)) + .findFirst(); + if (found.isEmpty()) { + return false; + } + JadxPluginMetadata plugin = found.get(); + deletePluginJar(plugin); + plugins.getInstalled().remove(plugin); + savePluginsJson(plugins); + return true; + } + + private void deletePluginJar(JadxPluginMetadata plugin) { + try { + Files.deleteIfExists(installed.resolve(plugin.getJar())); + } catch (IOException e) { + // ignore + } + } + + public List getInstalled() { + return loadPluginsJson().getInstalled(); + } + + public List getAllPluginJars() { + List list = new ArrayList<>(); + for (JadxPluginMetadata pluginMetadata : loadPluginsJson().getInstalled()) { + list.add(installed.resolve(pluginMetadata.getJar())); + } + collectFromDir(list, dropins); + return list; + } + + private @Nullable JadxPluginMetadata update(JadxPluginMetadata plugin) { + IJadxPluginResolver resolver = ResolversRegistry.getById(plugin.getResolverId()); + if (!resolver.isUpdateSupported()) { + return null; + } + Optional updateOpt = resolver.resolve(plugin.getLocationId()); + if (updateOpt.isEmpty()) { + return null; + } + JadxPluginMetadata update = updateOpt.get(); + if (update.getVersion().equals(plugin.getVersion())) { + return null; + } + install(update); + return update; + } + + private void install(JadxPluginMetadata metadata) { + Path tmpJar; + if (needDownload(metadata.getJar())) { + tmpJar = FileUtils.createTempFile("plugin.jar"); + downloadJar(metadata.getJar(), tmpJar); + } else { + tmpJar = Paths.get(metadata.getJar()); + } + fillPluginInfoFromJar(metadata, tmpJar); + + Path pluginJar = installed.resolve(metadata.getPluginId() + '-' + metadata.getVersion() + ".jar"); + copyJar(tmpJar, pluginJar); + metadata.setJar(installed.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().add(metadata); + plugins.setUpdated(System.currentTimeMillis()); + savePluginsJson(plugins); + } + + private void fillPluginInfoFromJar(JadxPluginMetadata metadata, Path jar) { + try (JadxExternalPluginsLoader loader = new JadxExternalPluginsLoader()) { + JadxPlugin jadxPlugin = loader.loadFromJar(jar); + JadxPluginInfo pluginInfo = jadxPlugin.getPluginInfo(); + metadata.setPluginId(pluginInfo.getPluginId()); + metadata.setName(pluginInfo.getName()); + metadata.setDescription(pluginInfo.getDescription()); + } + } + + private boolean needDownload(String jar) { + return jar.startsWith("https://") || jar.startsWith("http://"); + } + + private void downloadJar(String sourceJar, Path destPath) { + try (InputStream in = new URL(sourceJar).openStream()) { + Files.copy(in, destPath, StandardCopyOption.REPLACE_EXISTING); + } catch (Exception e) { + throw new RuntimeException("Failed to download jar: " + sourceJar, e); + } + } + + private void copyJar(Path sourceJar, Path destJar) { + try { + Files.copy(sourceJar, destJar, StandardCopyOption.REPLACE_EXISTING); + } catch (Exception e) { + throw new RuntimeException("Failed to copy plugin jar: " + sourceJar + " to: " + destJar, e); + } + } + + private static Gson buildGson() { + return new GsonBuilder().setPrettyPrinting().create(); + } + + private JadxInstalledPlugins loadPluginsJson() { + if (!Files.isRegularFile(pluginsJson)) { + return new JadxInstalledPlugins(); + } + try (Reader reader = Files.newBufferedReader(pluginsJson, StandardCharsets.UTF_8)) { + return buildGson().fromJson(reader, JadxInstalledPlugins.class); + } catch (Exception e) { + throw new RuntimeException("Failed to read file: " + pluginsJson); + } + } + + private void savePluginsJson(JadxInstalledPlugins data) { + if (data.getInstalled().isEmpty()) { + try { + Files.deleteIfExists(pluginsJson); + } catch (Exception e) { + throw new RuntimeException("Failed to remove file: " + pluginsJson, e); + } + return; + } + data.getInstalled().sort(null); + try (Writer writer = Files.newBufferedWriter(pluginsJson, StandardCharsets.UTF_8)) { + buildGson().toJson(data, writer); + } catch (Exception e) { + throw new RuntimeException("Error saving file: " + pluginsJson, e); + } + } + + private static void collectFromDir(List list, Path dir) { + try (Stream files = Files.list(dir)) { + files.filter(p -> p.getFileName().toString().endsWith(".jar")).forEach(list::add); + } catch (IOException e) { + throw new RuntimeException(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 new file mode 100644 index 000000000..5310f3577 --- /dev/null +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/data/JadxInstalledPlugins.java @@ -0,0 +1,27 @@ +package jadx.plugins.tools.data; + +import java.util.ArrayList; +import java.util.List; + +public class JadxInstalledPlugins { + + private long updated; + + private List installed = new ArrayList<>(); + + public long getUpdated() { + return updated; + } + + public void setUpdated(long updated) { + this.updated = updated; + } + + public List getInstalled() { + return installed; + } + + public void setInstalled(List installed) { + this.installed = installed; + } +} 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 new file mode 100644 index 000000000..ed5c9ee5b --- /dev/null +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/data/JadxPluginMetadata.java @@ -0,0 +1,101 @@ +package jadx.plugins.tools.data; + +import org.jetbrains.annotations.NotNull; + +public class JadxPluginMetadata implements Comparable { + private String pluginId; + private String name; + private String description; + private String version; + private String locationId; + private String resolverId; + private String jar; + + public String getPluginId() { + return pluginId; + } + + public void setPluginId(String pluginId) { + this.pluginId = pluginId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getLocationId() { + return locationId; + } + + public void setLocationId(String locationId) { + this.locationId = locationId; + } + + public String getResolverId() { + return resolverId; + } + + public void setResolverId(String resolverId) { + this.resolverId = resolverId; + } + + public String getJar() { + return jar; + } + + public void setJar(String jar) { + this.jar = jar; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof JadxPluginMetadata)) { + return false; + } + return pluginId.equals(((JadxPluginMetadata) other).pluginId); + } + + @Override + public int hashCode() { + return pluginId.hashCode(); + } + + @Override + public int compareTo(@NotNull JadxPluginMetadata o) { + return pluginId.compareTo(o.pluginId); + } + + @Override + public String toString() { + return "JadxPluginMetadata{" + + "id=" + pluginId + + ", name=" + name + + ", version=" + version + + ", locationId=" + locationId + + ", jar=" + jar + + '}'; + } +} diff --git a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/data/JadxPluginUpdate.java b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/data/JadxPluginUpdate.java new file mode 100644 index 000000000..9aecd9969 --- /dev/null +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/data/JadxPluginUpdate.java @@ -0,0 +1,37 @@ +package jadx.plugins.tools.data; + +public class JadxPluginUpdate { + private final JadxPluginMetadata oldVersion; + private final JadxPluginMetadata newVersion; + + public JadxPluginUpdate(JadxPluginMetadata oldVersion, JadxPluginMetadata newVersion) { + this.oldVersion = oldVersion; + this.newVersion = newVersion; + } + + public JadxPluginMetadata getOld() { + return oldVersion; + } + + public JadxPluginMetadata getNew() { + return newVersion; + } + + public String getPluginId() { + return newVersion.getPluginId(); + } + + public String getOldVersion() { + return oldVersion.getVersion(); + } + + public String getNewVersion() { + return newVersion.getVersion(); + } + + @Override + public String toString() { + return "PluginUpdate{" + oldVersion.getPluginId() + + ": " + oldVersion.getVersion() + " -> " + newVersion.getVersion() + "}"; + } +} 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 new file mode 100644 index 000000000..c72757daf --- /dev/null +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/IJadxPluginResolver.java @@ -0,0 +1,14 @@ +package jadx.plugins.tools.resolvers; + +import java.util.Optional; + +import jadx.plugins.tools.data.JadxPluginMetadata; + +public interface IJadxPluginResolver { + + String id(); + + boolean isUpdateSupported(); + + Optional resolve(String locationId); +} diff --git a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/README.md b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/README.md new file mode 100644 index 000000000..0d9194803 --- /dev/null +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/README.md @@ -0,0 +1,28 @@ +### Supported publish locations for Jadx plugins + +--- + +#### GitHub release artifact + +Pattern: `github::[:][:]` + +Examples: `github:skylot:jadx`, `github:skylot:jadx:sample-plugin` or `github:skylot:jadx:0.1.0` + +`` - exact version to install (optional), should be equal to release name + +Artifact should have a name: `-.jar`. + +Default value for `` is a repo name, +`release-version-name` should have a `x.x.x` format. + +--- + +#### Local file + +Install local jar file. + +Pattern: `file:.jar` + +Example: `file:/home/user/plugin.jar` + +As alternative to install, plugin jars can be copied to `plugins/dropins` folder. 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 new file mode 100644 index 000000000..0a1092159 --- /dev/null +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/ResolversRegistry.java @@ -0,0 +1,41 @@ +package jadx.plugins.tools.resolvers; + +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; + +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<>(); + + static { + register(new LocalFileResolver()); + register(new GithubReleaseResolver()); + } + + private static void register(IJadxPluginResolver resolver) { + 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; + } + } + return Optional.empty(); + } + + public static IJadxPluginResolver getById(String resolverId) { + IJadxPluginResolver resolver = RESOLVERS_MAP.get(resolverId); + if (resolver == null) { + throw new IllegalArgumentException("Unknown resolverId: " + resolverId); + } + return resolver; + } +} 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 new file mode 100644 index 000000000..cc7c7ac04 --- /dev/null +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/file/LocalFileResolver.java @@ -0,0 +1,37 @@ +package jadx.plugins.tools.resolvers.file; + +import java.io.File; +import java.util.Optional; + +import jadx.plugins.tools.data.JadxPluginMetadata; +import jadx.plugins.tools.resolvers.IJadxPluginResolver; + +import static jadx.plugins.tools.utils.PluginsUtils.removePrefix; + +public class LocalFileResolver implements IJadxPluginResolver { + @Override + public String id() { + return "file"; + } + + @Override + public boolean isUpdateSupported() { + return false; + } + + @Override + public Optional resolve(String locationId) { + if (!locationId.startsWith("file:") || !locationId.endsWith(".jar")) { + return Optional.empty(); + } + File jarFile = new File(removePrefix(locationId, "file:")); + if (!jarFile.isFile()) { + throw new RuntimeException("File not found: " + jarFile.getAbsolutePath()); + } + JadxPluginMetadata metadata = new JadxPluginMetadata(); + metadata.setLocationId(locationId); + metadata.setResolverId(id()); + metadata.setJar(jarFile.getAbsolutePath()); + return Optional.of(metadata); + } +} 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 new file mode 100644 index 000000000..576814213 --- /dev/null +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/GithubReleaseResolver.java @@ -0,0 +1,129 @@ +package jadx.plugins.tools.resolvers.github; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.lang.reflect.Type; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import jadx.plugins.tools.data.JadxPluginMetadata; +import jadx.plugins.tools.resolvers.IJadxPluginResolver; +import jadx.plugins.tools.resolvers.github.data.Asset; +import jadx.plugins.tools.resolvers.github.data.Release; + +import static jadx.plugins.tools.utils.PluginsUtils.removePrefix; + +public class GithubReleaseResolver implements IJadxPluginResolver { + private static final String GITHUB_API_URL = "https://api.github.com/"; + private static final Pattern VERSION_PATTERN = Pattern.compile("v?\\d+\\.\\d+(\\.\\d+)?"); + + private static final Type RELEASE_TYPE = new TypeToken() { + }.getType(); + private static final Type RELEASE_LIST_TYPE = new TypeToken>() { + }.getType(); + + @Override + public Optional resolve(String locationId) { + if (!locationId.startsWith("github:")) { + return Optional.empty(); + } + String[] parts = locationId.split(":"); + if (parts.length < 3) { + return Optional.empty(); + } + String owner = parts[1]; + String project = parts[2]; + String version = null; + String artifactPrefix = project; + if (parts.length >= 4) { + String part = parts[3]; + if (VERSION_PATTERN.matcher(part).matches()) { + version = part; + if (parts.length >= 5) { + artifactPrefix = parts[4]; + } + } else { + artifactPrefix = part; + } + } + Release release = getRelease(owner, project, version); + String releaseVersion = removePrefix(release.getName(), "v"); + String artifactName = artifactPrefix + '-' + releaseVersion + ".jar"; + Asset asset = release.getAssets().stream() + .filter(a -> a.getName().equals(artifactName)) + .findFirst() + .orElseThrow(() -> new RuntimeException("Release artifact with name '" + artifactName + "' not found")); + + JadxPluginMetadata metadata = new JadxPluginMetadata(); + metadata.setResolverId(id()); + metadata.setVersion(releaseVersion); + metadata.setLocationId(buildLocationId(owner, project, artifactPrefix)); // exclude version for later updates + metadata.setJar(asset.getDownloadUrl()); + return Optional.of(metadata); + } + + @NotNull + private static String buildLocationId(String owner, String project, String artifactPrefix) { + if (project.equals(artifactPrefix)) { + return "github:" + owner + ':' + project; + } + return "github:" + owner + ':' + project + ':' + artifactPrefix; + } + + private static Release getRelease(String owner, String project, @Nullable String version) { + String projectUrl = GITHUB_API_URL + "repos/" + owner + "/" + project; + if (version == null) { + // 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); + return releases.stream() + .filter(r -> r.getName().equals(version)) + .findFirst() + .orElseThrow(() -> new RuntimeException("Release with version: " + version + " not found." + + " Available versions: " + releases.stream().map(Release::getName).collect(Collectors.joining(", ")))); + } + + private static T get(String url, Type type) { + HttpURLConnection con; + try { + con = (HttpURLConnection) new URL(url).openConnection(); + con.setRequestMethod("GET"); + int code = con.getResponseCode(); + if (code != 200) { + // TODO: support redirects? + throw new RuntimeException("Request failed, response: " + code + ", url: " + url); + } + } catch (IOException e) { + throw new RuntimeException("Request failed, url: " + url, e); + } + try (Reader reader = new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8)) { + return new Gson().fromJson(reader, type); + } catch (Exception e) { + throw new RuntimeException("Failed to parse response, url: " + url, e); + } + } + + @Override + public String id() { + return "github-release"; + } + + @Override + public boolean isUpdateSupported() { + return true; + } +} diff --git a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/data/Asset.java b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/data/Asset.java new file mode 100644 index 000000000..7410a4291 --- /dev/null +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/data/Asset.java @@ -0,0 +1,63 @@ +package jadx.plugins.tools.resolvers.github.data; + +import com.google.gson.annotations.SerializedName; + +public class Asset { + private int id; + private String name; + private long size; + + @SerializedName("browser_download_url") + private String downloadUrl; + + @SerializedName("created_at") + private String createdAt; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public String getDownloadUrl() { + return downloadUrl; + } + + public void setDownloadUrl(String downloadUrl) { + this.downloadUrl = downloadUrl; + } + + public String getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(String createdAt) { + this.createdAt = createdAt; + } + + @Override + public String toString() { + return name + + ", size: " + String.format("%.2fMB", size / 1024. / 1024.) + + ", url: " + downloadUrl + + ", date: " + createdAt; + } +} diff --git a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/data/Release.java b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/data/Release.java new file mode 100644 index 000000000..bb1e12a14 --- /dev/null +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/data/Release.java @@ -0,0 +1,44 @@ +package jadx.plugins.tools.resolvers.github.data; + +import java.util.List; + +public class Release { + private int id; + private String name; + private List assets; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public List getAssets() { + return assets; + } + + public void setAssets(List assets) { + this.assets = assets; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(name); + for (Asset asset : getAssets()) { + sb.append("\n "); + sb.append(asset); + } + return sb.toString(); + } +} diff --git a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/utils/PluginsUtils.java b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/utils/PluginsUtils.java new file mode 100644 index 000000000..ee00825b1 --- /dev/null +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/utils/PluginsUtils.java @@ -0,0 +1,11 @@ +package jadx.plugins.tools.utils; + +public class PluginsUtils { + + public static String removePrefix(String str, String prefix) { + if (str.startsWith(prefix)) { + return str.substring(prefix.length()); + } + return str; + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index a8d050466..44fb808da 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,6 +8,8 @@ include("jadx-core") include("jadx-cli") include("jadx-gui") +include("jadx-plugins-tools") + include("jadx-plugins:jadx-input-api") include("jadx-plugins:jadx-dex-input") include("jadx-plugins:jadx-java-input")