feat: add gradle export templates, support android app/lib and simple java
This commit is contained in:
@@ -2,6 +2,7 @@ package jadx.cli;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -13,6 +14,8 @@ import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import com.beust.jcommander.DynamicParameter;
|
||||
import com.beust.jcommander.IStringConverter;
|
||||
import com.beust.jcommander.Parameter;
|
||||
@@ -29,13 +32,14 @@ import jadx.api.args.ResourceNameSource;
|
||||
import jadx.api.args.UseSourceNameAsClassNameAlias;
|
||||
import jadx.api.args.UserRenamesMappingsMode;
|
||||
import jadx.core.deobf.conditions.DeobfWhitelist;
|
||||
import jadx.core.export.ExportGradleType;
|
||||
import jadx.core.utils.exceptions.JadxArgsValidateException;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
|
||||
public class JadxCLIArgs {
|
||||
|
||||
@Parameter(description = "<input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .apkm, .jadx.kts)")
|
||||
protected List<String> files;
|
||||
protected List<String> files = Collections.emptyList();
|
||||
|
||||
@Parameter(names = { "-d", "--output-dir" }, description = "output directory")
|
||||
protected String outDir;
|
||||
@@ -52,6 +56,9 @@ public class JadxCLIArgs {
|
||||
@Parameter(names = { "-s", "--no-src" }, description = "do not decompile source code")
|
||||
protected boolean skipSources = false;
|
||||
|
||||
@Parameter(names = { "-j", "--threads-count" }, description = "processing threads count")
|
||||
protected int threadsCount = JadxArgs.DEFAULT_THREADS_COUNT;
|
||||
|
||||
@Parameter(names = { "--single-class" }, description = "decompile a single class, full name, raw or alias")
|
||||
protected String singleClass = null;
|
||||
|
||||
@@ -61,11 +68,19 @@ public class JadxCLIArgs {
|
||||
@Parameter(names = { "--output-format" }, description = "can be 'java' or 'json'")
|
||||
protected String outputFormat = "java";
|
||||
|
||||
@Parameter(names = { "-e", "--export-gradle" }, description = "save as android gradle project")
|
||||
@Parameter(names = { "-e", "--export-gradle" }, description = "save as gradle project (set '--export-gradle-type' to 'auto')")
|
||||
protected boolean exportAsGradleProject = false;
|
||||
|
||||
@Parameter(names = { "-j", "--threads-count" }, description = "processing threads count")
|
||||
protected int threadsCount = JadxArgs.DEFAULT_THREADS_COUNT;
|
||||
@Parameter(
|
||||
names = { "--export-gradle-type" },
|
||||
description = "Gradle project template for export:"
|
||||
+ "\n 'auto' - detect automatically"
|
||||
+ "\n 'android-app' - Android Application (apk)"
|
||||
+ "\n 'android-library' - Android Library (aar)"
|
||||
+ "\n 'simple-java' - simple Java",
|
||||
converter = ExportGradleTypeConverter.class
|
||||
)
|
||||
protected @Nullable ExportGradleType exportGradleType = null;
|
||||
|
||||
@Parameter(
|
||||
names = { "-m", "--decompilation-mode" },
|
||||
@@ -344,7 +359,10 @@ public class JadxCLIArgs {
|
||||
args.setResourceNameSource(resourceNameSource);
|
||||
args.setEscapeUnicode(escapeUnicode);
|
||||
args.setRespectBytecodeAccModifiers(respectBytecodeAccessModifiers);
|
||||
args.setExportAsGradleProject(exportAsGradleProject);
|
||||
args.setExportGradleType(exportGradleType);
|
||||
if (exportAsGradleProject && exportGradleType == null) {
|
||||
args.setExportGradleType(ExportGradleType.AUTO);
|
||||
}
|
||||
args.setSkipXmlPrettyPrint(skipXmlPrettyPrint);
|
||||
args.setUseImports(useImports);
|
||||
args.setDebugInfo(debugInfo);
|
||||
@@ -649,6 +667,12 @@ public class JadxCLIArgs {
|
||||
}
|
||||
}
|
||||
|
||||
public static class ExportGradleTypeConverter extends BaseEnumConverter<ExportGradleType> {
|
||||
public ExportGradleTypeConverter() {
|
||||
super(ExportGradleType::valueOf, ExportGradleType::values);
|
||||
}
|
||||
}
|
||||
|
||||
public static class LogLevelConverter extends BaseEnumConverter<LogHelper.LogLevelEnum> {
|
||||
public LogLevelConverter() {
|
||||
super(LogHelper.LogLevelEnum::valueOf, LogHelper.LogLevelEnum::values);
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
package jadx.cli;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.PathMatcher;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.plugins.loader.JadxBasePluginLoader;
|
||||
import jadx.core.plugins.files.SingleDirFilesGetter;
|
||||
import jadx.core.utils.Utils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
|
||||
public class BaseCliIntegrationTest {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BaseCliIntegrationTest.class);
|
||||
|
||||
static final PathMatcher LOG_ALL_FILES = path -> {
|
||||
LOG.debug("File in result dir: {}", path);
|
||||
return true;
|
||||
};
|
||||
|
||||
@TempDir
|
||||
Path testDir;
|
||||
|
||||
Path outputDir;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
outputDir = testDir.resolve("output");
|
||||
}
|
||||
|
||||
int execJadxCli(String sampleName, String... options) {
|
||||
return execJadxCli(buildArgs(List.of(options), sampleName));
|
||||
}
|
||||
|
||||
int execJadxCli(String[] args) {
|
||||
return JadxCLI.execute(args, jadxArgs -> {
|
||||
// don't use global config and plugins
|
||||
jadxArgs.setFilesGetter(new SingleDirFilesGetter(testDir));
|
||||
jadxArgs.setPluginLoader(new JadxBasePluginLoader());
|
||||
});
|
||||
}
|
||||
|
||||
String[] buildArgs(List<String> options, String... inputSamples) {
|
||||
List<String> args = new ArrayList<>(options);
|
||||
args.add("-v");
|
||||
args.add("-d");
|
||||
args.add(outputDir.toAbsolutePath().toString());
|
||||
|
||||
for (String inputSample : inputSamples) {
|
||||
try {
|
||||
URL resource = getClass().getClassLoader().getResource(inputSample);
|
||||
assertThat(resource).isNotNull();
|
||||
String sampleFile = resource.toURI().getRawPath();
|
||||
args.add(sampleFile);
|
||||
} catch (URISyntaxException e) {
|
||||
fail("Failed to load sample: " + inputSample, e);
|
||||
}
|
||||
}
|
||||
return args.toArray(new String[0]);
|
||||
}
|
||||
|
||||
void decompile(String... inputSamples) throws IOException {
|
||||
int result = execJadxCli(buildArgs(List.of(), inputSamples));
|
||||
assertThat(result).isEqualTo(0);
|
||||
List<Path> resultJavaFiles = collectJavaFilesInDir(outputDir);
|
||||
assertThat(resultJavaFiles).isNotEmpty();
|
||||
|
||||
// do not copy input files as resources
|
||||
for (Path path : collectFilesInDir(outputDir, LOG_ALL_FILES)) {
|
||||
for (String inputSample : inputSamples) {
|
||||
assertThat(path.toAbsolutePath().toString()).doesNotContain(inputSample);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void printFiles(List<Path> files) {
|
||||
LOG.info("Output files (count: {}):", files.size());
|
||||
for (Path file : files) {
|
||||
LOG.info(" {}", file);
|
||||
}
|
||||
LOG.info("");
|
||||
}
|
||||
|
||||
Path printFileContent(Path file) {
|
||||
try {
|
||||
String content = Files.readString(outputDir.resolve(file));
|
||||
String spacer = Utils.strRepeat("=", 70);
|
||||
LOG.info("File content: {}\n{}\n{}\n{}", file, spacer, content, spacer);
|
||||
return file;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to load file: " + file, e);
|
||||
}
|
||||
}
|
||||
|
||||
static List<Path> collectJavaFilesInDir(Path dir) throws IOException {
|
||||
PathMatcher javaMatcher = dir.getFileSystem().getPathMatcher("glob:**.java");
|
||||
return collectFilesInDir(dir, javaMatcher);
|
||||
}
|
||||
|
||||
static List<Path> collectAllFilesInDir(Path dir) throws IOException {
|
||||
try (Stream<Path> pathStream = Files.walk(dir)) {
|
||||
List<Path> files = pathStream
|
||||
.filter(Files::isRegularFile)
|
||||
.map(dir::relativize)
|
||||
.collect(Collectors.toList());
|
||||
printFiles(files);
|
||||
return files;
|
||||
}
|
||||
}
|
||||
|
||||
static List<Path> collectFilesInDir(Path dir, PathMatcher matcher) throws IOException {
|
||||
try (Stream<Path> pathStream = Files.walk(dir)) {
|
||||
List<Path> files = pathStream
|
||||
.filter(p -> Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS))
|
||||
.filter(matcher::matches)
|
||||
.collect(Collectors.toList());
|
||||
printFiles(files);
|
||||
return files;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package jadx.cli;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.assertj.core.api.Condition;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class TestExport extends BaseCliIntegrationTest {
|
||||
|
||||
@Test
|
||||
public void testBasicExport() throws Exception {
|
||||
int result = execJadxCli("samples/small.apk");
|
||||
assertThat(result).isEqualTo(0);
|
||||
assertThat(collectAllFilesInDir(outputDir))
|
||||
.map(Path::toString)
|
||||
.haveExactly(2, new Condition<>(f -> f.startsWith("sources/") && f.endsWith(".java"), "sources"))
|
||||
.haveExactly(10, new Condition<>(f -> f.startsWith("resources/"), "resources"))
|
||||
.haveExactly(1, new Condition<>(f -> f.equals("resources/AndroidManifest.xml"), "manifest"))
|
||||
.hasSize(12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGradleExportApk() throws Exception {
|
||||
int result = execJadxCli("samples/small.apk", "--export-gradle");
|
||||
assertThat(result).isEqualTo(0);
|
||||
assertThat(collectAllFilesInDir(outputDir))
|
||||
.describedAs("check output files")
|
||||
.map(Path::toString)
|
||||
.haveExactly(2, new Condition<>(f -> f.endsWith(".java"), "java classes"))
|
||||
.haveExactly(0, new Condition<>(f -> f.endsWith("classes.dex"), "dex files"))
|
||||
.hasSize(15);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGradleExportAAR() throws Exception {
|
||||
int result = execJadxCli("samples/test-lib.aar", "--export-gradle");
|
||||
assertThat(result).isEqualTo(0);
|
||||
assertThat(collectAllFilesInDir(outputDir))
|
||||
.describedAs("check output files")
|
||||
.map(this::printFileContent)
|
||||
.map(Path::toString)
|
||||
.haveExactly(1, new Condition<>(f -> f.startsWith("lib/src/main/java/") && f.endsWith(".java"), "java"))
|
||||
.haveExactly(0, new Condition<>(f -> f.endsWith(".jar"), "jar files"))
|
||||
.hasSize(8);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGradleExportSimpleJava() throws Exception {
|
||||
int result = execJadxCli("samples/HelloWorld.class", "--export-gradle");
|
||||
assertThat(result).isEqualTo(0);
|
||||
assertThat(collectAllFilesInDir(outputDir))
|
||||
.describedAs("check output files")
|
||||
.map(this::printFileContent)
|
||||
.map(Path::toString)
|
||||
.haveExactly(1, new Condition<>(f -> f.endsWith(".java") && f.startsWith("app/src/main/java/"), "java"))
|
||||
.haveExactly(0, new Condition<>(f -> f.endsWith(".class"), "class files"))
|
||||
.haveExactly(1, new Condition<>(f -> f.equals("settings.gradle.kts"), "settings"))
|
||||
.haveExactly(1, new Condition<>(f -> f.equals("app/build.gradle.kts"), "build"))
|
||||
.hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGradleExportInvalidType() throws Exception {
|
||||
int result = execJadxCli("samples/HelloWorld.class", "--export-gradle-type", "android-app");
|
||||
assertThat(result).isEqualTo(0);
|
||||
// expect output in 'android-app' template, but most fields will be set to UNKNOWN.
|
||||
assertThat(collectAllFilesInDir(outputDir))
|
||||
.describedAs("check output files")
|
||||
.map(this::printFileContent)
|
||||
.map(Path::toString)
|
||||
.haveExactly(1, new Condition<>(f -> f.endsWith(".java") && f.startsWith("app/src/main/java/"), "java"))
|
||||
.haveExactly(1, new Condition<>(f -> f.equals("settings.gradle"), "settings"))
|
||||
.haveExactly(1, new Condition<>(f -> f.equals("build.gradle"), "build"))
|
||||
.haveExactly(1, new Condition<>(f -> f.equals("app/build.gradle"), "app build"))
|
||||
.hasSize(4);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,14 @@
|
||||
package jadx.cli;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.PathMatcher;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.assertj.core.api.Condition;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.plugins.loader.JadxBasePluginLoader;
|
||||
import jadx.core.plugins.files.SingleDirFilesGetter;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class TestInput {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TestInput.class);
|
||||
|
||||
private static final PathMatcher LOG_ALL_FILES = path -> {
|
||||
LOG.debug("File in result dir: {}", path);
|
||||
return true;
|
||||
};
|
||||
|
||||
@TempDir
|
||||
Path testDir;
|
||||
|
||||
Path outputDir;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
outputDir = testDir.resolve("output");
|
||||
}
|
||||
public class TestInput extends BaseCliIntegrationTest {
|
||||
|
||||
@Test
|
||||
public void testHelp() {
|
||||
@@ -52,16 +20,13 @@ public class TestInput {
|
||||
public void testApkInput() throws Exception {
|
||||
int result = execJadxCli(buildArgs(List.of(), "samples/small.apk"));
|
||||
assertThat(result).isEqualTo(0);
|
||||
List<Path> resultFiles = collectAllFilesInDir(outputDir);
|
||||
printFiles(resultFiles);
|
||||
assertThat(resultFiles)
|
||||
assertThat(collectAllFilesInDir(outputDir))
|
||||
.describedAs("check output files")
|
||||
.map(p -> p.getFileName().toString())
|
||||
.haveExactly(2, new Condition<>(f -> f.endsWith(".java"), "java classes"))
|
||||
.haveExactly(9, new Condition<>(f -> f.endsWith(".xml"), "xml resources"))
|
||||
.haveExactly(1, new Condition<>(f -> f.equals("classes.dex"), "dex"))
|
||||
.haveExactly(1, new Condition<>(f -> f.equals("AndroidManifest.xml"), "manifest"))
|
||||
.hasSize(13);
|
||||
.hasSize(12);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -108,70 +73,4 @@ public class TestInput {
|
||||
path -> path.getFileName().toString().equalsIgnoreCase("AndroidManifest.xml"));
|
||||
assertThat(files).isNotEmpty();
|
||||
}
|
||||
|
||||
private void decompile(String... inputSamples) throws URISyntaxException, IOException {
|
||||
int result = execJadxCli(buildArgs(List.of(), inputSamples));
|
||||
assertThat(result).isEqualTo(0);
|
||||
List<Path> resultJavaFiles = collectJavaFilesInDir(outputDir);
|
||||
assertThat(resultJavaFiles).isNotEmpty();
|
||||
|
||||
// do not copy input files as resources
|
||||
for (Path path : collectFilesInDir(outputDir, LOG_ALL_FILES)) {
|
||||
for (String inputSample : inputSamples) {
|
||||
assertThat(path.toAbsolutePath().toString()).doesNotContain(inputSample);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int execJadxCli(String[] args) {
|
||||
return JadxCLI.execute(args, jadxArgs -> {
|
||||
// don't use global config and plugins
|
||||
jadxArgs.setFilesGetter(new SingleDirFilesGetter(testDir));
|
||||
jadxArgs.setPluginLoader(new JadxBasePluginLoader());
|
||||
});
|
||||
}
|
||||
|
||||
private String[] buildArgs(List<String> options, String... inputSamples) throws URISyntaxException {
|
||||
List<String> args = new ArrayList<>(options);
|
||||
args.add("-v");
|
||||
args.add("-d");
|
||||
args.add(outputDir.toAbsolutePath().toString());
|
||||
|
||||
for (String inputSample : inputSamples) {
|
||||
URL resource = getClass().getClassLoader().getResource(inputSample);
|
||||
assertThat(resource).isNotNull();
|
||||
String sampleFile = resource.toURI().getRawPath();
|
||||
args.add(sampleFile);
|
||||
}
|
||||
return args.toArray(new String[0]);
|
||||
}
|
||||
|
||||
private void printFiles(List<Path> files) {
|
||||
LOG.info("Output files (count: {}):", files.size());
|
||||
for (Path file : files) {
|
||||
LOG.info(" {}", outputDir.relativize(file));
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Path> collectJavaFilesInDir(Path dir) throws IOException {
|
||||
PathMatcher javaMatcher = dir.getFileSystem().getPathMatcher("glob:**.java");
|
||||
return collectFilesInDir(dir, javaMatcher);
|
||||
}
|
||||
|
||||
private static List<Path> collectAllFilesInDir(Path dir) throws IOException {
|
||||
try (Stream<Path> pathStream = Files.walk(dir)) {
|
||||
return pathStream
|
||||
.filter(Files::isRegularFile)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Path> collectFilesInDir(Path dir, PathMatcher matcher) throws IOException {
|
||||
try (Stream<Path> pathStream = Files.walk(dir)) {
|
||||
return pathStream
|
||||
.filter(p -> Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS))
|
||||
.filter(matcher::matches)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user