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;
}
}
@@ -0,0 +1,27 @@
package jadx.core.plugins.versions;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class VerifyRequiredVersionTest {
@Test
public void test() {
isCompatible("1.5.0, r2000", "1.5.1", true);
isCompatible("1.5.1, r3000", "1.5.1", true);
isCompatible("1.5.1, r3000", "1.6.0", true);
isCompatible("1.5.1, r3000", "1.5.0", false);
isCompatible("1.5.1, r3000", "r3001.417bb7a", true);
isCompatible("1.5.1, r3000", "r4000", true);
isCompatible("1.5.1, r3000", "r3000", true);
isCompatible("1.5.1, r3000", "r2000", false);
}
private static void isCompatible(String requiredVersion, String jadxVersion, boolean result) {
assertThat(new VerifyRequiredVersion(jadxVersion).isCompatible(requiredVersion))
.as("Expect plugin with required version %s is%s compatible with jadx %s",
requiredVersion, result ? "" : " not", jadxVersion)
.isEqualTo(result);
}
}
@@ -0,0 +1,44 @@
package jadx.core.plugins.versions;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class VersionComparatorTest {
@Test
public void testCompare() {
checkCompare("", "", 0);
checkCompare("1", "1", 0);
checkCompare("1", "2", -1);
checkCompare("1.1", "1.1", 0);
checkCompare("0.5", "0.5", 0);
checkCompare("0.5", "0.5.0", 0);
checkCompare("0.5", "0.5.00", 0);
checkCompare("0.5", "0.5.0.0", 0);
checkCompare("0.5", "0.5.0.1", -1);
checkCompare("0.5.0", "0.5.0", 0);
checkCompare("0.5.0", "0.5.1", -1);
checkCompare("0.5", "0.5.1", -1);
checkCompare("0.4.8", "0.5", -1);
checkCompare("0.4.8", "0.5.0", -1);
checkCompare("0.4.8", "0.6", -1);
checkCompare("1.3.3.1", "1.3.3", 1);
checkCompare("1.3.3-1", "1.3.3", 1);
checkCompare("1.3.3.1-1", "1.3.3", 1);
}
@Test
public void testCompareUnstable() {
checkCompare("r2190.ce527ed", "jadx-r2299.742d30d", -1);
}
private static void checkCompare(String a, String b, int result) {
assertThat(VersionComparator.checkAndCompare(a, b))
.as("Compare %s and %s expect %d", a, b, result)
.isEqualTo(result);
assertThat(VersionComparator.checkAndCompare(b, a))
.as("Compare %s and %s expect %d", b, a, -result)
.isEqualTo(-result);
}
}