未验证 提交 94fb91ce 编写于 作者: S Skylot

feat: add options for java-convert plugin

上级 c54dd77f
......@@ -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_J8("d8-j8", test -> {
test.useTargetJavaVersion(8);
test.useDexInput("d8");
}),
D8_J11("d8-java-11", test -> {
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);
......
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()
......
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() {
......
......@@ -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);
}
......
......@@ -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)) {
......
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;
}
}
......@@ -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();
}
}
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);
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册