feat(plugins): allow to set minimum required jadx version in plugin info (#2314)

This commit is contained in:
Skylot
2024-11-06 16:29:43 +00:00
parent 5d064d3e50
commit be6cb573b1
19 changed files with 453 additions and 96 deletions
@@ -1,15 +1,29 @@
package jadx.api.plugins;
import org.jetbrains.annotations.Nullable;
public class JadxPluginInfo {
private final String pluginId;
private final String name;
private final String description;
private final String homepage;
private String homepage;
/**
* Conflicting plugins should have the same 'provides' property; only one will be loaded
*/
private final String provides;
private String provides;
/**
* Minimum required jadx version to run this plugin.
* <br>
* Format: "<stable version>, r<revision number of unstable version>".
* Example: "1.5.1, r2305"
*
* @see <a href="https://github.com/skylot/jadx/wiki/Jadx-plugins-guide#required-jadx-version">wiki
* page</a>
* for details.
*/
private @Nullable String requiredJadxVersion;
public JadxPluginInfo(String id, String name, String description) {
this(id, name, description, "", id);
@@ -43,10 +57,26 @@ public class JadxPluginInfo {
return homepage;
}
public void setHomepage(String homepage) {
this.homepage = homepage;
}
public String getProvides() {
return provides;
}
public void setProvides(String provides) {
this.provides = provides;
}
public @Nullable String getRequiredJadxVersion() {
return requiredJadxVersion;
}
public void setRequiredJadxVersion(@Nullable String requiredJadxVersion) {
this.requiredJadxVersion = requiredJadxVersion;
}
@Override
public String toString() {
return pluginId + ": " + name + " - '" + description + '\'';
@@ -4,11 +4,14 @@ import java.util.Objects;
import org.jetbrains.annotations.Nullable;
import jadx.core.plugins.versions.VerifyRequiredVersion;
public class JadxPluginInfoBuilder {
private String pluginId;
private String name;
private String description;
private String homepage = "";
private @Nullable String requiredJadxVersion;
private @Nullable String provides;
/**
@@ -43,6 +46,11 @@ public class JadxPluginInfoBuilder {
return this;
}
public JadxPluginInfoBuilder requiredJadxVersion(String versions) {
this.requiredJadxVersion = versions;
return this;
}
public JadxPluginInfo build() {
Objects.requireNonNull(pluginId, "PluginId is required");
Objects.requireNonNull(name, "Name is required");
@@ -50,6 +58,11 @@ public class JadxPluginInfoBuilder {
if (provides == null) {
provides = pluginId;
}
return new JadxPluginInfo(pluginId, name, description, homepage, provides);
if (requiredJadxVersion != null) {
VerifyRequiredVersion.verify(requiredJadxVersion);
}
JadxPluginInfo pluginInfo = new JadxPluginInfo(pluginId, name, description, homepage, provides);
pluginInfo.setRequiredJadxVersion(requiredJadxVersion);
return pluginInfo;
}
}
@@ -21,6 +21,7 @@ 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.plugins.versions.VerifyRequiredVersion;
import jadx.core.utils.exceptions.JadxRuntimeException;
public class JadxPluginManager {
@@ -50,15 +51,16 @@ public class JadxPluginManager {
public void load(JadxPluginLoader pluginLoader) {
allPlugins.clear();
VerifyRequiredVersion verifyRequiredVersion = new VerifyRequiredVersion();
for (JadxPlugin plugin : pluginLoader.load()) {
addPlugin(plugin);
addPlugin(plugin, verifyRequiredVersion);
}
resolve();
}
public void register(JadxPlugin plugin) {
Objects.requireNonNull(plugin);
PluginContext addedPlugin = addPlugin(plugin);
PluginContext addedPlugin = addPlugin(plugin, new VerifyRequiredVersion());
if (addedPlugin == null) {
LOG.debug("Can't register plugin, it was disabled: {}", plugin.getPluginInfo().getPluginId());
return;
@@ -67,11 +69,17 @@ public class JadxPluginManager {
resolve();
}
private @Nullable PluginContext addPlugin(JadxPlugin plugin) {
private @Nullable PluginContext addPlugin(JadxPlugin plugin, VerifyRequiredVersion verifyRequiredVersion) {
PluginContext pluginContext = new PluginContext(decompiler, pluginsData, plugin);
if (disabledPlugins.contains(pluginContext.getPluginId())) {
return null;
}
String requiredJadxVersion = pluginContext.getPluginInfo().getRequiredJadxVersion();
if (!verifyRequiredVersion.isCompatible(requiredJadxVersion)) {
LOG.warn("Plugin '{}' not loaded: requires '{}' jadx version which it is not compatible with current: {}",
pluginContext, requiredJadxVersion, verifyRequiredVersion.getJadxVersion());
return null;
}
LOG.debug("Loading plugin: {}", pluginContext);
if (!allPlugins.add(pluginContext)) {
throw new IllegalArgumentException("Duplicate plugin id: " + pluginContext + ", class " + plugin.getClass());
@@ -0,0 +1,85 @@
package jadx.core.plugins.versions;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jetbrains.annotations.Nullable;
import jadx.core.Jadx;
public class VerifyRequiredVersion {
public static boolean isJadxCompatible(@Nullable String reqVersionStr) {
return new VerifyRequiredVersion().isCompatible(reqVersionStr);
}
public static void verify(String requiredJadxVersion) {
try {
parse(requiredJadxVersion);
} catch (Exception e) {
throw new IllegalArgumentException("Malformed 'requiredJadxVersion': " + e.getMessage(), e);
}
}
private final String jadxVersion;
private final boolean unstable;
private final boolean dev;
public VerifyRequiredVersion() {
this(Jadx.getVersion());
}
public VerifyRequiredVersion(String jadxVersion) {
this.jadxVersion = jadxVersion;
this.unstable = jadxVersion.startsWith("r");
this.dev = jadxVersion.equals(Jadx.VERSION_DEV);
}
public boolean isCompatible(@Nullable String reqVersionStr) {
if (reqVersionStr == null || reqVersionStr.isEmpty()) {
return true;
}
RequiredVersionData reqVer = parse(reqVersionStr);
if (dev) {
// keep version str parsing for verification
return true;
}
if (unstable) {
return VersionComparator.checkAndCompare(jadxVersion, reqVer.getUnstableRev()) >= 0;
}
return VersionComparator.checkAndCompare(jadxVersion, reqVer.getReleaseVer()) >= 0;
}
public String getJadxVersion() {
return jadxVersion;
}
private static final Pattern REQ_VER_FORMAT = Pattern.compile("(\\d+\\.\\d+\\.\\d+),\\s+(r\\d+)");
private static RequiredVersionData parse(String reqVersionStr) {
Matcher matcher = REQ_VER_FORMAT.matcher(reqVersionStr);
if (!matcher.matches()) {
throw new RuntimeException("Expect format: " + REQ_VER_FORMAT + ", got: " + reqVersionStr);
}
return new RequiredVersionData(matcher.group(1), matcher.group(2));
}
private static final class RequiredVersionData {
private final String releaseVer;
private final String unstableRev;
private RequiredVersionData(String releaseVer, String unstableRev) {
this.releaseVer = releaseVer;
this.unstableRev = unstableRev;
}
public String getReleaseVer() {
return releaseVer;
}
public String getUnstableRev() {
return unstableRev;
}
}
}
@@ -0,0 +1,72 @@
package jadx.core.plugins.versions;
public class VersionComparator {
private VersionComparator() {
}
public static int checkAndCompare(String str1, String str2) {
return compare(clean(str1), clean(str2));
}
private static String clean(String str) {
if (str == null || str.isEmpty()) {
return "";
}
String result = str.trim().toLowerCase();
if (result.startsWith("jadx-gui-")) {
result = result.substring(9);
}
if (result.startsWith("jadx-")) {
result = result.substring(5);
}
if (result.charAt(0) == 'v') {
result = result.substring(1);
}
if (result.charAt(0) == 'r') {
result = result.substring(1);
int dot = result.indexOf('.');
if (dot != -1) {
result = result.substring(0, dot);
}
}
// treat a package version as part of version
result = result.replace('-', '.');
return result;
}
private static int compare(String str1, String str2) {
String[] s1 = str1.split("\\.");
int l1 = s1.length;
String[] s2 = str2.split("\\.");
int l2 = s2.length;
int i = 0;
// skip equals parts
while (i < l1 && i < l2) {
if (!s1[i].equals(s2[i])) {
break;
}
i++;
}
// compare first non-equal ordinal number
if (i < l1 && i < l2) {
return Integer.valueOf(s1[i]).compareTo(Integer.valueOf(s2[i]));
}
boolean checkFirst = l1 > l2;
boolean zeroTail = isZeroTail(checkFirst ? s1 : s2, i);
if (zeroTail) {
return 0;
}
return checkFirst ? 1 : -1;
}
private static boolean isZeroTail(String[] arr, int pos) {
for (int i = pos; i < arr.length; i++) {
if (Integer.parseInt(arr[i]) != 0) {
return false;
}
}
return true;
}
}