diff --git a/jadx-plugins/jadx-java-input/build.gradle b/jadx-plugins/jadx-java-input/build.gradle index 27d0872f1..0ac2fef9e 100644 --- a/jadx-plugins/jadx-java-input/build.gradle +++ b/jadx-plugins/jadx-java-input/build.gradle @@ -7,4 +7,6 @@ dependencies { // show bytecode disassemble implementation 'io.github.skylot:raung-disasm:0.0.3' + + testImplementation(project(":jadx-core")) } diff --git a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaFileLoader.java b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputLoader.java similarity index 90% rename from jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaFileLoader.java rename to jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputLoader.java index 073b079e6..bd7b22efb 100644 --- a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaFileLoader.java +++ b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputLoader.java @@ -19,8 +19,8 @@ import org.slf4j.LoggerFactory; import jadx.api.plugins.utils.CommonFileUtils; import jadx.api.plugins.utils.ZipSecurity; -public class JavaFileLoader { - private static final Logger LOG = LoggerFactory.getLogger(JavaFileLoader.class); +public class JavaInputLoader { + private static final Logger LOG = LoggerFactory.getLogger(JavaInputLoader.class); private static final int MAX_MAGIC_SIZE = 4; private static final byte[] JAVA_CLASS_FILE_MAGIC = { (byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE }; @@ -37,6 +37,14 @@ public class JavaFileLoader { .collect(Collectors.toList()); } + public List loadInputStream(InputStream in, String name) throws IOException { + return loadReader(in, name, null, null); + } + + public JavaClassReader loadClass(byte[] content, String fileName) { + return new JavaClassReader(getNextUniqId(), fileName, content); + } + private List loadFromFile(File file) { try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) { return loadReader(inputStream, file.getName(), file, null); diff --git a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputPlugin.java b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputPlugin.java index 7cc44297e..57dc8a159 100644 --- a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputPlugin.java +++ b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputPlugin.java @@ -1,8 +1,11 @@ package jadx.plugins.input.java; import java.io.Closeable; +import java.io.InputStream; import java.nio.file.Path; +import java.util.Collections; import java.util.List; +import java.util.function.Function; import org.jetbrains.annotations.Nullable; @@ -10,6 +13,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.java.utils.JavaClassParseException; public class JavaInputPlugin implements JadxInputPlugin { @@ -29,10 +33,51 @@ public class JavaInputPlugin implements JadxInputPlugin { } public static ILoadResult loadClassFiles(List inputFiles, @Nullable Closeable closeable) { - List readers = new JavaFileLoader().collectFiles(inputFiles); + List readers = new JavaInputLoader().collectFiles(inputFiles); if (readers.isEmpty()) { return EmptyLoadResult.INSTANCE; } return new JavaLoadResult(readers, closeable); } + + public static ILoadResult loadClassFiles(List inputFiles) { + return loadClassFiles(inputFiles, null); + } + + /** + * Method for provide several inputs by using load methods from {@link JavaInputLoader} class. + */ + public static ILoadResult load(Function> loader) { + return wrapClassReaders(loader.apply(new JavaInputLoader())); + } + + /** + * Convenient method for load class file or jar from input stream. + * Should be used only once per JadxDecompiler instance. + * For load several times use {@link JavaInputPlugin#load(Function)} method. + */ + public static ILoadResult loadFromInputStream(InputStream in, String fileName) { + try { + return wrapClassReaders(new JavaInputLoader().loadInputStream(in, fileName)); + } catch (Exception e) { + throw new JavaClassParseException("Failed to read input stream", e); + } + } + + /** + * Convenient method for load single class file by content. + * Should be used only once per JadxDecompiler instance. + * For load several times use {@link JavaInputPlugin#load(Function)} method. + */ + public static ILoadResult loadSingleClass(byte[] content, String fileName) { + JavaClassReader reader = new JavaInputLoader().loadClass(content, fileName); + return new JavaLoadResult(Collections.singletonList(reader)); + } + + public static ILoadResult wrapClassReaders(List readers) { + if (readers.isEmpty()) { + return EmptyLoadResult.INSTANCE; + } + return new JavaLoadResult(readers); + } } diff --git a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaLoadResult.java b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaLoadResult.java index e14cd7050..add098fc9 100644 --- a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaLoadResult.java +++ b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaLoadResult.java @@ -20,6 +20,10 @@ public class JavaLoadResult implements ILoadResult { @Nullable private final Closeable closeable; + public JavaLoadResult(List readers) { + this(readers, null); + } + public JavaLoadResult(List readers, @Nullable Closeable closeable) { this.readers = readers; this.closeable = closeable; @@ -47,7 +51,6 @@ public class JavaLoadResult implements ILoadResult { @Override public void close() throws IOException { - readers.clear(); if (closeable != null) { closeable.close(); } diff --git a/jadx-plugins/jadx-java-input/src/test/java/jadx/plugins/input/java/CustomLoadTest.java b/jadx-plugins/jadx-java-input/src/test/java/jadx/plugins/input/java/CustomLoadTest.java new file mode 100644 index 000000000..2069cf3c2 --- /dev/null +++ b/jadx-plugins/jadx-java-input/src/test/java/jadx/plugins/input/java/CustomLoadTest.java @@ -0,0 +1,129 @@ +package jadx.plugins.input.java; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jadx.api.JadxArgs; +import jadx.api.JadxDecompiler; +import jadx.api.plugins.input.data.ILoadResult; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +class CustomLoadTest { + + private JadxDecompiler jadx; + + @BeforeEach + void init() { + jadx = new JadxDecompiler(new JadxArgs()); + } + + @AfterEach + void close() { + jadx.close(); + } + + @Test + void loadFiles() { + List files = Stream.of("HelloWorld.class", "HelloWorld$HelloInner.class") + .map(this::getSample) + .collect(Collectors.toList()); + ILoadResult loadResult = JavaInputPlugin.loadClassFiles(files); + loadDecompiler(loadResult); + assertThat(jadx.getClassesWithInners()) + .hasSize(2) + .satisfiesOnlyOnce(cls -> assertThat(cls.getName()).isEqualTo("HelloWorld")) + .satisfiesOnlyOnce(cls -> assertThat(cls.getName()).isEqualTo("HelloInner")); + } + + @Test + void loadFromInputStream() throws IOException { + String fileName = "HelloWorld$HelloInner.class"; + try (InputStream in = Files.newInputStream(getSample(fileName))) { + ILoadResult loadResult = JavaInputPlugin.loadFromInputStream(in, fileName); + loadDecompiler(loadResult); + assertThat(jadx.getClassesWithInners()) + .hasSize(1) + .satisfiesOnlyOnce(cls -> assertThat(cls.getName()).isEqualTo("HelloWorld$HelloInner")); + + System.out.println(jadx.getClassesWithInners().get(0).getCode()); + } + } + + @Test + void loadSingleClass() throws IOException { + String fileName = "HelloWorld.class"; + byte[] content = Files.readAllBytes(getSample(fileName)); + ILoadResult loadResult = JavaInputPlugin.loadSingleClass(content, fileName); + loadDecompiler(loadResult); + assertThat(jadx.getClassesWithInners()) + .hasSize(1) + .satisfiesOnlyOnce(cls -> assertThat(cls.getName()).isEqualTo("HelloWorld")); + + System.out.println(jadx.getClassesWithInners().get(0).getCode()); + } + + @Test + void load() { + ILoadResult loadResult = JavaInputPlugin.load(loader -> { + List inputs = new ArrayList<>(2); + try { + String hello = "HelloWorld.class"; + byte[] content = Files.readAllBytes(getSample(hello)); + inputs.add(loader.loadClass(content, hello)); + + String helloInner = "HelloWorld$HelloInner.class"; + InputStream in = Files.newInputStream(getSample(helloInner)); + inputs.addAll(loader.loadInputStream(in, helloInner)); + } catch (Exception e) { + fail(e); + } + return inputs; + }); + loadDecompiler(loadResult); + assertThat(jadx.getClassesWithInners()) + .hasSize(2) + .satisfiesOnlyOnce(cls -> assertThat(cls.getName()).isEqualTo("HelloWorld")) + .satisfiesOnlyOnce(cls -> { + assertThat(cls.getName()).isEqualTo("HelloInner"); + assertThat(cls.getCode()).isEqualTo(""); // no code for moved inner class + }); + + assertThat(jadx.getClasses()) + .hasSize(1) + .satisfiesOnlyOnce(cls -> assertThat(cls.getName()).isEqualTo("HelloWorld")) + .satisfiesOnlyOnce(cls -> assertThat(cls.getInnerClasses()).hasSize(1) + .satisfiesOnlyOnce(inner -> assertThat(inner.getName()).isEqualTo("HelloInner"))); + + jadx.getClassesWithInners().forEach(cls -> System.out.println(cls.getCode())); + } + + public void loadDecompiler(ILoadResult load) { + try { + jadx.addCustomLoad(load); + jadx.load(); + } catch (Exception e) { + fail(e); + } + } + + public Path getSample(String name) { + try { + return Paths.get(ClassLoader.getSystemResource("samples/" + name).toURI()); + } catch (Exception e) { + return fail(e); + } + } +} diff --git a/jadx-plugins/jadx-java-input/src/test/resources/samples/HelloWorld$HelloInner.class b/jadx-plugins/jadx-java-input/src/test/resources/samples/HelloWorld$HelloInner.class new file mode 100644 index 000000000..cd60ebd24 Binary files /dev/null and b/jadx-plugins/jadx-java-input/src/test/resources/samples/HelloWorld$HelloInner.class differ diff --git a/jadx-plugins/jadx-java-input/src/test/resources/samples/HelloWorld.class b/jadx-plugins/jadx-java-input/src/test/resources/samples/HelloWorld.class new file mode 100644 index 000000000..d1d94f7aa Binary files /dev/null and b/jadx-plugins/jadx-java-input/src/test/resources/samples/HelloWorld.class differ