diff --git a/src/main/java/the/bytecode/club/bytecodeviewer/decompilers/impl/CFRDecompiler.java b/src/main/java/the/bytecode/club/bytecodeviewer/decompilers/impl/CFRDecompiler.java index 396fd0d0..86883766 100644 --- a/src/main/java/the/bytecode/club/bytecodeviewer/decompilers/impl/CFRDecompiler.java +++ b/src/main/java/the/bytecode/club/bytecodeviewer/decompilers/impl/CFRDecompiler.java @@ -1,31 +1,46 @@ 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.benf.cfr.reader.api.CfrDriver; import org.benf.cfr.reader.api.ClassFileSource; import org.benf.cfr.reader.api.OutputSinkFactory; import org.benf.cfr.reader.api.SinkReturns; 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 the.bytecode.club.bytecodeviewer.BytecodeViewer; -import the.bytecode.club.bytecodeviewer.api.ASMUtil; import the.bytecode.club.bytecodeviewer.api.ExceptionUI; import the.bytecode.club.bytecodeviewer.decompilers.InternalDecompiler; +import the.bytecode.club.bytecodeviewer.resources.ResourceContainer; 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.translation.TranslatedStrings.CFR; import static the.bytecode.club.bytecodeviewer.translation.TranslatedStrings.ERROR; + /*************************************************************************** * Bytecode Viewer (BCV) - Java & Android Reverse Engineering Suite * * Copyright (C) 2014 Kalen 'Konloch' Kinloch - http://bytecodeviewer.com * @@ -48,187 +63,192 @@ import static the.bytecode.club.bytecodeviewer.translation.TranslatedStrings.ERR * CFR Java Wrapper * * @author GraxCode - * Taken mostly out of Threadtear. + * Taken mostly out of Threadtear. */ - public class CFRDecompiler extends InternalDecompiler { - private String result; - private static final String CLASS_SUFFIX = ".class"; + private static final String CLASS_SUFFIX = ".class"; - @Override - public String decompileClassNode(ClassNode cn, byte[] bytes) { - String name = cn.name; - return decompile(bytes, name); - } + @Override + public String decompileClassNode(ClassNode cn, byte[] content) { + return decompile(cn, cn.name, content); + } + + private String decompile(ClassNode cn, String name, byte[] content) { + try { + String classPath = name + (name.endsWith(CLASS_SUFFIX) ? "" : CLASS_SUFFIX); + + StringBuilder builder = new StringBuilder(); + Consumer dumpDecompiled = d -> builder.append(d.getJava()); + + Options options = generateOptions(); + ClassFileSource source = new BCVDataSource(options, cn, classPath, content); + CfrDriver driver = new CfrDriver.Builder() + .withClassFileSource(source) + .withBuiltOptions(options) + .withOutputSink(new BCVOutputSinkFactory(dumpDecompiled)) + .build(); + driver.analyse(Collections.singletonList(name)); + + return builder.toString(); + } catch (Throwable t) { + t.printStackTrace(); + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + t.printStackTrace(pw); + return CFR + " " + ERROR + "! " + ExceptionUI.SEND_STACKTRACE_TO + + nl + nl + TranslatedStrings.SUGGESTED_FIX_DECOMPILER_ERROR + + nl + nl + sw; + } + } + + @Override + public void decompileToZip(String sourceJar, String outJar) { + try (JarFile jfile = new JarFile(new File(sourceJar)); + FileOutputStream dest = new FileOutputStream(outJar); + BufferedOutputStream buffDest = new BufferedOutputStream(dest); + ZipOutputStream out = new ZipOutputStream(buffDest)) { + byte[] data = new byte[1024]; + + Enumeration ent = jfile.entries(); + Set history = new HashSet<>(); + while (ent.hasMoreElements()) { + JarEntry entry = ent.nextElement(); + if (entry.getName().endsWith(CLASS_SUFFIX)) { + JarEntry etn = new JarEntry(entry.getName().replace(CLASS_SUFFIX, ".java")); + if (history.add(etn)) { + out.putNextEntry(etn); + try { + IOUtils.write(decompile(null, entry.getName(), + IOUtils.toByteArray(jfile.getInputStream(entry))), + out, StandardCharsets.UTF_8); + } finally { + out.closeEntry(); + } + } + } else { + try { + JarEntry etn = new JarEntry(entry.getName()); + if (history.add(etn)) continue; + history.add(etn); + out.putNextEntry(etn); + try (InputStream in = jfile.getInputStream(entry)) { + if (in != null) { + int count; + while ((count = in.read(data, 0, 1024)) != -1) { + out.write(data, 0, count); + } + } + } finally { + out.closeEntry(); + } + } catch (ZipException ze) { + // some jars contain duplicate pom.xml entries: ignore it + if (!ze.getMessage().contains("duplicate")) { + throw ze; + } + } + } + } + } catch (StackOverflowError | Exception e) { + BytecodeViewer.handleException(e); + } + } + + public Options generateOptions() { + Map options = new HashMap<>(); + options.put("decodeenumswitch", String.valueOf(BytecodeViewer.viewer.decodeEnumSwitch.isSelected())); + options.put("sugarenums", String.valueOf(BytecodeViewer.viewer.sugarEnums.isSelected())); + options.put("decodestringswitch", String.valueOf(BytecodeViewer.viewer.decodeStringSwitch.isSelected())); + options.put("arrayiter", String.valueOf(BytecodeViewer.viewer.arrayiter.isSelected())); + options.put("collectioniter", String.valueOf(BytecodeViewer.viewer.collectioniter.isSelected())); + options.put("innerclasses", String.valueOf(BytecodeViewer.viewer.innerClasses.isSelected())); + options.put("removeboilerplate", String.valueOf(BytecodeViewer.viewer.removeBoilerPlate.isSelected())); + options.put("removeinnerclasssynthetics", + String.valueOf(BytecodeViewer.viewer.removeInnerClassSynthetics.isSelected())); + options.put("decodelambdas", String.valueOf(BytecodeViewer.viewer.decodeLambdas.isSelected())); + options.put("hidebridgemethods", String.valueOf(BytecodeViewer.viewer.hideBridgeMethods.isSelected())); + options.put("liftconstructorinit", String.valueOf(BytecodeViewer.viewer.liftConstructorInit.isSelected())); + options.put("removebadgenerics", String.valueOf(BytecodeViewer.viewer.removeBadGenerics.isSelected())); + options.put("sugarasserts", String.valueOf(BytecodeViewer.viewer.sugarAsserts.isSelected())); + options.put("sugarboxing", String.valueOf(BytecodeViewer.viewer.sugarBoxing.isSelected())); + options.put("showversion", String.valueOf(BytecodeViewer.viewer.showVersion.isSelected())); + options.put("decodefinally", String.valueOf(BytecodeViewer.viewer.decodeFinally.isSelected())); + options.put("tidymonitors", String.valueOf(BytecodeViewer.viewer.tidyMonitors.isSelected())); + options.put("lenient", String.valueOf(BytecodeViewer.viewer.lenient.isSelected())); + options.put("dumpclasspath", String.valueOf(BytecodeViewer.viewer.dumpClassPath.isSelected())); + options.put("comments", String.valueOf(BytecodeViewer.viewer.comments.isSelected())); + options.put("forcetopsort", String.valueOf(BytecodeViewer.viewer.forceTopSort.isSelected())); + options.put("forcetopsortaggress", String.valueOf(BytecodeViewer.viewer.forceTopSortAggress.isSelected())); + options.put("stringbuffer", String.valueOf(BytecodeViewer.viewer.stringBuffer.isSelected())); + options.put("stringbuilder", String.valueOf(BytecodeViewer.viewer.stringBuilder.isSelected())); + options.put("silent", String.valueOf(BytecodeViewer.viewer.silent.isSelected())); + options.put("recover", String.valueOf(BytecodeViewer.viewer.recover.isSelected())); + options.put("eclipse", String.valueOf(BytecodeViewer.viewer.eclipse.isSelected())); + options.put("override", String.valueOf(BytecodeViewer.viewer.override.isSelected())); + options.put("showinferrable", String.valueOf(BytecodeViewer.viewer.showInferrable.isSelected())); + options.put("aexagg", String.valueOf(BytecodeViewer.viewer.aexagg.isSelected())); + options.put("hideutf", String.valueOf(BytecodeViewer.viewer.hideUTF.isSelected())); + options.put("hidelongstrings", String.valueOf(BytecodeViewer.viewer.hideLongStrings.isSelected())); + options.put("commentmonitors", String.valueOf(BytecodeViewer.viewer.commentMonitor.isSelected())); + options.put("allowcorrecting", String.valueOf(BytecodeViewer.viewer.allowCorrecting.isSelected())); + options.put("labelledblocks", String.valueOf(BytecodeViewer.viewer.labelledBlocks.isSelected())); + options.put("j14classobj", String.valueOf(BytecodeViewer.viewer.j14ClassOBJ.isSelected())); + options.put("hidelangimports", String.valueOf(BytecodeViewer.viewer.hideLangImports.isSelected())); + options.put("recovertypehints", String.valueOf(BytecodeViewer.viewer.recoveryTypehInts.isSelected())); + options.put("forcereturningifs", String.valueOf(BytecodeViewer.viewer.forceTurningIFs.isSelected())); + options.put("forloopaggcapture", String.valueOf(BytecodeViewer.viewer.forLoopAGGCapture.isSelected())); + 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; + } - private String decompile(byte[] bytes, String name) { - try { - this.result = null; - OutputSinkFactory mySink = new OutputSinkFactory() { @Override - public List getSupportedSinks(SinkType sinkType, Collection collection) { - if (sinkType == SinkType.JAVA && collection.contains(SinkClass.DECOMPILED)) { - return Arrays.asList(SinkClass.DECOMPILED, SinkClass.STRING); - } else { - return Collections.singletonList(SinkClass.STRING); - } + public Pair 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 dumpDecompiled; + + private BCVOutputSinkFactory(Consumer dumpDecompiled) { + this.dumpDecompiled = dumpDecompiled; + } + + @Override + public List getSupportedSinks(SinkType sinkType, Collection available) { + return Collections.singletonList(SinkClass.DECOMPILED); } @Override public Sink getSink(SinkType sinkType, SinkClass sinkClass) { - 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 - public String getPossiblyRenamedPath(String path) { - return path; - } - - @Override - public Pair 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 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) { - t.printStackTrace(); - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - t.printStackTrace(pw); - 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 - public void decompileToZip(String sourceJar, String zipName) { - try { - doSaveJarDecompiled(new File(sourceJar), new File(zipName)); - } catch (StackOverflowError | Exception e) { - BytecodeViewer.handleException(e); - } - } - - 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]; - - Enumeration ent = jfile.entries(); - Set history = new HashSet<>(); - while (ent.hasMoreElements()) { - JarEntry entry = ent.nextElement(); - if (entry.getName().endsWith(CLASS_SUFFIX)) { - JarEntry etn = new JarEntry(entry.getName().replace(CLASS_SUFFIX, ".java")); - if (history.add(etn)) { - out.putNextEntry(etn); - try { - String internalName = StringUtilities.removeRight(entry.getName(), CLASS_SUFFIX); - IOUtils.write(decompile(IOUtils.toByteArray(jfile.getInputStream(entry)), internalName), out, StandardCharsets.UTF_8); - } finally { - out.closeEntry(); + if (sinkType == SinkType.JAVA && sinkClass == SinkClass.DECOMPILED) { + return x -> dumpDecompiled.accept((SinkReturns.Decompiled) x); } - } - } else { - try { - JarEntry etn = new JarEntry(entry.getName()); - if (history.add(etn)) continue; - history.add(etn); - out.putNextEntry(etn); - try (InputStream in = jfile.getInputStream(entry)) { - if (in != null) { - int count; - while ((count = in.read(data, 0, 1024)) != -1) { - out.write(data, 0, count); - } - } - } finally { - out.closeEntry(); - } - } catch (ZipException ze) { - // some jars contain duplicate pom.xml entries: ignore it - if (!ze.getMessage().contains("duplicate")) { - throw ze; - } - } + return ignore -> { + }; } - } - } - } - private Map generateOptions() { - Map options = new HashMap<>(); - options.put("decodeenumswitch", String.valueOf(BytecodeViewer.viewer.decodeEnumSwitch.isSelected())); - options.put("sugarenums", String.valueOf(BytecodeViewer.viewer.sugarEnums.isSelected())); - options.put("decodestringswitch", String.valueOf(BytecodeViewer.viewer.decodeStringSwitch.isSelected())); - options.put("arrayiter", String.valueOf(BytecodeViewer.viewer.arrayiter.isSelected())); - options.put("collectioniter", String.valueOf(BytecodeViewer.viewer.collectioniter.isSelected())); - options.put("innerclasses", String.valueOf(BytecodeViewer.viewer.innerClasses.isSelected())); - options.put("removeboilerplate", String.valueOf(BytecodeViewer.viewer.removeBoilerPlate.isSelected())); - options.put("removeinnerclasssynthetics", String.valueOf(BytecodeViewer.viewer.removeInnerClassSynthetics.isSelected())); - options.put("decodelambdas", String.valueOf(BytecodeViewer.viewer.decodeLambdas.isSelected())); - options.put("hidebridgemethods", String.valueOf(BytecodeViewer.viewer.hideBridgeMethods.isSelected())); - options.put("liftconstructorinit", String.valueOf(BytecodeViewer.viewer.liftConstructorInit.isSelected())); - options.put("removebadgenerics", String.valueOf(BytecodeViewer.viewer.removeBadGenerics.isSelected())); - options.put("sugarasserts", String.valueOf(BytecodeViewer.viewer.sugarAsserts.isSelected())); - options.put("sugarboxing", String.valueOf(BytecodeViewer.viewer.sugarBoxing.isSelected())); - options.put("showversion", String.valueOf(BytecodeViewer.viewer.showVersion.isSelected())); - options.put("decodefinally", String.valueOf(BytecodeViewer.viewer.decodeFinally.isSelected())); - options.put("tidymonitors", String.valueOf(BytecodeViewer.viewer.tidyMonitors.isSelected())); - options.put("lenient", String.valueOf(BytecodeViewer.viewer.lenient.isSelected())); - options.put("dumpclasspath", String.valueOf(BytecodeViewer.viewer.dumpClassPath.isSelected())); - options.put("comments", String.valueOf(BytecodeViewer.viewer.comments.isSelected())); - options.put("forcetopsort", String.valueOf(BytecodeViewer.viewer.forceTopSort.isSelected())); - options.put("forcetopsortaggress", String.valueOf(BytecodeViewer.viewer.forceTopSortAggress.isSelected())); - options.put("stringbuffer", String.valueOf(BytecodeViewer.viewer.stringBuffer.isSelected())); - options.put("stringbuilder", String.valueOf(BytecodeViewer.viewer.stringBuilder.isSelected())); - options.put("silent", String.valueOf(BytecodeViewer.viewer.silent.isSelected())); - options.put("recover", String.valueOf(BytecodeViewer.viewer.recover.isSelected())); - options.put("eclipse", String.valueOf(BytecodeViewer.viewer.eclipse.isSelected())); - options.put("override", String.valueOf(BytecodeViewer.viewer.override.isSelected())); - options.put("showinferrable", String.valueOf(BytecodeViewer.viewer.showInferrable.isSelected())); - options.put("aexagg", String.valueOf(BytecodeViewer.viewer.aexagg.isSelected())); - options.put("hideutf", String.valueOf(BytecodeViewer.viewer.hideUTF.isSelected())); - options.put("hidelongstrings", String.valueOf(BytecodeViewer.viewer.hideLongStrings.isSelected())); - options.put("commentmonitors", String.valueOf(BytecodeViewer.viewer.commentMonitor.isSelected())); - options.put("allowcorrecting", String.valueOf(BytecodeViewer.viewer.allowCorrecting.isSelected())); - options.put("labelledblocks", String.valueOf(BytecodeViewer.viewer.labelledBlocks.isSelected())); - options.put("j14classobj", String.valueOf(BytecodeViewer.viewer.j14ClassOBJ.isSelected())); - options.put("hidelangimports", String.valueOf(BytecodeViewer.viewer.hideLangImports.isSelected())); - options.put("recovertypehints", String.valueOf(BytecodeViewer.viewer.recoveryTypehInts.isSelected())); - options.put("forcereturningifs", String.valueOf(BytecodeViewer.viewer.forceTurningIFs.isSelected())); - options.put("forloopaggcapture", String.valueOf(BytecodeViewer.viewer.forLoopAGGCapture.isSelected())); - return options; - } + } + }