mirror of
https://github.com/skylot/jadx.git
synced 2024-11-26 22:20:50 +00:00
feat(cli): install and manage plugins from command line
This commit is contained in:
parent
67054bda3d
commit
8a67c39279
@ -79,7 +79,12 @@ and also packed to `build/jadx-<version>.zip`
|
||||
|
||||
### Usage
|
||||
```
|
||||
jadx[-gui] [options] <input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab)
|
||||
jadx[-gui] [command] [options] <input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab)
|
||||
commands (use '<command> --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
|
||||
|
@ -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'))
|
||||
|
@ -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<T> {
|
||||
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<T> {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean processCommands() {
|
||||
String parsedCommand = jc.getParsedCommand();
|
||||
if (parsedCommand == null) {
|
||||
return false;
|
||||
}
|
||||
return JadxCLICommands.process(this, jc, parsedCommand);
|
||||
}
|
||||
|
||||
public void overrideProvided(JadxCLIArgs obj) {
|
||||
List<ParameterDescription> fieldsParams = jc.getParameters();
|
||||
List<ParameterDescription> parameters = new ArrayList<>(1 + fieldsParams.size());
|
||||
@ -73,6 +85,10 @@ public class JCommanderWrapper<T> {
|
||||
return value;
|
||||
}
|
||||
|
||||
public List<String> getUnknownOptions() {
|
||||
return jc.getUnknownOptions();
|
||||
}
|
||||
|
||||
public void printUsage() {
|
||||
LogHelper.setLogLevel(LogHelper.LogLevelEnum.ERROR); // mute logger while printing help
|
||||
|
||||
@ -81,7 +97,32 @@ public class JCommanderWrapper<T> {
|
||||
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 '<command> --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<ParameterDescription> params = jc.getParameters();
|
||||
@ -96,7 +137,7 @@ public class JCommanderWrapper<T> {
|
||||
}
|
||||
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<T> {
|
||||
} 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<Field> getFields(Class<?> clazz) {
|
||||
private static List<Field> getFields(Class<?> clazz) {
|
||||
List<Field> fieldList = new ArrayList<>();
|
||||
while (clazz != null) {
|
||||
fieldList.addAll(Arrays.asList(clazz.getDeclaredFields()));
|
||||
@ -147,7 +183,7 @@ public class JCommanderWrapper<T> {
|
||||
}
|
||||
|
||||
@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<T> {
|
||||
// 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();
|
||||
|
@ -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)) {
|
||||
|
@ -264,6 +264,10 @@ public class JadxCLIArgs {
|
||||
}
|
||||
|
||||
private boolean process(JCommanderWrapper<JadxCLIArgs> jcw) {
|
||||
files.addAll(jcw.getUnknownOptions());
|
||||
if (jcw.processCommands()) {
|
||||
return false;
|
||||
}
|
||||
if (printHelp) {
|
||||
jcw.printUsage();
|
||||
return false;
|
||||
|
35
jadx-cli/src/main/java/jadx/cli/JadxCLICommands.java
Normal file
35
jadx-cli/src/main/java/jadx/cli/JadxCLICommands.java
Normal file
@ -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<String, ICommand> 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;
|
||||
}
|
||||
}
|
@ -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<ICodeLoader> loadedInputs = new ArrayList<>();
|
||||
for (JadxCodeInput inputPlugin : pluginManager.getCodeInputs()) {
|
||||
|
81
jadx-cli/src/main/java/jadx/cli/commands/CommandPlugins.java
Normal file
81
jadx-cli/src/main/java/jadx/cli/commands/CommandPlugins.java
Normal file
@ -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<JadxPluginUpdate> 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<JadxPluginMetadata> 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());
|
||||
}
|
||||
}
|
11
jadx-cli/src/main/java/jadx/cli/commands/ICommand.java
Normal file
11
jadx-cli/src/main/java/jadx/cli/commands/ICommand.java
Normal file
@ -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);
|
||||
}
|
@ -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<String, String> 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
|
||||
*/
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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<JadxPlugin> load() {
|
||||
List<JadxPlugin> list = new ArrayList<>();
|
||||
ServiceLoader<JadxPlugin> plugins = ServiceLoader.load(JadxPlugin.class);
|
||||
for (JadxPlugin plugin : plugins) {
|
||||
list.add(plugin);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
// nothing to close
|
||||
}
|
||||
}
|
@ -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<JadxPlugin> load();
|
||||
}
|
@ -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<JadxPlugin> jadxPlugins = ServiceLoader.load(JadxPlugin.class);
|
||||
for (JadxPlugin plugin : jadxPlugins) {
|
||||
for (JadxPlugin plugin : pluginLoader.load()) {
|
||||
addPlugin(plugin);
|
||||
}
|
||||
resolve();
|
||||
|
@ -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')
|
||||
|
@ -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);
|
||||
|
@ -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<PluginContext> missingPlugins = new TreeSet<>();
|
||||
for (PluginContext context : pluginManager.getAllPluginContexts()) {
|
||||
if (!allPlugins.contains(context)) {
|
||||
|
10
jadx-plugins-tools/build.gradle.kts
Normal file
10
jadx-plugins-tools/build.gradle.kts
Normal file
@ -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")
|
||||
}
|
@ -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<URLClassLoader> classLoaders = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public List<JadxPlugin> load() {
|
||||
close();
|
||||
long start = System.currentTimeMillis();
|
||||
Map<Class<? extends JadxPlugin>, JadxPlugin> map = new HashMap<>();
|
||||
ClassLoader classLoader = JadxPluginsTools.class.getClassLoader();
|
||||
loadFromClsLoader(map, classLoader);
|
||||
loadInstalledPlugins(map, classLoader);
|
||||
|
||||
List<JadxPlugin> 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<Class<? extends JadxPlugin>, JadxPlugin> map = new HashMap<>();
|
||||
ClassLoader classLoader = JadxPluginsTools.class.getClassLoader();
|
||||
loadFromClsLoader(map, classLoader);
|
||||
Set<Class<? extends JadxPlugin>> 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<Class<? extends JadxPlugin>, 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<Class<? extends JadxPlugin>, JadxPlugin> map, ClassLoader classLoader) {
|
||||
List<Path> jars = JadxPluginsTools.getInstance().getAllPluginJars();
|
||||
for (Path jar : jars) {
|
||||
classLoaders.add(loadFromJar(map, classLoader, jar));
|
||||
}
|
||||
}
|
||||
|
||||
private URLClassLoader loadFromJar(Map<Class<? extends JadxPlugin>, 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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<JadxPluginUpdate> updateAll() {
|
||||
JadxInstalledPlugins plugins = loadPluginsJson();
|
||||
int size = plugins.getInstalled().size();
|
||||
List<JadxPluginUpdate> updates = new ArrayList<>(size);
|
||||
List<JadxPluginMetadata> 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<JadxPluginUpdate> 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<JadxPluginMetadata> 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<JadxPluginMetadata> getInstalled() {
|
||||
return loadPluginsJson().getInstalled();
|
||||
}
|
||||
|
||||
public List<Path> getAllPluginJars() {
|
||||
List<Path> 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<JadxPluginMetadata> 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<Path> list, Path dir) {
|
||||
try (Stream<Path> files = Files.list(dir)) {
|
||||
files.filter(p -> p.getFileName().toString().endsWith(".jar")).forEach(list::add);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -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<JadxPluginMetadata> installed = new ArrayList<>();
|
||||
|
||||
public long getUpdated() {
|
||||
return updated;
|
||||
}
|
||||
|
||||
public void setUpdated(long updated) {
|
||||
this.updated = updated;
|
||||
}
|
||||
|
||||
public List<JadxPluginMetadata> getInstalled() {
|
||||
return installed;
|
||||
}
|
||||
|
||||
public void setInstalled(List<JadxPluginMetadata> installed) {
|
||||
this.installed = installed;
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
package jadx.plugins.tools.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class JadxPluginMetadata implements Comparable<JadxPluginMetadata> {
|
||||
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
|
||||
+ '}';
|
||||
}
|
||||
}
|
@ -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() + "}";
|
||||
}
|
||||
}
|
@ -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<JadxPluginMetadata> resolve(String locationId);
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
### Supported publish locations for Jadx plugins
|
||||
|
||||
---
|
||||
|
||||
#### GitHub release artifact
|
||||
|
||||
Pattern: `github:<owner>:<repo>[:<version>][:<artifact name prefix>]`
|
||||
|
||||
Examples: `github:skylot:jadx`, `github:skylot:jadx:sample-plugin` or `github:skylot:jadx:0.1.0`
|
||||
|
||||
`<version>` - exact version to install (optional), should be equal to release name
|
||||
|
||||
Artifact should have a name: `<artifact name prefix>-<release-version-name>.jar`.
|
||||
|
||||
Default value for `<artifact name prefix>` is a repo name,
|
||||
`release-version-name` should have a `x.x.x` format.
|
||||
|
||||
---
|
||||
|
||||
#### Local file
|
||||
|
||||
Install local jar file.
|
||||
|
||||
Pattern: `file:<path to file>.jar`
|
||||
|
||||
Example: `file:/home/user/plugin.jar`
|
||||
|
||||
As alternative to install, plugin jars can be copied to `plugins/dropins` folder.
|
@ -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<String, IJadxPluginResolver> 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<JadxPluginMetadata> resolve(String locationId) {
|
||||
for (IJadxPluginResolver resolver : RESOLVERS_MAP.values()) {
|
||||
Optional<JadxPluginMetadata> 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;
|
||||
}
|
||||
}
|
@ -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<JadxPluginMetadata> 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);
|
||||
}
|
||||
}
|
@ -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<Release>() {
|
||||
}.getType();
|
||||
private static final Type RELEASE_LIST_TYPE = new TypeToken<List<Release>>() {
|
||||
}.getType();
|
||||
|
||||
@Override
|
||||
public Optional<JadxPluginMetadata> 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<Release> 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> 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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<Asset> 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<Asset> getAssets() {
|
||||
return assets;
|
||||
}
|
||||
|
||||
public void setAssets(List<Asset> 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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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")
|
||||
|
Loading…
Reference in New Issue
Block a user