feat: add gradle export templates, support android app/lib and simple java

This commit is contained in:
Skylot
2025-04-29 21:07:52 +01:00
parent 9981949a2b
commit e3aa49aaa9
54 changed files with 1153 additions and 570 deletions
+96 -88
View File
@@ -91,102 +91,110 @@ commands (use '<command> --help' for command options):
plugins - manage jadx plugins
options:
-d, --output-dir - output directory
-ds, --output-dir-src - output directory for sources
-dr, --output-dir-res - output directory for resources
-r, --no-res - do not decode resources
-s, --no-src - do not decompile source code
--single-class - decompile a single class, full name, raw or alias
--single-class-output - file or dir for write if decompile a single class
--output-format - can be 'java' or 'json', default: java
-e, --export-gradle - save as android gradle project
-j, --threads-count - processing threads count, default: 4
-m, --decompilation-mode - code output mode:
'auto' - trying best options (default)
'restructure' - restore code structure (normal java code)
'simple' - simplified instructions (linear, with goto's)
'fallback' - raw instructions without modifications
--show-bad-code - show inconsistent code (incorrectly decompiled)
--no-xml-pretty-print - do not prettify XML
--no-imports - disable use of imports, always write entire package name
--no-debug-info - disable debug info parsing and processing
--add-debug-lines - add comments with debug line numbers if available
--no-inline-anonymous - disable anonymous classes inline
--no-inline-methods - disable methods inline
--no-move-inner-classes - disable move inner classes into parent
--no-inline-kotlin-lambda - disable inline for Kotlin lambdas
--no-finally - don't extract finally block
--no-restore-switch-over-string - don't restore switch over string
--no-replace-consts - don't replace constant value with matching constant field
--escape-unicode - escape non latin characters in strings (with \u)
--respect-bytecode-access-modifiers - don't change original access modifiers
--mappings-path - deobfuscation mappings file or directory. Allowed formats: Tiny and Tiny v2 (both '.tiny'), Enigma (.mapping) or Enigma directory
--mappings-mode - set mode for handling the deobfuscation mapping file:
'read' - just read, user can always save manually (default)
'read-and-autosave-every-change' - read and autosave after every change
'read-and-autosave-before-closing' - read and autosave before exiting the app or closing the project
'ignore' - don't read or save (can be used to skip loading mapping files referenced in the project file)
--deobf - activate deobfuscation
--deobf-min - min length of name, renamed if shorter, default: 3
--deobf-max - max length of name, renamed if longer, default: 64
--deobf-whitelist - space separated list of classes (full name) and packages (ends with '.*') to exclude from deobfuscation, default: android.support.v4.* android.support.v7.* android.support.v4.os.* android.support.annotation.Px androidx.core.os.* androidx.annotation.Px
--deobf-cfg-file - deobfuscation mappings file used for JADX auto-generated names (in the JOBF file format), default: same dir and name as input file with '.jobf' extension
--deobf-cfg-file-mode - set mode for handling the JADX auto-generated names' deobfuscation map file:
'read' - read if found, don't save (default)
'read-or-save' - read if found, save otherwise (don't overwrite)
'overwrite' - don't read, always save
'ignore' - don't read and don't save
--deobf-res-name-source - better name source for resources:
'auto' - automatically select best name (default)
'resources' - use resources names
'code' - use R class fields names
--use-source-name-as-class-name-alias - use source name as class name alias:
'always' - always use source name if it's available
'if-better' - use source name if it seems better than the current one
'never' - never use source name, even if it's available
-d, --output-dir - output directory
-ds, --output-dir-src - output directory for sources
-dr, --output-dir-res - output directory for resources
-r, --no-res - do not decode resources
-s, --no-src - do not decompile source code
-j, --threads-count - processing threads count, default: 4
--single-class - decompile a single class, full name, raw or alias
--single-class-output - file or dir for write if decompile a single class
--output-format - can be 'java' or 'json', default: java
-e, --export-gradle - save as gradle project (set '--export-gradle-type' to 'auto')
--export-gradle-type - Gradle project template for export:
'auto' - detect automatically
'android-app' - Android Application (apk)
'android-library' - Android Library (aar)
'simple-java' - simple Java
-m, --decompilation-mode - code output mode:
'auto' - trying best options (default)
'restructure' - restore code structure (normal java code)
'simple' - simplified instructions (linear, with goto's)
'fallback' - raw instructions without modifications
--show-bad-code - show inconsistent code (incorrectly decompiled)
--no-xml-pretty-print - do not prettify XML
--no-imports - disable use of imports, always write entire package name
--no-debug-info - disable debug info parsing and processing
--add-debug-lines - add comments with debug line numbers if available
--no-inline-anonymous - disable anonymous classes inline
--no-inline-methods - disable methods inline
--no-move-inner-classes - disable move inner classes into parent
--no-inline-kotlin-lambda - disable inline for Kotlin lambdas
--no-finally - don't extract finally block
--no-restore-switch-over-string - don't restore switch over string
--no-replace-consts - don't replace constant value with matching constant field
--escape-unicode - escape non latin characters in strings (with \u)
--respect-bytecode-access-modifiers - don't change original access modifiers
--mappings-path - deobfuscation mappings file or directory. Allowed formats: Tiny and Tiny v2 (both '.tiny'), Enigma (.mapping) or Enigma directory
--mappings-mode - set mode for handling the deobfuscation mapping file:
'read' - just read, user can always save manually (default)
'read-and-autosave-every-change' - read and autosave after every change
'read-and-autosave-before-closing' - read and autosave before exiting the app or closing the project
'ignore' - don't read or save (can be used to skip loading mapping files referenced in the project file)
--deobf - activate deobfuscation
--deobf-min - min length of name, renamed if shorter, default: 3
--deobf-max - max length of name, renamed if longer, default: 64
--deobf-whitelist - space separated list of classes (full name) and packages (ends with '.*') to exclude from deobfuscation, default: android.support.v4.* android.support.v7.* android.support.v4.os.* android.support.annotation.Px androidx.core.os.* androidx.annotation.Px
--deobf-cfg-file - deobfuscation mappings file used for JADX auto-generated names (in the JOBF file format), default: same dir and name as input file with '.jobf' extension
--deobf-cfg-file-mode - set mode for handling the JADX auto-generated names' deobfuscation map file:
'read' - read if found, don't save (default)
'read-or-save' - read if found, save otherwise (don't overwrite)
'overwrite' - don't read, always save
'ignore' - don't read and don't save
--deobf-res-name-source - better name source for resources:
'auto' - automatically select best name (default)
'resources' - use resources names
'code' - use R class fields names
--use-source-name-as-class-name-alias - use source name as class name alias:
'always' - always use source name if it's available
'if-better' - use source name if it seems better than the current one
'never' - never use source name, even if it's available
--source-name-repeat-limit - allow using source name if it appears less than a limit number, default: 10
--use-kotlin-methods-for-var-names - use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide, default: apply
--rename-flags - fix options (comma-separated list of):
'case' - fix case sensitivity issues (according to --fs-case-sensitive option),
'valid' - rename java identifiers to make them valid,
'printable' - remove non-printable chars from identifiers,
or single 'none' - to disable all renames
or single 'all' - to enable all (default)
--integer-format - how integers are displayed:
'auto' - automatically select (default)
'decimal' - use decimal
'hexadecimal' - use hexadecimal
--fs-case-sensitive - treat filesystem as case sensitive, false by default
--cfg - save methods control flow graph to dot file
--raw-cfg - save methods control flow graph (use raw instructions)
-f, --fallback - set '--decompilation-mode' to 'fallback' (deprecated)
--use-dx - use dx/d8 to convert java bytecode
--comments-level - set code comments level, values: error, warn, info, debug, user-only, none, default: info
--log-level - set log level, values: quiet, progress, error, warn, info, debug, default: progress
-v, --verbose - verbose output (set --log-level to DEBUG)
-q, --quiet - turn off output (set --log-level to QUIET)
--version - print jadx version
-h, --help - print this help
--use-kotlin-methods-for-var-names - use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide, default: apply
--rename-flags - fix options (comma-separated list of):
'case' - fix case sensitivity issues (according to --fs-case-sensitive option),
'valid' - rename java identifiers to make them valid,
'printable' - remove non-printable chars from identifiers,
or single 'none' - to disable all renames
or single 'all' - to enable all (default)
--integer-format - how integers are displayed:
'auto' - automatically select (default)
'decimal' - use decimal
'hexadecimal' - use hexadecimal
--fs-case-sensitive - treat filesystem as case sensitive, false by default
--cfg - save methods control flow graph to dot file
--raw-cfg - save methods control flow graph (use raw instructions)
-f, --fallback - set '--decompilation-mode' to 'fallback' (deprecated)
--use-dx - use dx/d8 to convert java bytecode
--comments-level - set code comments level, values: error, warn, info, debug, user-only, none, default: info
--log-level - set log level, values: quiet, progress, error, warn, info, debug, default: progress
-v, --verbose - verbose output (set --log-level to DEBUG)
-q, --quiet - turn off output (set --log-level to QUIET)
--disable-plugins - comma separated list of plugin ids to disable, default:
--version - print jadx version
-h, --help - print this help
Plugin options (-P<name>=<value>):
dex-input: Load .dex and .apk files
- dex-input.verify-checksum - verify dex file checksum before load, values: [yes, no], default: yes
- dex-input.verify-checksum - verify dex file checksum before load, values: [yes, no], default: yes
java-convert: Convert .class, .jar and .aar files to dex
- java-convert.mode - convert mode, values: [dx, d8, both], default: both
- java-convert.d8-desugar - use desugar in d8, values: [yes, no], default: no
- java-convert.mode - convert mode, values: [dx, d8, both], default: both
- java-convert.d8-desugar - use desugar in d8, values: [yes, no], default: no
kotlin-metadata: Use kotlin.Metadata annotation for code generation
- kotlin-metadata.class-alias - rename class alias, values: [yes, no], default: yes
- kotlin-metadata.method-args - rename function arguments, values: [yes, no], default: yes
- kotlin-metadata.fields - rename fields, values: [yes, no], default: yes
- kotlin-metadata.companion - rename companion object, values: [yes, no], default: yes
- kotlin-metadata.data-class - add data class modifier, values: [yes, no], default: yes
- kotlin-metadata.to-string - rename fields using toString, values: [yes, no], default: yes
- kotlin-metadata.getters - rename simple getters to field names, values: [yes, no], default: yes
- kotlin-metadata.class-alias - rename class alias, values: [yes, no], default: yes
- kotlin-metadata.method-args - rename function arguments, values: [yes, no], default: yes
- kotlin-metadata.fields - rename fields, values: [yes, no], default: yes
- kotlin-metadata.companion - rename companion object, values: [yes, no], default: yes
- kotlin-metadata.data-class - add data class modifier, values: [yes, no], default: yes
- kotlin-metadata.to-string - rename fields using toString, values: [yes, no], default: yes
- kotlin-metadata.getters - rename simple getters to field names, values: [yes, no], default: yes
kotlin-smap: Use kotlin.SourceDebugExtension annotation for rename class alias
- kotlin-smap.class-alias-source-dbg - rename class alias from SourceDebugExtension, values: [yes, no], default: no
rename-mappings: various mappings support
- rename-mappings.format - mapping format, values: [AUTO, TINY_FILE, TINY_2_FILE, ENIGMA_FILE, ENIGMA_DIR, SRG_FILE, XSRG_FILE, JAM_FILE, CSRG_FILE, TSRG_FILE, TSRG_2_FILE, PROGUARD_FILE, RECAF_SIMPLE_FILE, JOBF_FILE], default: AUTO
- rename-mappings.invert - invert mapping on load, values: [yes, no], default: no
- rename-mappings.format - mapping format, values: [AUTO, TINY_FILE, TINY_2_FILE, ENIGMA_FILE, ENIGMA_DIR, SRG_FILE, XSRG_FILE, JAM_FILE, CSRG_FILE, TSRG_FILE, TSRG_2_FILE, PROGUARD_FILE, INTELLIJ_MIGRATION_MAP_FILE, RECAF_SIMPLE_FILE, JOBF_FILE], default: AUTO
- rename-mappings.invert - invert mapping on load, values: [yes, no], default: no
smali-input: Load .smali files
- smali-input.api-level - Android API level, default: 27
- smali-input.api-level - Android API level, default: 27
Environment variables:
JADX_DISABLE_XML_SECURITY - set to 'true' to disable all security checks for XML files
@@ -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);
}
}
+3 -104
View File
@@ -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.
+19 -4
View File
@@ -39,6 +39,7 @@ import jadx.api.usage.impl.InMemoryUsageInfoCache;
import jadx.core.deobf.DeobfAliasProvider;
import jadx.core.deobf.conditions.DeobfWhitelist;
import jadx.core.deobf.conditions.JadxRenameConditions;
import jadx.core.export.ExportGradleType;
import jadx.core.plugins.PluginContext;
import jadx.core.plugins.files.IJadxFilesGetter;
import jadx.core.plugins.files.TempFilesGetter;
@@ -133,7 +134,7 @@ public class JadxArgs implements Closeable {
private boolean escapeUnicode = false;
private boolean replaceConsts = true;
private boolean respectBytecodeAccModifiers = false;
private boolean exportAsGradleProject = false;
private @Nullable ExportGradleType exportGradleType = null;
private boolean restoreSwitchOverString = true;
@@ -567,11 +568,25 @@ public class JadxArgs implements Closeable {
}
public boolean isExportAsGradleProject() {
return exportAsGradleProject;
return exportGradleType != null;
}
public void setExportAsGradleProject(boolean exportAsGradleProject) {
this.exportAsGradleProject = exportAsGradleProject;
if (exportAsGradleProject) {
if (exportGradleType == null) {
exportGradleType = ExportGradleType.AUTO;
}
} else {
exportGradleType = null;
}
}
public @Nullable ExportGradleType getExportGradleType() {
return exportGradleType;
}
public void setExportGradleType(@Nullable ExportGradleType exportGradleType) {
this.exportGradleType = exportGradleType;
}
public boolean isRestoreSwitchOverString() {
@@ -861,7 +876,7 @@ public class JadxArgs implements Closeable {
+ ", replaceConsts=" + replaceConsts
+ ", restoreSwitchOverString=" + restoreSwitchOverString
+ ", respectBytecodeAccModifiers=" + respectBytecodeAccModifiers
+ ", exportAsGradleProject=" + exportAsGradleProject
+ ", exportGradleType=" + exportGradleType
+ ", skipXmlPrettyPrint=" + skipXmlPrettyPrint
+ ", fsCaseSensitive=" + fsCaseSensitive
+ ", renameFlags=" + renameFlags
@@ -7,6 +7,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -42,7 +43,8 @@ import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.PackageNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.SaveCode;
import jadx.core.export.ExportGradleTask;
import jadx.core.export.ExportGradle;
import jadx.core.export.OutDirs;
import jadx.core.plugins.JadxPluginManager;
import jadx.core.plugins.PluginContext;
import jadx.core.plugins.events.JadxEventsImpl;
@@ -297,31 +299,28 @@ public final class JadxDecompiler implements Closeable {
if (root == null) {
throw new JadxRuntimeException("No loaded files");
}
File sourcesOutDir;
File resOutDir;
ExportGradleTask gradleExportTask;
if (args.isExportAsGradleProject()) {
gradleExportTask = new ExportGradleTask(resources, root, args.getOutDir());
gradleExportTask.init();
sourcesOutDir = gradleExportTask.getSrcOutDir();
resOutDir = gradleExportTask.getResOutDir();
OutDirs outDirs;
ExportGradle gradleExport;
if (args.getExportGradleType() != null) {
gradleExport = new ExportGradle(root, args.getOutDir(), getResources());
outDirs = gradleExport.init();
} else {
sourcesOutDir = args.getOutDirSrc();
resOutDir = args.getOutDirRes();
gradleExportTask = null;
gradleExport = null;
outDirs = new OutDirs(args.getOutDirSrc(), args.getOutDirRes());
outDirs.makeDirs();
}
TaskExecutor executor = new TaskExecutor();
executor.setThreadsCount(args.getThreadsCount());
if (saveResources) {
// save resources first because decompilation can stop or fail
appendResourcesSaveTasks(executor, resOutDir);
appendResourcesSaveTasks(executor, outDirs.getResOutDir());
}
if (saveSources) {
appendSourcesSave(executor, sourcesOutDir);
appendSourcesSave(executor, outDirs.getSrcOutDir());
}
if (gradleExportTask != null) {
executor.addSequentialTask(gradleExportTask);
if (gradleExport != null) {
executor.addSequentialTask(gradleExport::generateGradleFiles);
}
return executor;
}
@@ -340,6 +339,8 @@ public final class JadxDecompiler implements Closeable {
Set<String> inputFileNames = args.getInputFiles().stream()
.map(File::getAbsolutePath)
.collect(Collectors.toSet());
Set<String> codeSources = collectCodeSources();
List<Runnable> tasks = new ArrayList<>();
for (ResourceFile resourceFile : getResources()) {
ResourceType resType = resourceFile.getType();
@@ -347,9 +348,14 @@ public final class JadxDecompiler implements Closeable {
// already processed
continue;
}
if (resType != ResourceType.ARSC
&& inputFileNames.contains(resourceFile.getOriginalName())) {
// ignore resource made from input file
String resOriginalName = resourceFile.getOriginalName();
if (resType != ResourceType.ARSC && inputFileNames.contains(resOriginalName)) {
// ignore resource made from an input file
continue;
}
if (codeSources.contains(resOriginalName)) {
// don't output code source resources (.dex, .class, etc)
// do not trust file extensions, use only sources set as class inputs
continue;
}
tasks.add(new ResourcesSaver(this, outDir, resourceFile));
@@ -357,6 +363,29 @@ public final class JadxDecompiler implements Closeable {
executor.addParallelTasks(tasks);
}
private Set<String> collectCodeSources() {
Set<String> set = new HashSet<>();
for (ClassNode cls : root.getClasses(true)) {
if (cls.getClsData() == null) {
// exclude synthetic classes
continue;
}
String inputFileName = cls.getInputFileName();
if (inputFileName.endsWith(".class")) {
// cut .class name to get source .jar file
// current template: "<optional input files>:<.jar>:<full class name>"
// TODO: add property to set file name or reference to resource name
int endIdx = inputFileName.lastIndexOf(':');
if (endIdx != -1) {
int startIdx = inputFileName.lastIndexOf(':', endIdx - 1) + 1;
inputFileName = inputFileName.substring(startIdx, endIdx);
}
}
set.add(inputFileName);
}
return set;
}
private void appendSourcesSave(ITaskExecutor executor, File outDir) {
List<JavaClass> classes = getClasses();
List<JavaClass> processQueue = filterClasses(classes);
@@ -0,0 +1,17 @@
package jadx.api;
import jadx.core.xmlgen.ResContainer;
public class ResourceFileContainer extends ResourceFile {
private final ResContainer container;
public ResourceFileContainer(String name, ResourceType type, ResContainer container) {
super(null, name, type);
this.container = container;
}
@Override
public ResContainer loadContent() {
return container;
}
}
@@ -0,0 +1,80 @@
package jadx.core.export;
import java.io.File;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ResourceFile;
import jadx.api.ResourceType;
import jadx.core.dex.nodes.RootNode;
import jadx.core.export.gen.AndroidGradleGenerator;
import jadx.core.export.gen.IExportGradleGenerator;
import jadx.core.export.gen.SimpleJavaGradleGenerator;
import jadx.core.utils.android.AndroidManifestParser;
import jadx.core.utils.exceptions.JadxRuntimeException;
public class ExportGradle {
private static final Logger LOG = LoggerFactory.getLogger(ExportGradle.class);
private final RootNode root;
private final File projectDir;
private final List<ResourceFile> resources;
private IExportGradleGenerator generator;
public ExportGradle(RootNode root, File projectDir, List<ResourceFile> resources) {
this.root = root;
this.projectDir = projectDir;
this.resources = resources;
}
public OutDirs init() {
ExportGradleType exportType = getExportGradleType();
LOG.info("Export Gradle project using '{}' template", exportType);
switch (exportType) {
case ANDROID_APP:
case ANDROID_LIBRARY:
generator = new AndroidGradleGenerator(root, projectDir, resources, exportType);
break;
case SIMPLE_JAVA:
generator = new SimpleJavaGradleGenerator(root, projectDir, resources);
break;
default:
throw new JadxRuntimeException("Unexpected export type: " + exportType);
}
generator.init();
OutDirs outDirs = generator.getOutDirs();
outDirs.makeDirs();
return outDirs;
}
private ExportGradleType getExportGradleType() {
ExportGradleType argsExportType = root.getArgs().getExportGradleType();
ExportGradleType detectedType = detectExportType(root, resources);
if (argsExportType == null
|| argsExportType == ExportGradleType.AUTO
|| argsExportType == detectedType) {
return detectedType;
}
return argsExportType;
}
public static ExportGradleType detectExportType(RootNode root, List<ResourceFile> resources) {
ResourceFile androidManifest = AndroidManifestParser.getAndroidManifest(resources);
if (androidManifest != null) {
if (resources.stream().anyMatch(r -> r.getOriginalName().equals("classes.jar"))) {
return ExportGradleType.ANDROID_LIBRARY;
}
if (resources.stream().anyMatch(r -> r.getType() == ResourceType.ARSC)) {
return ExportGradleType.ANDROID_APP;
}
}
return ExportGradleType.SIMPLE_JAVA;
}
public void generateGradleFiles() {
generator.generateFiles();
}
}
@@ -1,119 +0,0 @@
package jadx.core.export;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.regex.Pattern;
import jadx.api.ResourceFile;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.android.AndroidManifestParser;
import jadx.core.utils.android.AppAttribute;
import jadx.core.utils.android.ApplicationParams;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.xmlgen.ResContainer;
public class ExportGradleProject {
private static final Pattern ILLEGAL_GRADLE_CHARS = Pattern.compile("[/\\\\:>\"?*|]");
private final RootNode root;
private final File projectDir;
private final File appDir;
private final ApplicationParams applicationParams;
public ExportGradleProject(RootNode root, File projectDir, ResourceFile androidManifest, ResContainer appStrings) {
this.root = root;
this.projectDir = projectDir;
this.appDir = new File(projectDir, "app");
this.applicationParams = getApplicationParams(androidManifest, appStrings);
}
public void generateGradleFiles() {
try {
saveProjectBuildGradle();
saveApplicationBuildGradle();
saveSettingsGradle();
saveGradleProperties();
} catch (Exception e) {
throw new JadxRuntimeException("Gradle export failed", e);
}
}
private void saveGradleProperties() throws IOException {
GradleInfoStorage gradleInfo = root.getGradleInfoStorage();
/*
* For Android Gradle Plugin >=8.0.0 the property "android.nonFinalResIds=false" has to be set in
* "gradle.properties" when resource identifiers are used as constant expressions.
*/
if (gradleInfo.isNonFinalResIds()) {
File gradlePropertiesFile = new File(projectDir, "gradle.properties");
try (FileOutputStream fos = new FileOutputStream(gradlePropertiesFile)) {
fos.write("android.nonFinalResIds=false".getBytes(StandardCharsets.UTF_8));
}
}
}
private void saveProjectBuildGradle() throws IOException {
TemplateFile tmpl = TemplateFile.fromResources("/export/build.gradle.tmpl");
tmpl.save(new File(projectDir, "build.gradle"));
}
private void saveSettingsGradle() throws IOException {
TemplateFile tmpl = TemplateFile.fromResources("/export/settings.gradle.tmpl");
tmpl.add("applicationName", ILLEGAL_GRADLE_CHARS.matcher(applicationParams.getApplicationName()).replaceAll(""));
tmpl.save(new File(projectDir, "settings.gradle"));
}
private void saveApplicationBuildGradle() throws IOException {
TemplateFile tmpl = TemplateFile.fromResources("/export/app.build.gradle.tmpl");
String appPackage = root.getAppPackage();
if (appPackage == null) {
appPackage = "UNKNOWN";
}
Integer minSdkVersion = applicationParams.getMinSdkVersion();
tmpl.add("applicationId", appPackage);
tmpl.add("minSdkVersion", minSdkVersion);
tmpl.add("targetSdkVersion", applicationParams.getTargetSdkVersion());
tmpl.add("versionCode", applicationParams.getVersionCode());
tmpl.add("versionName", applicationParams.getVersionName());
List<String> additionalOptions = new ArrayList<>();
GradleInfoStorage gradleInfo = root.getGradleInfoStorage();
if (gradleInfo.isVectorPathData() && minSdkVersion < 21 || gradleInfo.isVectorFillType() && minSdkVersion < 24) {
additionalOptions.add("vectorDrawables.useSupportLibrary = true");
}
if (gradleInfo.isUseApacheHttpLegacy()) {
additionalOptions.add("useLibrary 'org.apache.http.legacy'");
}
genAdditionalAndroidPluginOptions(tmpl, additionalOptions);
tmpl.save(new File(appDir, "build.gradle"));
}
private void genAdditionalAndroidPluginOptions(TemplateFile tmpl, List<String> additionalOptions) {
StringBuilder sb = new StringBuilder();
for (String additionalOption : additionalOptions) {
sb.append(" ").append(additionalOption).append('\n');
}
tmpl.add("additionalOptions", sb.toString());
}
private ApplicationParams getApplicationParams(ResourceFile androidManifest, ResContainer appStrings) {
AndroidManifestParser parser = new AndroidManifestParser(androidManifest, appStrings, EnumSet.of(
AppAttribute.APPLICATION_LABEL,
AppAttribute.MIN_SDK_VERSION,
AppAttribute.TARGET_SDK_VERSION,
AppAttribute.VERSION_CODE,
AppAttribute.VERSION_NAME),
root.getArgs().getSecurity());
return parser.parse();
}
}
@@ -1,70 +0,0 @@
package jadx.core.export;
import java.io.File;
import java.util.List;
import jadx.api.ResourceFile;
import jadx.api.ResourceType;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.android.AndroidManifestParser;
import jadx.core.utils.files.FileUtils;
import jadx.core.xmlgen.ResContainer;
public class ExportGradleTask implements Runnable {
private final List<ResourceFile> resources;
private final RootNode root;
private final File projectDir;
private final File srcOutDir;
private final File resOutDir;
public ExportGradleTask(List<ResourceFile> resources, RootNode root, File projectDir) {
this.resources = resources;
this.projectDir = projectDir;
this.root = root;
File appDir = new File(projectDir, "app");
this.srcOutDir = new File(appDir, "src/main/java");
this.resOutDir = new File(appDir, "src/main");
}
public void init() {
FileUtils.makeDirs(srcOutDir);
FileUtils.makeDirs(resOutDir);
}
@Override
public void run() {
ResourceFile androidManifest = AndroidManifestParser.getAndroidManifest(resources);
if (androidManifest == null) {
throw new IllegalStateException("Could not find AndroidManifest.xml");
}
List<ResContainer> resContainers = resources.stream()
.filter(resourceFile -> resourceFile.getType() == ResourceType.ARSC)
.findFirst()
.orElseThrow(IllegalStateException::new)
.loadContent()
.getSubFiles();
ResContainer strings = resContainers
.stream()
.filter(resContainer -> resContainer.getName().contains("values/strings.xml"))
.findFirst()
.orElseGet(() -> resContainers.stream()
.filter(resContainer -> resContainer.getFileName().contains("strings.xml"))
.findFirst()
.orElse(null));
ExportGradleProject export = new ExportGradleProject(root, projectDir, androidManifest, strings);
export.generateGradleFiles();
}
public File getSrcOutDir() {
return srcOutDir;
}
public File getResOutDir() {
return resOutDir;
}
}
@@ -0,0 +1,23 @@
package jadx.core.export;
public enum ExportGradleType {
AUTO("Auto"),
ANDROID_APP("Android App"),
ANDROID_LIBRARY("Android Library"),
SIMPLE_JAVA("Simple Java");
private final String desc;
ExportGradleType(String desc) {
this.desc = desc;
}
public String getDesc() {
return desc;
}
@Override
public String toString() {
return desc;
}
}
@@ -0,0 +1,28 @@
package jadx.core.export;
import java.io.File;
import jadx.core.utils.files.FileUtils;
public class OutDirs {
private final File srcOutDir;
private final File resOutDir;
public OutDirs(File srcOutDir, File resOutDir) {
this.srcOutDir = srcOutDir;
this.resOutDir = resOutDir;
}
public File getSrcOutDir() {
return srcOutDir;
}
public File getResOutDir() {
return resOutDir;
}
public void makeDirs() {
FileUtils.makeDirs(srcOutDir);
FileUtils.makeDirs(resOutDir);
}
}
@@ -0,0 +1,197 @@
package jadx.core.export.gen;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ResourceFile;
import jadx.api.ResourceType;
import jadx.api.security.IJadxSecurity;
import jadx.core.dex.nodes.RootNode;
import jadx.core.export.ExportGradleType;
import jadx.core.export.GradleInfoStorage;
import jadx.core.export.OutDirs;
import jadx.core.export.TemplateFile;
import jadx.core.utils.Utils;
import jadx.core.utils.android.AndroidManifestParser;
import jadx.core.utils.android.AppAttribute;
import jadx.core.utils.android.ApplicationParams;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.xmlgen.ResContainer;
public class AndroidGradleGenerator implements IExportGradleGenerator {
private static final Logger LOG = LoggerFactory.getLogger(AndroidGradleGenerator.class);
private static final Pattern ILLEGAL_GRADLE_CHARS = Pattern.compile("[/\\\\:>\"?*|]");
private static final ApplicationParams UNKNOWN_APP_PARAMS = new ApplicationParams("UNKNOWN", 0, 0, 0, "UNKNOWN", "UNKNOWN", "UNKNOWN");
private final RootNode root;
private final File projectDir;
private final List<ResourceFile> resources;
private final boolean exportApp;
private OutDirs outDirs;
private File baseDir;
private ApplicationParams applicationParams;
public AndroidGradleGenerator(RootNode root, File projectDir, List<ResourceFile> resources, ExportGradleType exportType) {
this.root = root;
this.projectDir = projectDir;
this.resources = resources;
this.exportApp = exportType == ExportGradleType.ANDROID_APP;
}
@Override
public void init() {
String moduleDir = exportApp ? "app" : "lib";
baseDir = new File(projectDir, moduleDir);
outDirs = new OutDirs(new File(baseDir, "src/main/java"), new File(baseDir, "src/main"));
applicationParams = parseApplicationParams();
}
@Override
public void generateFiles() {
try {
saveProjectBuildGradle();
if (exportApp) {
saveApplicationBuildGradle();
} else {
saveLibraryBuildGradle();
}
saveSettingsGradle();
saveGradleProperties();
} catch (Exception e) {
throw new JadxRuntimeException("Gradle export failed", e);
}
}
@Override
public OutDirs getOutDirs() {
return outDirs;
}
private ApplicationParams parseApplicationParams() {
try {
ResourceFile androidManifest = AndroidManifestParser.getAndroidManifest(resources);
if (androidManifest == null) {
LOG.warn("AndroidManifest.xml not found, exported files will contains 'UNKNOWN' fields");
return UNKNOWN_APP_PARAMS;
}
ResContainer strings = null;
if (exportApp) {
ResourceFile arscFile = resources.stream()
.filter(resourceFile -> resourceFile.getType() == ResourceType.ARSC)
.findFirst().orElse(null);
if (arscFile != null) {
List<ResContainer> resContainers = arscFile.loadContent().getSubFiles();
strings = resContainers
.stream()
.filter(resContainer -> resContainer.getName().contains("values/strings.xml"))
.findFirst()
.orElseGet(() -> resContainers.stream()
.filter(resContainer -> resContainer.getName().contains("strings.xml"))
.findFirst().orElse(null));
}
}
EnumSet<AppAttribute> attrs = EnumSet.noneOf(AppAttribute.class);
attrs.add(AppAttribute.MIN_SDK_VERSION);
if (exportApp) {
attrs.add(AppAttribute.APPLICATION_LABEL);
attrs.add(AppAttribute.TARGET_SDK_VERSION);
attrs.add(AppAttribute.VERSION_NAME);
attrs.add(AppAttribute.VERSION_CODE);
}
IJadxSecurity security = root.getArgs().getSecurity();
AndroidManifestParser parser = new AndroidManifestParser(androidManifest, strings, attrs, security);
return parser.parse();
} catch (Throwable t) {
LOG.warn("Failed to parse AndroidManifest.xml", t);
return UNKNOWN_APP_PARAMS;
}
}
private void saveGradleProperties() throws IOException {
GradleInfoStorage gradleInfo = root.getGradleInfoStorage();
/*
* For Android Gradle Plugin >=8.0.0 the property "android.nonFinalResIds=false" has to be set in
* "gradle.properties" when resource identifiers are used as constant expressions.
*/
if (gradleInfo.isNonFinalResIds()) {
File gradlePropertiesFile = new File(projectDir, "gradle.properties");
try (FileOutputStream fos = new FileOutputStream(gradlePropertiesFile)) {
fos.write("android.nonFinalResIds=false".getBytes(StandardCharsets.UTF_8));
}
}
}
private void saveProjectBuildGradle() throws IOException {
TemplateFile tmpl = TemplateFile.fromResources("/export/android/build.gradle.tmpl");
tmpl.save(new File(projectDir, "build.gradle"));
}
private void saveSettingsGradle() throws IOException {
TemplateFile tmpl = TemplateFile.fromResources("/export/android/settings.gradle.tmpl");
String appName = applicationParams.getApplicationName();
String projectName;
if (appName != null) {
projectName = ILLEGAL_GRADLE_CHARS.matcher(appName).replaceAll("");
} else {
projectName = GradleGeneratorTools.guessProjectName(root);
}
tmpl.add("projectName", projectName);
tmpl.add("mainModuleName", baseDir.getName());
tmpl.save(new File(projectDir, "settings.gradle"));
}
private void saveApplicationBuildGradle() throws IOException {
String appPackage = Utils.getOrElse(root.getAppPackage(), "UNKNOWN");
int minSdkVersion = Utils.getOrElse(applicationParams.getMinSdkVersion(), 0);
TemplateFile tmpl = TemplateFile.fromResources("/export/android/app.build.gradle.tmpl");
tmpl.add("applicationId", appPackage);
tmpl.add("minSdkVersion", minSdkVersion);
tmpl.add("targetSdkVersion", applicationParams.getTargetSdkVersion());
tmpl.add("versionCode", applicationParams.getVersionCode());
tmpl.add("versionName", applicationParams.getVersionName());
tmpl.add("additionalOptions", genAdditionalAndroidPluginOptions(minSdkVersion));
tmpl.save(new File(baseDir, "build.gradle"));
}
private void saveLibraryBuildGradle() throws IOException {
String pkg = Utils.getOrElse(root.getAppPackage(), "UNKNOWN");
int minSdkVersion = Utils.getOrElse(applicationParams.getMinSdkVersion(), 0);
TemplateFile tmpl = TemplateFile.fromResources("/export/android/lib.build.gradle.tmpl");
tmpl.add("packageId", pkg);
tmpl.add("minSdkVersion", minSdkVersion);
tmpl.add("additionalOptions", genAdditionalAndroidPluginOptions(minSdkVersion));
tmpl.save(new File(baseDir, "build.gradle"));
}
private String genAdditionalAndroidPluginOptions(int minSdkVersion) {
List<String> additionalOptions = new ArrayList<>();
GradleInfoStorage gradleInfo = root.getGradleInfoStorage();
if (gradleInfo.isVectorPathData() && minSdkVersion < 21 || gradleInfo.isVectorFillType() && minSdkVersion < 24) {
additionalOptions.add("vectorDrawables.useSupportLibrary = true");
}
if (gradleInfo.isUseApacheHttpLegacy()) {
additionalOptions.add("useLibrary 'org.apache.http.legacy'");
}
StringBuilder sb = new StringBuilder();
for (String additionalOption : additionalOptions) {
sb.append(" ").append(additionalOption).append('\n');
}
return sb.toString();
}
}
@@ -0,0 +1,19 @@
package jadx.core.export.gen;
import java.io.File;
import java.util.List;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.files.FileUtils;
public class GradleGeneratorTools {
public static String guessProjectName(RootNode root) {
List<File> inputFiles = root.getArgs().getInputFiles();
if (inputFiles.size() == 1) {
return FileUtils.getPathBaseName(inputFiles.get(0).toPath());
}
// default
return "PROJECT_NAME";
}
}
@@ -0,0 +1,12 @@
package jadx.core.export.gen;
import jadx.core.export.OutDirs;
public interface IExportGradleGenerator {
void init();
OutDirs getOutDirs();
void generateFiles();
}
@@ -0,0 +1,60 @@
package jadx.core.export.gen;
import java.io.File;
import java.io.IOException;
import java.util.List;
import jadx.api.ResourceFile;
import jadx.core.dex.nodes.RootNode;
import jadx.core.export.OutDirs;
import jadx.core.export.TemplateFile;
import jadx.core.utils.exceptions.JadxRuntimeException;
public class SimpleJavaGradleGenerator implements IExportGradleGenerator {
private final RootNode root;
private final File projectDir;
private final List<ResourceFile> resources;
private OutDirs outDirs;
private File appDir;
public SimpleJavaGradleGenerator(RootNode root, File projectDir, List<ResourceFile> resources) {
this.root = root;
this.projectDir = projectDir;
this.resources = resources;
}
@Override
public void init() {
appDir = new File(projectDir, "app");
File srcOutDir = new File(appDir, "src/main/java");
File resOutDir = new File(appDir, "src/main/resources");
outDirs = new OutDirs(srcOutDir, resOutDir);
}
@Override
public void generateFiles() {
try {
saveSettingsGradle();
saveBuildGradle();
} catch (Exception e) {
throw new JadxRuntimeException("Failed to generate gradle files", e);
}
}
private void saveSettingsGradle() throws IOException {
TemplateFile tmpl = TemplateFile.fromResources("/export/java/settings.gradle.kts.tmpl");
tmpl.add("projectName", GradleGeneratorTools.guessProjectName(root));
tmpl.save(new File(projectDir, "settings.gradle.kts"));
}
private void saveBuildGradle() throws IOException {
TemplateFile tmpl = TemplateFile.fromResources("/export/java/build.gradle.kts.tmpl");
tmpl.save(new File(appDir, "build.gradle.kts"));
}
@Override
public OutDirs getOutDirs() {
return outDirs;
}
}
@@ -4,6 +4,7 @@ import java.nio.file.Path;
import jadx.api.plugins.JadxPluginInfo;
import jadx.api.plugins.data.IJadxFiles;
import jadx.core.utils.files.FileUtils;
public class JadxFilesData implements IJadxFiles {
private static final String PLUGINS_DATA_DIR = "plugins-data";
@@ -32,6 +33,8 @@ public class JadxFilesData implements IJadxFiles {
}
private Path toPluginPath(Path dir) {
return dir.resolve(PLUGINS_DATA_DIR).resolve(pluginInfo.getPluginId());
Path dirPath = dir.resolve(PLUGINS_DATA_DIR).resolve(pluginInfo.getPluginId());
FileUtils.makeDirs(dirPath);
return dirPath;
}
}
@@ -20,7 +20,7 @@ import jadx.core.xmlgen.ResContainer;
public class AndroidManifestParser {
private final Document androidManifest;
private final Document appStrings;
private final @Nullable Document appStrings;
private final EnumSet<AppAttribute> parseAttrs;
private final IJadxSecurity security;
@@ -28,7 +28,7 @@ public class AndroidManifestParser {
this(androidManifestRes, null, parseAttrs, security);
}
public AndroidManifestParser(ResourceFile androidManifestRes, ResContainer appStrings,
public AndroidManifestParser(ResourceFile androidManifestRes, @Nullable ResContainer appStrings,
EnumSet<AppAttribute> parseAttrs, IJadxSecurity security) {
this.parseAttrs = parseAttrs;
this.security = Objects.requireNonNull(security);
@@ -10,17 +10,17 @@ public class ApplicationParams {
private final Integer targetSdkVersion;
private final Integer versionCode;
private final String versionName;
private final String mainActivtiy;
private final String mainActivity;
private final String application;
public ApplicationParams(String applicationLabel, Integer minSdkVersion, Integer targetSdkVersion, Integer versionCode,
String versionName, String mainActivtiy, String application) {
String versionName, String mainActivity, String application) {
this.applicationLabel = applicationLabel;
this.minSdkVersion = minSdkVersion;
this.targetSdkVersion = targetSdkVersion;
this.versionCode = versionCode;
this.versionName = versionName;
this.mainActivtiy = mainActivtiy;
this.mainActivity = mainActivity;
this.application = application;
}
@@ -45,11 +45,11 @@ public class ApplicationParams {
}
public String getMainActivity() {
return mainActivtiy;
return mainActivity;
}
public JavaClass getMainActivityJavaClass(JadxDecompiler decompiler) {
return decompiler.searchJavaClassByOrigFullName(mainActivtiy);
return decompiler.searchJavaClassByOrigFullName(mainActivity);
}
public String getApplication() {
@@ -38,5 +38,5 @@ android {
}
dependencies {
// some dependencies
// TODO: dependencies
}
@@ -1,6 +1,6 @@
buildscript {
repositories {
google()
google()
mavenCentral()
}
dependencies {
@@ -0,0 +1,36 @@
plugins {
id 'com.android.library'
}
android {
namespace '{{packageId}}'
compileSdk 30
defaultConfig {
minSdk {{minSdkVersion}}
{{additionalOptions}}
}
buildTypes {
release {
minifyEnabled false
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
lintOptions {
abortOnError false
}
buildFeatures {
buildConfig = false
}
}
dependencies {
// TODO: dependencies
}
@@ -0,0 +1,3 @@
rootProject.name = '{{projectName}}'
include '{{mainModuleName}}'
@@ -0,0 +1,12 @@
plugins {
java
}
repositories {
google()
mavenCentral()
}
dependencies {
// some dependencies
}
@@ -0,0 +1,3 @@
rootProject.name = "{{projectName}}"
include("app")
@@ -1,2 +0,0 @@
include ':app'
rootProject.name = '{{applicationName}}'
@@ -2,6 +2,8 @@ package jadx.tests.api;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.List;
@@ -10,19 +12,21 @@ import org.junit.jupiter.api.io.TempDir;
import jadx.api.ICodeInfo;
import jadx.api.JadxArgs;
import jadx.api.ResourceFile;
import jadx.api.ResourceFileContainer;
import jadx.api.ResourceFileContent;
import jadx.api.ResourceType;
import jadx.api.impl.SimpleCodeInfo;
import jadx.core.dex.nodes.RootNode;
import jadx.core.export.ExportGradleProject;
import jadx.core.export.ExportGradleTask;
import jadx.core.export.ExportGradle;
import jadx.core.export.ExportGradleType;
import jadx.core.export.OutDirs;
import jadx.core.xmlgen.ResContainer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
public abstract class ExportGradleTest {
private static final String MANIFEST_TESTS_DIR = "src/test/manifest";
private static final String MANIFEST_TESTS_DIR = "manifest";
private final RootNode root = new RootNode(new JadxArgs());
@@ -30,7 +34,7 @@ public abstract class ExportGradleTest {
private File exportDir;
protected ICodeInfo loadResource(String filename) {
return new SimpleCodeInfo(loadFileContent(new File(MANIFEST_TESTS_DIR, filename)));
return new SimpleCodeInfo(loadResourceContent(MANIFEST_TESTS_DIR, filename));
}
private static String loadFileContent(File filePath) {
@@ -42,21 +46,37 @@ public abstract class ExportGradleTest {
}
}
private String loadResourceContent(String dir, String filename) {
String resPath = dir + '/' + filename;
try (InputStream in = getClass().getClassLoader().getResourceAsStream(resPath)) {
if (in == null) {
fail("Resource not found: " + resPath);
return "";
}
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
} catch (Exception e) {
fail("Loading file failed: " + resPath, e);
return "";
}
}
protected RootNode getRootNode() {
return root;
}
protected void exportGradle(String manifestFilename, String stringsFileName) {
ResourceFile androidManifest = new ResourceFileContent(manifestFilename,
ResourceType.XML, loadResource(manifestFilename));
ResourceFile androidManifest =
new ResourceFileContent("AndroidManifest.xml", ResourceType.MANIFEST, loadResource(manifestFilename));
ResContainer strings = ResContainer.textResource(stringsFileName, loadResource(stringsFileName));
ResContainer arsc = ResContainer.resourceTable("resources.arsc", List.of(strings), new SimpleCodeInfo("empty"));
ResourceFile arscFile = new ResourceFileContainer("resources.arsc", ResourceType.ARSC, arsc);
List<ResourceFile> resources = List.of(androidManifest, arscFile);
ExportGradleTask exportGradleTask = new ExportGradleTask(List.of(androidManifest), root, exportDir);
exportGradleTask.init();
assertThat(exportGradleTask.getSrcOutDir()).exists();
assertThat(exportGradleTask.getResOutDir()).exists();
ExportGradleProject export = new ExportGradleProject(root, exportDir, androidManifest, strings);
root.getArgs().setExportGradleType(ExportGradleType.ANDROID_APP);
ExportGradle export = new ExportGradle(root, exportDir, resources);
OutDirs outDirs = export.init();
assertThat(outDirs.getSrcOutDir()).exists();
assertThat(outDirs.getResOutDir()).exists();
export.generateGradleFiles();
}
@@ -72,7 +92,7 @@ public abstract class ExportGradleTest {
return new File(exportDir, "gradle.properties");
}
protected String getGradleProperies() {
protected String getGradleProperties() {
return loadFileContent(getGradleProperiesFile());
}
}
@@ -19,6 +19,6 @@ public class TestNonFinalResIds extends ExportGradleTest {
gradleInfo.setNonFinalResIds(true);
exportGradle("OptionalTargetSdkVersion.xml", "strings.xml");
assertThat(getGradleProperies()).containsOne("android.nonFinalResIds=false");
assertThat(getGradleProperties()).containsOne("android.nonFinalResIds=false");
}
}
@@ -10,7 +10,7 @@ public class TemplateFileTest {
@Test
public void testBuildGradle() throws Exception {
TemplateFile tmpl = TemplateFile.fromResources("/export/app.build.gradle.tmpl");
TemplateFile tmpl = TemplateFile.fromResources("/export/android/app.build.gradle.tmpl");
tmpl.add("applicationId", "SOME_ID");
tmpl.add("minSdkVersion", 1);
tmpl.add("targetSdkVersion", 2);
@@ -7,6 +7,7 @@ import javax.swing.JOptionPane;
import jadx.api.ICodeCache;
import jadx.api.utils.tasks.ITaskExecutor;
import jadx.gui.JadxWrapper;
import jadx.gui.cache.code.CodeCacheMode;
import jadx.gui.cache.code.FixedCodeCache;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.NLS;
@@ -42,9 +43,11 @@ public class ExportTask extends CancelableBackgroundTask {
private void wrapCodeCache() {
uiCodeCache = wrapper.getArgs().getCodeCache();
// do not save newly decompiled code in cache to not increase memory usage
// TODO: maybe make memory limited cache?
wrapper.getArgs().setCodeCache(new FixedCodeCache(uiCodeCache));
if (mainWindow.getSettings().getCodeCacheMode() != CodeCacheMode.DISK) {
// do not save newly decompiled code in cache to not increase memory usage
// TODO: maybe make memory limited cache?
wrapper.getArgs().setCodeCache(new FixedCodeCache(uiCodeCache));
}
}
@Override
@@ -32,7 +32,7 @@ import jadx.gui.utils.UiUtils;
public class TreeExpansionService {
private static final Logger LOG = LoggerFactory.getLogger(TreeExpansionService.class);
private static final boolean DEBUG = UiUtils.JADX_GUI_DEBUG;
private static final boolean DEBUG = false;
private static final Comparator<TreePath> PATH_LENGTH_REVERSE = Comparator.comparingInt(p -> -p.getPathCount());
@@ -74,6 +74,9 @@ import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.formdev.flatlaf.extras.FlatInspector;
import com.formdev.flatlaf.extras.FlatUIDefaultsInspector;
import ch.qos.logback.classic.Level;
import jadx.api.JadxArgs;
@@ -111,7 +114,6 @@ import jadx.gui.plugins.context.CommonGuiPluginsContext;
import jadx.gui.plugins.context.TreePopupMenuEntry;
import jadx.gui.plugins.mappings.RenameMappingsGui;
import jadx.gui.plugins.quark.QuarkDialog;
import jadx.gui.settings.ExportProjectProperties;
import jadx.gui.settings.JadxProject;
import jadx.gui.settings.JadxSettings;
import jadx.gui.settings.ui.JadxSettingsWindow;
@@ -132,9 +134,9 @@ import jadx.gui.ui.codearea.EditorViewState;
import jadx.gui.ui.dialog.ADBDialog;
import jadx.gui.ui.dialog.AboutDialog;
import jadx.gui.ui.dialog.ExceptionDialog;
import jadx.gui.ui.dialog.ExportProjectDialog;
import jadx.gui.ui.dialog.LogViewerDialog;
import jadx.gui.ui.dialog.SearchDialog;
import jadx.gui.ui.export.ExportProjectDialog;
import jadx.gui.ui.filedialog.FileDialogWrapper;
import jadx.gui.ui.filedialog.FileOpenMode;
import jadx.gui.ui.menu.HiddenMenuItem;
@@ -798,25 +800,23 @@ public class MainWindow extends JFrame {
backgroundExecutor.cancelAll();
}
private void exportProject() {
ExportProjectDialog dialog = new ExportProjectDialog(this, this::saveAll);
public void exportProject() {
ExportProjectDialog dialog = new ExportProjectDialog(this, props -> {
JadxArgs args = wrapper.getArgs();
if (props.isAsGradleMode()) {
args.setExportGradleType(props.getExportGradleType());
args.setSkipSources(false);
args.setSkipResources(false);
} else {
args.setExportGradleType(null);
args.setSkipSources(props.isSkipSources());
args.setSkipResources(props.isSkipResources());
}
backgroundExecutor.execute(new ExportTask(this, wrapper, new File(props.getExportPath())));
});
dialog.setVisible(true);
}
private void saveAll(ExportProjectProperties exportProjectProperties) {
JadxArgs decompilerArgs = wrapper.getArgs();
decompilerArgs.setExportAsGradleProject(exportProjectProperties.isAsGradleMode());
if (exportProjectProperties.isAsGradleMode()) {
decompilerArgs.setSkipSources(false);
decompilerArgs.setSkipResources(false);
} else {
decompilerArgs.setSkipSources(exportProjectProperties.isSkipSources());
decompilerArgs.setSkipResources(exportProjectProperties.isSkipResources());
}
File saveDir = new File(exportProjectProperties.getExportPath());
backgroundExecutor.execute(new ExportTask(this, wrapper, saveDir));
}
public void initTree() {
treeRoot = new JRoot(wrapper);
treeRoot.setFlatPackages(isFlattenPackage);
@@ -1422,6 +1422,11 @@ public class MainWindow extends JFrame {
mainPanel.add(bottomSplitPane, BorderLayout.CENTER);
setContentPane(mainPanel);
setTitle(DEFAULT_TITLE);
if (UiUtils.JADX_GUI_DEBUG) {
FlatInspector.install("ctrl shift alt X");
FlatUIDefaultsInspector.install("ctrl shift alt Y");
}
}
public void setLocationAndPosition() {
@@ -27,7 +27,7 @@ public abstract class CommonDialog extends JDialog {
UiUtils.addEscapeShortCutToDispose(this);
setLocationRelativeTo(null);
pack();
UiUtils.uiRunAndWait(this::pack);
Dimension minSize = getSize();
setMinimumSize(minSize);
if (!mainWindow.getSettings().loadWindowPos(this)) {
@@ -1,10 +1,9 @@
package jadx.gui.ui.dialog;
package jadx.gui.ui.export;
import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.event.ItemEvent;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.function.Consumer;
@@ -12,16 +11,23 @@ import java.util.function.Consumer;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.GroupLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import jadx.gui.settings.ExportProjectProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.export.ExportGradle;
import jadx.core.export.ExportGradleType;
import jadx.core.utils.files.FileUtils;
import jadx.gui.JadxWrapper;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.dialog.CommonDialog;
import jadx.gui.ui.filedialog.FileDialogWrapper;
import jadx.gui.ui.filedialog.FileOpenMode;
import jadx.gui.utils.NLS;
@@ -29,6 +35,7 @@ import jadx.gui.utils.TextStandardActions;
import jadx.gui.utils.ui.DocumentUpdateListener;
public class ExportProjectDialog extends CommonDialog {
private static final Logger LOG = LoggerFactory.getLogger(ExportProjectDialog.class);
private final ExportProjectProperties exportProjectProperties = new ExportProjectProperties();
private final Consumer<ExportProjectProperties> exportListener;
@@ -40,31 +47,26 @@ public class ExportProjectDialog extends CommonDialog {
}
private void initUI() {
JPanel contentPane = makeContentPane();
JPanel buttonPane = initButtonsPanel();
Container container = getContentPane();
container.add(contentPane, BorderLayout.CENTER);
container.add(buttonPane, BorderLayout.PAGE_END);
JPanel contentPanel = new JPanel();
contentPanel.setLayout(new BorderLayout(5, 5));
contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
contentPanel.add(makeContentPane(), BorderLayout.PAGE_START);
contentPanel.add(initButtonsPanel(), BorderLayout.PAGE_END);
getContentPane().add(contentPanel);
setTitle(NLS.str("export_dialog.title"));
commonWindowInit();
}
private JPanel makeContentPane() {
// top layout
JLabel label = new JLabel(NLS.str("export_dialog.save_path"));
JLabel pathLbl = new JLabel(NLS.str("export_dialog.save_path"));
JTextField pathField = new JTextField();
pathField.setText(mainWindow.getSettings().getLastSaveFilePath().toString());
pathField.getDocument().addDocumentListener(new DocumentUpdateListener(ev -> setExportProjectPath(pathField)));
pathField.setText(mainWindow.getSettings().getLastSaveFilePath().toString());
TextStandardActions.attach(pathField);
JButton browseButton = makeEditorBrowseButton(pathField);
// check box layout
JPanel exportOptionsPanel = new JPanel();
exportOptionsPanel.setBorder(BorderFactory.createTitledBorder(NLS.str("export_dialog.export_options")));
exportOptionsPanel.setLayout(new BoxLayout(exportOptionsPanel, BoxLayout.PAGE_AXIS));
JCheckBox resourceDecode = new JCheckBox(NLS.str("preferences.skipResourcesDecode"));
resourceDecode.setSelected(mainWindow.getSettings().isSkipResources());
resourceDecode.addItemListener(e -> {
@@ -77,55 +79,70 @@ public class ExportProjectDialog extends CommonDialog {
exportProjectProperties.setSkipSources(e.getStateChange() == ItemEvent.SELECTED);
});
JLabel exportTypeLbl = new JLabel(NLS.str("export_dialog.export_gradle_type"));
JComboBox<ExportGradleType> exportTypeComboBox = new JComboBox<>(ExportGradleType.values());
exportTypeLbl.setLabelFor(exportTypeComboBox);
ExportGradleType initialExportType = getExportGradleType();
exportProjectProperties.setExportGradleType(initialExportType);
exportTypeComboBox.setSelectedItem(initialExportType);
exportTypeComboBox.addItemListener(e -> {
exportProjectProperties.setExportGradleType((ExportGradleType) e.getItem());
});
exportTypeComboBox.setEnabled(false);
JCheckBox exportAsGradleProject = new JCheckBox(NLS.str("export_dialog.export_gradle"));
exportAsGradleProject.addItemListener(e -> {
boolean isSelected = e.getStateChange() == ItemEvent.SELECTED;
exportProjectProperties.setAsGradleMode(isSelected);
resourceDecode.setEnabled(!isSelected);
skipSources.setEnabled(!isSelected);
boolean enableGradle = e.getStateChange() == ItemEvent.SELECTED;
exportProjectProperties.setAsGradleMode(enableGradle);
exportTypeComboBox.setEnabled(enableGradle);
resourceDecode.setEnabled(!enableGradle);
skipSources.setEnabled(!enableGradle);
});
JPanel pathPanel = new JPanel();
pathPanel.setLayout(new BoxLayout(pathPanel, BoxLayout.LINE_AXIS));
pathPanel.setAlignmentX(LEFT_ALIGNMENT);
pathPanel.add(pathLbl);
pathPanel.add(Box.createRigidArea(new Dimension(5, 0)));
pathPanel.add(pathField);
pathPanel.add(Box.createRigidArea(new Dimension(5, 0)));
pathPanel.add(browseButton);
JPanel typePanel = new JPanel();
typePanel.setLayout(new BoxLayout(typePanel, BoxLayout.LINE_AXIS));
typePanel.setAlignmentX(LEFT_ALIGNMENT);
typePanel.add(Box.createRigidArea(new Dimension(20, 0)));
typePanel.add(exportTypeLbl);
typePanel.add(Box.createRigidArea(new Dimension(5, 0)));
typePanel.add(exportTypeComboBox);
typePanel.add(Box.createHorizontalGlue());
JPanel exportOptionsPanel = new JPanel();
exportOptionsPanel.setBorder(BorderFactory.createTitledBorder(NLS.str("export_dialog.export_options")));
exportOptionsPanel.setLayout(new BoxLayout(exportOptionsPanel, BoxLayout.PAGE_AXIS));
exportOptionsPanel.add(exportAsGradleProject);
exportOptionsPanel.add(typePanel);
exportOptionsPanel.add(resourceDecode);
exportOptionsPanel.add(skipSources);
// build group box layout
JPanel groupBoxPanel = new JPanel();
GroupLayout groupBoxLayout = new GroupLayout(groupBoxPanel);
groupBoxLayout.setAutoCreateGaps(true);
groupBoxLayout.setAutoCreateContainerGaps(true);
groupBoxPanel.setLayout(groupBoxLayout);
groupBoxLayout.setHorizontalGroup(groupBoxLayout.createParallelGroup()
.addComponent(exportOptionsPanel, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Integer.MAX_VALUE));
groupBoxLayout.setVerticalGroup(groupBoxLayout.createSequentialGroup()
.addComponent(exportOptionsPanel));
// main layout
JPanel mainPanel = new JPanel();
GroupLayout layout = new GroupLayout(mainPanel);
mainPanel.setLayout(layout);
layout.setAutoCreateGaps(true);
layout.setAutoCreateContainerGaps(true);
// arrange components using GroupLayout
layout.setHorizontalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addComponent(label)
.addComponent(pathField)
.addComponent(browseButton))
.addComponent(groupBoxPanel));
layout.setVerticalGroup(layout.createSequentialGroup()
.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
.addComponent(label)
.addComponent(pathField)
.addComponent(browseButton))
.addComponent(groupBoxPanel));
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.PAGE_AXIS));
mainPanel.add(pathPanel);
mainPanel.add(Box.createRigidArea(new Dimension(0, 10)));
mainPanel.add(exportOptionsPanel);
return mainPanel;
}
private ExportGradleType getExportGradleType() {
try {
JadxWrapper wrapper = mainWindow.getWrapper();
return ExportGradle.detectExportType(wrapper.getRootNode(), wrapper.getResources());
} catch (Exception e) {
LOG.warn("Failed to detect export type", e);
return ExportGradleType.AUTO;
}
}
private void setExportProjectPath(JTextField field) {
String path = field.getText();
if (!path.isEmpty()) {
@@ -143,8 +160,6 @@ public class ExportProjectDialog extends CommonDialog {
JPanel buttonPane = new JPanel();
buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS));
buttonPane.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10));
buttonPane.add(Box.createRigidArea(new Dimension(10, 0)));
buttonPane.add(Box.createHorizontalGlue());
buttonPane.add(exportProjectButton);
buttonPane.add(Box.createRigidArea(new Dimension(10, 0)));
@@ -168,12 +183,33 @@ public class ExportProjectDialog extends CommonDialog {
}
private void exportProject() {
if (!new File(exportProjectProperties.getExportPath()).exists()) {
String exportPathStr = exportProjectProperties.getExportPath();
if (!validateAndMakeDir(exportPathStr)) {
JOptionPane.showMessageDialog(this, NLS.str("message.enter_valid_path"),
NLS.str("message.errorTitle"), JOptionPane.WARNING_MESSAGE);
return;
}
mainWindow.getSettings().setLastSaveFilePath(Path.of(exportPathStr));
LOG.debug("Export properties: {}", exportProjectProperties);
exportListener.accept(exportProjectProperties);
dispose();
}
private static boolean validateAndMakeDir(String exportPath) {
if (exportPath == null || exportPath.isBlank()) {
return false;
}
try {
Path path = Path.of(exportPath);
if (Files.isRegularFile(path)) {
// dir exists as a file
return false;
}
FileUtils.makeDirs(path);
return true;
} catch (Exception e) {
LOG.warn("Export path validate error, path string:{}", exportPath, e);
return false;
}
}
}
@@ -1,9 +1,14 @@
package jadx.gui.settings;
package jadx.gui.ui.export;
import org.jetbrains.annotations.Nullable;
import jadx.core.export.ExportGradleType;
public class ExportProjectProperties {
private boolean skipSources;
private boolean skipResources;
private boolean asGradleMode;
private @Nullable ExportGradleType exportGradleType;
private String exportPath;
public boolean isSkipSources() {
@@ -30,6 +35,14 @@ public class ExportProjectProperties {
this.asGradleMode = asGradleMode;
}
public @Nullable ExportGradleType getExportGradleType() {
return exportGradleType;
}
public void setExportGradleType(@Nullable ExportGradleType exportGradleType) {
this.exportGradleType = exportGradleType;
}
public String getExportPath() {
return exportPath;
}
@@ -37,4 +50,14 @@ public class ExportProjectProperties {
public void setExportPath(String exportPath) {
this.exportPath = exportPath;
}
@Override
public String toString() {
return "ExportProjectProperties{exportPath='" + exportPath + '\''
+ ", asGradleMode=" + asGradleMode
+ ", exportGradleType=" + exportGradleType
+ ", skipSources=" + skipSources
+ ", skipResources=" + skipResources
+ '}';
}
}
@@ -180,6 +180,7 @@ export_dialog.save_path=Speicherpfad:
export_dialog.browse=Durchsuchen
export_dialog.export_options=Exportoptionen
export_dialog.export_gradle=Als Gradle-Projekt exportieren
#export_dialog.export_gradle_type=Gradle template:
log_viewer.title=Protokollanzeige
log_viewer.log_level=Protokollstufe:
@@ -175,11 +175,12 @@ comment_dialog.usage=Use 'Shift + Enter' to start a new line
rename_dialog.class_help=Enter full name to move class to another package. Start with '.' to move to default (empty) package
export_dialog.title=Export to source code
export_dialog.title=Export
export_dialog.save_path=Save path:
export_dialog.browse=Browse
export_dialog.export_options=Export options
export_dialog.export_gradle=Export as gradle project
export_dialog.export_gradle=Export as a Gradle project
export_dialog.export_gradle_type=Gradle template:
log_viewer.title=Log Viewer
log_viewer.log_level=Log level:
@@ -180,6 +180,7 @@ usage_dialog.label=Usage for:
#export_dialog.browse=Browse
#export_dialog.export_options=Export options
#export_dialog.export_gradle=Export as gradle project
#export_dialog.export_gradle_type=Gradle template:
log_viewer.title=Visor log
log_viewer.log_level=Nivel log:
@@ -180,6 +180,7 @@ comment_dialog.usage=Gunakan Shift + Enter untuk memulai baris baru
#export_dialog.browse=Browse
#export_dialog.export_options=Export options
#export_dialog.export_gradle=Export as gradle project
#export_dialog.export_gradle_type=Gradle template:
log_viewer.title=Pemantau Log
log_viewer.log_level=Tingkat log:
@@ -180,6 +180,7 @@ comment_dialog.usage=Shift + Enter 를 입력해 새 라인에 입력
#export_dialog.browse=Browse
#export_dialog.export_options=Export options
#export_dialog.export_gradle=Export as gradle project
#export_dialog.export_gradle_type=Gradle template:
log_viewer.title=로그 뷰어
log_viewer.log_level=로그 레벨:
@@ -180,6 +180,7 @@ comment_dialog.usage=Use Shift + Enter para pular uma linha
#export_dialog.browse=Browse
#export_dialog.export_options=Export options
#export_dialog.export_gradle=Export as gradle project
#export_dialog.export_gradle_type=Gradle template:
log_viewer.title=Visualizador de log
log_viewer.log_level=Nível do log:
@@ -180,6 +180,7 @@ rename_dialog.class_help=Введите полный путь к пакету,
#export_dialog.browse=Browse
#export_dialog.export_options=Export options
#export_dialog.export_gradle=Export as gradle project
#export_dialog.export_gradle_type=Gradle template:
log_viewer.title=Просмотр логов
log_viewer.log_level=Уровень лога:
@@ -180,6 +180,7 @@ export_dialog.save_path=保存路径:
export_dialog.browse=浏览
export_dialog.export_options=导出选项
export_dialog.export_gradle=导出为 Gradle 项目
#export_dialog.export_gradle_type=Gradle template:
log_viewer.title=日志查看器
log_viewer.log_level=日志等级:
@@ -180,6 +180,7 @@ export_dialog.save_path=儲存路徑:
export_dialog.browse=瀏覽
export_dialog.export_options=匯出選項
export_dialog.export_gradle=匯出成 Gradle 專案
#export_dialog.export_gradle_type=Gradle template:
log_viewer.title=記錄檔檢視器
log_viewer.log_level=記錄層級:
@@ -31,7 +31,7 @@ data class ScriptAnalyzeResult(
)
class ScriptServices {
private val compileConf = ScriptEval.compileConf
private val compileConf = ScriptEval().buildCompileConf()
private val replCompiler = KJvmReplCompilerWithIdeServices(
compileConf[ScriptCompilationConfiguration.hostConfiguration]
?: defaultJvmScriptingHostConfiguration,
@@ -7,16 +7,15 @@ import jadx.plugins.script.passes.JadxScriptAfterLoadPass
import jadx.plugins.script.runtime.data.JadxScriptAllOptions
class JadxScriptPlugin : JadxPlugin {
private val scriptOptions = JadxScriptAllOptions()
override fun getPluginInfo() = JadxPluginInfo("jadx-script", "Jadx Script", "Scripting support for jadx")
override fun init(init: JadxPluginContext) {
init.registerOptions(scriptOptions)
val scripts = ScriptEval().process(init, scriptOptions)
override fun init(context: JadxPluginContext) {
val scriptOptions = JadxScriptAllOptions()
context.registerOptions(scriptOptions)
val scripts = ScriptEval().process(context, scriptOptions)
if (scripts.isNotEmpty()) {
init.addPass(JadxScriptAfterLoadPass(scripts))
init.guiContext?.let { JadxScriptOptionsUI.setup(it, scriptOptions) }
context.addPass(JadxScriptAfterLoadPass(scripts))
context.guiContext?.let { JadxScriptOptionsUI.setup(it, scriptOptions) }
}
}
}
@@ -1,6 +1,7 @@
package jadx.plugins.script
import jadx.commons.app.JadxCommonFiles
import jadx.api.plugins.JadxPluginContext
import jadx.core.utils.files.FileUtils
import java.io.File
import java.security.MessageDigest
import kotlin.script.experimental.api.CompiledScript
@@ -14,11 +15,11 @@ import kotlin.script.experimental.jvmhost.saveToJar
class ScriptCache {
private val enableCache = System.getProperty("JADX_SCRIPT_CACHE_ENABLE", "true").equals("true", ignoreCase = true)
fun build(): CompiledJvmScriptsCache {
fun build(context: JadxPluginContext): CompiledJvmScriptsCache {
if (!enableCache) {
return CompiledJvmScriptsCache.NoCache
}
return JadxScriptsCache(getCacheDir())
return JadxScriptsCache(getCacheDir(context))
}
/**
@@ -38,7 +39,7 @@ class ScriptCache {
}
return file.loadScriptFromJar() ?: run {
// invalidate cache if the script cannot be loaded
cacheDir.deleteRecursively()
FileUtils.deleteDir(cacheDir)
null
}
}
@@ -60,9 +61,9 @@ class ScriptCache {
}
}
private fun getCacheDir(): File {
val cacheBaseDir = JadxCommonFiles.getCacheDir().resolve("scripts").toFile()
cacheBaseDir.mkdirs()
private fun getCacheDir(context: JadxPluginContext): File {
val cacheBaseDir = context.files().pluginCacheDir.resolve("compiled").toFile()
FileUtils.makeDirs(cacheBaseDir)
return cacheBaseDir
}
@@ -4,13 +4,12 @@ import jadx.api.plugins.JadxPluginContext
import jadx.plugins.script.runtime.JadxScriptData
import jadx.plugins.script.runtime.JadxScriptTemplate
import jadx.plugins.script.runtime.data.JadxScriptAllOptions
import kotlin.script.experimental.api.CompiledScript
import kotlin.script.experimental.api.EvaluationResult
import kotlin.script.experimental.api.ResultValue
import kotlin.script.experimental.api.ResultWithDiagnostics
import kotlin.script.experimental.api.ScriptCompilationConfiguration
import kotlin.script.experimental.api.ScriptDiagnostic.Severity
import kotlin.script.experimental.api.ScriptEvaluationConfiguration
import kotlin.script.experimental.api.SourceCode
import kotlin.script.experimental.api.constructorArgs
import kotlin.script.experimental.host.ScriptingHostConfiguration
import kotlin.script.experimental.host.toScriptSource
@@ -25,52 +24,32 @@ import kotlin.time.toDuration
class ScriptEval {
companion object {
val scriptingHost = BasicJvmScriptingHost(
baseHostConfiguration = ScriptingHostConfiguration {
jvm {
compilationCache(ScriptCache().build())
}
},
)
val compileConf = createJvmCompilationConfigurationFromTemplate<JadxScriptTemplate>()
private val baseEvalConf = createJvmEvaluationConfigurationFromTemplate<JadxScriptTemplate>()
private fun buildEvalConf(scriptData: JadxScriptData) =
ScriptEvaluationConfiguration(baseEvalConf) {
constructorArgs(scriptData)
}
}
fun process(init: JadxPluginContext, scriptOptions: JadxScriptAllOptions): List<JadxScriptData> {
val jadx = init.decompiler
fun process(context: JadxPluginContext, scriptOptions: JadxScriptAllOptions): List<JadxScriptData> {
val jadx = context.decompiler
val scripts = jadx.args.inputFiles.filter { f -> f.name.endsWith(".jadx.kts") }
if (scripts.isEmpty()) {
return emptyList()
}
val scriptingHost = buildScriptingHost(context)
val compileConf = buildCompileConf()
val scriptDataList = mutableListOf<JadxScriptData>()
for (scriptFile in scripts) {
val scriptData = JadxScriptData(jadx, init, scriptOptions, scriptFile)
val scriptData = JadxScriptData(jadx, context, scriptOptions, scriptFile)
scriptDataList.add(scriptData)
eval(scriptData)
eval(scriptingHost, compileConf, scriptData)
}
return scriptDataList
}
suspend fun compile(script: SourceCode): ResultWithDiagnostics<CompiledScript> {
return scriptingHost.compiler(script, compileConf)
}
private fun eval(scriptData: JadxScriptData) {
private fun eval(
scriptingHost: BasicJvmScriptingHost,
compileConf: ScriptCompilationConfiguration,
scriptData: JadxScriptData,
) {
scriptData.log.debug { "Loading script: ${scriptData.scriptFile.absolutePath}" }
val evalConf = buildEvalConf(scriptData)
val execTime = measureTimeMillis {
val result = scriptingHost.eval(
scriptData.scriptFile.toScriptSource(),
compileConf,
buildEvalConf(scriptData),
)
val result = scriptingHost.eval(scriptData.scriptFile.toScriptSource(), compileConf, evalConf)
processEvalResult(result, scriptData)
}
scriptData.log.debug { "Script '${scriptData.scriptName}' executed in ${execTime.toDuration(DurationUnit.MILLISECONDS)}" }
@@ -103,4 +82,21 @@ class ScriptEval {
}
}
}
fun buildScriptingHost(context: JadxPluginContext) = BasicJvmScriptingHost(
baseHostConfiguration = ScriptingHostConfiguration {
jvm {
compilationCache(ScriptCache().build(context))
}
},
)
fun buildCompileConf() = createJvmCompilationConfigurationFromTemplate<JadxScriptTemplate>()
fun buildEvalConf(scriptData: JadxScriptData): ScriptEvaluationConfiguration {
val baseEvalConf = createJvmEvaluationConfigurationFromTemplate<JadxScriptTemplate>()
return ScriptEvaluationConfiguration(baseEvalConf) {
constructorArgs(scriptData)
}
}
}