Support the Vineflower decompiler (#951)

This commit is contained in:
modmuss
2023-09-11 11:29:01 +01:00
committed by GitHub
parent 0a3779f41d
commit 71b7bea854
17 changed files with 790 additions and 64 deletions

View File

@@ -24,17 +24,27 @@
package net.fabricmc.loom.decompilers;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Map;
import javax.inject.Inject;
import org.gradle.api.NamedDomainObjectProvider;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.api.decompilers.DecompilationMetadata;
import net.fabricmc.loom.api.decompilers.LoomDecompiler;
import net.fabricmc.loom.decompilers.cfr.LoomCFRDecompiler;
import net.fabricmc.loom.decompilers.fernflower.FabricFernFlowerDecompiler;
import net.fabricmc.loom.decompilers.vineflower.VineflowerDecompiler;
import net.fabricmc.loom.util.LoomVersions;
import net.fabricmc.loom.util.ZipUtils;
public abstract class DecompilerConfiguration implements Runnable {
@Inject
@@ -44,9 +54,11 @@ public abstract class DecompilerConfiguration implements Runnable {
public void run() {
var fernflowerConfiguration = createConfiguration("fernflower", LoomVersions.FERNFLOWER);
var cfrConfiguration = createConfiguration("cfr", LoomVersions.CFR);
var vineflowerConfiguration = createConfiguration("vineflower", LoomVersions.VINEFLOWER);
registerDecompiler(getProject(), "fernFlower", FabricFernFlowerDecompiler.class, fernflowerConfiguration);
registerDecompiler(getProject(), "cfr", LoomCFRDecompiler.class, cfrConfiguration);
registerDecompiler(getProject(), "fernFlower", BuiltinFernflower.class, fernflowerConfiguration);
registerDecompiler(getProject(), "cfr", BuiltinCfr.class, cfrConfiguration);
registerDecompiler(getProject(), "vineflower", BuiltinVineflower.class, vineflowerConfiguration);
}
private NamedDomainObjectProvider<Configuration> createConfiguration(String name, LoomVersions version) {
@@ -62,4 +74,96 @@ public abstract class DecompilerConfiguration implements Runnable {
options.getClasspath().from(configuration);
});
}
// We need to wrap the internal API with the public API.
// This is needed as the sourceset containing fabric's decompilers do not have access to loom classes.
private abstract static sealed class BuiltinDecompiler implements LoomDecompiler permits BuiltinFernflower, BuiltinCfr, BuiltinVineflower {
private final LoomInternalDecompiler internalDecompiler;
BuiltinDecompiler(LoomInternalDecompiler internalDecompiler) {
this.internalDecompiler = internalDecompiler;
}
@Override
public void decompile(Path compiledJar, Path sourcesDestination, Path linemapDestination, DecompilationMetadata metaData) {
final Logger slf4jLogger = LoggerFactory.getLogger(internalDecompiler.getClass());
final var logger = new LoomInternalDecompiler.Logger() {
@Override
public void accept(String data) throws IOException {
metaData.logger().accept(data);
}
@Override
public void error(String msg) {
slf4jLogger.error(msg);
}
};
internalDecompiler.decompile(new LoomInternalDecompiler.Context() {
@Override
public Path compiledJar() {
return compiledJar;
}
@Override
public Path sourcesDestination() {
return sourcesDestination;
}
@Override
public Path linemapDestination() {
return linemapDestination;
}
@Override
public int numberOfThreads() {
return metaData.numberOfThreads();
}
@Override
public Path javaDocs() {
return metaData.javaDocs();
}
@Override
public Collection<Path> libraries() {
return metaData.libraries();
}
@Override
public LoomInternalDecompiler.Logger logger() {
return logger;
}
@Override
public Map<String, String> options() {
return metaData.options();
}
@Override
public byte[] unpackZip(Path zip, String path) throws IOException {
return ZipUtils.unpack(zip, path);
}
});
}
}
public static final class BuiltinFernflower extends BuiltinDecompiler {
public BuiltinFernflower() {
super(new FabricFernFlowerDecompiler());
}
}
public static final class BuiltinCfr extends BuiltinDecompiler {
public BuiltinCfr() {
super(new LoomCFRDecompiler());
}
}
public static final class BuiltinVineflower extends BuiltinDecompiler {
public BuiltinVineflower() {
super(new VineflowerDecompiler());
}
}
}

View File

@@ -1,248 +0,0 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2021 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.decompilers.cfr;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import org.benf.cfr.reader.bytecode.analysis.types.JavaRefTypeInstance;
import org.benf.cfr.reader.bytecode.analysis.types.JavaTypeInstance;
import org.benf.cfr.reader.bytecode.analysis.types.MethodPrototype;
import org.benf.cfr.reader.entities.AccessFlag;
import org.benf.cfr.reader.entities.ClassFile;
import org.benf.cfr.reader.entities.ClassFileField;
import org.benf.cfr.reader.entities.Field;
import org.benf.cfr.reader.mapping.NullMapping;
import org.benf.cfr.reader.util.output.DelegatingDumper;
import org.benf.cfr.reader.util.output.Dumper;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.mappingio.MappingReader;
import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch;
import net.fabricmc.mappingio.tree.MappingTree;
import net.fabricmc.mappingio.tree.MemoryMappingTree;
public class CFRObfuscationMapping extends NullMapping {
private final MappingTree mappingTree;
public CFRObfuscationMapping(Path mappings) {
mappingTree = readMappings(mappings);
}
@Override
public Dumper wrap(Dumper d) {
return new JavadocProvidingDumper(d);
}
private static MappingTree readMappings(Path input) {
try (BufferedReader reader = Files.newBufferedReader(input)) {
MemoryMappingTree mappingTree = new MemoryMappingTree();
MappingSourceNsSwitch nsSwitch = new MappingSourceNsSwitch(mappingTree, MappingsNamespace.NAMED.toString());
MappingReader.read(reader, nsSwitch);
return mappingTree;
} catch (IOException e) {
throw new RuntimeException("Failed to read mappings", e);
}
}
private class JavadocProvidingDumper extends DelegatingDumper {
JavadocProvidingDumper(Dumper delegate) {
super(delegate);
}
@Override
public Dumper dumpClassDoc(JavaTypeInstance owner) {
MappingTree.ClassMapping mapping = getClassMapping(owner);
if (mapping == null) {
return this;
}
List<String> recordComponentDocs = new LinkedList<>();
if (isRecord(owner)) {
ClassFile classFile = ((JavaRefTypeInstance) owner).getClassFile();
for (ClassFileField field : classFile.getFields()) {
if (field.getField().testAccessFlag(AccessFlag.ACC_STATIC)) {
continue;
}
MappingTree.FieldMapping fieldMapping = mapping.getField(field.getFieldName(), field.getField().getDescriptor());
if (fieldMapping == null) {
continue;
}
String comment = fieldMapping.getComment();
if (comment != null) {
recordComponentDocs.add(String.format("@param %s %s", fieldMapping.getSrcName(), comment));
}
}
}
String comment = mapping.getComment();
if (comment != null || !recordComponentDocs.isEmpty()) {
print("/**").newln();
if (comment != null) {
for (String line : comment.split("\\R")) {
print(" * ").print(line).newln();
}
if (!recordComponentDocs.isEmpty()) {
print(" * ").newln();
}
}
if (comment != null && !recordComponentDocs.isEmpty()) {
print(" * ");
}
for (String componentDoc : recordComponentDocs) {
print(" * ").print(componentDoc).newln();
}
print(" */").newln();
}
return this;
}
@Override
public Dumper dumpMethodDoc(MethodPrototype method) {
MappingTree.ClassMapping classMapping = getClassMapping(method.getOwner());
if (classMapping == null) {
return this;
}
List<String> lines = new ArrayList<>();
MappingTree.MethodMapping mapping = classMapping.getMethod(method.getName(), method.getOriginalDescriptor());
if (mapping != null) {
String comment = mapping.getComment();
if (comment != null) {
lines.addAll(Arrays.asList(comment.split("\\R")));
}
final Collection<? extends MappingTree.MethodArgMapping> methodArgs = mapping.getArgs();
final List<String> params = new ArrayList<>();
for (MappingTree.MethodArgMapping arg : methodArgs) {
String argComment = arg.getComment();
if (argComment != null) {
params.addAll(Arrays.asList(("@param " + arg.getSrcName() + " " + argComment).split("\\R")));
}
}
// Add a blank line between params and the comment.
if (!lines.isEmpty() && !params.isEmpty()) {
lines.add("");
}
lines.addAll(params);
}
if (!lines.isEmpty()) {
print("/**").newln();
for (String line : lines) {
print(" * ").print(line).newln();
}
print(" */").newln();
}
return this;
}
@Override
public Dumper dumpFieldDoc(Field field, JavaTypeInstance owner) {
// None static fields in records are handled in the class javadoc.
if (isRecord(owner) && !isStatic(field)) {
return this;
}
MappingTree.ClassMapping classMapping = getClassMapping(owner);
if (classMapping == null) {
return this;
}
MappingTree.FieldMapping fieldMapping = classMapping.getField(field.getFieldName(), field.getDescriptor());
if (fieldMapping != null) {
dumpComment(fieldMapping.getComment());
}
return this;
}
private MappingTree.ClassMapping getClassMapping(JavaTypeInstance type) {
String qualifiedName = type.getRawName().replace('.', '/');
return mappingTree.getClass(qualifiedName);
}
private boolean isRecord(JavaTypeInstance javaTypeInstance) {
if (javaTypeInstance instanceof JavaRefTypeInstance) {
ClassFile classFile = ((JavaRefTypeInstance) javaTypeInstance).getClassFile();
return classFile.getClassSignature().getSuperClass().getRawName().equals("java.lang.Record");
}
return false;
}
private boolean isStatic(Field field) {
return field.testAccessFlag(AccessFlag.ACC_STATIC);
}
private void dumpComment(String comment) {
if (comment == null || comment.isBlank()) {
return;
}
print("/**").newln();
for (String line : comment.split("\n")) {
print(" * ").print(line).newln();
}
print(" */").newln();
}
}
}

View File

@@ -1,151 +0,0 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2021 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.decompilers.cfr;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Set;
import java.util.TreeMap;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import com.google.common.base.Charsets;
import org.benf.cfr.reader.api.OutputSinkFactory;
import org.benf.cfr.reader.api.SinkReturns;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.fabricmc.loom.util.IOStringConsumer;
public class CFRSinkFactory implements OutputSinkFactory {
private static final Logger ERROR_LOGGER = LoggerFactory.getLogger(CFRSinkFactory.class);
private final JarOutputStream outputStream;
private final IOStringConsumer logger;
private final Set<String> addedDirectories = new HashSet<>();
private final Map<String, Map<Integer, Integer>> lineMap = new TreeMap<>();
public CFRSinkFactory(JarOutputStream outputStream, IOStringConsumer logger) {
this.outputStream = outputStream;
this.logger = logger;
}
@Override
public List<SinkClass> getSupportedSinks(SinkType sinkType, Collection<SinkClass> available) {
return switch (sinkType) {
case JAVA -> Collections.singletonList(SinkClass.DECOMPILED);
case LINENUMBER -> Collections.singletonList(SinkClass.LINE_NUMBER_MAPPING);
default -> Collections.emptyList();
};
}
@Override
public <T> Sink<T> getSink(SinkType sinkType, SinkClass sinkClass) {
return switch (sinkType) {
case JAVA -> (Sink<T>) decompiledSink();
case LINENUMBER -> (Sink<T>) lineNumberMappingSink();
case EXCEPTION -> (e) -> ERROR_LOGGER.error((String) e);
default -> null;
};
}
private Sink<SinkReturns.Decompiled> decompiledSink() {
return sinkable -> {
String filename = sinkable.getPackageName().replace('.', '/');
if (!filename.isEmpty()) filename += "/";
filename += sinkable.getClassName() + ".java";
byte[] data = sinkable.getJava().getBytes(Charsets.UTF_8);
writeToJar(filename, data);
};
}
private Sink<SinkReturns.LineNumberMapping> lineNumberMappingSink() {
return sinkable -> {
final String className = sinkable.getClassName();
final NavigableMap<Integer, Integer> classFileMappings = sinkable.getClassFileMappings();
final NavigableMap<Integer, Integer> mappings = sinkable.getMappings();
if (classFileMappings == null || mappings == null) return;
for (Map.Entry<Integer, Integer> entry : mappings.entrySet()) {
// New line number
Integer dstLineNumber = entry.getValue();
// Line mapping in the original jar
Integer srcLineNumber = classFileMappings.get(entry.getKey());
if (srcLineNumber == null || dstLineNumber == null) continue;
lineMap.computeIfAbsent(className, (c) -> new TreeMap<>()).put(srcLineNumber, dstLineNumber);
}
};
}
private synchronized void writeToJar(String filename, byte[] data) {
String[] path = filename.split("/");
String pathPart = "";
for (int i = 0; i < path.length - 1; i++) {
pathPart += path[i] + "/";
if (addedDirectories.add(pathPart)) {
JarEntry entry = new JarEntry(pathPart);
entry.setTime(new Date().getTime());
try {
outputStream.putNextEntry(entry);
outputStream.closeEntry();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
JarEntry entry = new JarEntry(filename);
entry.setTime(new Date().getTime());
entry.setSize(data.length);
try {
logger.accept("Writing: " + filename);
outputStream.putNextEntry(entry);
outputStream.write(data);
outputStream.closeEntry();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public Map<String, Map<Integer, Integer>> getLineMap() {
return Collections.unmodifiableMap(lineMap);
}
}

View File

@@ -1,128 +0,0 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2021 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.decompilers.cfr;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import org.benf.cfr.reader.Driver;
import org.benf.cfr.reader.state.ClassFileSourceImpl;
import org.benf.cfr.reader.state.DCCommonState;
import org.benf.cfr.reader.util.AnalysisType;
import org.benf.cfr.reader.util.getopt.Options;
import org.benf.cfr.reader.util.getopt.OptionsImpl;
import org.benf.cfr.reader.util.output.SinkDumperFactory;
import net.fabricmc.loom.api.decompilers.DecompilationMetadata;
import net.fabricmc.loom.api.decompilers.LoomDecompiler;
public final class LoomCFRDecompiler implements LoomDecompiler {
private static final Map<String, String> DECOMPILE_OPTIONS = Map.of(
"renameillegalidents", "true",
"trackbytecodeloc", "true",
"comments", "false"
);
@Override
public void decompile(Path compiledJar, Path sourcesDestination, Path linemapDestination, DecompilationMetadata metaData) {
final String path = compiledJar.toAbsolutePath().toString();
final Map<String, String> allOptions = new HashMap<>(DECOMPILE_OPTIONS);
allOptions.putAll(metaData.options());
final Options options = OptionsImpl.getFactory().create(allOptions);
ClassFileSourceImpl classFileSource = new ClassFileSourceImpl(options);
for (Path library : metaData.libraries()) {
classFileSource.addJarContent(library.toAbsolutePath().toString(), AnalysisType.JAR);
}
classFileSource.informAnalysisRelativePathDetail(null, null);
DCCommonState state = new DCCommonState(options, classFileSource);
if (metaData.javaDocs() != null) {
state = new DCCommonState(state, new CFRObfuscationMapping(metaData.javaDocs()));
}
final Manifest manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
Map<String, Map<Integer, Integer>> lineMap;
try (JarOutputStream outputStream = new JarOutputStream(Files.newOutputStream(sourcesDestination), manifest)) {
CFRSinkFactory cfrSinkFactory = new CFRSinkFactory(outputStream, metaData.logger());
SinkDumperFactory dumperFactory = new SinkDumperFactory(cfrSinkFactory, options);
Driver.doJar(state, path, AnalysisType.JAR, dumperFactory);
lineMap = cfrSinkFactory.getLineMap();
} catch (IOException e) {
throw new UncheckedIOException("Failed to decompile", e);
}
writeLineMap(linemapDestination, lineMap);
}
private void writeLineMap(Path output, Map<String, Map<Integer, Integer>> lineMap) {
try (Writer writer = Files.newBufferedWriter(output, StandardCharsets.UTF_8)) {
for (Map.Entry<String, Map<Integer, Integer>> classEntry : lineMap.entrySet()) {
final String name = classEntry.getKey().replace(".", "/");
final Map<Integer, Integer> mapping = classEntry.getValue();
int maxLine = 0;
int maxLineDest = 0;
StringBuilder builder = new StringBuilder();
for (Map.Entry<Integer, Integer> mappingEntry : mapping.entrySet()) {
final int src = mappingEntry.getKey();
final int dst = mappingEntry.getValue();
maxLine = Math.max(maxLine, src);
maxLineDest = Math.max(maxLineDest, dst);
builder.append("\t").append(src).append("\t").append(dst).append("\n");
}
writer.write(String.format(Locale.ENGLISH, "%s\t%d\t%d\n", name, maxLine, maxLineDest));
writer.write(builder.toString());
writer.write("\n");
}
} catch (IOException e) {
throw new UncheckedIOException("Failed to write line map", e);
}
}
}

View File

@@ -1,71 +0,0 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2019-2021 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.decompilers.fernflower;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import org.jetbrains.java.decompiler.main.Fernflower;
import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences;
import org.jetbrains.java.decompiler.main.extern.IResultSaver;
import net.fabricmc.fernflower.api.IFabricJavadocProvider;
import net.fabricmc.loom.api.decompilers.DecompilationMetadata;
import net.fabricmc.loom.api.decompilers.LoomDecompiler;
public final class FabricFernFlowerDecompiler implements LoomDecompiler {
@Override
public void decompile(Path compiledJar, Path sourcesDestination, Path linemapDestination, DecompilationMetadata metaData) {
final Map<String, Object> options = new HashMap<>(
Map.of(
IFernflowerPreferences.DECOMPILE_GENERIC_SIGNATURES, "1",
IFernflowerPreferences.BYTECODE_SOURCE_MAPPING, "1",
IFernflowerPreferences.REMOVE_SYNTHETIC, "1",
IFernflowerPreferences.LOG_LEVEL, "trace",
IFernflowerPreferences.THREADS, String.valueOf(metaData.numberOfThreads()),
IFernflowerPreferences.INDENT_STRING, "\t",
IFabricJavadocProvider.PROPERTY_NAME, new TinyJavadocProvider(metaData.javaDocs().toFile())
)
);
options.putAll(metaData.options());
IResultSaver saver = new ThreadSafeResultSaver(sourcesDestination::toFile, linemapDestination::toFile);
Fernflower ff = new Fernflower(FernFlowerUtils::getBytecode, saver, options, new FernflowerLogger(metaData.logger()));
for (Path library : metaData.libraries()) {
ff.addLibrary(library.toFile());
}
ff.addSource(compiledJar.toFile());
try {
ff.decompileContext();
} finally {
ff.clearContext();
}
}
}

View File

@@ -1,44 +0,0 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2016-2022 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.decompilers.fernflower;
import java.io.File;
import java.io.IOException;
import org.jetbrains.java.decompiler.util.InterpreterUtil;
import net.fabricmc.loom.util.ZipUtils;
public class FernFlowerUtils {
public static byte[] getBytecode(String externalPath, String internalPath) throws IOException {
File file = new File(externalPath);
if (internalPath == null) {
return InterpreterUtil.getBytes(file);
} else {
return ZipUtils.unpack(file.toPath(), internalPath);
}
}
}

View File

@@ -1,84 +0,0 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2021 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.decompilers.fernflower;
import java.io.IOException;
import org.jetbrains.java.decompiler.main.extern.IFernflowerLogger;
import net.fabricmc.loom.util.IOStringConsumer;
public class FernflowerLogger extends IFernflowerLogger {
private final IOStringConsumer logger;
public FernflowerLogger(IOStringConsumer logger) {
this.logger = logger;
}
@Override
public void writeMessage(String message, Severity severity) {
if (message.contains("Inconsistent inner class entries for")) return;
if (message.contains("Inconsistent generic signature in method")) return;
System.err.println(message);
}
@Override
public void writeMessage(String message, Severity severity, Throwable t) {
writeMessage(message, severity);
}
private void write(String data) {
try {
logger.accept(data);
} catch (IOException e) {
throw new RuntimeException("Failed to log", e);
}
}
@Override
public void startReadingClass(String className) {
write("Decompiling " + className);
}
@Override
public void startClass(String className) {
write("Decompiling " + className);
}
@Override
public void startWriteClass(String className) {
// Nope
}
@Override
public void startMethod(String methodName) {
// Nope
}
@Override
public void endMethod() {
// Nope
}
}

View File

@@ -1,177 +0,0 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2019-2020 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.decompilers.fernflower;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.Supplier;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.jetbrains.java.decompiler.main.DecompilerContext;
import org.jetbrains.java.decompiler.main.extern.IResultSaver;
import net.fabricmc.fernflower.api.IFabricResultSaver;
/**
* Created by covers1624 on 18/02/19.
*/
public class ThreadSafeResultSaver implements IResultSaver, IFabricResultSaver {
private final Supplier<File> output;
private final Supplier<File> lineMapFile;
public Map<String, ZipOutputStream> outputStreams = new HashMap<>();
public Map<String, ExecutorService> saveExecutors = new HashMap<>();
public PrintWriter lineMapWriter;
public ThreadSafeResultSaver(Supplier<File> output, Supplier<File> lineMapFile) {
this.output = output;
this.lineMapFile = lineMapFile;
}
@Override
public void createArchive(String path, String archiveName, Manifest manifest) {
String key = path + "/" + archiveName;
File file = output.get();
try {
FileOutputStream fos = new FileOutputStream(file);
ZipOutputStream zos = manifest == null ? new ZipOutputStream(fos) : new JarOutputStream(fos, manifest);
outputStreams.put(key, zos);
saveExecutors.put(key, Executors.newSingleThreadExecutor());
} catch (IOException e) {
throw new RuntimeException("Unable to create archive: " + file, e);
}
if (lineMapFile.get() != null) {
try {
lineMapWriter = new PrintWriter(new FileWriter(lineMapFile.get()));
} catch (IOException e) {
throw new RuntimeException("Unable to create line mapping file: " + lineMapFile.get(), e);
}
}
}
@Override
public void saveClassEntry(String path, String archiveName, String qualifiedName, String entryName, String content) {
this.saveClassEntry(path, archiveName, qualifiedName, entryName, content, null);
}
@Override
public void saveClassEntry(String path, String archiveName, String qualifiedName, String entryName, String content, int[] mapping) {
String key = path + "/" + archiveName;
ExecutorService executor = saveExecutors.get(key);
executor.submit(() -> {
ZipOutputStream zos = outputStreams.get(key);
try {
zos.putNextEntry(new ZipEntry(entryName));
if (content != null) {
zos.write(content.getBytes(StandardCharsets.UTF_8));
}
} catch (IOException e) {
DecompilerContext.getLogger().writeMessage("Cannot write entry " + entryName, e);
}
if (mapping != null && lineMapWriter != null) {
int maxLine = 0;
int maxLineDest = 0;
StringBuilder builder = new StringBuilder();
for (int i = 0; i < mapping.length; i += 2) {
maxLine = Math.max(maxLine, mapping[i]);
maxLineDest = Math.max(maxLineDest, mapping[i + 1]);
builder.append("\t").append(mapping[i]).append("\t").append(mapping[i + 1]).append("\n");
}
lineMapWriter.println(qualifiedName + "\t" + maxLine + "\t" + maxLineDest);
lineMapWriter.println(builder.toString());
}
});
}
@Override
public void closeArchive(String path, String archiveName) {
String key = path + "/" + archiveName;
ExecutorService executor = saveExecutors.get(key);
Future<?> closeFuture = executor.submit(() -> {
ZipOutputStream zos = outputStreams.get(key);
try {
zos.close();
} catch (IOException e) {
throw new RuntimeException("Unable to close zip. " + key, e);
}
});
executor.shutdown();
try {
closeFuture.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
outputStreams.remove(key);
saveExecutors.remove(key);
if (lineMapWriter != null) {
lineMapWriter.flush();
lineMapWriter.close();
}
}
@Override
public void saveFolder(String path) {
}
@Override
public void copyFile(String source, String path, String entryName) {
}
@Override
public void saveClassFile(String path, String qualifiedName, String entryName, String content, int[] mapping) {
}
@Override
public void saveDirEntry(String path, String archiveName, String entryName) {
}
@Override
public void copyEntry(String source, String path, String archiveName, String entry) {
}
}

View File

@@ -1,187 +0,0 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2019-2021 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.decompilers.fernflower;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import org.jetbrains.java.decompiler.struct.StructClass;
import org.jetbrains.java.decompiler.struct.StructField;
import org.jetbrains.java.decompiler.struct.StructMethod;
import org.jetbrains.java.decompiler.struct.StructRecordComponent;
import org.objectweb.asm.Opcodes;
import net.fabricmc.fernflower.api.IFabricJavadocProvider;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.mappingio.MappingReader;
import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch;
import net.fabricmc.mappingio.tree.MappingTree;
import net.fabricmc.mappingio.tree.MemoryMappingTree;
public class TinyJavadocProvider implements IFabricJavadocProvider {
private final MappingTree mappingTree;
public TinyJavadocProvider(File tinyFile) {
mappingTree = readMappings(tinyFile);
}
@Override
public String getClassDoc(StructClass structClass) {
MappingTree.ClassMapping classMapping = mappingTree.getClass(structClass.qualifiedName);
if (classMapping == null) {
return null;
}
if (!isRecord(structClass)) {
return classMapping.getComment();
}
/**
* Handle the record component docs here.
*
* Record components are mapped via the field name, thus take the docs from the fields and display them on then class.
*/
List<String> parts = new ArrayList<>();
if (classMapping.getComment() != null) {
parts.add(classMapping.getComment());
}
boolean addedParam = false;
for (StructRecordComponent component : structClass.getRecordComponents()) {
// The component will always match the field name and descriptor
MappingTree.FieldMapping fieldMapping = classMapping.getField(component.getName(), component.getDescriptor());
if (fieldMapping == null) {
continue;
}
String comment = fieldMapping.getComment();
if (comment != null) {
if (!addedParam && classMapping.getComment() != null) {
//Add a blank line before components when the class has a comment
parts.add("");
addedParam = true;
}
parts.add(String.format("@param %s %s", fieldMapping.getName(MappingsNamespace.NAMED.toString()), comment));
}
}
if (parts.isEmpty()) {
return null;
}
return String.join("\n", parts);
}
@Override
public String getFieldDoc(StructClass structClass, StructField structField) {
// None static fields in records are handled in the class javadoc.
if (isRecord(structClass) && !isStatic(structField)) {
return null;
}
MappingTree.ClassMapping classMapping = mappingTree.getClass(structClass.qualifiedName);
if (classMapping == null) {
return null;
}
MappingTree.FieldMapping fieldMapping = classMapping.getField(structField.getName(), structField.getDescriptor());
return fieldMapping != null ? fieldMapping.getComment() : null;
}
@Override
public String getMethodDoc(StructClass structClass, StructMethod structMethod) {
MappingTree.ClassMapping classMapping = mappingTree.getClass(structClass.qualifiedName);
if (classMapping == null) {
return null;
}
MappingTree.MethodMapping methodMapping = classMapping.getMethod(structMethod.getName(), structMethod.getDescriptor());
if (methodMapping != null) {
List<String> parts = new ArrayList<>();
if (methodMapping.getComment() != null) {
parts.add(methodMapping.getComment());
}
boolean addedParam = false;
for (MappingTree.MethodArgMapping argMapping : methodMapping.getArgs()) {
String comment = argMapping.getComment();
if (comment != null) {
if (!addedParam && methodMapping.getComment() != null) {
//Add a blank line before params when the method has a comment
parts.add("");
addedParam = true;
}
parts.add(String.format("@param %s %s", argMapping.getName(MappingsNamespace.NAMED.toString()), comment));
}
}
if (parts.isEmpty()) {
return null;
}
return String.join("\n", parts);
}
return null;
}
private static MappingTree readMappings(File input) {
try (BufferedReader reader = Files.newBufferedReader(input.toPath())) {
MemoryMappingTree mappingTree = new MemoryMappingTree();
MappingSourceNsSwitch nsSwitch = new MappingSourceNsSwitch(mappingTree, MappingsNamespace.NAMED.toString());
MappingReader.read(reader, nsSwitch);
return mappingTree;
} catch (IOException e) {
throw new RuntimeException("Failed to read mappings", e);
}
}
public static boolean isRecord(StructClass structClass) {
return (structClass.getAccessFlags() & Opcodes.ACC_RECORD) != 0;
}
public static boolean isStatic(StructField structField) {
return (structField.getAccessFlags() & Opcodes.ACC_STATIC) != 0;
}
}