From b8dc2d21d53f9eb02515f330648c006615cdfda2 Mon Sep 17 00:00:00 2001 From: NebelNidas <48808497+NebelNidas@users.noreply.github.com> Date: Wed, 6 Mar 2024 23:34:23 +0100 Subject: [PATCH] Use Vineflower instead of Fernflower (#39) --- build.gradle | 6 +- gradle.properties | 2 +- .../matcher/srcprocess/BuiltinDecompiler.java | 2 +- .../java/matcher/srcprocess/Fernflower.java | 322 ------------------ .../java/matcher/srcprocess/Vineflower.java | 145 ++++++++ src/main/java/module-info.java | 2 +- 6 files changed, 151 insertions(+), 328 deletions(-) delete mode 100644 src/main/java/matcher/srcprocess/Fernflower.java create mode 100644 src/main/java/matcher/srcprocess/Vineflower.java diff --git a/build.gradle b/build.gradle index ad5b7452..e74c8882 100644 --- a/build.gradle +++ b/build.gradle @@ -62,7 +62,7 @@ dependencies { implementation "org.ow2.asm:asm-util:${asm_version}" implementation "com.github.javaparser:javaparser-core:${javaparser_version}" implementation "net.fabricmc:cfr:${fabric_cfr_version}" - implementation "net.fabricmc:fabric-fernflower:${fabric_fernflower_version}" + implementation "org.vineflower:vineflower:${vineflower_version}" implementation "org.bitbucket.mstrobel:procyon-compilertools:${procyon_version}" // JavaFX for all platforms (needed for cross-platform fat jar) @@ -89,8 +89,8 @@ extraJavaModuleInfo { // CFR automaticModule("net.fabricmc:cfr", "cfr") - // Fernflower - automaticModule("net.fabricmc:fabric-fernflower", "intellij.fernflower") + // Vineflower + automaticModule("org.vineflower:vineflower", "org.vineflower.vineflower") // Procyon automaticModule("org.bitbucket.mstrobel:procyon-compilertools", "procyon.compilertools") diff --git a/gradle.properties b/gradle.properties index 54dfe34a..2c775d7f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ version = 0.1.0 # Project Dependencies asm_version = 9.6 fabric_cfr_version = 0.2.1 -fabric_fernflower_version = 2.0.0 +vineflower_version = 1.9.3 procyon_version = 0.6.0 mappingio_version = 0.5.0 javaparser_version = 3.25.6 diff --git a/src/main/java/matcher/srcprocess/BuiltinDecompiler.java b/src/main/java/matcher/srcprocess/BuiltinDecompiler.java index 28b7d237..5ccffd91 100644 --- a/src/main/java/matcher/srcprocess/BuiltinDecompiler.java +++ b/src/main/java/matcher/srcprocess/BuiltinDecompiler.java @@ -4,7 +4,7 @@ public enum BuiltinDecompiler { CFR("CFR", Cfr::new), - FERNFLOWER("Fernflower", Fernflower::new), + VINEFLOWER("Vineflower", Vineflower::new), PROCYON("Procyon", Procyon::new); BuiltinDecompiler(String name, Supplier supplier) { diff --git a/src/main/java/matcher/srcprocess/Fernflower.java b/src/main/java/matcher/srcprocess/Fernflower.java deleted file mode 100644 index d3d37f93..00000000 --- a/src/main/java/matcher/srcprocess/Fernflower.java +++ /dev/null @@ -1,322 +0,0 @@ -package matcher.srcprocess; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.lang.reflect.Field; -import java.util.AbstractMap; -import java.util.ArrayDeque; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Queue; -import java.util.Set; -import java.util.jar.Manifest; - -import org.jetbrains.java.decompiler.main.ClassesProcessor; -import org.jetbrains.java.decompiler.main.DecompilerContext; -import org.jetbrains.java.decompiler.main.ClassesProcessor.ClassNode; -import org.jetbrains.java.decompiler.main.decompiler.PrintStreamLogger; -import org.jetbrains.java.decompiler.main.extern.IBytecodeProvider; -import org.jetbrains.java.decompiler.main.extern.IFernflowerLogger; -import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences; -import org.jetbrains.java.decompiler.main.extern.IIdentifierRenamer; -import org.jetbrains.java.decompiler.main.extern.IResultSaver; -import org.jetbrains.java.decompiler.modules.renamer.ConverterHelper; -import org.jetbrains.java.decompiler.modules.renamer.IdentifierConverter; -import org.jetbrains.java.decompiler.modules.renamer.PoolInterceptor; -import org.jetbrains.java.decompiler.struct.ContextUnit; -import org.jetbrains.java.decompiler.struct.IDecompiledData; -import org.jetbrains.java.decompiler.struct.StructClass; -import org.jetbrains.java.decompiler.struct.StructContext; -import org.jetbrains.java.decompiler.struct.lazy.LazyLoader; -import org.jetbrains.java.decompiler.util.DataInputFullStream; -import org.jetbrains.java.decompiler.util.TextBuffer; - -import matcher.NameType; -import matcher.type.ClassEnv; -import matcher.type.ClassFeatureExtractor; -import matcher.type.ClassInstance; - -public class Fernflower implements Decompiler { - @Override - public String decompile(ClassInstance cls, ClassFeatureExtractor env, NameType nameType) { - // invoke ff with on-demand class lookup into matcher's state and string based output - Map properties = new HashMap<>(IFernflowerPreferences.DEFAULTS); - properties.put(IFernflowerPreferences.DECOMPILE_GENERIC_SIGNATURES, "1"); - - ResultSaver resultSaver = new ResultSaver(); - DecompiledData data = new DecompiledData(); - BytecodeProvider bcProvider = new BytecodeProvider(env, nameType); - MatcherStructContext structContext = new MatcherStructContext(resultSaver, data, new LazyLoader(bcProvider), env, nameType, bcProvider); - ClassesProcessor classProcessor = new ClassesProcessor(structContext); - PoolInterceptor interceptor = null; - IIdentifierRenamer renamer; - IdentifierConverter converter; - - if ("1".equals(properties.get(IFernflowerPreferences.RENAME_ENTITIES))) { - renamer = new ConverterHelper(); - interceptor = new PoolInterceptor(); - converter = new IdentifierConverter(structContext, renamer, interceptor); - } else { - renamer = null; - converter = null; - } - - data.classProcessor = classProcessor; - data.converter = converter; - - IFernflowerLogger logger = new PrintStreamLogger(System.out); - DecompilerContext context = new DecompilerContext(properties, logger, structContext, classProcessor, interceptor); - DecompilerContext.setCurrentContext(context); - - try { - structContext.addSpace(cls, true); - - // queue inner classes as well - if (!cls.getInnerClasses().isEmpty()) { - Queue toAdd = new ArrayDeque<>(cls.getInnerClasses()); - ClassInstance innerCls; - - while ((innerCls = toAdd.poll()) != null) { - structContext.addSpace(innerCls, true); - toAdd.addAll(innerCls.getInnerClasses()); - } - } - - if (converter != null) converter.rename(); - - classProcessor.loadClasses(renamer); - structContext.saveContext(); - } finally { - DecompilerContext.setCurrentContext(null); - } - - String ret = resultSaver.results.get(cls.getName(nameType)); - - if (ret != null) { - return ret; - } else { - throw new RuntimeException("decompiling "+cls+" didn't yield the expected result (available: "+resultSaver.results.keySet()+")"); - } - } - - private static class MatcherStructContext extends StructContext { - @SuppressWarnings("unchecked") - MatcherStructContext(IResultSaver saver, IDecompiledData decompiledData, LazyLoader loader, ClassEnv env, NameType nameType, BytecodeProvider bcProvider) { - super(saver, decompiledData, loader); - - this.loader = loader; - this.env = env; - this.nameType = nameType; - this.bcProvider = bcProvider; - - try { - Field f = StructContext.class.getDeclaredField("units"); - f.setAccessible(true); - units = (Map) f.get(this); - - f = StructContext.class.getDeclaredField("classes"); - f.setAccessible(true); - classes = (Map) f.get(this); - } catch (ReflectiveOperationException e) { - throw new RuntimeException(e); - } - - ownedUnit = units.get(""); - unownedUnit = new ContextUnit(ContextUnit.TYPE_FOLDER, null, unownedUnitFilename, false, saver, decompiledData); // avoids producing superfluous output - units.put(unownedUnitFilename, unownedUnit); - } - - public void addSpace(ClassInstance cls, boolean isOwn) { - try { - addStructClass(cls, isOwn); - } catch (IOException ex) { - String message = "Corrupted class: " + cls.getName(nameType); - DecompilerContext.getLogger().writeMessage(message, ex); - } - } - - @Override - public StructClass getClass(String name) { - if (DEBUG) System.out.printf("getClass(%s)%n", name); - - // use classes as a cache, load anything missing on demand - StructClass ret = classes.get(name); - if (ret != null) return ret; - - ClassInstance cls = env.getClsByName(name, nameType); - if (cls == null || !cls.isReal()) return null; - - try { - return addStructClass(cls, false); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - private StructClass addStructClass(ClassInstance cls, boolean isOwn) throws IOException { - String name = cls.getName(nameType); - byte[] data = bcProvider.get(name); // BytecodeProvider has a name->byte[] cache to avoid redundant cls.serialize invocations - if (data == null) throw new IllegalStateException(); - DataInputFullStream in = new DataInputFullStream(data); - - StructClass cl = StructClass.create(in, isOwn, loader); - classes.put(cl.qualifiedName, cl); - - ContextUnit unit = isOwn ? ownedUnit : unownedUnit; - unit.addClass(cl, name.substring(name.lastIndexOf('/') + 1)+".class"); - loader.addClassLink(cl.qualifiedName, new LazyLoader.Link(pathPrefix+name+pathSuffix, null)); - - return cl; - } - - @Override - public Map getClasses() { - return emulatedClasses; - } - - protected final LazyLoader loader; - protected final ClassEnv env; - protected final NameType nameType; - protected final BytecodeProvider bcProvider; - protected final Map units; - protected final Map classes; - protected final ContextUnit ownedUnit; - protected final ContextUnit unownedUnit; - - private final Map emulatedClasses = new AbstractMap() { - @Override - public boolean containsKey(Object key) { - return get(key) != null; - } - - @Override - public StructClass get(Object key) { - if (!(key instanceof String)) return null; - - return MatcherStructContext.this.getClass((String) key); - } - - @Override - public int size() { - return classes.size(); - } - - @Override - public Set> entrySet() { - Set> snapshot = new HashSet<>(classes.entrySet()); // copy to hide concurrent modifications from on-demand additions - - return snapshot; - } - }; - } - - private static class ResultSaver implements IResultSaver { - @Override - public void saveFolder(String path) { } - @Override - public void copyFile(String source, String path, String entryName) { } - @Override - public void createArchive(String path, String archiveName, Manifest manifest) { } - @Override - public void saveDirEntry(String path, String archiveName, String entryName) { } - @Override - public void copyEntry(String source, String path, String archiveName, String entry) { } - @Override - public void closeArchive(String path, String archiveName) { } - @Override - public void saveClassEntry(String path, String archiveName, String qualifiedName, String entryName, String content) { } - - @Override - public void saveClassFile(String path, String qualifiedName, String entryName, String content, int[] mapping) { - if (DEBUG) System.out.printf("saveClassFile(%s, %s, %s, %s, %s)%n", path, qualifiedName, entryName, content, Arrays.toString(mapping)); - - results.put(qualifiedName, content); - } - - Map results = new HashMap<>(); - } - - private static class DecompiledData implements IDecompiledData { - @Override - public String getClassEntryName(StructClass cl, String entryname) { - if (DEBUG) System.out.printf("getClassEntryName(%s, %s)%n", cl, entryname); - - ClassNode node = classProcessor.getMapRootClasses().get(cl.qualifiedName); - - if (node.type != ClassNode.CLASS_ROOT) { - return null; - } else if (converter != null) { - String simpleClassName = cl.qualifiedName.substring(cl.qualifiedName.lastIndexOf('/') + 1); - return entryname.substring(0, entryname.lastIndexOf('/') + 1) + simpleClassName + ".java"; - } else { - return entryname.substring(0, entryname.lastIndexOf(".class")) + ".java"; - } - } - - @Override - public String getClassContent(StructClass cl) { - if (DEBUG) System.out.printf("getClassContent(%s)%n", cl); - - try { - TextBuffer buffer = new TextBuffer(ClassesProcessor.AVERAGE_CLASS_SIZE); - buffer.append(DecompilerContext.getProperty(IFernflowerPreferences.BANNER).toString()); - classProcessor.writeClass(cl, buffer); - return buffer.toString(); - } catch (Throwable t) { - DecompilerContext.getLogger().writeMessage("Class " + cl.qualifiedName + " couldn't be fully decompiled.", t); - return null; - } - } - - protected ClassesProcessor classProcessor; - protected IdentifierConverter converter; - } - - private static class BytecodeProvider implements IBytecodeProvider { - BytecodeProvider(ClassEnv env, NameType nameType) { - this.env = env; - this.nameType = nameType; - } - - @Override - public byte[] getBytecode(String externalPath, String internalPath) throws IOException { - if (DEBUG) System.out.printf("getBytecode(%s, %s)%n", externalPath, internalPath); - - if (externalPath.startsWith(pathPrefix) && externalPath.endsWith(pathSuffix)) { - String name = externalPath.substring(pathPrefix.length(), externalPath.length() - pathSuffix.length()); - byte[] ret = get(name); - if (ret != null) return ret; - } - - throw new FileNotFoundException("can't find class for "+externalPath); - } - - public byte[] get(String name) { - byte[] ret = cache.get(name); - if (ret != null) return ret; - - ClassInstance cls = env.getClsByName(name, nameType); - - if (cls != null) { - ret = cls.serialize(nameType); - cache.put(name, ret); - } - - return ret; - } - - protected final ClassEnv env; - protected final NameType nameType; - - private final Map cache = new HashMap<>(); - } - - private static final boolean DEBUG = false; - - private static final String pathPrefix = "/matchenv/"; - private static final String pathSuffix = ".class"; - private static final String unownedUnitFilename = "foreign"; -} diff --git a/src/main/java/matcher/srcprocess/Vineflower.java b/src/main/java/matcher/srcprocess/Vineflower.java new file mode 100644 index 00000000..81ffe027 --- /dev/null +++ b/src/main/java/matcher/srcprocess/Vineflower.java @@ -0,0 +1,145 @@ +package matcher.srcprocess; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.jar.Manifest; + +import org.jetbrains.java.decompiler.main.decompiler.BaseDecompiler; +import org.jetbrains.java.decompiler.main.decompiler.PrintStreamLogger; +import org.jetbrains.java.decompiler.main.extern.IContextSource; +import org.jetbrains.java.decompiler.main.extern.IContextSource.IOutputSink; +import org.jetbrains.java.decompiler.main.extern.IFernflowerLogger; +import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences; +import org.jetbrains.java.decompiler.main.extern.IResultSaver; + +import matcher.NameType; +import matcher.type.ClassFeatureExtractor; +import matcher.type.ClassInstance; + +public class Vineflower implements Decompiler { + @Override + public String decompile(ClassInstance cls, ClassFeatureExtractor env, NameType nameType) { + // invoke VF with on-demand class lookup into matcher's state and string based output + Map properties = new HashMap<>(IFernflowerPreferences.DEFAULTS); + properties.put(IFernflowerPreferences.REMOVE_BRIDGE, "0"); + properties.put(IFernflowerPreferences.REMOVE_SYNTHETIC, "0"); + properties.put(IFernflowerPreferences.INDENT_STRING, "\n"); + properties.put(IFernflowerPreferences.THREADS, String.valueOf(Math.max(1, Runtime.getRuntime().availableProcessors() - 2))); + properties.put(IFernflowerPreferences.LOG_LEVEL, IFernflowerLogger.Severity.WARN.name()); + + OutputSink sink = new OutputSink(); + BaseDecompiler decompiler = new BaseDecompiler(NopResultSaver.INSTANCE, properties, new PrintStreamLogger(System.out)); + decompiler.addSource(new MatcherClsSource(cls, nameType, sink)); + decompiler.decompileContext(); + return sink.results.get(cls.getName(nameType)); + } + + private static class NopResultSaver implements IResultSaver { + @Override + public void saveFolder(String path) { } + @Override + public void copyFile(String source, String path, String entryName) { } + @Override + public void createArchive(String path, String archiveName, Manifest manifest) { } + @Override + public void saveDirEntry(String path, String archiveName, String entryName) { } + @Override + public void copyEntry(String source, String path, String archiveName, String entry) { } + @Override + public void closeArchive(String path, String archiveName) { } + @Override + public void saveClassEntry(String path, String archiveName, String qualifiedName, String entryName, String content) { } + @Override + public void saveClassFile(String path, String qualifiedName, String entryName, String content, int[] mapping) { } + + static final NopResultSaver INSTANCE = new NopResultSaver(); + } + + private static class MatcherClsSource implements IContextSource { + MatcherClsSource(ClassInstance cls, NameType nameType, IOutputSink outputSink) { + this.cls = cls; + this.nameType = nameType; + this.outputSink = outputSink; + } + + @Override + public String getName() { + return "Matcher class provider"; + } + + private String getClsName(ClassInstance cls, NameType nameType) { + String name = cls.getName(nameType); + return name == null || name.isEmpty() ? cls.getName() : name; + } + + @Override + public Entries getEntries() { + String name = getClsName(cls, nameType); + List entries = new ArrayList<>(); + entries.add(Entry.parse(name)); + bytecodeByClsName.put(name, cls.serialize(nameType)); + + for (ClassInstance innerCls : cls.getInnerClasses()) { + String innerName = getClsName(innerCls, nameType); + entries.add(Entry.parse(innerName)); + bytecodeByClsName.put(innerName, innerCls.serialize(nameType)); + } + + return new Entries(entries, List.of(), List.of()); + } + + @Override + public InputStream getInputStream(String resource) throws IOException { + resource = resource.substring(0, resource.length() - ".class".length()); + byte[] bytecode; + + if ((bytecode = bytecodeByClsName.get(resource)) == null) { + throw new IOException("Requested class not in decompilation scope: "+resource); + } + + return new ByteArrayInputStream(bytecode); + } + + @Override + public IOutputSink createOutputSink(IResultSaver saver) { + return outputSink; + } + + private final ClassInstance cls; + private final NameType nameType; + private final IOutputSink outputSink; + /** name->byte[] cache to avoid redundant cls.serialize invocations. */ + private final Map bytecodeByClsName = new HashMap<>(); + } + + private static class OutputSink implements IOutputSink { + @Override + public void begin() { } + + @Override + public void acceptClass(String qualifiedName, String fileName, String content, int[] mapping) { + if (DEBUG) System.out.printf("acceptClass(%s, %s, %s, %s)%n", qualifiedName, fileName, content, Arrays.toString(mapping)); + + results.put(qualifiedName, content); + } + + @Override + public void acceptDirectory(String directory) { } + + @Override + public void acceptOther(String path) { } + + @Override + public void close() throws IOException { } + + private Map results = new HashMap<>(); + } + + private static final boolean DEBUG = false; +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 861c1551..d6a200d7 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -13,7 +13,7 @@ requires cfr; requires com.github.javaparser.core; - requires intellij.fernflower; + requires org.vineflower.vineflower; requires java.prefs; requires transitive javafx.base; requires transitive javafx.controls;