core: export as android gradle project
This commit is contained in:
@@ -46,6 +46,7 @@ options:
|
||||
-j, --threads-count - processing threads count
|
||||
-r, --no-res - do not decode resources
|
||||
-s, --no-src - do not decompile source code
|
||||
-e, --export-gradle - save as android gradle project
|
||||
--show-bad-code - show inconsistent code (incorrectly decompiled)
|
||||
--no-replace-consts - don't replace constant value with matching constant field
|
||||
--escape-unicode - escape non latin characters in strings (with \u)
|
||||
|
||||
@@ -40,6 +40,9 @@ public class JadxCLIArgs implements IJadxArgs {
|
||||
@Parameter(names = {"-s", "--no-src"}, description = "do not decompile source code")
|
||||
protected boolean skipSources = false;
|
||||
|
||||
@Parameter(names = {"-e", "--export-gradle"}, description = "save as android gradle project")
|
||||
protected boolean exportAsGradleProject = false;
|
||||
|
||||
@Parameter(names = {"--show-bad-code"}, description = "show inconsistent code (incorrectly decompiled)")
|
||||
protected boolean showInconsistentCode = false;
|
||||
|
||||
@@ -281,4 +284,9 @@ public class JadxCLIArgs implements IJadxArgs {
|
||||
public boolean isReplaceConsts() {
|
||||
return replaceConsts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExportAsGradleProject() {
|
||||
return exportAsGradleProject;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,4 +37,9 @@ public interface IJadxArgs {
|
||||
* Replace constant values with static final fields with same value
|
||||
*/
|
||||
boolean isReplaceConsts();
|
||||
|
||||
/**
|
||||
* Save as gradle project
|
||||
*/
|
||||
boolean isExportAsGradleProject();
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ public class JadxArgs implements IJadxArgs {
|
||||
|
||||
private boolean escapeUnicode = false;
|
||||
private boolean replaceConsts = true;
|
||||
private boolean exportAsGradleProject = false;
|
||||
|
||||
@Override
|
||||
public File getOutDir() {
|
||||
@@ -170,4 +171,13 @@ public class JadxArgs implements IJadxArgs {
|
||||
public void setReplaceConsts(boolean replaceConsts) {
|
||||
this.replaceConsts = replaceConsts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExportAsGradleProject() {
|
||||
return exportAsGradleProject;
|
||||
}
|
||||
|
||||
public void setExportAsGradleProject(boolean exportAsGradleProject) {
|
||||
this.exportAsGradleProject = exportAsGradleProject;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@ package jadx.api;
|
||||
import jadx.core.Jadx;
|
||||
import jadx.core.ProcessClass;
|
||||
import jadx.core.codegen.CodeGen;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.FieldNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.dex.visitors.IDexTreeVisitor;
|
||||
import jadx.core.dex.visitors.SaveCode;
|
||||
import jadx.core.export.ExportGradleProject;
|
||||
import jadx.core.utils.exceptions.DecodeException;
|
||||
import jadx.core.utils.exceptions.JadxException;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
@@ -150,7 +152,7 @@ public final class JadxDecompiler {
|
||||
return getSaveExecutor(!args.isSkipSources(), !args.isSkipResources());
|
||||
}
|
||||
|
||||
private ExecutorService getSaveExecutor(boolean saveSources, final boolean saveResources) {
|
||||
private ExecutorService getSaveExecutor(boolean saveSources, boolean saveResources) {
|
||||
if (root == null) {
|
||||
throw new JadxRuntimeException("No loaded files");
|
||||
}
|
||||
@@ -159,25 +161,48 @@ public final class JadxDecompiler {
|
||||
|
||||
LOG.info("processing ...");
|
||||
ExecutorService executor = Executors.newFixedThreadPool(threadsCount);
|
||||
|
||||
File sourcesOutDir;
|
||||
File resOutDir;
|
||||
if (args.isExportAsGradleProject()) {
|
||||
ExportGradleProject export = new ExportGradleProject(root, outDir);
|
||||
export.init();
|
||||
sourcesOutDir = export.getSrcOutDir();
|
||||
resOutDir = export.getResOutDir();
|
||||
} else {
|
||||
sourcesOutDir = outDir;
|
||||
resOutDir = outDir;
|
||||
}
|
||||
if (saveSources) {
|
||||
for (final JavaClass cls : getClasses()) {
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
cls.decompile();
|
||||
SaveCode.save(outDir, args, cls.getClassNode());
|
||||
}
|
||||
});
|
||||
}
|
||||
appendSourcesSave(executor, sourcesOutDir);
|
||||
}
|
||||
if (saveResources) {
|
||||
for (final ResourceFile resourceFile : getResources()) {
|
||||
executor.execute(new ResourcesSaver(outDir, resourceFile));
|
||||
}
|
||||
appendResourcesSave(executor, resOutDir);
|
||||
}
|
||||
return executor;
|
||||
}
|
||||
|
||||
private void appendResourcesSave(ExecutorService executor, File outDir) {
|
||||
for (ResourceFile resourceFile : getResources()) {
|
||||
executor.execute(new ResourcesSaver(outDir, resourceFile));
|
||||
}
|
||||
}
|
||||
|
||||
private void appendSourcesSave(ExecutorService executor, final File outDir) {
|
||||
for (final JavaClass cls : getClasses()) {
|
||||
if (cls.getClassNode().contains(AFlag.DONT_GENERATE)) {
|
||||
continue;
|
||||
}
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
cls.decompile();
|
||||
SaveCode.save(outDir, args, cls.getClassNode());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public List<JavaClass> getClasses() {
|
||||
if (root == null) {
|
||||
return Collections.emptyList();
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package jadx.core.export;
|
||||
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.DexNode;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class ExportGradleProject {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ExportGradleProject.class);
|
||||
|
||||
private static final Set<String> IGNORE_CLS_NAMES = new HashSet<String>(Arrays.asList(
|
||||
"R",
|
||||
"BuildConfig"
|
||||
));
|
||||
|
||||
private final RootNode root;
|
||||
private final File outDir;
|
||||
private File srcOutDir;
|
||||
private File resOutDir;
|
||||
|
||||
public ExportGradleProject(RootNode root, File outDir) {
|
||||
this.root = root;
|
||||
this.outDir = outDir;
|
||||
this.srcOutDir = new File(outDir, "src/main/java");
|
||||
this.resOutDir = new File(outDir, "src/main");
|
||||
}
|
||||
|
||||
public void init() {
|
||||
try {
|
||||
FileUtils.makeDirsForFile(srcOutDir);
|
||||
FileUtils.makeDirsForFile(resOutDir);
|
||||
saveBuildGradle();
|
||||
skipGeneratedClasses();
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Gradle export failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveBuildGradle() throws IOException {
|
||||
TemplateFile tmpl = TemplateFile.fromResources("/export/build.gradle.tmpl");
|
||||
String appPackage = root.getAppPackage();
|
||||
if (appPackage == null) {
|
||||
appPackage = "UNKNOWN";
|
||||
}
|
||||
tmpl.add("applicationId", appPackage);
|
||||
// TODO: load from AndroidManifest.xml
|
||||
tmpl.add("minSdkVersion", 9);
|
||||
tmpl.add("targetSdkVersion", 21);
|
||||
tmpl.save(new File(outDir, "build.gradle"));
|
||||
}
|
||||
|
||||
private void skipGeneratedClasses() {
|
||||
for (DexNode dexNode : root.getDexNodes()) {
|
||||
List<ClassNode> classes = dexNode.getClasses();
|
||||
for (ClassNode cls : classes) {
|
||||
String shortName = cls.getClassInfo().getShortName();
|
||||
if (IGNORE_CLS_NAMES.contains(shortName)) {
|
||||
cls.add(AFlag.DONT_GENERATE);
|
||||
LOG.debug("Skip class: {}", cls);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public File getSrcOutDir() {
|
||||
return srcOutDir;
|
||||
}
|
||||
|
||||
public File getResOutDir() {
|
||||
return resOutDir;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package jadx.core.export;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import static jadx.core.utils.files.FileUtils.close;
|
||||
|
||||
/**
|
||||
* Simple template engine
|
||||
* Syntax for replace variable with value: '{{variable}}'
|
||||
*/
|
||||
public class TemplateFile {
|
||||
|
||||
private enum State {
|
||||
NONE, START, VARIABLE, END
|
||||
}
|
||||
|
||||
private static class ParserState {
|
||||
private State state = State.NONE;
|
||||
private StringBuilder curVariable;
|
||||
private boolean skip;
|
||||
}
|
||||
|
||||
private final String templateName;
|
||||
private final InputStream template;
|
||||
private final Map<String, String> values = new HashMap<String, String>();
|
||||
|
||||
public static TemplateFile fromResources(String path) throws FileNotFoundException {
|
||||
InputStream res = TemplateFile.class.getResourceAsStream(path);
|
||||
if (res == null) {
|
||||
throw new FileNotFoundException("Resource not found: " + path);
|
||||
}
|
||||
return new TemplateFile(path, res);
|
||||
}
|
||||
|
||||
private TemplateFile(String name, InputStream in) throws FileNotFoundException {
|
||||
this.templateName = name;
|
||||
this.template = in;
|
||||
}
|
||||
|
||||
public void add(String name, @NotNull Object value) {
|
||||
values.put(name, value.toString());
|
||||
}
|
||||
|
||||
public String build() throws IOException {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
try {
|
||||
process(out);
|
||||
} finally {
|
||||
close(out);
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
public void save(File outFile) throws IOException {
|
||||
OutputStream out = new FileOutputStream(outFile);
|
||||
try {
|
||||
process(out);
|
||||
} finally {
|
||||
close(out);
|
||||
}
|
||||
}
|
||||
|
||||
private void process(OutputStream out) throws IOException {
|
||||
if (template.available() == 0) {
|
||||
throw new IOException("Template already processed");
|
||||
}
|
||||
InputStream in = null;
|
||||
try {
|
||||
in = new BufferedInputStream(template);
|
||||
ParserState state = new ParserState();
|
||||
while (true) {
|
||||
int ch = in.read();
|
||||
if (ch == -1) {
|
||||
break;
|
||||
}
|
||||
String str = process(state, (char) ch);
|
||||
if (str != null) {
|
||||
out.write(str.getBytes());
|
||||
} else if (!state.skip) {
|
||||
out.write(ch);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
close(in);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String process(ParserState parser, char ch) {
|
||||
State state = parser.state;
|
||||
switch (ch) {
|
||||
case '{':
|
||||
switch (state) {
|
||||
case START:
|
||||
parser.state = State.VARIABLE;
|
||||
parser.curVariable = new StringBuilder();
|
||||
break;
|
||||
|
||||
default:
|
||||
parser.state = State.START;
|
||||
break;
|
||||
}
|
||||
parser.skip = true;
|
||||
return null;
|
||||
|
||||
case '}':
|
||||
switch (state) {
|
||||
case VARIABLE:
|
||||
parser.state = State.END;
|
||||
parser.skip = true;
|
||||
return null;
|
||||
|
||||
case END:
|
||||
parser.state = State.NONE;
|
||||
String varName = parser.curVariable.toString();
|
||||
parser.curVariable = new StringBuilder();
|
||||
return processVar(varName);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
switch (state) {
|
||||
case VARIABLE:
|
||||
parser.curVariable.append(ch);
|
||||
parser.skip = true;
|
||||
return null;
|
||||
|
||||
case START:
|
||||
parser.state = State.NONE;
|
||||
return "{" + ch;
|
||||
|
||||
case END:
|
||||
throw new RuntimeException("Expected variable end: '" + parser.curVariable
|
||||
+ "' (missing second '}')");
|
||||
}
|
||||
break;
|
||||
}
|
||||
parser.skip = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
private String processVar(String varName) {
|
||||
String str = values.get(varName);
|
||||
if (str == null) {
|
||||
throw new RuntimeException("Unknown variable: '" + varName
|
||||
+ "' in template: " + templateName);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:1.5.0'
|
||||
}
|
||||
}
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 23
|
||||
buildToolsVersion '23.0.1'
|
||||
|
||||
defaultConfig {
|
||||
applicationId '{{applicationId}}'
|
||||
minSdkVersion {{minSdkVersion}}
|
||||
targetSdkVersion {{targetSdkVersion}}
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// some dependencies
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package jadx.tests.functional;
|
||||
|
||||
import jadx.core.export.TemplateFile;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
public class TemplateFileTest {
|
||||
|
||||
@Test
|
||||
public void testBuildGradle() throws Exception {
|
||||
TemplateFile tmpl = TemplateFile.fromResources("/export/build.gradle.tmpl");
|
||||
tmpl.add("applicationId", "SOME_ID");
|
||||
tmpl.add("minSdkVersion", 1);
|
||||
tmpl.add("targetSdkVersion", 2);
|
||||
String res = tmpl.build();
|
||||
System.out.println(res);
|
||||
|
||||
assertThat(res, containsString("applicationId 'SOME_ID'"));
|
||||
assertThat(res, containsString("targetSdkVersion 2"));
|
||||
}
|
||||
}
|
||||
@@ -180,6 +180,10 @@ public class JadxSettings extends JadxCLIArgs {
|
||||
this.autoStartJobs = autoStartJobs;
|
||||
}
|
||||
|
||||
public void setExportAsGradleProject(boolean exportAsGradleProject) {
|
||||
this.exportAsGradleProject = exportAsGradleProject;
|
||||
}
|
||||
|
||||
public Font getFont() {
|
||||
if (fontStr.isEmpty()) {
|
||||
return DEFAULT_FONT;
|
||||
|
||||
@@ -89,6 +89,7 @@ public class MainWindow extends JFrame {
|
||||
|
||||
private static final ImageIcon ICON_OPEN = Utils.openIcon("folder");
|
||||
private static final ImageIcon ICON_SAVE_ALL = Utils.openIcon("disk_multiple");
|
||||
private static final ImageIcon ICON_EXPORT = Utils.openIcon("database_save");
|
||||
private static final ImageIcon ICON_CLOSE = Utils.openIcon("cross");
|
||||
private static final ImageIcon ICON_SYNC = Utils.openIcon("sync");
|
||||
private static final ImageIcon ICON_FLAT_PKG = Utils.openIcon("empty_logical_package_obj");
|
||||
@@ -232,7 +233,13 @@ public class MainWindow extends JFrame {
|
||||
}
|
||||
}
|
||||
|
||||
private void saveAll() {
|
||||
private void saveAll(boolean export) {
|
||||
settings.setExportAsGradleProject(export);
|
||||
if (export) {
|
||||
settings.setSkipSources(false);
|
||||
settings.setSkipResources(false);
|
||||
}
|
||||
|
||||
JFileChooser fileChooser = new JFileChooser();
|
||||
fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
|
||||
fileChooser.setToolTipText(NLS.str("file.save_all_msg"));
|
||||
@@ -351,12 +358,21 @@ public class MainWindow extends JFrame {
|
||||
Action saveAllAction = new AbstractAction(NLS.str("file.save_all"), ICON_SAVE_ALL) {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
saveAll();
|
||||
saveAll(false);
|
||||
}
|
||||
};
|
||||
saveAllAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.save_all"));
|
||||
saveAllAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_S, KeyEvent.CTRL_DOWN_MASK));
|
||||
|
||||
Action exportAction = new AbstractAction(NLS.str("file.export_gradle"), ICON_EXPORT) {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
saveAll(true);
|
||||
}
|
||||
};
|
||||
exportAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.export_gradle"));
|
||||
exportAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_E, KeyEvent.CTRL_DOWN_MASK));
|
||||
|
||||
JMenu recentFiles = new JMenu(NLS.str("menu.recent_files"));
|
||||
recentFiles.addMenuListener(new RecentFilesMenuListener(recentFiles));
|
||||
|
||||
@@ -467,6 +483,7 @@ public class MainWindow extends JFrame {
|
||||
file.setMnemonic(KeyEvent.VK_F);
|
||||
file.add(openAction);
|
||||
file.add(saveAllAction);
|
||||
file.add(exportAction);
|
||||
file.addSeparator();
|
||||
file.add(recentFiles);
|
||||
file.addSeparator();
|
||||
@@ -523,6 +540,7 @@ public class MainWindow extends JFrame {
|
||||
toolbar.setFloatable(false);
|
||||
toolbar.add(openAction);
|
||||
toolbar.add(saveAllAction);
|
||||
toolbar.add(exportAction);
|
||||
toolbar.addSeparator();
|
||||
toolbar.add(syncAction);
|
||||
toolbar.add(flatPkgButton);
|
||||
|
||||
@@ -16,6 +16,7 @@ menu.update_label=New version %s available!
|
||||
|
||||
file.open=Open file
|
||||
file.save_all=Save all
|
||||
file.export_gradle=Save as gradle project
|
||||
file.save_all_msg=Select directory for save decompiled sources
|
||||
file.select=Select
|
||||
file.exit=Exit
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 755 B |
+19
-14
@@ -1,11 +1,10 @@
|
||||
project.ext {
|
||||
ext {
|
||||
testAppDir = 'test-app'
|
||||
testAppTmpDir = 'test-app-tmp'
|
||||
|
||||
buildFile = "${testAppTmpDir}/build.gradle"
|
||||
tmpBuildFile = "${testAppTmpDir}/build.gradle"
|
||||
apkFile = "${testAppTmpDir}/build/outputs/apk/test-app-tmp-debug.apk"
|
||||
outSrcDir = "${testAppTmpDir}/src/main/java"
|
||||
outResDir = "${testAppTmpDir}/src/main"
|
||||
outCodeDir = "${testAppTmpDir}/src/main"
|
||||
checkTask = 'connectedCheck'
|
||||
}
|
||||
|
||||
@@ -32,24 +31,30 @@ task buildApp(type:Exec, dependsOn: copyApp) {
|
||||
}
|
||||
|
||||
task removeSource(type:Delete, dependsOn: buildApp) {
|
||||
delete "${outResDir}/**"
|
||||
delete outCodeDir
|
||||
}
|
||||
|
||||
task runJadxSrc(type: JavaExec, dependsOn: removeSource) {
|
||||
task runJadx(type: JavaExec, dependsOn: removeSource) {
|
||||
classpath = sourceSets.main.output + configurations.compile
|
||||
main = project(':jadx-cli').mainClassName
|
||||
args = ['-d', outSrcDir, '-r', apkFile, '-v']
|
||||
args = ['-d', testAppTmpDir, apkFile, '-v', '-e']
|
||||
}
|
||||
|
||||
task runJadxResources(type: JavaExec, dependsOn: runJadxSrc) {
|
||||
classpath = sourceSets.main.output + configurations.compile
|
||||
main = project(':jadx-cli').mainClassName
|
||||
args = ['-d', outResDir, '-s', apkFile, '-v']
|
||||
task decompile(dependsOn: runJadx) {
|
||||
doLast {
|
||||
injectDependencies()
|
||||
}
|
||||
}
|
||||
|
||||
task decompile(type:Delete, dependsOn: runJadxResources) {
|
||||
delete "${outSrcDir}/com/github/skylot/jadx/testapp/BuildConfig.java"
|
||||
delete "${outSrcDir}/com/github/skylot/jadx/testapp/R.java"
|
||||
def injectDependencies() {
|
||||
def fileContent = file(tmpBuildFile).getText('UTF-8')
|
||||
def updatedContent = fileContent.replaceAll(
|
||||
'// some dependencies',
|
||||
"""
|
||||
androidTestCompile 'junit:junit:4.12'
|
||||
androidTestCompile 'org.hamcrest:hamcrest-library:1.3'
|
||||
""")
|
||||
file(tmpBuildFile).write(updatedContent, 'UTF-8')
|
||||
}
|
||||
|
||||
task runChecks(type:Exec, dependsOn: decompile) {
|
||||
|
||||
Reference in New Issue
Block a user