Merge pull request #401 from ThexXTURBOXx/master
Better approach to CFR decompilation
This commit is contained in:
commit
7b19597014
2 changed files with 204 additions and 184 deletions
6
pom.xml
6
pom.xml
|
@ -31,8 +31,8 @@
|
||||||
<darklaf.version>2.7.3</darklaf.version>
|
<darklaf.version>2.7.3</darklaf.version>
|
||||||
<darklaf-extensions-rsta.version>0.3.4</darklaf-extensions-rsta.version>
|
<darklaf-extensions-rsta.version>0.3.4</darklaf-extensions-rsta.version>
|
||||||
<decompiler-fernflower.version>5.2.1.Final</decompiler-fernflower.version>
|
<decompiler-fernflower.version>5.2.1.Final</decompiler-fernflower.version>
|
||||||
<dex2jar.version>v45</dex2jar.version>
|
<dex2jar.version>v46</dex2jar.version>
|
||||||
<fernflower.version>b803ad9</fernflower.version>
|
<fernflower.version>5a2b2cc</fernflower.version>
|
||||||
<gson.version>2.9.0</gson.version>
|
<gson.version>2.9.0</gson.version>
|
||||||
<guava.version>31.0.1-jre</guava.version>
|
<guava.version>31.0.1-jre</guava.version>
|
||||||
<imgscalr-lib.version>4.2</imgscalr-lib.version>
|
<imgscalr-lib.version>4.2</imgscalr-lib.version>
|
||||||
|
@ -44,7 +44,7 @@
|
||||||
<objenesis.version>3.2</objenesis.version>
|
<objenesis.version>3.2</objenesis.version>
|
||||||
<paged-data.version>0.2.0</paged-data.version>
|
<paged-data.version>0.2.0</paged-data.version>
|
||||||
<procyon.version>0.6.0</procyon.version>
|
<procyon.version>0.6.0</procyon.version>
|
||||||
<rsyntaxtextarea.version>3.1.6</rsyntaxtextarea.version>
|
<rsyntaxtextarea.version>3.2.0</rsyntaxtextarea.version>
|
||||||
<semantic-version.version>2.1.1</semantic-version.version>
|
<semantic-version.version>2.1.1</semantic-version.version>
|
||||||
<slf4j.version>1.7.36</slf4j.version>
|
<slf4j.version>1.7.36</slf4j.version>
|
||||||
<smali.version>2.5.2</smali.version>
|
<smali.version>2.5.2</smali.version>
|
||||||
|
|
|
@ -1,31 +1,46 @@
|
||||||
package the.bytecode.club.bytecodeviewer.decompilers.impl;
|
package the.bytecode.club.bytecodeviewer.decompilers.impl;
|
||||||
|
|
||||||
import com.strobel.core.StringUtilities;
|
import java.io.BufferedOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.jar.JarEntry;
|
||||||
|
import java.util.jar.JarFile;
|
||||||
|
import java.util.zip.ZipException;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
import org.benf.cfr.reader.api.CfrDriver;
|
import org.benf.cfr.reader.api.CfrDriver;
|
||||||
import org.benf.cfr.reader.api.ClassFileSource;
|
import org.benf.cfr.reader.api.ClassFileSource;
|
||||||
import org.benf.cfr.reader.api.OutputSinkFactory;
|
import org.benf.cfr.reader.api.OutputSinkFactory;
|
||||||
import org.benf.cfr.reader.api.SinkReturns;
|
import org.benf.cfr.reader.api.SinkReturns;
|
||||||
import org.benf.cfr.reader.bytecode.analysis.parse.utils.Pair;
|
import org.benf.cfr.reader.bytecode.analysis.parse.utils.Pair;
|
||||||
|
import org.benf.cfr.reader.state.ClassFileSourceImpl;
|
||||||
|
import org.benf.cfr.reader.util.getopt.Options;
|
||||||
|
import org.benf.cfr.reader.util.getopt.OptionsImpl;
|
||||||
import org.objectweb.asm.tree.ClassNode;
|
import org.objectweb.asm.tree.ClassNode;
|
||||||
import the.bytecode.club.bytecodeviewer.BytecodeViewer;
|
import the.bytecode.club.bytecodeviewer.BytecodeViewer;
|
||||||
import the.bytecode.club.bytecodeviewer.api.ASMUtil;
|
|
||||||
import the.bytecode.club.bytecodeviewer.api.ExceptionUI;
|
import the.bytecode.club.bytecodeviewer.api.ExceptionUI;
|
||||||
import the.bytecode.club.bytecodeviewer.decompilers.InternalDecompiler;
|
import the.bytecode.club.bytecodeviewer.decompilers.InternalDecompiler;
|
||||||
|
import the.bytecode.club.bytecodeviewer.resources.ResourceContainer;
|
||||||
import the.bytecode.club.bytecodeviewer.translation.TranslatedStrings;
|
import the.bytecode.club.bytecodeviewer.translation.TranslatedStrings;
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.jar.JarEntry;
|
|
||||||
import java.util.jar.JarFile;
|
|
||||||
import java.util.zip.ZipException;
|
|
||||||
import java.util.zip.ZipOutputStream;
|
|
||||||
|
|
||||||
import static the.bytecode.club.bytecodeviewer.Constants.nl;
|
import static the.bytecode.club.bytecodeviewer.Constants.nl;
|
||||||
import static the.bytecode.club.bytecodeviewer.translation.TranslatedStrings.CFR;
|
import static the.bytecode.club.bytecodeviewer.translation.TranslatedStrings.CFR;
|
||||||
import static the.bytecode.club.bytecodeviewer.translation.TranslatedStrings.ERROR;
|
import static the.bytecode.club.bytecodeviewer.translation.TranslatedStrings.ERROR;
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
* Bytecode Viewer (BCV) - Java & Android Reverse Engineering Suite *
|
* Bytecode Viewer (BCV) - Java & Android Reverse Engineering Suite *
|
||||||
* Copyright (C) 2014 Kalen 'Konloch' Kinloch - http://bytecodeviewer.com *
|
* Copyright (C) 2014 Kalen 'Konloch' Kinloch - http://bytecodeviewer.com *
|
||||||
|
@ -50,99 +65,49 @@ import static the.bytecode.club.bytecodeviewer.translation.TranslatedStrings.ERR
|
||||||
* @author GraxCode
|
* @author GraxCode
|
||||||
* Taken mostly out of Threadtear.
|
* Taken mostly out of Threadtear.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class CFRDecompiler extends InternalDecompiler {
|
public class CFRDecompiler extends InternalDecompiler {
|
||||||
|
|
||||||
private String result;
|
|
||||||
private static final String CLASS_SUFFIX = ".class";
|
private static final String CLASS_SUFFIX = ".class";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String decompileClassNode(ClassNode cn, byte[] bytes) {
|
public String decompileClassNode(ClassNode cn, byte[] content) {
|
||||||
String name = cn.name;
|
return decompile(cn, cn.name, content);
|
||||||
return decompile(bytes, name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String decompile(byte[] bytes, String name) {
|
private String decompile(ClassNode cn, String name, byte[] content) {
|
||||||
try {
|
try {
|
||||||
this.result = null;
|
String classPath = name + (name.endsWith(CLASS_SUFFIX) ? "" : CLASS_SUFFIX);
|
||||||
OutputSinkFactory mySink = new OutputSinkFactory() {
|
|
||||||
@Override
|
|
||||||
public List<SinkClass> getSupportedSinks(SinkType sinkType, Collection<SinkClass> collection) {
|
|
||||||
if (sinkType == SinkType.JAVA && collection.contains(SinkClass.DECOMPILED)) {
|
|
||||||
return Arrays.asList(SinkClass.DECOMPILED, SinkClass.STRING);
|
|
||||||
} else {
|
|
||||||
return Collections.singletonList(SinkClass.STRING);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
StringBuilder builder = new StringBuilder();
|
||||||
public <T> Sink<T> getSink(SinkType sinkType, SinkClass sinkClass) {
|
Consumer<SinkReturns.Decompiled> dumpDecompiled = d -> builder.append(d.getJava());
|
||||||
if (sinkType == SinkType.JAVA && sinkClass == SinkClass.DECOMPILED) {
|
|
||||||
return x -> result = ((SinkReturns.Decompiled) x).getJava();
|
|
||||||
}
|
|
||||||
return ignore -> {
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
ClassFileSource source = new ClassFileSource() {
|
|
||||||
@Override
|
|
||||||
public void informAnalysisRelativePathDetail(String a, String b) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
Options options = generateOptions();
|
||||||
public String getPossiblyRenamedPath(String path) {
|
ClassFileSource source = new BCVDataSource(options, cn, classPath, content);
|
||||||
return path;
|
CfrDriver driver = new CfrDriver.Builder()
|
||||||
}
|
.withClassFileSource(source)
|
||||||
|
.withBuiltOptions(options)
|
||||||
|
.withOutputSink(new BCVOutputSinkFactory(dumpDecompiled))
|
||||||
|
.build();
|
||||||
|
driver.analyse(Collections.singletonList(name));
|
||||||
|
|
||||||
@Override
|
return builder.toString();
|
||||||
public Pair<byte[], String> getClassFileContent(String path) throws IOException {
|
|
||||||
String clzName = path.substring(0, path.length() - 6);
|
|
||||||
if (clzName.equals(name)) {
|
|
||||||
return Pair.make(bytes, clzName);
|
|
||||||
}
|
|
||||||
URL url = CFRDecompiler.class.getResource("/" + path);
|
|
||||||
if (url != null) {
|
|
||||||
return Pair.make(IOUtils.toByteArray(url), path);
|
|
||||||
}
|
|
||||||
// don't load extra classes. we don't care about improper API usage.
|
|
||||||
ClassNode dummy = new ClassNode();
|
|
||||||
dummy.name = clzName;
|
|
||||||
dummy.version = 52;
|
|
||||||
return Pair.make(ASMUtil.nodeToBytes(dummy), clzName);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Collection<String> addJar(String arg0) {
|
|
||||||
throw new RuntimeException("This should not be called");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
CfrDriver cfrDriver = new CfrDriver.Builder().withClassFileSource(source).withOutputSink(mySink).withOptions(generateOptions()).build();
|
|
||||||
cfrDriver.analyse(Collections.singletonList(name));
|
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
t.printStackTrace();
|
t.printStackTrace();
|
||||||
StringWriter sw = new StringWriter();
|
StringWriter sw = new StringWriter();
|
||||||
PrintWriter pw = new PrintWriter(sw);
|
PrintWriter pw = new PrintWriter(sw);
|
||||||
t.printStackTrace(pw);
|
t.printStackTrace(pw);
|
||||||
return CFR + " " + ERROR + "! " + ExceptionUI.SEND_STACKTRACE_TO + nl + nl + TranslatedStrings.SUGGESTED_FIX_DECOMPILER_ERROR + nl + nl + sw;
|
return CFR + " " + ERROR + "! " + ExceptionUI.SEND_STACKTRACE_TO +
|
||||||
|
nl + nl + TranslatedStrings.SUGGESTED_FIX_DECOMPILER_ERROR +
|
||||||
|
nl + nl + sw;
|
||||||
}
|
}
|
||||||
if (result != null && !result.trim().isEmpty()) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "No CFR output received.";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void decompileToZip(String sourceJar, String zipName) {
|
public void decompileToZip(String sourceJar, String outJar) {
|
||||||
try {
|
try (JarFile jfile = new JarFile(new File(sourceJar));
|
||||||
doSaveJarDecompiled(new File(sourceJar), new File(zipName));
|
FileOutputStream dest = new FileOutputStream(outJar);
|
||||||
} catch (StackOverflowError | Exception e) {
|
BufferedOutputStream buffDest = new BufferedOutputStream(dest);
|
||||||
BytecodeViewer.handleException(e);
|
ZipOutputStream out = new ZipOutputStream(buffDest)) {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doSaveJarDecompiled(File inFile, File outFile) throws IOException {
|
|
||||||
try (JarFile jfile = new JarFile(inFile); FileOutputStream dest = new FileOutputStream(outFile); BufferedOutputStream buffDest = new BufferedOutputStream(dest); ZipOutputStream out = new ZipOutputStream(buffDest)) {
|
|
||||||
byte[] data = new byte[1024];
|
byte[] data = new byte[1024];
|
||||||
|
|
||||||
Enumeration<JarEntry> ent = jfile.entries();
|
Enumeration<JarEntry> ent = jfile.entries();
|
||||||
|
@ -154,8 +119,9 @@ public class CFRDecompiler extends InternalDecompiler {
|
||||||
if (history.add(etn)) {
|
if (history.add(etn)) {
|
||||||
out.putNextEntry(etn);
|
out.putNextEntry(etn);
|
||||||
try {
|
try {
|
||||||
String internalName = StringUtilities.removeRight(entry.getName(), CLASS_SUFFIX);
|
IOUtils.write(decompile(null, entry.getName(),
|
||||||
IOUtils.write(decompile(IOUtils.toByteArray(jfile.getInputStream(entry)), internalName), out, StandardCharsets.UTF_8);
|
IOUtils.toByteArray(jfile.getInputStream(entry))),
|
||||||
|
out, StandardCharsets.UTF_8);
|
||||||
} finally {
|
} finally {
|
||||||
out.closeEntry();
|
out.closeEntry();
|
||||||
}
|
}
|
||||||
|
@ -184,10 +150,12 @@ public class CFRDecompiler extends InternalDecompiler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (StackOverflowError | Exception e) {
|
||||||
|
BytecodeViewer.handleException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, String> generateOptions() {
|
public Options generateOptions() {
|
||||||
Map<String, String> options = new HashMap<>();
|
Map<String, String> options = new HashMap<>();
|
||||||
options.put("decodeenumswitch", String.valueOf(BytecodeViewer.viewer.decodeEnumSwitch.isSelected()));
|
options.put("decodeenumswitch", String.valueOf(BytecodeViewer.viewer.decodeEnumSwitch.isSelected()));
|
||||||
options.put("sugarenums", String.valueOf(BytecodeViewer.viewer.sugarEnums.isSelected()));
|
options.put("sugarenums", String.valueOf(BytecodeViewer.viewer.sugarEnums.isSelected()));
|
||||||
|
@ -196,7 +164,8 @@ public class CFRDecompiler extends InternalDecompiler {
|
||||||
options.put("collectioniter", String.valueOf(BytecodeViewer.viewer.collectioniter.isSelected()));
|
options.put("collectioniter", String.valueOf(BytecodeViewer.viewer.collectioniter.isSelected()));
|
||||||
options.put("innerclasses", String.valueOf(BytecodeViewer.viewer.innerClasses.isSelected()));
|
options.put("innerclasses", String.valueOf(BytecodeViewer.viewer.innerClasses.isSelected()));
|
||||||
options.put("removeboilerplate", String.valueOf(BytecodeViewer.viewer.removeBoilerPlate.isSelected()));
|
options.put("removeboilerplate", String.valueOf(BytecodeViewer.viewer.removeBoilerPlate.isSelected()));
|
||||||
options.put("removeinnerclasssynthetics", String.valueOf(BytecodeViewer.viewer.removeInnerClassSynthetics.isSelected()));
|
options.put("removeinnerclasssynthetics",
|
||||||
|
String.valueOf(BytecodeViewer.viewer.removeInnerClassSynthetics.isSelected()));
|
||||||
options.put("decodelambdas", String.valueOf(BytecodeViewer.viewer.decodeLambdas.isSelected()));
|
options.put("decodelambdas", String.valueOf(BytecodeViewer.viewer.decodeLambdas.isSelected()));
|
||||||
options.put("hidebridgemethods", String.valueOf(BytecodeViewer.viewer.hideBridgeMethods.isSelected()));
|
options.put("hidebridgemethods", String.valueOf(BytecodeViewer.viewer.hideBridgeMethods.isSelected()));
|
||||||
options.put("liftconstructorinit", String.valueOf(BytecodeViewer.viewer.liftConstructorInit.isSelected()));
|
options.put("liftconstructorinit", String.valueOf(BytecodeViewer.viewer.liftConstructorInit.isSelected()));
|
||||||
|
@ -229,6 +198,57 @@ public class CFRDecompiler extends InternalDecompiler {
|
||||||
options.put("recovertypehints", String.valueOf(BytecodeViewer.viewer.recoveryTypehInts.isSelected()));
|
options.put("recovertypehints", String.valueOf(BytecodeViewer.viewer.recoveryTypehInts.isSelected()));
|
||||||
options.put("forcereturningifs", String.valueOf(BytecodeViewer.viewer.forceTurningIFs.isSelected()));
|
options.put("forcereturningifs", String.valueOf(BytecodeViewer.viewer.forceTurningIFs.isSelected()));
|
||||||
options.put("forloopaggcapture", String.valueOf(BytecodeViewer.viewer.forLoopAGGCapture.isSelected()));
|
options.put("forloopaggcapture", String.valueOf(BytecodeViewer.viewer.forLoopAGGCapture.isSelected()));
|
||||||
return options;
|
return new OptionsImpl(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class BCVDataSource extends ClassFileSourceImpl {
|
||||||
|
|
||||||
|
private final ResourceContainer container;
|
||||||
|
private final String classFilePath;
|
||||||
|
private final byte[] content;
|
||||||
|
|
||||||
|
private BCVDataSource(Options options, ClassNode cn, String classFilePath, byte[] content) {
|
||||||
|
super(options);
|
||||||
|
this.container = BytecodeViewer.getResourceContainers().stream()
|
||||||
|
.filter(rc -> rc.resourceClasses.containsValue(cn))
|
||||||
|
.findFirst().orElse(null);
|
||||||
|
this.classFilePath = classFilePath;
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Pair<byte[], String> getClassFileContent(String classFilePath) throws IOException {
|
||||||
|
if (classFilePath.equals(this.classFilePath) && content != null) return Pair.make(content, classFilePath);
|
||||||
|
if (container == null) return super.getClassFileContent(classFilePath);
|
||||||
|
byte[] data = container.resourceClassBytes.get(classFilePath);
|
||||||
|
if (data == null) return super.getClassFileContent(classFilePath);
|
||||||
|
return Pair.make(data, classFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class BCVOutputSinkFactory implements OutputSinkFactory {
|
||||||
|
|
||||||
|
private final Consumer<SinkReturns.Decompiled> dumpDecompiled;
|
||||||
|
|
||||||
|
private BCVOutputSinkFactory(Consumer<SinkReturns.Decompiled> dumpDecompiled) {
|
||||||
|
this.dumpDecompiled = dumpDecompiled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SinkClass> getSupportedSinks(SinkType sinkType, Collection<SinkClass> available) {
|
||||||
|
return Collections.singletonList(SinkClass.DECOMPILED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> Sink<T> getSink(SinkType sinkType, SinkClass sinkClass) {
|
||||||
|
if (sinkType == SinkType.JAVA && sinkClass == SinkClass.DECOMPILED) {
|
||||||
|
return x -> dumpDecompiled.accept((SinkReturns.Decompiled) x);
|
||||||
|
}
|
||||||
|
return ignore -> {
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue