feat(cli): install and manage plugins from command line
This commit is contained in:
@@ -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() + "}";
|
||||
}
|
||||
}
|
||||
+14
@@ -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;
|
||||
}
|
||||
}
|
||||
+37
@@ -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);
|
||||
}
|
||||
}
|
||||
+129
@@ -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;
|
||||
}
|
||||
}
|
||||
+44
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user