diff --git a/.gitignore b/.gitignore index 9d20058a1c1d81824d0058c3988c1add5bd8f3c8..cf8723ec484ee02ad1b4ffbb55196b9e91d6e993 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ node_modules/ jadx-output/ *-tmp/ **/tmp/ +*.jobf *.class *.dump diff --git a/build.gradle b/build.gradle index 2133cbbb034c1adbe94fa0a57e32d566e03fa212..49bac002a8afbbdbd0cf65ed75b9fab3c023c890 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'org.sonarqube' version '3.0' - id 'com.github.ben-manes.versions' version '0.28.0' + id 'com.github.ben-manes.versions' version '0.29.0' id "com.diffplug.gradle.spotless" version "4.5.1" } diff --git a/jadx-cli/build.gradle b/jadx-cli/build.gradle index b528d2e238dc61f97810531be275a4771828057c..e0e77ee509550c744d6f3ad47522eff4afcec7e0 100644 --- a/jadx-cli/build.gradle +++ b/jadx-cli/build.gradle @@ -6,6 +6,7 @@ dependencies { implementation(project(':jadx-core')) runtimeOnly(project(':jadx-plugins:jadx-dex-input')) + runtimeOnly(project(':jadx-plugins:jadx-smali-input')) runtimeOnly(project(':jadx-plugins:jadx-java-convert')) implementation 'com.beust:jcommander:1.78' diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLI.java b/jadx-cli/src/main/java/jadx/cli/JadxCLI.java index ef876cc373eb9c26c2c0b218e23c73fd920ad712..7cec8c6cac215ef73d91d59f942950c85b4b81fb 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLI.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLI.java @@ -15,10 +15,7 @@ public class JadxCLI { public static void main(String[] args) { int result = 0; try { - JadxCLIArgs jadxArgs = new JadxCLIArgs(); - if (jadxArgs.processArgs(args)) { - result = processAndSave(jadxArgs.toJadxArgs()); - } + result = execute(args); } catch (JadxArgsValidateException e) { LOG.error("Incorrect arguments: {}", e.getMessage()); result = 1; @@ -31,7 +28,15 @@ public class JadxCLI { } } - static int processAndSave(JadxArgs jadxArgs) { + public static int execute(String[] args) { + JadxCLIArgs jadxArgs = new JadxCLIArgs(); + if (jadxArgs.processArgs(args)) { + return processAndSave(jadxArgs.toJadxArgs()); + } + return 0; + } + + private static int processAndSave(JadxArgs jadxArgs) { jadxArgs.setCodeCache(new NoOpCodeCache()); try (JadxDecompiler jadx = new JadxDecompiler(jadxArgs)) { jadx.load(); diff --git a/jadx-cli/src/test/java/jadx/cli/TestInput.java b/jadx-cli/src/test/java/jadx/cli/TestInput.java new file mode 100644 index 0000000000000000000000000000000000000000..3289dbd1b4e796060fd06234287b730b41994f7b --- /dev/null +++ b/jadx-cli/src/test/java/jadx/cli/TestInput.java @@ -0,0 +1,68 @@ +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.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.core.utils.files.FileUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestInput { + private static final Logger LOG = LoggerFactory.getLogger(TestInput.class); + + @Test + public void testDexInput() throws Exception { + decompile("dex", "samples/hello.dex"); + } + + @Test + public void testSmaliInput() throws Exception { + decompile("smali", "samples/HelloWorld.smali"); + } + + private void decompile(String tmpDirName, String inputSample) throws URISyntaxException, IOException { + StringBuilder args = new StringBuilder(); + Path tempDir = FileUtils.createTempDir(tmpDirName); + args.append("-v "); + args.append("-d ").append(tempDir.toAbsolutePath()).append(' '); + + URL resource = getClass().getClassLoader().getResource(inputSample); + assertThat(resource).isNotNull(); + String sampleFile = resource.toURI().getRawPath(); + args.append(sampleFile); + + int result = JadxCLI.execute(args.toString().split(" ")); + assertThat(result).isEqualTo(0); + List resultJavaFiles = collectJavaFilesInDir(tempDir); + assertThat(resultJavaFiles).isNotEmpty(); + } + + private static List collectJavaFilesInDir(Path dir) throws IOException { + PathMatcher matcher = dir.getFileSystem().getPathMatcher("glob:**.java"); + try (Stream pathStream = Files.walk(dir)) { + return pathStream + .filter(p -> Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS)) + .peek(f -> LOG.debug("File in result dir: {}", f)) + .filter(matcher::matches) + .collect(Collectors.toList()); + } + } + + @AfterAll + public static void cleanup() { + FileUtils.clearTempRootDir(); + } +} diff --git a/jadx-cli/src/test/resources/samples/HelloWorld.smali b/jadx-cli/src/test/resources/samples/HelloWorld.smali new file mode 100644 index 0000000000000000000000000000000000000000..924b46bc42af117bbaacb48666385917a070146e --- /dev/null +++ b/jadx-cli/src/test/resources/samples/HelloWorld.smali @@ -0,0 +1,26 @@ +.class LHelloWorld; +.super Ljava/lang/Object; +.source "HelloWorld.java" + +.method constructor ()V + .registers 1 + + .line 1 + invoke-direct {p0}, Ljava/lang/Object;->()V + + return-void +.end method + +.method public static main([Ljava/lang/String;)V + .registers 2 + + .line 3 + sget-object p0, Ljava/lang/System;->out:Ljava/io/PrintStream; + + const-string v0, "Hello, World" + + invoke-virtual {p0, v0}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V + + .line 4 + return-void +.end method diff --git a/jadx-cli/src/test/resources/samples/hello.dex b/jadx-cli/src/test/resources/samples/hello.dex new file mode 100644 index 0000000000000000000000000000000000000000..442644355310f39ff7eabe8f0c712785455a59b7 Binary files /dev/null and b/jadx-cli/src/test/resources/samples/hello.dex differ diff --git a/jadx-core/build.gradle b/jadx-core/build.gradle index 8b77bcb14e03027a514367e1664f7cb0e08e6cbc..b0d2698da56f8563fcb715be5856b27255ba70c2 100644 --- a/jadx-core/build.gradle +++ b/jadx-core/build.gradle @@ -19,6 +19,7 @@ dependencies { testImplementation 'org.apache.commons:commons-lang3:3.10' testRuntimeOnly(project(':jadx-plugins:jadx-dex-input')) + testRuntimeOnly(project(':jadx-plugins:jadx-smali-input')) testRuntimeOnly(project(':jadx-plugins:jadx-java-convert')) } diff --git a/jadx-core/src/main/java/jadx/api/JadxArgsValidator.java b/jadx-core/src/main/java/jadx/api/JadxArgsValidator.java index dab7678f0d49b25e87dc58e7c94954e61627cd0c..eac2c28fc96a88ceaee2ba844eb57c834181044b 100644 --- a/jadx-core/src/main/java/jadx/api/JadxArgsValidator.java +++ b/jadx-core/src/main/java/jadx/api/JadxArgsValidator.java @@ -27,14 +27,11 @@ public class JadxArgsValidator { if (inputFiles.isEmpty()) { throw new JadxArgsValidateException("Please specify input file"); } - if (inputFiles.size() > 1) { - for (File inputFile : inputFiles) { - String fileName = inputFile.getName(); - if (fileName.startsWith("--")) { - throw new JadxArgsValidateException("Unknown argument: " + fileName); - } + for (File inputFile : inputFiles) { + String fileName = inputFile.getName(); + if (fileName.startsWith("--")) { + throw new JadxArgsValidateException("Unknown argument: " + fileName); } - throw new JadxArgsValidateException("Only one input file supported"); } for (File file : inputFiles) { checkFile(file); diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index 2939f99563c727ffd4941fdc743226c708fb81b7..05b63121ce8fc3fde4c60a037986941d16cf6074 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -108,7 +108,10 @@ public final class JadxDecompiler implements Closeable { loadedInputs.clear(); List inputPaths = Utils.collectionMap(args.getInputFiles(), File::toPath); for (JadxInputPlugin inputPlugin : pluginManager.getInputPlugins()) { - loadedInputs.add(inputPlugin.loadFiles(inputPaths)); + ILoadResult loadResult = inputPlugin.loadFiles(inputPaths); + if (loadResult != null && !loadResult.isEmpty()) { + loadedInputs.add(loadResult); + } } } diff --git a/jadx-core/src/main/java/jadx/api/ResourcesLoader.java b/jadx-core/src/main/java/jadx/api/ResourcesLoader.java index 22a6293b0a106ca0677d450c91ab71be86acfe91..7ebe2e09dfe52e5c0c09a51651273b51eaec10e1 100644 --- a/jadx-core/src/main/java/jadx/api/ResourcesLoader.java +++ b/jadx-core/src/main/java/jadx/api/ResourcesLoader.java @@ -21,6 +21,7 @@ import jadx.core.codegen.CodeWriter; import jadx.core.utils.Utils; import jadx.core.utils.android.Res9patchStreamDecoder; import jadx.core.utils.exceptions.JadxException; +import jadx.core.utils.files.FileUtils; import jadx.core.utils.files.ZipSecurity; import jadx.core.xmlgen.ResContainer; import jadx.core.xmlgen.ResTableParser; @@ -127,16 +128,19 @@ public final class ResourcesLoader { if (file == null) { return; } - try (ZipFile zip = new ZipFile(file)) { - Enumeration entries = zip.entries(); - while (entries.hasMoreElements()) { - ZipEntry entry = entries.nextElement(); - if (ZipSecurity.isValidZipEntry(entry)) { - addEntry(list, file, entry); + if (FileUtils.isZipFile(file)) { + try (ZipFile zip = new ZipFile(file)) { + Enumeration entries = zip.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + if (ZipSecurity.isValidZipEntry(entry)) { + addEntry(list, file, entry); + } } + } catch (Exception e) { + LOG.warn("Failed to open zip file: {}", file.getAbsolutePath()); } - } catch (Exception e) { - LOG.debug("Not a zip file: {}", file.getAbsolutePath()); + } else { addResourceFile(list, file); } } diff --git a/jadx-core/src/main/java/jadx/core/clsp/ClsSet.java b/jadx-core/src/main/java/jadx/core/clsp/ClsSet.java index 240fcbac366ba88ce0c6739769f8889f7837246d..db98c74f1b0473ee9c1a74feb1bb747ac59ef3ed 100644 --- a/jadx-core/src/main/java/jadx/core/clsp/ClsSet.java +++ b/jadx-core/src/main/java/jadx/core/clsp/ClsSet.java @@ -87,7 +87,7 @@ public class ClsSet { if (LOG.isDebugEnabled()) { long time = System.currentTimeMillis() - startTime; int methodsCount = Stream.of(classes).mapToInt(clspClass -> clspClass.getMethodsMap().size()).sum(); - LOG.debug("Load class set in {}ms, classes: {}, methods: {}", time, classes.length, methodsCount); + LOG.debug("Clst file loaded in {}ms, classes: {}, methods: {}", time, classes.length, methodsCount); } } diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java index 028bbf23b11ab126257f4a60361c4a684eba5552..e4c451fa1533f9caf834f6db8c57816122d6406d 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java @@ -90,6 +90,7 @@ public class RootNode { // sort classes by name, expect top classes before inner classes.sort(Comparator.comparing(ClassNode::getFullName)); initInnerClasses(); + LOG.debug("Classes loaded: {}", classes.size()); } private void addDummyClass(IClassData classData, Exception exc) { diff --git a/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java b/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java index ae4c82a5797683df770cc0a72bf0339aab21023e..7a40139f2b29af831665c67e4456d8376679d08c 100644 --- a/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java @@ -222,7 +222,7 @@ public class FileUtils { return new String(hexChars); } - private static boolean isZipFile(File file) { + public static boolean isZipFile(File file) { try (InputStream is = new FileInputStream(file)) { byte[] headers = new byte[4]; int read = is.read(headers, 0, 4); diff --git a/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java b/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java index c27a1d6fd068c9eb09137bec526f869351c5ddd5..23becb7faa2174b490da483d42106180757cd9e0 100644 --- a/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java +++ b/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java @@ -104,7 +104,7 @@ public abstract class IntegrationTest extends TestUtils { DebugChecks.checksEnabled = true; } - private JadxDecompiler jadxDecompiler; + protected JadxDecompiler jadxDecompiler; @BeforeEach public void init() { @@ -142,7 +142,7 @@ public abstract class IntegrationTest extends TestUtils { public ClassNode getClassNode(Class clazz) { try { File jar = getJarForClass(clazz); - return getClassNodeFromFile(jar, clazz.getName()); + return getClassNodeFromFiles(Collections.singletonList(jar), clazz.getName()); } catch (Exception e) { e.printStackTrace(); fail(e.getMessage()); @@ -150,8 +150,8 @@ public abstract class IntegrationTest extends TestUtils { return null; } - public ClassNode getClassNodeFromFile(File file, String clsName) { - jadxDecompiler = loadFiles(Collections.singletonList(file)); + public ClassNode getClassNodeFromFiles(List files, String clsName) { + jadxDecompiler = loadFiles(files); RootNode root = JadxInternalAccess.getRoot(jadxDecompiler); ClassNode cls = root.resolveClass(clsName); @@ -173,9 +173,8 @@ public abstract class IntegrationTest extends TestUtils { } protected JadxDecompiler loadFiles(List inputFiles) { - JadxDecompiler d; args.setInputFiles(inputFiles); - d = new JadxDecompiler(args); + JadxDecompiler d = new JadxDecompiler(args); try { d.load(); } catch (Exception e) { diff --git a/jadx-core/src/test/java/jadx/tests/api/SmaliTest.java b/jadx-core/src/test/java/jadx/tests/api/SmaliTest.java index 2cdd5473be385a1a11fc2541815ec3065fe5721b..ea2d3781760fa00204a7e1411cfdd6de511fc6ae 100644 --- a/jadx-core/src/test/java/jadx/tests/api/SmaliTest.java +++ b/jadx-core/src/test/java/jadx/tests/api/SmaliTest.java @@ -7,10 +7,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.jetbrains.annotations.Nullable; -import org.jf.smali.Smali; -import org.jf.smali.SmaliOptions; -import jadx.api.JadxDecompiler; import jadx.api.JadxInternalAccess; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.RootNode; @@ -26,9 +23,7 @@ public abstract class SmaliTest extends IntegrationTest { protected ClassNode getClassNodeFromSmali(String file, String clsName) { File smaliFile = getSmaliFile(file); - File outDex = createTempFile(".dex"); - compileSmali(outDex, Collections.singletonList(smaliFile)); - return getClassNodeFromFile(outDex, clsName); + return getClassNodeFromFiles(Collections.singletonList(smaliFile), clsName); } /** @@ -51,9 +46,7 @@ public abstract class SmaliTest extends IntegrationTest { } protected ClassNode getClassNodeFromSmaliFiles(String pkg, String testName, String clsName) { - File outDex = createTempFile(".dex"); - compileSmali(outDex, collectSmaliFiles(pkg, testName)); - return getClassNodeFromFile(outDex, pkg + '.' + clsName); + return getClassNodeFromFiles(collectSmaliFiles(pkg, testName), pkg + '.' + clsName); } protected ClassNode getClassNodeFromSmaliFiles(String clsName) { @@ -61,13 +54,10 @@ public abstract class SmaliTest extends IntegrationTest { } protected List loadFromSmaliFiles() { - File outDex = createTempFile(".dex"); - compileSmali(outDex, collectSmaliFiles(getTestPkg(), getTestName())); - - JadxDecompiler d = loadFiles(Collections.singletonList(outDex)); - RootNode root = JadxInternalAccess.getRoot(d); + jadxDecompiler = loadFiles(collectSmaliFiles(getTestPkg(), getTestName())); + RootNode root = JadxInternalAccess.getRoot(jadxDecompiler); List classes = root.getClasses(false); - decompileAndCheck(d, classes); + decompileAndCheck(jadxDecompiler, classes); return classes; } @@ -97,17 +87,4 @@ public abstract class SmaliTest extends IntegrationTest { } throw new AssertionError("Smali file not found: " + smaliFile.getPath()); } - - private static boolean compileSmali(File output, List inputFiles) { - try { - SmaliOptions options = new SmaliOptions(); - options.outputDexFile = output.getAbsolutePath(); - options.verboseErrors = true; - List inputFileNames = inputFiles.stream().map(File::getAbsolutePath).collect(Collectors.toList()); - Smali.assemble(options, inputFileNames); - } catch (Exception e) { - throw new AssertionError("Smali assemble error", e); - } - return true; - } } diff --git a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexFileLoader.java b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexFileLoader.java index 72050f3705669ee6bfc10422f7e2f4e38b8c3a26..56fc240a3c9026818bc6dddca80754fc52e35bcc 100644 --- a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexFileLoader.java +++ b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexFileLoader.java @@ -26,7 +26,8 @@ public class DexFileLoader { public static List collectDexFiles(List pathsList) { return pathsList.stream() - .map((Path path) -> loadDexFromPath(path, 0)) + .map(path -> loadDexFromPath(path, 0)) + .filter(list -> !list.isEmpty()) .flatMap(Collection::stream) .peek(dr -> LOG.debug("Loading dex: {}", dr)) .collect(Collectors.toList()); diff --git a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputPlugin.java b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputPlugin.java index 049506827e1ab0cd5a1200f0efc7b9ebf422ef61..ab5e9a533e326f63d7e26191384546c41a9b8bed 100644 --- a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputPlugin.java +++ b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputPlugin.java @@ -1,11 +1,13 @@ package jadx.plugins.input.dex; +import java.io.Closeable; import java.nio.file.Path; import java.util.List; import jadx.api.plugins.JadxPluginInfo; import jadx.api.plugins.input.JadxInputPlugin; import jadx.api.plugins.input.data.ILoadResult; +import jadx.api.plugins.input.data.impl.EmptyLoadResult; public class DexInputPlugin implements JadxInputPlugin { @@ -16,6 +18,14 @@ public class DexInputPlugin implements JadxInputPlugin { @Override public ILoadResult loadFiles(List input) { - return new DexLoadResult(DexFileLoader.collectDexFiles(input)); + return loadDexFiles(input, null); + } + + public static ILoadResult loadDexFiles(List inputFiles, Closeable closeable) { + List dexReaders = DexFileLoader.collectDexFiles(inputFiles); + if (dexReaders.isEmpty()) { + return EmptyLoadResult.INSTANCE; + } + return new DexLoadResult(dexReaders, closeable); } } diff --git a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexLoadResult.java b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexLoadResult.java index 967b917377a8236e3938ac78e917d8ad6a66aaad..f0378a306aba38434a47080e7233dde86bf3f176 100644 --- a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexLoadResult.java +++ b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexLoadResult.java @@ -1,18 +1,28 @@ package jadx.plugins.input.dex; +import java.io.Closeable; import java.io.IOException; import java.util.List; import java.util.function.Consumer; +import org.jetbrains.annotations.Nullable; + import jadx.api.plugins.input.data.IClassData; import jadx.api.plugins.input.data.ILoadResult; import jadx.api.plugins.input.data.IResourceData; public class DexLoadResult implements ILoadResult { private final List dexReaders; + @Nullable + private final Closeable closeable; public DexLoadResult(List dexReaders) { + this(dexReaders, null); + } + + public DexLoadResult(List dexReaders, Closeable closeable) { this.dexReaders = dexReaders; + this.closeable = closeable; } @Override @@ -31,5 +41,13 @@ public class DexLoadResult implements ILoadResult { for (DexReader dexReader : dexReaders) { dexReader.close(); } + if (closeable != null) { + closeable.close(); + } + } + + @Override + public boolean isEmpty() { + return dexReaders.isEmpty(); } } diff --git a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/ConvertResult.java b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/ConvertResult.java index c050782d81b77f13a9b6730f5d7262b24b340448..e05895f3b66aaf2e5ccd6fc4f3c5e4a5663f6770 100644 --- a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/ConvertResult.java +++ b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/ConvertResult.java @@ -1,5 +1,6 @@ package jadx.plugins.input.javaconvert; +import java.io.Closeable; import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -12,7 +13,7 @@ import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class ConvertResult { +public class ConvertResult implements Closeable { private static final Logger LOG = LoggerFactory.getLogger(ConvertResult.class); private final List converted = new ArrayList<>(); @@ -34,7 +35,8 @@ public class ConvertResult { return converted.isEmpty(); } - public void deleteTemp() { + @Override + public void close() { for (Path tmpPath : tmpPaths) { try { delete(tmpPath); diff --git a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java index 8227a11ada55262e8f4ea051ad950c4e3d13e948..7933d60211db99b18e789301494603ffc0241477 100644 --- a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java +++ b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java @@ -1,6 +1,5 @@ package jadx.plugins.input.javaconvert; -import java.io.IOException; import java.nio.file.Path; import java.util.List; @@ -8,9 +7,7 @@ import jadx.api.plugins.JadxPluginInfo; import jadx.api.plugins.input.JadxInputPlugin; import jadx.api.plugins.input.data.ILoadResult; import jadx.api.plugins.input.data.impl.EmptyLoadResult; -import jadx.plugins.input.dex.DexFileLoader; -import jadx.plugins.input.dex.DexLoadResult; -import jadx.plugins.input.dex.DexReader; +import jadx.plugins.input.dex.DexInputPlugin; public class JavaConvertPlugin implements JadxInputPlugin { @@ -23,16 +20,9 @@ public class JavaConvertPlugin implements JadxInputPlugin { public ILoadResult loadFiles(List input) { ConvertResult result = JavaConvertLoader.process(input); if (result.isEmpty()) { - result.deleteTemp(); + result.close(); return EmptyLoadResult.INSTANCE; } - List dexReaders = DexFileLoader.collectDexFiles(result.getConverted()); - return new DexLoadResult(dexReaders) { - @Override - public void close() throws IOException { - super.close(); - result.deleteTemp(); - } - }; + return DexInputPlugin.loadDexFiles(result.getConverted(), result); } } diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/ILoadResult.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/ILoadResult.java index 932afa7c5975c00115dd84c089b726e9c0b23e71..d4fd16f39e7ab6470301f3456ef9db8f11fe9706 100644 --- a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/ILoadResult.java +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/ILoadResult.java @@ -7,4 +7,6 @@ public interface ILoadResult extends Closeable { void visitClasses(Consumer consumer); void visitResources(Consumer consumer); + + boolean isEmpty(); } diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/impl/EmptyLoadResult.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/impl/EmptyLoadResult.java index 94fb0e74f53744c9189c58cb0d892b32112e18a4..84d60cbc19107576bb0ca16b3cb2238dbecf2444 100644 --- a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/impl/EmptyLoadResult.java +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/impl/EmptyLoadResult.java @@ -11,6 +11,11 @@ public class EmptyLoadResult implements ILoadResult { public static final EmptyLoadResult INSTANCE = new EmptyLoadResult(); + @Override + public boolean isEmpty() { + return true; + } + @Override public void visitClasses(Consumer consumer) { } diff --git a/jadx-plugins/jadx-smali-input/build.gradle b/jadx-plugins/jadx-smali-input/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..70704906995d5889e23c48959e83f35587b49589 --- /dev/null +++ b/jadx-plugins/jadx-smali-input/build.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java-library' +} + +dependencies { + api(project(":jadx-plugins:jadx-plugins-api")) + + implementation(project(":jadx-plugins:jadx-dex-input")) + + implementation 'org.smali:smali:2.4.0' + implementation 'com.google.guava:guava:29.0-jre' // force latest version for smali +} diff --git a/jadx-plugins/jadx-smali-input/src/main/java/jadx/plugins/input/smali/SmaliConvert.java b/jadx-plugins/jadx-smali-input/src/main/java/jadx/plugins/input/smali/SmaliConvert.java new file mode 100644 index 0000000000000000000000000000000000000000..ccaa7747a416df67b841dca76e50896b07219487 --- /dev/null +++ b/jadx-plugins/jadx-smali-input/src/main/java/jadx/plugins/input/smali/SmaliConvert.java @@ -0,0 +1,107 @@ +package jadx.plugins.input.smali; + +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.stream.Collectors; + +import org.jetbrains.annotations.Nullable; +import org.jf.smali.Smali; +import org.jf.smali.SmaliOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SmaliConvert implements Closeable { + private static final Logger LOG = LoggerFactory.getLogger(SmaliConvert.class); + + @Nullable + private Path tmpDex; + + public boolean execute(List input) { + List smaliFiles = filterSmaliFiles(input); + if (smaliFiles.isEmpty()) { + return false; + } + try { + this.tmpDex = Files.createTempFile("jadx-", ".dex"); + boolean result = compileSmali(tmpDex, smaliFiles); + if (result) { + return true; + } + } catch (Exception e) { + LOG.error("Smali process error", e); + } + close(); + return false; + } + + private static boolean compileSmali(Path output, List inputFiles) throws IOException { + SmaliOptions options = new SmaliOptions(); + options.outputDexFile = output.toAbsolutePath().toString(); + options.verboseErrors = true; + + List inputFileNames = inputFiles.stream() + .map(p -> p.toAbsolutePath().toString()) + .distinct() + .collect(Collectors.toList()); + + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + boolean result = collectSystemErrors(out, () -> Smali.assemble(options, inputFileNames)); + if (!result) { + LOG.error("Smali compilation error:\n{}", out); + } + return result; + } + } + + private static boolean collectSystemErrors(OutputStream out, Callable exec) { + PrintStream systemErr = System.err; + try (PrintStream err = new PrintStream(out)) { + System.setErr(err); + try { + return exec.call(); + } catch (Exception e) { + e.printStackTrace(err); + return false; + } + } finally { + System.setErr(systemErr); + } + } + + private List filterSmaliFiles(List input) { + PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:**.smali"); + return input.stream() + .filter(p -> Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS)) + .filter(matcher::matches) + .collect(Collectors.toList()); + } + + public List getDexFiles() { + if (tmpDex == null) { + return Collections.emptyList(); + } + return Collections.singletonList(tmpDex); + } + + @Override + public void close() { + try { + if (tmpDex != null) { + Files.deleteIfExists(tmpDex); + } + } catch (Exception e) { + LOG.error("Failed to remove tmp dex file: {}", tmpDex, e); + } + } +} diff --git a/jadx-plugins/jadx-smali-input/src/main/java/jadx/plugins/input/smali/SmaliInputPlugin.java b/jadx-plugins/jadx-smali-input/src/main/java/jadx/plugins/input/smali/SmaliInputPlugin.java new file mode 100644 index 0000000000000000000000000000000000000000..bcd4332d063eea948c14011bd85ec6b4356e8fd9 --- /dev/null +++ b/jadx-plugins/jadx-smali-input/src/main/java/jadx/plugins/input/smali/SmaliInputPlugin.java @@ -0,0 +1,27 @@ +package jadx.plugins.input.smali; + +import java.nio.file.Path; +import java.util.List; + +import jadx.api.plugins.JadxPluginInfo; +import jadx.api.plugins.input.JadxInputPlugin; +import jadx.api.plugins.input.data.ILoadResult; +import jadx.api.plugins.input.data.impl.EmptyLoadResult; +import jadx.plugins.input.dex.DexInputPlugin; + +public class SmaliInputPlugin implements JadxInputPlugin { + + @Override + public JadxPluginInfo getPluginInfo() { + return new JadxPluginInfo("smali-input", "SmaliInput", "Load .smali files"); + } + + @Override + public ILoadResult loadFiles(List input) { + SmaliConvert convert = new SmaliConvert(); + if (!convert.execute(input)) { + return EmptyLoadResult.INSTANCE; + } + return DexInputPlugin.loadDexFiles(convert.getDexFiles(), convert); + } +} diff --git a/jadx-plugins/jadx-smali-input/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin b/jadx-plugins/jadx-smali-input/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin new file mode 100644 index 0000000000000000000000000000000000000000..51fd482d7bb01398632cacb5c22edc7769760828 --- /dev/null +++ b/jadx-plugins/jadx-smali-input/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin @@ -0,0 +1 @@ +jadx.plugins.input.smali.SmaliInputPlugin diff --git a/settings.gradle b/settings.gradle index 17e164d65261525762d8aa48eca385abf92f0538..96feead1bbc92c10c7e3fbe0e61de907c75d3e76 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,4 +7,5 @@ include 'jadx-samples' include 'jadx-plugins' include 'jadx-plugins:jadx-plugins-api' include 'jadx-plugins:jadx-dex-input' +include 'jadx-plugins:jadx-smali-input' include 'jadx-plugins:jadx-java-convert'