Skip to content

Commit

Permalink
feat(plugins): allow to load classes using input stream or byte array…
Browse files Browse the repository at this point in the history
… in jadx-input plugin (#1457)
  • Loading branch information
skylot committed Apr 10, 2023
1 parent 1ad6527 commit 4230cd5
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 4 deletions.
2 changes: 2 additions & 0 deletions jadx-plugins/jadx-java-input/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ dependencies {

// show bytecode disassemble
implementation 'io.github.skylot:raung-disasm:0.0.3'

testImplementation(project(":jadx-core"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
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 };
Expand All @@ -37,6 +37,14 @@ public List<JavaClassReader> collectFiles(List<Path> inputFiles) {
.collect(Collectors.toList());
}

public List<JavaClassReader> 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<JavaClassReader> loadFromFile(File file) {
try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
return loadReader(inputStream, file.getName(), file, null);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
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;

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 {

Expand All @@ -29,10 +33,51 @@ public ILoadResult loadFiles(List<Path> inputFiles) {
}

public static ILoadResult loadClassFiles(List<Path> inputFiles, @Nullable Closeable closeable) {
List<JavaClassReader> readers = new JavaFileLoader().collectFiles(inputFiles);
List<JavaClassReader> readers = new JavaInputLoader().collectFiles(inputFiles);
if (readers.isEmpty()) {
return EmptyLoadResult.INSTANCE;
}
return new JavaLoadResult(readers, closeable);
}

public static ILoadResult loadClassFiles(List<Path> inputFiles) {
return loadClassFiles(inputFiles, null);
}

/**
* Method for provide several inputs by using load methods from {@link JavaInputLoader} class.
*/
public static ILoadResult load(Function<JavaInputLoader, List<JavaClassReader>> 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<JavaClassReader> readers) {
if (readers.isEmpty()) {
return EmptyLoadResult.INSTANCE;
}
return new JavaLoadResult(readers);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ public class JavaLoadResult implements ILoadResult {
@Nullable
private final Closeable closeable;

public JavaLoadResult(List<JavaClassReader> readers) {
this(readers, null);
}

public JavaLoadResult(List<JavaClassReader> readers, @Nullable Closeable closeable) {
this.readers = readers;
this.closeable = closeable;
Expand Down Expand Up @@ -47,7 +51,6 @@ public boolean isEmpty() {

@Override
public void close() throws IOException {
readers.clear();
if (closeable != null) {
closeable.close();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Path> 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<JavaClassReader> 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);
}
}
}
Binary file not shown.
Binary file not shown.

0 comments on commit 4230cd5

Please sign in to comment.