提交 b2f41e95 编写于 作者: S Skylot

core: export as android gradle project

上级 e733c917
......@@ -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();
......
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;
}
}
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;
}
}
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
}
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
......
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) {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册