feat: add options for java-convert plugin
This commit is contained in:
@@ -104,7 +104,8 @@ public abstract class IntegrationTest extends TestUtils {
|
||||
private boolean printLineNumbers;
|
||||
private boolean printOffsets;
|
||||
private boolean printDisassemble;
|
||||
private Boolean useJavaInput = null;
|
||||
private @Nullable Boolean useJavaInput;
|
||||
private boolean removeParentClassOnInput;
|
||||
|
||||
private @Nullable TestCompiler sourceCompiler;
|
||||
private @Nullable TestCompiler decompiledCompiler;
|
||||
@@ -121,6 +122,8 @@ public abstract class IntegrationTest extends TestUtils {
|
||||
this.compile = true;
|
||||
this.compilerOptions = new CompilerOptions();
|
||||
this.resMap = Collections.emptyMap();
|
||||
this.removeParentClassOnInput = true;
|
||||
this.useJavaInput = null;
|
||||
|
||||
args = new JadxArgs();
|
||||
args.setOutDir(new File(OUT_DIR));
|
||||
@@ -173,8 +176,14 @@ public abstract class IntegrationTest extends TestUtils {
|
||||
|
||||
ClassNode cls = root.resolveClass(clsName);
|
||||
assertThat("Class not found: " + clsName, cls, notNullValue());
|
||||
assertThat(clsName, is(cls.getClassInfo().getFullName()));
|
||||
|
||||
if (removeParentClassOnInput) {
|
||||
assertThat(clsName, is(cls.getClassInfo().getFullName()));
|
||||
} else {
|
||||
LOG.info("Convert back to top level: {}", cls);
|
||||
cls.getTopParentClass().decompile(); // keep correct process order
|
||||
cls.getClassInfo().notInner(root);
|
||||
cls.updateParentClass();
|
||||
}
|
||||
decompileAndCheck(cls);
|
||||
return cls;
|
||||
}
|
||||
@@ -332,7 +341,7 @@ public abstract class IntegrationTest extends TestUtils {
|
||||
}
|
||||
|
||||
private void runAutoCheck(ClassNode cls) {
|
||||
String clsName = cls.getClassInfo().getFullName();
|
||||
String clsName = cls.getClassInfo().getRawName().replace('/', '.');
|
||||
try {
|
||||
// run 'check' method from original class
|
||||
if (runSourceAutoCheck(clsName)) {
|
||||
@@ -473,9 +482,11 @@ public abstract class IntegrationTest extends TestUtils {
|
||||
if (saveTestJar) {
|
||||
saveToJar(files, outTmp);
|
||||
}
|
||||
// remove classes which are parents for test class
|
||||
String clsName = clsFullName.substring(clsFullName.lastIndexOf('.') + 1);
|
||||
files.removeIf(next -> !next.getName().contains(clsName));
|
||||
if (removeParentClassOnInput) {
|
||||
// remove classes which are parents for test class
|
||||
String clsName = clsFullName.substring(clsFullName.lastIndexOf('.') + 1);
|
||||
files.removeIf(next -> !next.getName().contains(clsName));
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
@@ -561,10 +572,19 @@ public abstract class IntegrationTest extends TestUtils {
|
||||
this.useJavaInput = false;
|
||||
}
|
||||
|
||||
public void useDexInput(String mode) {
|
||||
useDexInput();
|
||||
this.getArgs().getPluginOptions().put("java-convert.mode", mode);
|
||||
}
|
||||
|
||||
protected boolean isJavaInput() {
|
||||
return Utils.getOrElse(useJavaInput, USE_JAVA_INPUT);
|
||||
}
|
||||
|
||||
public void keepParentClassOnInput() {
|
||||
this.removeParentClassOnInput = false;
|
||||
}
|
||||
|
||||
// Use only for debug purpose
|
||||
protected void printDisassemble() {
|
||||
this.printDisassemble = true;
|
||||
|
||||
@@ -5,13 +5,23 @@ import java.util.function.Consumer;
|
||||
import jadx.tests.api.IntegrationTest;
|
||||
|
||||
public enum TestProfile implements Consumer<IntegrationTest> {
|
||||
DX_J8("dx-java-8", test -> {
|
||||
DX_J8("dx-j8", test -> {
|
||||
test.useTargetJavaVersion(8);
|
||||
test.useDexInput();
|
||||
test.useDexInput("dx");
|
||||
}),
|
||||
D8_J11("d8-java-11", test -> {
|
||||
D8_J8("d8-j8", test -> {
|
||||
test.useTargetJavaVersion(8);
|
||||
test.useDexInput("d8");
|
||||
}),
|
||||
D8_J11("d8-j11", test -> {
|
||||
test.useTargetJavaVersion(11);
|
||||
test.useDexInput();
|
||||
test.useDexInput("d8");
|
||||
}),
|
||||
D8_J11_DESUGAR("d8-j11-desugar", test -> {
|
||||
test.useTargetJavaVersion(11);
|
||||
test.useDexInput("d8");
|
||||
test.keepParentClassOnInput();
|
||||
test.getArgs().getPluginOptions().put("java-convert.d8-desugar", "yes");
|
||||
}),
|
||||
JAVA8("java-8", test -> {
|
||||
test.useTargetJavaVersion(8);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package jadx.tests.integration.java8;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import jadx.NotYetImplemented;
|
||||
import jadx.tests.api.IntegrationTest;
|
||||
import jadx.tests.api.extensions.profiles.TestProfile;
|
||||
import jadx.tests.api.extensions.profiles.TestWithProfiles;
|
||||
|
||||
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
|
||||
|
||||
public class TestLambdaResugar extends IntegrationTest {
|
||||
|
||||
public static class TestCls {
|
||||
private String field;
|
||||
|
||||
public void test() {
|
||||
call(s -> {
|
||||
this.field = s;
|
||||
return s.length();
|
||||
});
|
||||
}
|
||||
|
||||
public void call(Function<String, Integer> func) {
|
||||
}
|
||||
}
|
||||
|
||||
@NotYetImplemented("Inline lambda methods")
|
||||
@TestWithProfiles(TestProfile.D8_J11_DESUGAR)
|
||||
public void test() {
|
||||
assertThat(getClassNode(TestCls.class))
|
||||
.code()
|
||||
.doesNotContain("lambda$");
|
||||
}
|
||||
}
|
||||
@@ -52,9 +52,8 @@ public class TestStringConcatJava11 extends RaungTest {
|
||||
"return str + \"test\" + str + \"7\";"); // dynamic concat add const to string recipe
|
||||
}
|
||||
|
||||
@TestWithProfiles({ TestProfile.DX_J8, TestProfile.JAVA8 })
|
||||
@TestWithProfiles({ TestProfile.D8_J11, TestProfile.JAVA11 })
|
||||
public void testJava11() {
|
||||
useTargetJavaVersion(11);
|
||||
noDebugInfo();
|
||||
assertThat(getClassNode(TestCls.class))
|
||||
.code()
|
||||
|
||||
+9
-26
@@ -1,15 +1,15 @@
|
||||
package jadx.plugins.input.dex;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import jadx.api.plugins.options.OptionDescription;
|
||||
import jadx.api.plugins.options.impl.BaseOptionsParser;
|
||||
import jadx.api.plugins.options.impl.JadxOptionDescription;
|
||||
|
||||
public class DexInputOptions {
|
||||
public class DexInputOptions extends BaseOptionsParser {
|
||||
|
||||
private static final String VERIFY_CHECKSUM_OPT = DexInputPlugin.PLUGIN_ID + ".verify-checksum";
|
||||
|
||||
@@ -20,29 +20,12 @@ public class DexInputOptions {
|
||||
}
|
||||
|
||||
public List<OptionDescription> buildOptionsDescriptions() {
|
||||
List<OptionDescription> list = new ArrayList<>(1);
|
||||
list.add(new JadxOptionDescription(
|
||||
VERIFY_CHECKSUM_OPT,
|
||||
"Verify dex file checksum before load",
|
||||
"yes",
|
||||
Arrays.asList("yes", "no")));
|
||||
return list;
|
||||
}
|
||||
|
||||
private boolean getBooleanOption(Map<String, String> options, String key, boolean defValue) {
|
||||
String val = options.get(key);
|
||||
if (val == null) {
|
||||
return defValue;
|
||||
}
|
||||
String valLower = val.toLowerCase(Locale.ROOT);
|
||||
if (valLower.equals("yes") || valLower.equals("true")) {
|
||||
return true;
|
||||
}
|
||||
if (valLower.equals("no") || valLower.equals("false")) {
|
||||
return false;
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown value '" + val + "' for option '" + key + "'"
|
||||
+ ", expect: 'yes' or 'no'");
|
||||
return Collections.singletonList(
|
||||
new JadxOptionDescription(
|
||||
VERIFY_CHECKSUM_OPT,
|
||||
"Verify dex file checksum before load",
|
||||
"yes",
|
||||
Arrays.asList("yes", "no")));
|
||||
}
|
||||
|
||||
public boolean isVerifyChecksum() {
|
||||
|
||||
+2
-2
@@ -16,14 +16,14 @@ import com.android.tools.r8.OutputMode;
|
||||
public class D8Converter {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(D8Converter.class);
|
||||
|
||||
public static void run(Path path, Path tempDirectory) throws CompilationFailedException {
|
||||
public static void run(Path path, Path tempDirectory, JavaConvertOptions options) throws CompilationFailedException {
|
||||
D8Command d8Command = D8Command.builder(new LogHandler())
|
||||
.addProgramFiles(path)
|
||||
.setOutput(tempDirectory, OutputMode.DexIndexed)
|
||||
.setMode(CompilationMode.DEBUG)
|
||||
.setMinApiLevel(30)
|
||||
.setIntermediate(true)
|
||||
.setDisableDesugaring(true)
|
||||
.setDisableDesugaring(!options.isD8Desugar())
|
||||
.build();
|
||||
D8.run(d8Command);
|
||||
}
|
||||
|
||||
+48
-17
@@ -23,7 +23,13 @@ import jadx.api.plugins.utils.ZipSecurity;
|
||||
public class JavaConvertLoader {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JavaConvertLoader.class);
|
||||
|
||||
public static ConvertResult process(List<Path> input) {
|
||||
private final JavaConvertOptions options;
|
||||
|
||||
public JavaConvertLoader(JavaConvertOptions options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
public ConvertResult process(List<Path> input) {
|
||||
ConvertResult result = new ConvertResult();
|
||||
processJars(input, result);
|
||||
processAars(input, result);
|
||||
@@ -31,7 +37,7 @@ public class JavaConvertLoader {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void processJars(List<Path> input, ConvertResult result) {
|
||||
private void processJars(List<Path> input, ConvertResult result) {
|
||||
PathMatcher jarMatcher = FileSystems.getDefault().getPathMatcher("glob:**.jar");
|
||||
input.stream()
|
||||
.filter(jarMatcher::matches)
|
||||
@@ -44,7 +50,7 @@ public class JavaConvertLoader {
|
||||
});
|
||||
}
|
||||
|
||||
private static void processClassFiles(List<Path> input, ConvertResult result) {
|
||||
private void processClassFiles(List<Path> input, ConvertResult result) {
|
||||
PathMatcher jarMatcher = FileSystems.getDefault().getPathMatcher("glob:**.class");
|
||||
List<Path> clsFiles = input.stream()
|
||||
.filter(jarMatcher::matches)
|
||||
@@ -72,7 +78,7 @@ public class JavaConvertLoader {
|
||||
}
|
||||
}
|
||||
|
||||
private static void processAars(List<Path> input, ConvertResult result) {
|
||||
private void processAars(List<Path> input, ConvertResult result) {
|
||||
PathMatcher aarMatcher = FileSystems.getDefault().getPathMatcher("glob:**.aar");
|
||||
input.stream()
|
||||
.filter(aarMatcher::matches)
|
||||
@@ -91,14 +97,14 @@ public class JavaConvertLoader {
|
||||
}));
|
||||
}
|
||||
|
||||
private static void convertJar(ConvertResult result, Path path) throws Exception {
|
||||
private void convertJar(ConvertResult result, Path path) throws Exception {
|
||||
if (repackAndConvertJar(result, path)) {
|
||||
return;
|
||||
}
|
||||
convertSimpleJar(result, path);
|
||||
}
|
||||
|
||||
private static boolean repackAndConvertJar(ConvertResult result, Path path) throws Exception {
|
||||
private boolean repackAndConvertJar(ConvertResult result, Path path) throws Exception {
|
||||
// check if jar need a full repackage
|
||||
Boolean repackNeeded = ZipSecurity.visitZipEntries(path.toFile(), (zipFile, zipEntry) -> {
|
||||
String entryName = zipEntry.getName();
|
||||
@@ -154,25 +160,50 @@ public class JavaConvertLoader {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void convertSimpleJar(ConvertResult result, Path path) throws Exception {
|
||||
private void convertSimpleJar(ConvertResult result, Path path) throws Exception {
|
||||
Path tempDirectory = Files.createTempDirectory("jadx-");
|
||||
result.addTempPath(tempDirectory);
|
||||
LOG.debug("Converting to dex ...");
|
||||
try {
|
||||
DxConverter.run(path, tempDirectory);
|
||||
} catch (Throwable e) {
|
||||
LOG.warn("DX convert failed, trying D8, path: {}", path);
|
||||
try {
|
||||
D8Converter.run(path, tempDirectory);
|
||||
} catch (Throwable ex) {
|
||||
LOG.error("D8 convert failed: {}", ex.getMessage());
|
||||
}
|
||||
}
|
||||
convert(path, tempDirectory);
|
||||
List<Path> dexFiles = collectFilesInDir(tempDirectory);
|
||||
LOG.debug("Converted {} to {} dex", path.toAbsolutePath(), dexFiles.size());
|
||||
result.addConvertedFiles(dexFiles);
|
||||
}
|
||||
|
||||
private void convert(Path path, Path tempDirectory) {
|
||||
JavaConvertOptions.Mode mode = options.getMode();
|
||||
switch (mode) {
|
||||
case DX:
|
||||
try {
|
||||
DxConverter.run(path, tempDirectory);
|
||||
} catch (Throwable e) {
|
||||
LOG.error("DX convert failed, path: {}", path, e);
|
||||
}
|
||||
break;
|
||||
|
||||
case D8:
|
||||
try {
|
||||
D8Converter.run(path, tempDirectory, options);
|
||||
} catch (Throwable e) {
|
||||
LOG.error("D8 convert failed, path: {}", path, e);
|
||||
}
|
||||
break;
|
||||
|
||||
case BOTH:
|
||||
try {
|
||||
DxConverter.run(path, tempDirectory);
|
||||
} catch (Throwable e) {
|
||||
LOG.warn("DX convert failed, trying D8, path: {}", path);
|
||||
try {
|
||||
D8Converter.run(path, tempDirectory, options);
|
||||
} catch (Throwable ex) {
|
||||
LOG.error("D8 convert failed: {}", ex.getMessage());
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Path> collectFilesInDir(Path tempDirectory) throws IOException {
|
||||
PathMatcher dexMatcher = FileSystems.getDefault().getPathMatcher("glob:**.dex");
|
||||
try (Stream<Path> pathStream = Files.walk(tempDirectory, 1)) {
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package jadx.plugins.input.javaconvert;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import jadx.api.plugins.options.OptionDescription;
|
||||
import jadx.api.plugins.options.impl.BaseOptionsParser;
|
||||
import jadx.api.plugins.options.impl.JadxOptionDescription;
|
||||
|
||||
public class JavaConvertOptions extends BaseOptionsParser {
|
||||
|
||||
private static final String MODE_OPT = JavaConvertPlugin.PLUGIN_ID + ".mode";
|
||||
private static final String D8_DESUGAR_OPT = JavaConvertPlugin.PLUGIN_ID + ".d8-desugar";
|
||||
|
||||
public enum Mode {
|
||||
DX, D8, BOTH
|
||||
}
|
||||
|
||||
private Mode mode = Mode.BOTH;
|
||||
private boolean d8Desugar = false;
|
||||
|
||||
public void apply(Map<String, String> options) {
|
||||
mode = getOption(options, MODE_OPT, name -> Mode.valueOf(name.toUpperCase(Locale.ROOT)), Mode.BOTH);
|
||||
d8Desugar = getBooleanOption(options, D8_DESUGAR_OPT, false);
|
||||
}
|
||||
|
||||
public List<OptionDescription> buildOptionsDescriptions() {
|
||||
return Arrays.asList(
|
||||
new JadxOptionDescription(
|
||||
MODE_OPT,
|
||||
"Convert mode",
|
||||
"both",
|
||||
Arrays.asList("dx", "d8", "both")),
|
||||
new JadxOptionDescription(
|
||||
D8_DESUGAR_OPT,
|
||||
"Use desugar in d8",
|
||||
"no",
|
||||
Arrays.asList("yes", "no")));
|
||||
}
|
||||
|
||||
public Mode getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
public boolean isD8Desugar() {
|
||||
return d8Desugar;
|
||||
}
|
||||
}
|
||||
+20
-3
@@ -2,21 +2,28 @@ package jadx.plugins.input.javaconvert;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import jadx.api.plugins.JadxPluginInfo;
|
||||
import jadx.api.plugins.input.JadxInputPlugin;
|
||||
import jadx.api.plugins.input.data.ILoadResult;
|
||||
import jadx.api.plugins.input.data.impl.EmptyLoadResult;
|
||||
import jadx.api.plugins.options.JadxPluginOptions;
|
||||
import jadx.api.plugins.options.OptionDescription;
|
||||
import jadx.plugins.input.dex.DexInputPlugin;
|
||||
|
||||
public class JavaConvertPlugin implements JadxInputPlugin {
|
||||
public class JavaConvertPlugin implements JadxInputPlugin, JadxPluginOptions {
|
||||
|
||||
public static final String PLUGIN_ID = "java-convert";
|
||||
|
||||
private final DexInputPlugin dexInput = new DexInputPlugin();
|
||||
private final JavaConvertOptions options = new JavaConvertOptions();
|
||||
private final JavaConvertLoader loader = new JavaConvertLoader(options);
|
||||
|
||||
@Override
|
||||
public JadxPluginInfo getPluginInfo() {
|
||||
return new JadxPluginInfo(
|
||||
"java-convert",
|
||||
PLUGIN_ID,
|
||||
"JavaConvert",
|
||||
"Convert .jar and .class files to dex",
|
||||
"java-input");
|
||||
@@ -24,11 +31,21 @@ public class JavaConvertPlugin implements JadxInputPlugin {
|
||||
|
||||
@Override
|
||||
public ILoadResult loadFiles(List<Path> input) {
|
||||
ConvertResult result = JavaConvertLoader.process(input);
|
||||
ConvertResult result = loader.process(input);
|
||||
if (result.isEmpty()) {
|
||||
result.close();
|
||||
return EmptyLoadResult.INSTANCE;
|
||||
}
|
||||
return dexInput.loadFiles(result.getConverted(), result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOptions(Map<String, String> options) {
|
||||
this.options.apply(options);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OptionDescription> getOptionsDescriptions() {
|
||||
return this.options.buildOptionsDescriptions();
|
||||
}
|
||||
}
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package jadx.api.plugins.options.impl;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class BaseOptionsParser {
|
||||
|
||||
public boolean getBooleanOption(Map<String, String> options, String key, boolean defValue) {
|
||||
String val = options.get(key);
|
||||
if (val == null) {
|
||||
return defValue;
|
||||
}
|
||||
String valLower = val.toLowerCase(Locale.ROOT);
|
||||
if (valLower.equals("yes") || valLower.equals("true")) {
|
||||
return true;
|
||||
}
|
||||
if (valLower.equals("no") || valLower.equals("false")) {
|
||||
return false;
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown value '" + val + "' for option '" + key + "'"
|
||||
+ ", expect: 'yes' or 'no'");
|
||||
}
|
||||
|
||||
public <T> T getOption(Map<String, String> options, String key, Function<String, T> parse, T defValue) {
|
||||
String val = options.get(key);
|
||||
if (val == null) {
|
||||
return defValue;
|
||||
}
|
||||
return parse.apply(val);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user