feat(cli): install and manage plugins from command line

This commit is contained in:
Skylot
2023-05-23 17:53:48 +01:00
parent 67054bda3d
commit 8a67c39279
32 changed files with 1162 additions and 25 deletions
+10
View 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;
}
}