feat(plugins): allow to use .zip as plugin artifact (with jars inside)
This commit is contained in:
+39
-13
@@ -1,8 +1,9 @@
|
||||
package jadx.plugins.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
@@ -19,6 +20,7 @@ import jadx.api.plugins.JadxPlugin;
|
||||
import jadx.api.plugins.loader.JadxPluginLoader;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
|
||||
public class JadxExternalPluginsLoader implements JadxPluginLoader {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JadxExternalPluginsLoader.class);
|
||||
@@ -44,16 +46,16 @@ public class JadxExternalPluginsLoader implements JadxPluginLoader {
|
||||
return list;
|
||||
}
|
||||
|
||||
public JadxPlugin loadFromJar(Path jar) {
|
||||
public JadxPlugin loadFromPath(Path pluginPath) {
|
||||
Map<String, JadxPlugin> map = new HashMap<>();
|
||||
loadFromJar(map, jar);
|
||||
loadFromPath(map, pluginPath);
|
||||
int loaded = map.size();
|
||||
if (loaded == 0) {
|
||||
throw new JadxRuntimeException("No plugin found in jar: " + jar);
|
||||
throw new JadxRuntimeException("No plugin found in jar: " + pluginPath);
|
||||
}
|
||||
if (loaded > 1) {
|
||||
String plugins = map.values().stream().map(p -> p.getPluginInfo().getPluginId()).collect(Collectors.joining(", "));
|
||||
throw new JadxRuntimeException("Expect only one plugin per jar: " + jar + ", but found: " + loaded + " - " + plugins);
|
||||
throw new JadxRuntimeException("Expect only one plugin per jar: " + pluginPath + ", but found: " + loaded + " - " + plugins);
|
||||
}
|
||||
return Utils.first(map.values());
|
||||
|
||||
@@ -72,22 +74,46 @@ public class JadxExternalPluginsLoader implements JadxPluginLoader {
|
||||
}
|
||||
|
||||
private void loadInstalledPlugins(Map<String, JadxPlugin> map) {
|
||||
List<Path> jars = JadxPluginsTools.getInstance().getEnabledPluginJars();
|
||||
for (Path jar : jars) {
|
||||
loadFromJar(map, jar);
|
||||
List<Path> paths = JadxPluginsTools.getInstance().getEnabledPluginPaths();
|
||||
for (Path pluginPath : paths) {
|
||||
loadFromPath(map, pluginPath);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadFromJar(Map<String, JadxPlugin> map, Path jar) {
|
||||
private void loadFromPath(Map<String, JadxPlugin> map, Path pluginPath) {
|
||||
try {
|
||||
File jarFile = jar.toFile();
|
||||
String clsLoaderName = JADX_PLUGIN_CLASSLOADER_PREFIX + jarFile.getName();
|
||||
URL[] urls = new URL[] { jarFile.toURI().toURL() };
|
||||
URL[] urls;
|
||||
if (Files.isDirectory(pluginPath)) {
|
||||
urls = FileUtils.listFiles(pluginPath, file -> FileUtils.hasExtension(file, ".jar"))
|
||||
.stream()
|
||||
.map(JadxExternalPluginsLoader::toURL)
|
||||
.toArray(URL[]::new);
|
||||
if (urls.length == 0) {
|
||||
throw new JadxRuntimeException("No jar files found in plugin directory");
|
||||
}
|
||||
} else if (Files.isRegularFile(pluginPath)) {
|
||||
if (FileUtils.hasExtension(pluginPath, ".jar")) {
|
||||
urls = new URL[] { toURL(pluginPath) };
|
||||
} else {
|
||||
throw new JadxRuntimeException("Unexpected plugin file format");
|
||||
}
|
||||
} else {
|
||||
throw new JadxRuntimeException("Plugin file not found");
|
||||
}
|
||||
String clsLoaderName = JADX_PLUGIN_CLASSLOADER_PREFIX + pluginPath.getFileName();
|
||||
URLClassLoader pluginClsLoader = new URLClassLoader(clsLoaderName, urls, thisClassLoader());
|
||||
classLoaders.add(pluginClsLoader);
|
||||
loadFromClsLoader(map, pluginClsLoader);
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Failed to load plugins from jar: " + jar, e);
|
||||
throw new JadxRuntimeException("Failed to load plugins from: " + pluginPath, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static URL toURL(Path pluginPath) {
|
||||
try {
|
||||
return pluginPath.toUri().toURL();
|
||||
} catch (MalformedURLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
@@ -23,17 +22,22 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.plugins.JadxPlugin;
|
||||
import jadx.api.plugins.JadxPluginInfo;
|
||||
import jadx.api.plugins.utils.CommonFileUtils;
|
||||
import jadx.core.Jadx;
|
||||
import jadx.core.plugins.versions.VerifyRequiredVersion;
|
||||
import jadx.core.utils.StringUtils;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
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 jadx.plugins.tools.utils.PluginUtils;
|
||||
import jadx.zip.IZipEntry;
|
||||
import jadx.zip.ZipContent;
|
||||
import jadx.zip.ZipReader;
|
||||
|
||||
import static jadx.core.utils.GsonUtils.buildGson;
|
||||
import static jadx.plugins.tools.utils.PluginFiles.DROPINS_DIR;
|
||||
@@ -165,7 +169,7 @@ public class JadxPluginsTools {
|
||||
return false;
|
||||
}
|
||||
JadxPluginMetadata plugin = found.get();
|
||||
deletePluginJar(plugin);
|
||||
deletePlugin(plugin);
|
||||
plugins.getInstalled().remove(plugin);
|
||||
savePluginsJson(plugins);
|
||||
return true;
|
||||
@@ -189,24 +193,15 @@ public class JadxPluginsTools {
|
||||
}
|
||||
}
|
||||
|
||||
public List<Path> getAllPluginJars() {
|
||||
List<Path> list = new ArrayList<>();
|
||||
for (JadxPluginMetadata pluginMetadata : loadPluginsJson().getInstalled()) {
|
||||
list.add(INSTALLED_DIR.resolve(pluginMetadata.getJar()));
|
||||
}
|
||||
collectJarsFromDir(list, DROPINS_DIR);
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<Path> getEnabledPluginJars() {
|
||||
public List<Path> getEnabledPluginPaths() {
|
||||
List<Path> list = new ArrayList<>();
|
||||
for (JadxPluginMetadata pluginMetadata : loadPluginsJson().getInstalled()) {
|
||||
if (pluginMetadata.isDisabled()) {
|
||||
continue;
|
||||
}
|
||||
list.add(INSTALLED_DIR.resolve(pluginMetadata.getJar()));
|
||||
list.add(INSTALLED_DIR.resolve(pluginMetadata.getPath()));
|
||||
}
|
||||
collectJarsFromDir(list, DROPINS_DIR);
|
||||
list.addAll(FileUtils.listFiles(DROPINS_DIR));
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -254,22 +249,31 @@ public class JadxPluginsTools {
|
||||
throw new JadxRuntimeException("Can't install plugin, required version: \"" + reqVersionStr + '\"'
|
||||
+ " is not compatible with current jadx version: " + Jadx.getVersion());
|
||||
}
|
||||
// remove previous version
|
||||
uninstall(metadata.getPluginId());
|
||||
|
||||
String version = metadata.getVersion();
|
||||
String fileName = metadata.getPluginId() + (StringUtils.notBlank(version) ? '-' + version : "") + ".jar";
|
||||
Path pluginJar = INSTALLED_DIR.resolve(fileName);
|
||||
copyJar(Paths.get(metadata.getJar()), pluginJar);
|
||||
metadata.setJar(INSTALLED_DIR.relativize(pluginJar).toString());
|
||||
|
||||
JadxInstalledPlugins plugins = loadPluginsJson();
|
||||
// remove previous version jar
|
||||
plugins.getInstalled().removeIf(p -> {
|
||||
if (p.getPluginId().equals(metadata.getPluginId())) {
|
||||
deletePluginJar(p);
|
||||
return true;
|
||||
String pluginBaseName = metadata.getPluginId() + (StringUtils.notBlank(version) ? '-' + version : "");
|
||||
String pluginPathStr = metadata.getPath();
|
||||
Path pluginPath = Paths.get(pluginPathStr);
|
||||
if (pluginPathStr.endsWith(".jar")) {
|
||||
Path pluginJar = INSTALLED_DIR.resolve(pluginBaseName + ".jar");
|
||||
copyJar(pluginPath, pluginJar);
|
||||
metadata.setPath(INSTALLED_DIR.relativize(pluginJar).toString());
|
||||
} else if (Files.isDirectory(pluginPath)) {
|
||||
Path pluginDir = INSTALLED_DIR.resolve(pluginBaseName);
|
||||
try {
|
||||
FileUtils.deleteDirIfExists(pluginDir);
|
||||
org.apache.commons.io.FileUtils.moveDirectory(pluginPath.toFile(), pluginDir.toFile());
|
||||
} catch (IOException e) {
|
||||
throw new JadxRuntimeException("Failed to install plugin: " + pluginBaseName, e);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
metadata.setPath(INSTALLED_DIR.relativize(pluginDir).toString());
|
||||
} else {
|
||||
throw new JadxRuntimeException("Unexpected plugin path type: " + pluginPathStr);
|
||||
}
|
||||
// update plugins json
|
||||
JadxInstalledPlugins plugins = loadPluginsJson();
|
||||
plugins.getInstalled().add(metadata);
|
||||
plugins.setUpdated(System.currentTimeMillis());
|
||||
savePluginsJson(plugins);
|
||||
@@ -277,23 +281,30 @@ public class JadxPluginsTools {
|
||||
|
||||
private void fillMetadata(JadxPluginMetadata metadata) {
|
||||
try {
|
||||
Path tmpJar;
|
||||
if (needDownload(metadata.getJar())) {
|
||||
tmpJar = Files.createTempFile(metadata.getName(), "plugin.jar");
|
||||
PluginUtils.downloadFile(metadata.getJar(), tmpJar);
|
||||
metadata.setJar(tmpJar.toAbsolutePath().toString());
|
||||
} else {
|
||||
tmpJar = Paths.get(metadata.getJar());
|
||||
String pluginPath = metadata.getPath();
|
||||
if (needDownload(pluginPath)) {
|
||||
// download plugin
|
||||
String ext = CommonFileUtils.getFileExtension(pluginPath);
|
||||
Path tmpJar = Files.createTempFile(metadata.getName(), "plugin." + ext);
|
||||
PluginUtils.downloadFile(pluginPath, tmpJar);
|
||||
pluginPath = tmpJar.toAbsolutePath().toString();
|
||||
}
|
||||
fillMetadataFromJar(metadata, tmpJar);
|
||||
if (pluginPath.endsWith(".zip")) {
|
||||
// unpack plugin zip
|
||||
Path tmpDir = Files.createTempDirectory(metadata.getName());
|
||||
unzip(Paths.get(pluginPath), tmpDir);
|
||||
pluginPath = tmpDir.toAbsolutePath().toString();
|
||||
}
|
||||
metadata.setPath(pluginPath);
|
||||
fillMetadataFromPath(metadata, Paths.get(pluginPath));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to fill plugin metadata, plugin: " + metadata.getPluginId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void fillMetadataFromJar(JadxPluginMetadata metadata, Path jar) {
|
||||
private void fillMetadataFromPath(JadxPluginMetadata metadata, Path pluginPath) {
|
||||
try (JadxExternalPluginsLoader loader = new JadxExternalPluginsLoader()) {
|
||||
JadxPlugin jadxPlugin = loader.loadFromJar(jar);
|
||||
JadxPlugin jadxPlugin = loader.loadFromPath(pluginPath);
|
||||
JadxPluginInfo pluginInfo = jadxPlugin.getPluginInfo();
|
||||
metadata.setPluginId(pluginInfo.getPluginId());
|
||||
metadata.setName(pluginInfo.getName());
|
||||
@@ -317,9 +328,14 @@ public class JadxPluginsTools {
|
||||
}
|
||||
}
|
||||
|
||||
private void deletePluginJar(JadxPluginMetadata plugin) {
|
||||
private void deletePlugin(JadxPluginMetadata plugin) {
|
||||
try {
|
||||
Files.deleteIfExists(INSTALLED_DIR.resolve(plugin.getJar()));
|
||||
Path pluginPath = INSTALLED_DIR.resolve(plugin.getPath());
|
||||
if (Files.isDirectory(pluginPath)) {
|
||||
FileUtils.deleteDir(pluginPath);
|
||||
} else {
|
||||
Files.deleteIfExists(pluginPath);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// ignore
|
||||
}
|
||||
@@ -363,11 +379,15 @@ public class JadxPluginsTools {
|
||||
}
|
||||
}
|
||||
|
||||
private static void collectJarsFromDir(List<Path> list, Path dir) {
|
||||
try (Stream<Path> files = Files.list(dir)) {
|
||||
files.filter(p -> p.getFileName().toString().endsWith(".jar")).forEach(list::add);
|
||||
private static void unzip(Path zipFile, Path outDir) {
|
||||
ZipReader zipReader = new ZipReader(); // TODO: pass zip options from jadx args
|
||||
try (ZipContent content = zipReader.open(zipFile.toFile())) {
|
||||
for (IZipEntry entry : content.getEntries()) {
|
||||
Path entryFile = outDir.resolve(entry.getName());
|
||||
Files.copy(entry.getInputStream(), entryFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
throw new JadxRuntimeException("Failed to unzip file: " + zipFile, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package jadx.plugins.tools.data;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public class JadxPluginMetadata implements Comparable<JadxPluginMetadata> {
|
||||
private String pluginId;
|
||||
private String name;
|
||||
@@ -11,8 +13,15 @@ public class JadxPluginMetadata implements Comparable<JadxPluginMetadata> {
|
||||
private @Nullable String requiredJadxVersion;
|
||||
|
||||
private @Nullable String version;
|
||||
|
||||
private String locationId;
|
||||
private String jar;
|
||||
|
||||
/**
|
||||
* Absolute path to '.jar' file or unpacked zip directory
|
||||
*/
|
||||
@SerializedName(value = "path", alternate = { "jar" })
|
||||
private String path;
|
||||
|
||||
private boolean disabled;
|
||||
|
||||
public String getPluginId() {
|
||||
@@ -71,12 +80,22 @@ public class JadxPluginMetadata implements Comparable<JadxPluginMetadata> {
|
||||
this.locationId = locationId;
|
||||
}
|
||||
|
||||
public String getJar() {
|
||||
return jar;
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public void setPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public String getJar() {
|
||||
return path;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void setJar(String jar) {
|
||||
this.jar = jar;
|
||||
this.path = jar;
|
||||
}
|
||||
|
||||
public boolean isDisabled() {
|
||||
@@ -115,7 +134,7 @@ public class JadxPluginMetadata implements Comparable<JadxPluginMetadata> {
|
||||
+ ", name=" + name
|
||||
+ ", version=" + (version != null ? version : "?")
|
||||
+ ", locationId=" + locationId
|
||||
+ ", jar=" + jar
|
||||
+ ", path=" + path
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
|
||||
+10
-5
@@ -20,18 +20,23 @@ public class LocalFileResolver implements IJadxPluginResolver {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isValidFileLocation(String locationId) {
|
||||
return locationId.startsWith("file:")
|
||||
&& (locationId.endsWith(".jar") || locationId.endsWith(".zip"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<JadxPluginMetadata> resolve(String locationId) {
|
||||
if (!locationId.startsWith("file:") || !locationId.endsWith(".jar")) {
|
||||
if (!isValidFileLocation(locationId)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
File jarFile = new File(removePrefix(locationId, "file:"));
|
||||
if (!jarFile.isFile()) {
|
||||
throw new RuntimeException("File not found: " + jarFile.getAbsolutePath());
|
||||
File pluginFile = new File(removePrefix(locationId, "file:"));
|
||||
if (!pluginFile.isFile()) {
|
||||
throw new RuntimeException("File not found: " + pluginFile.getAbsolutePath());
|
||||
}
|
||||
JadxPluginMetadata metadata = new JadxPluginMetadata();
|
||||
metadata.setLocationId(locationId);
|
||||
metadata.setJar(jarFile.getAbsolutePath());
|
||||
metadata.setPath(pluginFile.getAbsolutePath());
|
||||
return Optional.of(metadata);
|
||||
}
|
||||
|
||||
|
||||
+18
-8
@@ -5,6 +5,8 @@ import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.core.utils.ListUtils;
|
||||
import jadx.plugins.tools.data.JadxPluginMetadata;
|
||||
import jadx.plugins.tools.resolvers.IJadxPluginResolver;
|
||||
@@ -58,7 +60,7 @@ public class GithubReleaseResolver implements IJadxPluginResolver {
|
||||
JadxPluginMetadata metadata = new JadxPluginMetadata();
|
||||
metadata.setVersion(releaseVersion);
|
||||
metadata.setLocationId(buildLocationIdWithoutVersion(info)); // exclude version for later updates
|
||||
metadata.setJar(asset.getDownloadUrl());
|
||||
metadata.setPath(asset.getDownloadUrl());
|
||||
return metadata;
|
||||
}
|
||||
|
||||
@@ -89,20 +91,28 @@ public class GithubReleaseResolver implements IJadxPluginResolver {
|
||||
}
|
||||
|
||||
private static Asset searchPluginAsset(List<Asset> assets, String artifactPrefix, String releaseVersion) {
|
||||
String artifactName = artifactPrefix + '-' + releaseVersion + ".jar";
|
||||
Asset assetJar = searchAssetWithExt(assets, artifactPrefix, releaseVersion, ".jar");
|
||||
if (assetJar != null) {
|
||||
return assetJar;
|
||||
}
|
||||
Asset assetZip = searchAssetWithExt(assets, artifactPrefix, releaseVersion, ".zip");
|
||||
if (assetZip != null) {
|
||||
return assetZip;
|
||||
}
|
||||
throw new RuntimeException("Release artifact with prefix '" + artifactPrefix + "' not found");
|
||||
}
|
||||
|
||||
private static @Nullable Asset searchAssetWithExt(List<Asset> assets, String artifactPrefix, String releaseVersion, String ext) {
|
||||
String artifactName = artifactPrefix + '-' + releaseVersion + ext;
|
||||
Asset exactAsset = ListUtils.filterOnlyOne(assets, a -> a.getName().equals(artifactName));
|
||||
if (exactAsset != null) {
|
||||
return exactAsset;
|
||||
}
|
||||
// search without version filter
|
||||
Asset foundAsset = ListUtils.filterOnlyOne(assets, a -> {
|
||||
return ListUtils.filterOnlyOne(assets, a -> {
|
||||
String assetFileName = a.getName();
|
||||
return assetFileName.startsWith(artifactPrefix) && assetFileName.endsWith(".jar");
|
||||
return assetFileName.startsWith(artifactPrefix) && assetFileName.endsWith(ext);
|
||||
});
|
||||
if (foundAsset != null) {
|
||||
return foundAsset;
|
||||
}
|
||||
throw new RuntimeException("Release artifact with prefix '" + artifactPrefix + "' not found");
|
||||
}
|
||||
|
||||
private static String buildLocationIdWithoutVersion(LocationInfo info) {
|
||||
|
||||
Reference in New Issue
Block a user