diff --git a/README.md b/README.md index bd2106189..e0a86267c 100644 --- a/README.md +++ b/README.md @@ -91,102 +91,110 @@ commands (use ' --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=): 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 diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java index e1bc03a1e..cc9024a28 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java @@ -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 = " (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .apkm, .jadx.kts)") - protected List files; + protected List 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 { + public ExportGradleTypeConverter() { + super(ExportGradleType::valueOf, ExportGradleType::values); + } + } + public static class LogLevelConverter extends BaseEnumConverter { public LogLevelConverter() { super(LogHelper.LogLevelEnum::valueOf, LogHelper.LogLevelEnum::values); diff --git a/jadx-cli/src/test/java/jadx/cli/BaseCliIntegrationTest.java b/jadx-cli/src/test/java/jadx/cli/BaseCliIntegrationTest.java new file mode 100644 index 000000000..d279dc28f --- /dev/null +++ b/jadx-cli/src/test/java/jadx/cli/BaseCliIntegrationTest.java @@ -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 options, String... inputSamples) { + List 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 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 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 collectJavaFilesInDir(Path dir) throws IOException { + PathMatcher javaMatcher = dir.getFileSystem().getPathMatcher("glob:**.java"); + return collectFilesInDir(dir, javaMatcher); + } + + static List collectAllFilesInDir(Path dir) throws IOException { + try (Stream pathStream = Files.walk(dir)) { + List files = pathStream + .filter(Files::isRegularFile) + .map(dir::relativize) + .collect(Collectors.toList()); + printFiles(files); + return files; + } + } + + static List collectFilesInDir(Path dir, PathMatcher matcher) throws IOException { + try (Stream pathStream = Files.walk(dir)) { + List files = pathStream + .filter(p -> Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS)) + .filter(matcher::matches) + .collect(Collectors.toList()); + printFiles(files); + return files; + } + } +} diff --git a/jadx-cli/src/test/java/jadx/cli/TestExport.java b/jadx-cli/src/test/java/jadx/cli/TestExport.java new file mode 100644 index 000000000..883015b1d --- /dev/null +++ b/jadx-cli/src/test/java/jadx/cli/TestExport.java @@ -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); + } +} diff --git a/jadx-cli/src/test/java/jadx/cli/TestInput.java b/jadx-cli/src/test/java/jadx/cli/TestInput.java index 11a4777ed..6b96c2081 100644 --- a/jadx-cli/src/test/java/jadx/cli/TestInput.java +++ b/jadx-cli/src/test/java/jadx/cli/TestInput.java @@ -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 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 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 options, String... inputSamples) throws URISyntaxException { - List 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 files) { - LOG.info("Output files (count: {}):", files.size()); - for (Path file : files) { - LOG.info(" {}", outputDir.relativize(file)); - } - } - - private static List collectJavaFilesInDir(Path dir) throws IOException { - PathMatcher javaMatcher = dir.getFileSystem().getPathMatcher("glob:**.java"); - return collectFilesInDir(dir, javaMatcher); - } - - private static List collectAllFilesInDir(Path dir) throws IOException { - try (Stream pathStream = Files.walk(dir)) { - return pathStream - .filter(Files::isRegularFile) - .collect(Collectors.toList()); - } - } - - private static List collectFilesInDir(Path dir, PathMatcher matcher) throws IOException { - try (Stream pathStream = Files.walk(dir)) { - return pathStream - .filter(p -> Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS)) - .filter(matcher::matches) - .collect(Collectors.toList()); - } - } } diff --git a/jadx-cli/src/test/resources/samples/test-lib.aar b/jadx-cli/src/test/resources/samples/test-lib.aar new file mode 100644 index 000000000..59b5a6bd8 Binary files /dev/null and b/jadx-cli/src/test/resources/samples/test-lib.aar differ diff --git a/jadx-core/src/main/java/jadx/api/JadxArgs.java b/jadx-core/src/main/java/jadx/api/JadxArgs.java index b3677516d..d8a87300d 100644 --- a/jadx-core/src/main/java/jadx/api/JadxArgs.java +++ b/jadx-core/src/main/java/jadx/api/JadxArgs.java @@ -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 diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index 3fef15d03..97d6d683d 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -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 inputFileNames = args.getInputFiles().stream() .map(File::getAbsolutePath) .collect(Collectors.toSet()); + Set codeSources = collectCodeSources(); + List 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 collectCodeSources() { + Set 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: ":<.jar>:" + // 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 classes = getClasses(); List processQueue = filterClasses(classes); diff --git a/jadx-core/src/main/java/jadx/api/ResourceFileContainer.java b/jadx-core/src/main/java/jadx/api/ResourceFileContainer.java new file mode 100644 index 000000000..da7932adb --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/ResourceFileContainer.java @@ -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; + } +} diff --git a/jadx-core/src/main/java/jadx/core/export/ExportGradle.java b/jadx-core/src/main/java/jadx/core/export/ExportGradle.java new file mode 100644 index 000000000..a1337d82c --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/export/ExportGradle.java @@ -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 resources; + private IExportGradleGenerator generator; + + public ExportGradle(RootNode root, File projectDir, List 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 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(); + } + +} diff --git a/jadx-core/src/main/java/jadx/core/export/ExportGradleProject.java b/jadx-core/src/main/java/jadx/core/export/ExportGradleProject.java deleted file mode 100644 index 62b1e4aea..000000000 --- a/jadx-core/src/main/java/jadx/core/export/ExportGradleProject.java +++ /dev/null @@ -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 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 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(); - } -} diff --git a/jadx-core/src/main/java/jadx/core/export/ExportGradleTask.java b/jadx-core/src/main/java/jadx/core/export/ExportGradleTask.java deleted file mode 100644 index 0409dc97c..000000000 --- a/jadx-core/src/main/java/jadx/core/export/ExportGradleTask.java +++ /dev/null @@ -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 resources; - - private final RootNode root; - private final File projectDir; - private final File srcOutDir; - private final File resOutDir; - - public ExportGradleTask(List 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 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; - } -} diff --git a/jadx-core/src/main/java/jadx/core/export/ExportGradleType.java b/jadx-core/src/main/java/jadx/core/export/ExportGradleType.java new file mode 100644 index 000000000..45e4d2863 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/export/ExportGradleType.java @@ -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; + } +} diff --git a/jadx-core/src/main/java/jadx/core/export/OutDirs.java b/jadx-core/src/main/java/jadx/core/export/OutDirs.java new file mode 100644 index 000000000..de8fbcaaf --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/export/OutDirs.java @@ -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); + } +} diff --git a/jadx-core/src/main/java/jadx/core/export/gen/AndroidGradleGenerator.java b/jadx-core/src/main/java/jadx/core/export/gen/AndroidGradleGenerator.java new file mode 100644 index 000000000..6a1c06082 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/export/gen/AndroidGradleGenerator.java @@ -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 resources; + private final boolean exportApp; + + private OutDirs outDirs; + private File baseDir; + private ApplicationParams applicationParams; + + public AndroidGradleGenerator(RootNode root, File projectDir, List 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 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 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 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(); + } +} diff --git a/jadx-core/src/main/java/jadx/core/export/gen/GradleGeneratorTools.java b/jadx-core/src/main/java/jadx/core/export/gen/GradleGeneratorTools.java new file mode 100644 index 000000000..3b17138e4 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/export/gen/GradleGeneratorTools.java @@ -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 inputFiles = root.getArgs().getInputFiles(); + if (inputFiles.size() == 1) { + return FileUtils.getPathBaseName(inputFiles.get(0).toPath()); + } + // default + return "PROJECT_NAME"; + } +} diff --git a/jadx-core/src/main/java/jadx/core/export/gen/IExportGradleGenerator.java b/jadx-core/src/main/java/jadx/core/export/gen/IExportGradleGenerator.java new file mode 100644 index 000000000..22df31d0d --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/export/gen/IExportGradleGenerator.java @@ -0,0 +1,12 @@ +package jadx.core.export.gen; + +import jadx.core.export.OutDirs; + +public interface IExportGradleGenerator { + + void init(); + + OutDirs getOutDirs(); + + void generateFiles(); +} diff --git a/jadx-core/src/main/java/jadx/core/export/gen/SimpleJavaGradleGenerator.java b/jadx-core/src/main/java/jadx/core/export/gen/SimpleJavaGradleGenerator.java new file mode 100644 index 000000000..d1343b341 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/export/gen/SimpleJavaGradleGenerator.java @@ -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 resources; + + private OutDirs outDirs; + private File appDir; + + public SimpleJavaGradleGenerator(RootNode root, File projectDir, List 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; + } +} diff --git a/jadx-core/src/main/java/jadx/core/plugins/files/JadxFilesData.java b/jadx-core/src/main/java/jadx/core/plugins/files/JadxFilesData.java index 8f688c821..6c51c7e1a 100644 --- a/jadx-core/src/main/java/jadx/core/plugins/files/JadxFilesData.java +++ b/jadx-core/src/main/java/jadx/core/plugins/files/JadxFilesData.java @@ -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; } } diff --git a/jadx-core/src/main/java/jadx/core/utils/android/AndroidManifestParser.java b/jadx-core/src/main/java/jadx/core/utils/android/AndroidManifestParser.java index caa1a59bf..13f7398be 100644 --- a/jadx-core/src/main/java/jadx/core/utils/android/AndroidManifestParser.java +++ b/jadx-core/src/main/java/jadx/core/utils/android/AndroidManifestParser.java @@ -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 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 parseAttrs, IJadxSecurity security) { this.parseAttrs = parseAttrs; this.security = Objects.requireNonNull(security); diff --git a/jadx-core/src/main/java/jadx/core/utils/android/ApplicationParams.java b/jadx-core/src/main/java/jadx/core/utils/android/ApplicationParams.java index 702a1a46c..d9b7c7037 100644 --- a/jadx-core/src/main/java/jadx/core/utils/android/ApplicationParams.java +++ b/jadx-core/src/main/java/jadx/core/utils/android/ApplicationParams.java @@ -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() { diff --git a/jadx-core/src/main/resources/export/app.build.gradle.tmpl b/jadx-core/src/main/resources/export/android/app.build.gradle.tmpl similarity index 97% rename from jadx-core/src/main/resources/export/app.build.gradle.tmpl rename to jadx-core/src/main/resources/export/android/app.build.gradle.tmpl index cd1ee4005..7e9f6d9b3 100644 --- a/jadx-core/src/main/resources/export/app.build.gradle.tmpl +++ b/jadx-core/src/main/resources/export/android/app.build.gradle.tmpl @@ -38,5 +38,5 @@ android { } dependencies { - // some dependencies + // TODO: dependencies } diff --git a/jadx-core/src/main/resources/export/build.gradle.tmpl b/jadx-core/src/main/resources/export/android/build.gradle.tmpl similarity index 94% rename from jadx-core/src/main/resources/export/build.gradle.tmpl rename to jadx-core/src/main/resources/export/android/build.gradle.tmpl index 34efe05b2..338aa18d5 100644 --- a/jadx-core/src/main/resources/export/build.gradle.tmpl +++ b/jadx-core/src/main/resources/export/android/build.gradle.tmpl @@ -1,6 +1,6 @@ buildscript { repositories { - google() + google() mavenCentral() } dependencies { diff --git a/jadx-core/src/main/resources/export/android/lib.build.gradle.tmpl b/jadx-core/src/main/resources/export/android/lib.build.gradle.tmpl new file mode 100644 index 000000000..e776563eb --- /dev/null +++ b/jadx-core/src/main/resources/export/android/lib.build.gradle.tmpl @@ -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 +} diff --git a/jadx-core/src/main/resources/export/android/settings.gradle.tmpl b/jadx-core/src/main/resources/export/android/settings.gradle.tmpl new file mode 100644 index 000000000..a1c672779 --- /dev/null +++ b/jadx-core/src/main/resources/export/android/settings.gradle.tmpl @@ -0,0 +1,3 @@ +rootProject.name = '{{projectName}}' + +include '{{mainModuleName}}' diff --git a/jadx-core/src/main/resources/export/java/build.gradle.kts.tmpl b/jadx-core/src/main/resources/export/java/build.gradle.kts.tmpl new file mode 100644 index 000000000..c62613e8d --- /dev/null +++ b/jadx-core/src/main/resources/export/java/build.gradle.kts.tmpl @@ -0,0 +1,12 @@ +plugins { + java +} + +repositories { + google() + mavenCentral() +} + +dependencies { + // some dependencies +} diff --git a/jadx-core/src/main/resources/export/java/settings.gradle.kts.tmpl b/jadx-core/src/main/resources/export/java/settings.gradle.kts.tmpl new file mode 100644 index 000000000..c6558d04e --- /dev/null +++ b/jadx-core/src/main/resources/export/java/settings.gradle.kts.tmpl @@ -0,0 +1,3 @@ +rootProject.name = "{{projectName}}" + +include("app") diff --git a/jadx-core/src/main/resources/export/settings.gradle.tmpl b/jadx-core/src/main/resources/export/settings.gradle.tmpl deleted file mode 100644 index 62d298d44..000000000 --- a/jadx-core/src/main/resources/export/settings.gradle.tmpl +++ /dev/null @@ -1,2 +0,0 @@ -include ':app' -rootProject.name = '{{applicationName}}' diff --git a/jadx-core/src/test/java/jadx/tests/api/ExportGradleTest.java b/jadx-core/src/test/java/jadx/tests/api/ExportGradleTest.java index 804f89b7f..d0f60dd90 100644 --- a/jadx-core/src/test/java/jadx/tests/api/ExportGradleTest.java +++ b/jadx-core/src/test/java/jadx/tests/api/ExportGradleTest.java @@ -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 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()); } } diff --git a/jadx-core/src/test/java/jadx/tests/export/TestNonFinalResIds.java b/jadx-core/src/test/java/jadx/tests/export/TestNonFinalResIds.java index e4727ab46..f895b7b33 100644 --- a/jadx-core/src/test/java/jadx/tests/export/TestNonFinalResIds.java +++ b/jadx-core/src/test/java/jadx/tests/export/TestNonFinalResIds.java @@ -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"); } } diff --git a/jadx-core/src/test/java/jadx/tests/functional/TemplateFileTest.java b/jadx-core/src/test/java/jadx/tests/functional/TemplateFileTest.java index 86a4bfb83..db84c0233 100644 --- a/jadx-core/src/test/java/jadx/tests/functional/TemplateFileTest.java +++ b/jadx-core/src/test/java/jadx/tests/functional/TemplateFileTest.java @@ -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); diff --git a/jadx-core/src/test/manifest/IllegalCharsForGradleWrapper.xml b/jadx-core/src/test/resources/manifest/IllegalCharsForGradleWrapper.xml similarity index 100% rename from jadx-core/src/test/manifest/IllegalCharsForGradleWrapper.xml rename to jadx-core/src/test/resources/manifest/IllegalCharsForGradleWrapper.xml diff --git a/jadx-core/src/test/manifest/MinSdkVersion25.xml b/jadx-core/src/test/resources/manifest/MinSdkVersion25.xml similarity index 100% rename from jadx-core/src/test/manifest/MinSdkVersion25.xml rename to jadx-core/src/test/resources/manifest/MinSdkVersion25.xml diff --git a/jadx-core/src/test/manifest/OptionalTargetSdkVersion.xml b/jadx-core/src/test/resources/manifest/OptionalTargetSdkVersion.xml similarity index 100% rename from jadx-core/src/test/manifest/OptionalTargetSdkVersion.xml rename to jadx-core/src/test/resources/manifest/OptionalTargetSdkVersion.xml diff --git a/jadx-core/src/test/manifest/strings.xml b/jadx-core/src/test/resources/manifest/strings.xml similarity index 100% rename from jadx-core/src/test/manifest/strings.xml rename to jadx-core/src/test/resources/manifest/strings.xml diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/ExportTask.java b/jadx-gui/src/main/java/jadx/gui/jobs/ExportTask.java index 7f3ccf588..1c2de331f 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/ExportTask.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/ExportTask.java @@ -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 diff --git a/jadx-gui/src/main/java/jadx/gui/tree/TreeExpansionService.java b/jadx-gui/src/main/java/jadx/gui/tree/TreeExpansionService.java index 029a4de8d..81b9c2170 100644 --- a/jadx-gui/src/main/java/jadx/gui/tree/TreeExpansionService.java +++ b/jadx-gui/src/main/java/jadx/gui/tree/TreeExpansionService.java @@ -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 PATH_LENGTH_REVERSE = Comparator.comparingInt(p -> -p.getPathCount()); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index 4092df12f..462c52c8c 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -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() { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommonDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommonDialog.java index 8de380c15..0a9b8dff5 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommonDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommonDialog.java @@ -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)) { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/ExportProjectDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/export/ExportProjectDialog.java similarity index 54% rename from jadx-gui/src/main/java/jadx/gui/ui/dialog/ExportProjectDialog.java rename to jadx-gui/src/main/java/jadx/gui/ui/export/ExportProjectDialog.java index 0ca93b647..0fe302b86 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/dialog/ExportProjectDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/export/ExportProjectDialog.java @@ -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 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 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; + } + } } diff --git a/jadx-gui/src/main/java/jadx/gui/settings/ExportProjectProperties.java b/jadx-gui/src/main/java/jadx/gui/ui/export/ExportProjectProperties.java similarity index 53% rename from jadx-gui/src/main/java/jadx/gui/settings/ExportProjectProperties.java rename to jadx-gui/src/main/java/jadx/gui/ui/export/ExportProjectProperties.java index 0f9e5825b..0cd0e3d43 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/ExportProjectProperties.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/export/ExportProjectProperties.java @@ -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 + + '}'; + } } diff --git a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties index 1d2b46373..4a36c35e5 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -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: diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index 117b71233..c61f55016 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -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: diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties index 338ec6c66..0a73e53a5 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -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: diff --git a/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties b/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties index 352b917d8..9630db315 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties @@ -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: diff --git a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties index f3be41889..f93d17ec3 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -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=로그 레벨: diff --git a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties index 673e63e30..6ef629678 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties @@ -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: diff --git a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties index 220a6b00a..d2f337502 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties @@ -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=Уровень лога: diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties index 1ce6591a7..5fbc78ee8 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -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=日志等级: diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties index 1ff2815ec..b5e2d9599 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties @@ -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=記錄層級: diff --git a/jadx-plugins/jadx-script/jadx-script-ide/src/main/kotlin/ScriptServices.kt b/jadx-plugins/jadx-script/jadx-script-ide/src/main/kotlin/ScriptServices.kt index 9dcc6f720..aef3ef8ae 100644 --- a/jadx-plugins/jadx-script/jadx-script-ide/src/main/kotlin/ScriptServices.kt +++ b/jadx-plugins/jadx-script/jadx-script-ide/src/main/kotlin/ScriptServices.kt @@ -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, diff --git a/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/JadxScriptPlugin.kt b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/JadxScriptPlugin.kt index dd6a46708..13e2c3511 100644 --- a/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/JadxScriptPlugin.kt +++ b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/JadxScriptPlugin.kt @@ -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) } } } } diff --git a/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/ScriptCache.kt b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/ScriptCache.kt index bcc402e80..ac108f680 100644 --- a/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/ScriptCache.kt +++ b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/ScriptCache.kt @@ -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 } diff --git a/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/ScriptEval.kt b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/ScriptEval.kt index 8a6879093..1f6c1b7f5 100644 --- a/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/ScriptEval.kt +++ b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/ScriptEval.kt @@ -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() - - private val baseEvalConf = createJvmEvaluationConfigurationFromTemplate() - - private fun buildEvalConf(scriptData: JadxScriptData) = - ScriptEvaluationConfiguration(baseEvalConf) { - constructorArgs(scriptData) - } - } - - fun process(init: JadxPluginContext, scriptOptions: JadxScriptAllOptions): List { - val jadx = init.decompiler + fun process(context: JadxPluginContext, scriptOptions: JadxScriptAllOptions): List { + 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() 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 { - 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() + + fun buildEvalConf(scriptData: JadxScriptData): ScriptEvaluationConfiguration { + val baseEvalConf = createJvmEvaluationConfigurationFromTemplate() + return ScriptEvaluationConfiguration(baseEvalConf) { + constructorArgs(scriptData) + } + } }