From b2f41e95bfce73936b4d316917c95977daf6e1a4 Mon Sep 17 00:00:00 2001 From: Skylot Date: Sun, 27 Mar 2016 15:28:06 +0300 Subject: [PATCH] core: export as android gradle project --- README.md | 1 + .../src/main/java/jadx/cli/JadxCLIArgs.java | 8 + .../src/main/java/jadx/api/IJadxArgs.java | 5 + .../src/main/java/jadx/api/JadxArgs.java | 10 ++ .../main/java/jadx/api/JadxDecompiler.java | 51 ++++-- .../jadx/core/export/ExportGradleProject.java | 85 +++++++++ .../java/jadx/core/export/TemplateFile.java | 162 ++++++++++++++++++ .../main/resources/export/build.gradle.tmpl | 35 ++++ .../tests/functional/TemplateFileTest.java | 24 +++ .../java/jadx/gui/settings/JadxSettings.java | 4 + .../src/main/java/jadx/gui/ui/MainWindow.java | 22 ++- .../resources/i18n/Messages_en_US.properties | 1 + .../main/resources/icons-16/database_save.png | Bin 0 -> 755 bytes jadx-test-app/build.gradle | 33 ++-- 14 files changed, 412 insertions(+), 29 deletions(-) create mode 100644 jadx-core/src/main/java/jadx/core/export/ExportGradleProject.java create mode 100644 jadx-core/src/main/java/jadx/core/export/TemplateFile.java create mode 100644 jadx-core/src/main/resources/export/build.gradle.tmpl create mode 100644 jadx-core/src/test/java/jadx/tests/functional/TemplateFileTest.java create mode 100644 jadx-gui/src/main/resources/icons-16/database_save.png diff --git a/README.md b/README.md index 99b8aa46a..3170e13fa 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java index 632adb33a..8d70658fe 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java @@ -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; + } } diff --git a/jadx-core/src/main/java/jadx/api/IJadxArgs.java b/jadx-core/src/main/java/jadx/api/IJadxArgs.java index 212ae6199..7b7111f56 100644 --- a/jadx-core/src/main/java/jadx/api/IJadxArgs.java +++ b/jadx-core/src/main/java/jadx/api/IJadxArgs.java @@ -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(); } diff --git a/jadx-core/src/main/java/jadx/api/JadxArgs.java b/jadx-core/src/main/java/jadx/api/JadxArgs.java index 81c3746c4..804b3bf49 100644 --- a/jadx-core/src/main/java/jadx/api/JadxArgs.java +++ b/jadx-core/src/main/java/jadx/api/JadxArgs.java @@ -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; + } } diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index cbab76925..5382b02e0 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -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 getClasses() { if (root == null) { return Collections.emptyList(); diff --git a/jadx-core/src/main/java/jadx/core/export/ExportGradleProject.java b/jadx-core/src/main/java/jadx/core/export/ExportGradleProject.java new file mode 100644 index 000000000..9f912f681 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/export/ExportGradleProject.java @@ -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 IGNORE_CLS_NAMES = new HashSet(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 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; + } +} diff --git a/jadx-core/src/main/java/jadx/core/export/TemplateFile.java b/jadx-core/src/main/java/jadx/core/export/TemplateFile.java new file mode 100644 index 000000000..ce476142e --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/export/TemplateFile.java @@ -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 values = new HashMap(); + + 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; + } +} diff --git a/jadx-core/src/main/resources/export/build.gradle.tmpl b/jadx-core/src/main/resources/export/build.gradle.tmpl new file mode 100644 index 000000000..483ab5804 --- /dev/null +++ b/jadx-core/src/main/resources/export/build.gradle.tmpl @@ -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 +} diff --git a/jadx-core/src/test/java/jadx/tests/functional/TemplateFileTest.java b/jadx-core/src/test/java/jadx/tests/functional/TemplateFileTest.java new file mode 100644 index 000000000..eb980c1cd --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/functional/TemplateFileTest.java @@ -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")); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java index 02c7e90de..13c864e2f 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java @@ -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; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index c13644d84..e4a6f8912 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -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); diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index 7401a46d1..ac2387c0c 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -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 diff --git a/jadx-gui/src/main/resources/icons-16/database_save.png b/jadx-gui/src/main/resources/icons-16/database_save.png new file mode 100644 index 0000000000000000000000000000000000000000..44c06dddf19fbda14efe428b9b1793c13f46b2cf GIT binary patch literal 755 zcmV3^_07cLZBR}_>&jXObH zw2it@svr%qE?kJ(Xuudu+DSW|WWK!jNvbU^UO02#+Tt zYOko4%Vx8c4Gh!M(=Qem7g;XcE?n0Qi^XD?&*vX7@xPFCIh;%;@xMr?(;$(vo9j9i z6;riZMJyIWG#Z6r7^-I5HtO{{DwPWQ`}>&y+Y;!yjz*&a$8prX=XtO!3$0d5J>%Mz z1f8>Jnx-7^X2#7Yb#zC2VYfZ>c17@L{s)8{OuWBa3WHFfVXfhLv2t?V0V~q5R2D*D z&315l_#iF}b>Zoo?-;+7*`WOJWsMw(x3WXv`@U*s@Y-&edFEYpz0skP)dFfu zZ4wIp&Vbb!+|0+3Qa}p<*AH-eY>3q8s6?RA)zqP8W39IT5HLFG9m1F);gE|P`L7@@ zctjKsn1rA6!ZZR%R^(SjU!r=2o$yGp<$KViK~{B;AIcgvN+J+&Nvur+W(Sw&=H?z} zGMRW^U!Nl3AvWzQ3~C%Z*G*(?qLfNCq;tpg2yRW4@yl9;p3CK)O-@c8Sy))OUMiKc zQp#QYFZe-*@LZDInR^#F=Bm=!vA2i6tkEJ#i0aggzp2D%3!>h~r~3uLt(-IMoyFA