From 94fb91cec6360e9f6d9c176f7fd9dfa8cde479cd Mon Sep 17 00:00:00 2001 From: Skylot Date: Wed, 2 Mar 2022 13:43:16 +0000 Subject: [PATCH] feat: add options for java-convert plugin --- .../java/jadx/tests/api/IntegrationTest.java | 34 ++++++++-- .../api/extensions/profiles/TestProfile.java | 18 +++-- .../integration/java8/TestLambdaResugar.java | 35 ++++++++++ .../others/TestStringConcatJava11.java | 3 +- .../plugins/input/dex/DexInputOptions.java | 35 +++------- .../input/javaconvert/D8Converter.java | 4 +- .../input/javaconvert/JavaConvertLoader.java | 65 ++++++++++++++----- .../input/javaconvert/JavaConvertOptions.java | 50 ++++++++++++++ .../input/javaconvert/JavaConvertPlugin.java | 23 ++++++- .../options/impl/BaseOptionsParser.java | 32 +++++++++ 10 files changed, 238 insertions(+), 61 deletions(-) create mode 100644 jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaResugar.java create mode 100644 jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertOptions.java create mode 100644 jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/options/impl/BaseOptionsParser.java diff --git a/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java b/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java index 19ca91a3..348ecc07 100644 --- a/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java +++ b/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java @@ -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; diff --git a/jadx-core/src/test/java/jadx/tests/api/extensions/profiles/TestProfile.java b/jadx-core/src/test/java/jadx/tests/api/extensions/profiles/TestProfile.java index fd8ef332..4dbd5e39 100644 --- a/jadx-core/src/test/java/jadx/tests/api/extensions/profiles/TestProfile.java +++ b/jadx-core/src/test/java/jadx/tests/api/extensions/profiles/TestProfile.java @@ -5,13 +5,23 @@ import java.util.function.Consumer; import jadx.tests.api.IntegrationTest; public enum TestProfile implements Consumer { - 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); diff --git a/jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaResugar.java b/jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaResugar.java new file mode 100644 index 00000000..aa219d91 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaResugar.java @@ -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 func) { + } + } + + @NotYetImplemented("Inline lambda methods") + @TestWithProfiles(TestProfile.D8_J11_DESUGAR) + public void test() { + assertThat(getClassNode(TestCls.class)) + .code() + .doesNotContain("lambda$"); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/others/TestStringConcatJava11.java b/jadx-core/src/test/java/jadx/tests/integration/others/TestStringConcatJava11.java index 399bd5d3..984ce97f 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/others/TestStringConcatJava11.java +++ b/jadx-core/src/test/java/jadx/tests/integration/others/TestStringConcatJava11.java @@ -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() diff --git a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputOptions.java b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputOptions.java index 36f9b445..cc34c79e 100644 --- a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputOptions.java +++ b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputOptions.java @@ -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 buildOptionsDescriptions() { - List 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 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() { diff --git a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/D8Converter.java b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/D8Converter.java index a71fe787..24ca8a5c 100644 --- a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/D8Converter.java +++ b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/D8Converter.java @@ -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); } diff --git a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertLoader.java b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertLoader.java index 67d43950..e37350e9 100644 --- a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertLoader.java +++ b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertLoader.java @@ -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 input) { + private final JavaConvertOptions options; + + public JavaConvertLoader(JavaConvertOptions options) { + this.options = options; + } + + public ConvertResult process(List 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 input, ConvertResult result) { + private void processJars(List 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 input, ConvertResult result) { + private void processClassFiles(List input, ConvertResult result) { PathMatcher jarMatcher = FileSystems.getDefault().getPathMatcher("glob:**.class"); List clsFiles = input.stream() .filter(jarMatcher::matches) @@ -72,7 +78,7 @@ public class JavaConvertLoader { } } - private static void processAars(List input, ConvertResult result) { + private void processAars(List 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 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 collectFilesInDir(Path tempDirectory) throws IOException { PathMatcher dexMatcher = FileSystems.getDefault().getPathMatcher("glob:**.dex"); try (Stream pathStream = Files.walk(tempDirectory, 1)) { diff --git a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertOptions.java b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertOptions.java new file mode 100644 index 00000000..044c0828 --- /dev/null +++ b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertOptions.java @@ -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 options) { + mode = getOption(options, MODE_OPT, name -> Mode.valueOf(name.toUpperCase(Locale.ROOT)), Mode.BOTH); + d8Desugar = getBooleanOption(options, D8_DESUGAR_OPT, false); + } + + public List 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; + } +} diff --git a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java index 8c1c9d57..072e92af 100644 --- a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java +++ b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java @@ -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 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 options) { + this.options.apply(options); + } + + @Override + public List getOptionsDescriptions() { + return this.options.buildOptionsDescriptions(); + } } diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/options/impl/BaseOptionsParser.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/options/impl/BaseOptionsParser.java new file mode 100644 index 00000000..1526580d --- /dev/null +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/options/impl/BaseOptionsParser.java @@ -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 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 getOption(Map options, String key, Function parse, T defValue) { + String val = options.get(key); + if (val == null) { + return defValue; + } + return parse.apply(val); + } +} -- GitLab