Skip to content

Commit 2a2806e

Browse files
committed
feat(plugins): allow to use .zip as plugin artifact (with jars inside)
1 parent c7a0f7a commit 2a2806e

File tree

8 files changed

+183
-81
lines changed

8 files changed

+183
-81
lines changed

jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.Arrays;
2828
import java.util.Collections;
2929
import java.util.List;
30+
import java.util.function.Predicate;
3031
import java.util.jar.JarEntry;
3132
import java.util.jar.JarOutputStream;
3233
import java.util.stream.Collectors;
@@ -78,6 +79,22 @@ private static Path createTempRootDir() {
7879
}
7980
}
8081

82+
public static List<Path> listFiles(Path dir) {
83+
try (Stream<Path> files = Files.list(dir)) {
84+
return files.collect(Collectors.toList());
85+
} catch (IOException e) {
86+
throw new JadxRuntimeException("Failed to list files in directory: " + dir, e);
87+
}
88+
}
89+
90+
public static List<Path> listFiles(Path dir, Predicate<? super Path> filter) {
91+
try (Stream<Path> files = Files.list(dir)) {
92+
return files.filter(filter).collect(Collectors.toList());
93+
} catch (IOException e) {
94+
throw new JadxRuntimeException("Failed to list files in directory: " + dir, e);
95+
}
96+
}
97+
8198
public static List<Path> expandDirs(List<Path> paths) {
8299
List<Path> files = new ArrayList<>(paths.size());
83100
for (Path path : paths) {
@@ -148,6 +165,10 @@ public static boolean deleteDir(File dir) {
148165
return true;
149166
}
150167

168+
public static void deleteDir(Path dir) {
169+
deleteDir(dir, false);
170+
}
171+
151172
public static void deleteDirIfExists(Path dir) {
152173
if (Files.exists(dir)) {
153174
try {
@@ -158,10 +179,6 @@ public static void deleteDirIfExists(Path dir) {
158179
}
159180
}
160181

161-
private static void deleteDir(Path dir) {
162-
deleteDir(dir, false);
163-
}
164-
165182
private static void deleteDir(Path dir, boolean keepRootDir) {
166183
try {
167184
List<Path> files = new ArrayList<>();
@@ -424,6 +441,11 @@ public static String getPathBaseName(Path file) {
424441
return fileName.substring(0, extEndIndex);
425442
}
426443

444+
public static boolean hasExtension(Path path, String extension) {
445+
String fileName = path.getFileName().toString();
446+
return fileName.toLowerCase().endsWith(extension);
447+
}
448+
427449
public static File toFile(String path) {
428450
if (path == null) {
429451
return null;

jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/InstallPluginDialog.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import java.awt.BorderLayout;
44
import java.awt.Dimension;
55
import java.nio.file.Path;
6-
import java.util.Collections;
76
import java.util.List;
87

98
import javax.swing.BorderFactory;
@@ -61,7 +60,7 @@ private void init() {
6160
locationPanel.add(locationFld);
6261

6362
JButton fileBtn = new JButton(NLS.str("preferences.plugins.plugin_jar"));
64-
fileBtn.addActionListener(ev -> openPluginJar());
63+
fileBtn.addActionListener(ev -> openPluginFile());
6564
JLabel fileLbl = new JLabel(NLS.str("preferences.plugins.plugin_jar_label"));
6665
fileLbl.setLabelFor(fileBtn);
6766

@@ -105,10 +104,10 @@ private void init() {
105104
UiUtils.addEscapeShortCutToDispose(this);
106105
}
107106

108-
private void openPluginJar() {
107+
private void openPluginFile() {
109108
FileDialogWrapper fd = new FileDialogWrapper(mainWindow, FileOpenMode.CUSTOM_OPEN);
110109
fd.setTitle(NLS.str("preferences.plugins.plugin_jar"));
111-
fd.setFileExtList(Collections.singletonList("jar"));
110+
fd.setFileExtList(List.of("jar", "zip"));
112111
fd.setSelectionMode(JFileChooser.FILES_ONLY);
113112
List<Path> files = fd.show();
114113
if (files.size() == 1) {

jadx-plugins-tools/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ dependencies {
99
implementation(project(":jadx-commons:jadx-app-commons"))
1010

1111
implementation("com.google.code.gson:gson:2.13.2")
12+
implementation("commons-io:commons-io:2.21.0")
1213

1314
testImplementation("com.squareup.okhttp3:mockwebserver3:5.3.0")
1415
}

jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxExternalPluginsLoader.java

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package jadx.plugins.tools;
22

3-
import java.io.File;
3+
import java.net.MalformedURLException;
44
import java.net.URL;
55
import java.net.URLClassLoader;
6+
import java.nio.file.Files;
67
import java.nio.file.Path;
78
import java.util.ArrayList;
89
import java.util.Comparator;
@@ -19,6 +20,7 @@
1920
import jadx.api.plugins.loader.JadxPluginLoader;
2021
import jadx.core.utils.Utils;
2122
import jadx.core.utils.exceptions.JadxRuntimeException;
23+
import jadx.core.utils.files.FileUtils;
2224

2325
public class JadxExternalPluginsLoader implements JadxPluginLoader {
2426
private static final Logger LOG = LoggerFactory.getLogger(JadxExternalPluginsLoader.class);
@@ -44,16 +46,16 @@ public List<JadxPlugin> load() {
4446
return list;
4547
}
4648

47-
public JadxPlugin loadFromJar(Path jar) {
49+
public JadxPlugin loadFromPath(Path pluginPath) {
4850
Map<String, JadxPlugin> map = new HashMap<>();
49-
loadFromJar(map, jar);
51+
loadFromPath(map, pluginPath);
5052
int loaded = map.size();
5153
if (loaded == 0) {
52-
throw new JadxRuntimeException("No plugin found in jar: " + jar);
54+
throw new JadxRuntimeException("No plugin found in jar: " + pluginPath);
5355
}
5456
if (loaded > 1) {
5557
String plugins = map.values().stream().map(p -> p.getPluginInfo().getPluginId()).collect(Collectors.joining(", "));
56-
throw new JadxRuntimeException("Expect only one plugin per jar: " + jar + ", but found: " + loaded + " - " + plugins);
58+
throw new JadxRuntimeException("Expect only one plugin per jar: " + pluginPath + ", but found: " + loaded + " - " + plugins);
5759
}
5860
return Utils.first(map.values());
5961

@@ -72,22 +74,46 @@ private void loadFromClsLoader(Map<String, JadxPlugin> map, ClassLoader classLoa
7274
}
7375

7476
private void loadInstalledPlugins(Map<String, JadxPlugin> map) {
75-
List<Path> jars = JadxPluginsTools.getInstance().getEnabledPluginJars();
76-
for (Path jar : jars) {
77-
loadFromJar(map, jar);
77+
List<Path> paths = JadxPluginsTools.getInstance().getEnabledPluginPaths();
78+
for (Path pluginPath : paths) {
79+
loadFromPath(map, pluginPath);
7880
}
7981
}
8082

81-
private void loadFromJar(Map<String, JadxPlugin> map, Path jar) {
83+
private void loadFromPath(Map<String, JadxPlugin> map, Path pluginPath) {
8284
try {
83-
File jarFile = jar.toFile();
84-
String clsLoaderName = JADX_PLUGIN_CLASSLOADER_PREFIX + jarFile.getName();
85-
URL[] urls = new URL[] { jarFile.toURI().toURL() };
85+
URL[] urls;
86+
if (Files.isDirectory(pluginPath)) {
87+
urls = FileUtils.listFiles(pluginPath, file -> FileUtils.hasExtension(file, ".jar"))
88+
.stream()
89+
.map(JadxExternalPluginsLoader::toURL)
90+
.toArray(URL[]::new);
91+
if (urls.length == 0) {
92+
throw new JadxRuntimeException("No jar files found in plugin directory");
93+
}
94+
} else if (Files.isRegularFile(pluginPath)) {
95+
if (FileUtils.hasExtension(pluginPath, ".jar")) {
96+
urls = new URL[] { toURL(pluginPath) };
97+
} else {
98+
throw new JadxRuntimeException("Unexpected plugin file format");
99+
}
100+
} else {
101+
throw new JadxRuntimeException("Plugin file not found");
102+
}
103+
String clsLoaderName = JADX_PLUGIN_CLASSLOADER_PREFIX + pluginPath.getFileName();
86104
URLClassLoader pluginClsLoader = new URLClassLoader(clsLoaderName, urls, thisClassLoader());
87105
classLoaders.add(pluginClsLoader);
88106
loadFromClsLoader(map, pluginClsLoader);
89107
} catch (Exception e) {
90-
throw new JadxRuntimeException("Failed to load plugins from jar: " + jar, e);
108+
throw new JadxRuntimeException("Failed to load plugins from: " + pluginPath, e);
109+
}
110+
}
111+
112+
private static URL toURL(Path pluginPath) {
113+
try {
114+
return pluginPath.toUri().toURL();
115+
} catch (MalformedURLException e) {
116+
throw new RuntimeException(e);
91117
}
92118
}
93119

jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxPluginsTools.java

Lines changed: 63 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,29 @@
1515
import java.util.Optional;
1616
import java.util.function.Supplier;
1717
import java.util.stream.Collectors;
18-
import java.util.stream.Stream;
1918

2019
import org.jetbrains.annotations.Nullable;
2120
import org.slf4j.Logger;
2221
import org.slf4j.LoggerFactory;
2322

2423
import jadx.api.plugins.JadxPlugin;
2524
import jadx.api.plugins.JadxPluginInfo;
25+
import jadx.api.plugins.utils.CommonFileUtils;
2626
import jadx.core.Jadx;
2727
import jadx.core.plugins.versions.VerifyRequiredVersion;
2828
import jadx.core.utils.StringUtils;
2929
import jadx.core.utils.Utils;
3030
import jadx.core.utils.exceptions.JadxRuntimeException;
31+
import jadx.core.utils.files.FileUtils;
3132
import jadx.plugins.tools.data.JadxInstalledPlugins;
3233
import jadx.plugins.tools.data.JadxPluginMetadata;
3334
import jadx.plugins.tools.data.JadxPluginUpdate;
3435
import jadx.plugins.tools.resolvers.IJadxPluginResolver;
3536
import jadx.plugins.tools.resolvers.ResolversRegistry;
3637
import jadx.plugins.tools.utils.PluginUtils;
38+
import jadx.zip.IZipEntry;
39+
import jadx.zip.ZipContent;
40+
import jadx.zip.ZipReader;
3741

3842
import static jadx.core.utils.GsonUtils.buildGson;
3943
import static jadx.plugins.tools.utils.PluginFiles.DROPINS_DIR;
@@ -165,7 +169,7 @@ public boolean uninstall(String pluginId) {
165169
return false;
166170
}
167171
JadxPluginMetadata plugin = found.get();
168-
deletePluginJar(plugin);
172+
deletePlugin(plugin);
169173
plugins.getInstalled().remove(plugin);
170174
savePluginsJson(plugins);
171175
return true;
@@ -189,24 +193,15 @@ public List<JadxPluginInfo> getAllPluginsInfo() {
189193
}
190194
}
191195

192-
public List<Path> getAllPluginJars() {
193-
List<Path> list = new ArrayList<>();
194-
for (JadxPluginMetadata pluginMetadata : loadPluginsJson().getInstalled()) {
195-
list.add(INSTALLED_DIR.resolve(pluginMetadata.getJar()));
196-
}
197-
collectJarsFromDir(list, DROPINS_DIR);
198-
return list;
199-
}
200-
201-
public List<Path> getEnabledPluginJars() {
196+
public List<Path> getEnabledPluginPaths() {
202197
List<Path> list = new ArrayList<>();
203198
for (JadxPluginMetadata pluginMetadata : loadPluginsJson().getInstalled()) {
204199
if (pluginMetadata.isDisabled()) {
205200
continue;
206201
}
207-
list.add(INSTALLED_DIR.resolve(pluginMetadata.getJar()));
202+
list.add(INSTALLED_DIR.resolve(pluginMetadata.getPath()));
208203
}
209-
collectJarsFromDir(list, DROPINS_DIR);
204+
list.addAll(FileUtils.listFiles(DROPINS_DIR));
210205
return list;
211206
}
212207

@@ -254,46 +249,62 @@ private void install(JadxPluginMetadata metadata) {
254249
throw new JadxRuntimeException("Can't install plugin, required version: \"" + reqVersionStr + '\"'
255250
+ " is not compatible with current jadx version: " + Jadx.getVersion());
256251
}
252+
// remove previous version
253+
uninstall(metadata.getPluginId());
257254

258255
String version = metadata.getVersion();
259-
String fileName = metadata.getPluginId() + (StringUtils.notBlank(version) ? '-' + version : "") + ".jar";
260-
Path pluginJar = INSTALLED_DIR.resolve(fileName);
261-
copyJar(Paths.get(metadata.getJar()), pluginJar);
262-
metadata.setJar(INSTALLED_DIR.relativize(pluginJar).toString());
263-
264-
JadxInstalledPlugins plugins = loadPluginsJson();
265-
// remove previous version jar
266-
plugins.getInstalled().removeIf(p -> {
267-
if (p.getPluginId().equals(metadata.getPluginId())) {
268-
deletePluginJar(p);
269-
return true;
256+
String pluginBaseName = metadata.getPluginId() + (StringUtils.notBlank(version) ? '-' + version : "");
257+
String pluginPathStr = metadata.getPath();
258+
Path pluginPath = Paths.get(pluginPathStr);
259+
if (pluginPathStr.endsWith(".jar")) {
260+
Path pluginJar = INSTALLED_DIR.resolve(pluginBaseName + ".jar");
261+
copyJar(pluginPath, pluginJar);
262+
metadata.setPath(INSTALLED_DIR.relativize(pluginJar).toString());
263+
} else if (Files.isDirectory(pluginPath)) {
264+
Path pluginDir = INSTALLED_DIR.resolve(pluginBaseName);
265+
try {
266+
FileUtils.deleteDirIfExists(pluginDir);
267+
org.apache.commons.io.FileUtils.moveDirectory(pluginPath.toFile(), pluginDir.toFile());
268+
} catch (IOException e) {
269+
throw new JadxRuntimeException("Failed to install plugin: " + pluginBaseName, e);
270270
}
271-
return false;
272-
});
271+
metadata.setPath(INSTALLED_DIR.relativize(pluginDir).toString());
272+
} else {
273+
throw new JadxRuntimeException("Unexpected plugin path type: " + pluginPathStr);
274+
}
275+
// update plugins json
276+
JadxInstalledPlugins plugins = loadPluginsJson();
273277
plugins.getInstalled().add(metadata);
274278
plugins.setUpdated(System.currentTimeMillis());
275279
savePluginsJson(plugins);
276280
}
277281

278282
private void fillMetadata(JadxPluginMetadata metadata) {
279283
try {
280-
Path tmpJar;
281-
if (needDownload(metadata.getJar())) {
282-
tmpJar = Files.createTempFile(metadata.getName(), "plugin.jar");
283-
PluginUtils.downloadFile(metadata.getJar(), tmpJar);
284-
metadata.setJar(tmpJar.toAbsolutePath().toString());
285-
} else {
286-
tmpJar = Paths.get(metadata.getJar());
284+
String pluginPath = metadata.getPath();
285+
if (needDownload(pluginPath)) {
286+
// download plugin
287+
String ext = CommonFileUtils.getFileExtension(pluginPath);
288+
Path tmpJar = Files.createTempFile(metadata.getName(), "plugin." + ext);
289+
PluginUtils.downloadFile(pluginPath, tmpJar);
290+
pluginPath = tmpJar.toAbsolutePath().toString();
287291
}
288-
fillMetadataFromJar(metadata, tmpJar);
292+
if (pluginPath.endsWith(".zip")) {
293+
// unpack plugin zip
294+
Path tmpDir = Files.createTempDirectory(metadata.getName());
295+
unzip(Paths.get(pluginPath), tmpDir);
296+
pluginPath = tmpDir.toAbsolutePath().toString();
297+
}
298+
metadata.setPath(pluginPath);
299+
fillMetadataFromPath(metadata, Paths.get(pluginPath));
289300
} catch (Exception e) {
290301
throw new RuntimeException("Failed to fill plugin metadata, plugin: " + metadata.getPluginId(), e);
291302
}
292303
}
293304

294-
private void fillMetadataFromJar(JadxPluginMetadata metadata, Path jar) {
305+
private void fillMetadataFromPath(JadxPluginMetadata metadata, Path pluginPath) {
295306
try (JadxExternalPluginsLoader loader = new JadxExternalPluginsLoader()) {
296-
JadxPlugin jadxPlugin = loader.loadFromJar(jar);
307+
JadxPlugin jadxPlugin = loader.loadFromPath(pluginPath);
297308
JadxPluginInfo pluginInfo = jadxPlugin.getPluginInfo();
298309
metadata.setPluginId(pluginInfo.getPluginId());
299310
metadata.setName(pluginInfo.getName());
@@ -317,9 +328,14 @@ private void copyJar(Path sourceJar, Path destJar) {
317328
}
318329
}
319330

320-
private void deletePluginJar(JadxPluginMetadata plugin) {
331+
private void deletePlugin(JadxPluginMetadata plugin) {
321332
try {
322-
Files.deleteIfExists(INSTALLED_DIR.resolve(plugin.getJar()));
333+
Path pluginPath = INSTALLED_DIR.resolve(plugin.getPath());
334+
if (Files.isDirectory(pluginPath)) {
335+
FileUtils.deleteDir(pluginPath);
336+
} else {
337+
Files.deleteIfExists(pluginPath);
338+
}
323339
} catch (IOException e) {
324340
// ignore
325341
}
@@ -363,11 +379,15 @@ private void upgradePluginsData(JadxInstalledPlugins data) {
363379
}
364380
}
365381

366-
private static void collectJarsFromDir(List<Path> list, Path dir) {
367-
try (Stream<Path> files = Files.list(dir)) {
368-
files.filter(p -> p.getFileName().toString().endsWith(".jar")).forEach(list::add);
382+
private static void unzip(Path zipFile, Path outDir) {
383+
ZipReader zipReader = new ZipReader(); // TODO: pass zip options from jadx args
384+
try (ZipContent content = zipReader.open(zipFile.toFile())) {
385+
for (IZipEntry entry : content.getEntries()) {
386+
Path entryFile = outDir.resolve(entry.getName());
387+
Files.copy(entry.getInputStream(), entryFile, StandardCopyOption.REPLACE_EXISTING);
388+
}
369389
} catch (IOException e) {
370-
throw new RuntimeException(e);
390+
throw new JadxRuntimeException("Failed to unzip file: " + zipFile, e);
371391
}
372392
}
373393
}

0 commit comments

Comments
 (0)