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