aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/net/fabricmc/loom/decompilers/cfr
diff options
context:
space:
mode:
authormodmuss50 <modmuss50@gmail.com>2021-10-11 13:47:16 +0100
committerGitHub <noreply@github.com>2021-10-11 13:47:16 +0100
commite2439b7f57a82c365d4726d068b68ea2eb606f78 (patch)
tree5cc78058c70e46e22d1ada1a4411c9356cbdf906 /src/main/java/net/fabricmc/loom/decompilers/cfr
parent5315d3c5b24bcce47268ef8962c07aeb934b014b (diff)
downloadarchitectury-loom-e2439b7f57a82c365d4726d068b68ea2eb606f78.tar.gz
architectury-loom-e2439b7f57a82c365d4726d068b68ea2eb606f78.tar.bz2
architectury-loom-e2439b7f57a82c365d4726d068b68ea2eb606f78.zip
Rewrite GenSources including full support for CFR. (#511)
* Rewrite CFR decompiler interface. Support javadoc * CFR line numbers and fixes. * Cleanup and fix * Use WorkerExecutor to fork, massively cleans up the fernflower code, but does remove the fancy multithreaded logging. * Use IPC to get logging back from the decompilers. * Cleanup UnpickJarTask, fix leak in IPCServer * Used published CFR build * Handle older windows versions that do not support AF_UNIX. * Fixes and basic unit test * Improve memory handling of genSources * Stop decompile worker JVM
Diffstat (limited to 'src/main/java/net/fabricmc/loom/decompilers/cfr')
-rw-r--r--src/main/java/net/fabricmc/loom/decompilers/cfr/CFRObfuscationMapping.java230
-rw-r--r--src/main/java/net/fabricmc/loom/decompilers/cfr/CFRSinkFactory.java151
-rw-r--r--src/main/java/net/fabricmc/loom/decompilers/cfr/FabricCFRDecompiler.java233
-rw-r--r--src/main/java/net/fabricmc/loom/decompilers/cfr/LoomCFRDecompiler.java148
4 files changed, 529 insertions, 233 deletions
diff --git a/src/main/java/net/fabricmc/loom/decompilers/cfr/CFRObfuscationMapping.java b/src/main/java/net/fabricmc/loom/decompilers/cfr/CFRObfuscationMapping.java
new file mode 100644
index 00000000..83afc21b
--- /dev/null
+++ b/src/main/java/net/fabricmc/loom/decompilers/cfr/CFRObfuscationMapping.java
@@ -0,0 +1,230 @@
+/*
+ * 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.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();
+ }
+ }
+
+ 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")));
+ }
+
+ for (MappingTree.MethodArgMapping arg : mapping.getArgs()) {
+ String argComment = arg.getComment();
+
+ if (argComment != null) {
+ lines.addAll(Arrays.asList(("@param " + arg.getSrcName() + " " + argComment).split("\\R")));
+ }
+ }
+ }
+
+ 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 null;
+ }
+
+ MappingTree.ClassMapping classMapping = getClassMapping(owner);
+
+ if (classMapping == null) {
+ return null;
+ }
+
+ MappingTree.FieldMapping fieldMapping = classMapping.getField(field.getFieldName(), field.getDescriptor());
+ 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();
+ }
+ }
+}
diff --git a/src/main/java/net/fabricmc/loom/decompilers/cfr/CFRSinkFactory.java b/src/main/java/net/fabricmc/loom/decompilers/cfr/CFRSinkFactory.java
new file mode 100644
index 00000000..bdc5a28a
--- /dev/null
+++ b/src/main/java/net/fabricmc/loom/decompilers/cfr/CFRSinkFactory.java
@@ -0,0 +1,151 @@
+/*
+ * 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);
+ }
+}
diff --git a/src/main/java/net/fabricmc/loom/decompilers/cfr/FabricCFRDecompiler.java b/src/main/java/net/fabricmc/loom/decompilers/cfr/FabricCFRDecompiler.java
deleted file mode 100644
index 7237a499..00000000
--- a/src/main/java/net/fabricmc/loom/decompilers/cfr/FabricCFRDecompiler.java
+++ /dev/null
@@ -1,233 +0,0 @@
-/*
- * This file is part of fabric-loom, licensed under the MIT License (MIT).
- *
- * Copyright (c) 2020-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.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.function.Function;
-import java.util.jar.Attributes;
-import java.util.jar.JarEntry;
-import java.util.jar.JarOutputStream;
-import java.util.jar.Manifest;
-import java.util.stream.Collectors;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipFile;
-
-import com.google.common.base.Charsets;
-import com.google.common.collect.ImmutableMap;
-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.gradle.api.Project;
-import org.gradle.api.internal.project.ProjectInternal;
-import org.gradle.internal.logging.progress.ProgressLogger;
-import org.gradle.internal.logging.progress.ProgressLoggerFactory;
-import org.gradle.internal.service.ServiceRegistry;
-
-import net.fabricmc.loom.api.decompilers.DecompilationMetadata;
-import net.fabricmc.loom.api.decompilers.LoomDecompiler;
-
-public class FabricCFRDecompiler implements LoomDecompiler {
- private final Project project;
-
- public FabricCFRDecompiler(Project project) {
- this.project = project;
- }
-
- @Override
- public String name() {
- return "ExperimentalCfr";
- }
-
- @Override
- public void decompile(Path compiledJar, Path sourcesDestination, Path linemapDestination, DecompilationMetadata metaData) {
- project.getLogger().warn("!!!! The CFR decompiler support is currently incomplete, line numbers will not match up and there will be no javadocs in the generated source.");
-
- // Setups the multi threaded logger, the thread id is used as the key to the ProgressLogger's
- ServiceRegistry registry = ((ProjectInternal) project).getServices();
- ProgressLoggerFactory factory = registry.get(ProgressLoggerFactory.class);
- ProgressLogger progressGroup = factory.newOperation(getClass()).setDescription("Decompile");
-
- Map<Long, ProgressLogger> loggerMap = new ConcurrentHashMap<>();
- Function<Long, ProgressLogger> createLogger = (threadId) -> {
- ProgressLogger pl = factory.newOperation(getClass(), progressGroup);
- pl.setDescription("decompile worker");
- pl.started();
- return pl;
- };
-
- progressGroup.started();
-
- Manifest manifest = new Manifest();
- manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
- Set<String> addedDirectories = new HashSet<>();
-
- try (OutputStream fos = Files.newOutputStream(sourcesDestination); JarOutputStream jos = new JarOutputStream(fos, manifest); ZipFile inputZip = new ZipFile(compiledJar.toFile())) {
- CfrDriver driver = new CfrDriver.Builder()
- .withOptions(ImmutableMap.of(
- "renameillegalidents", "true",
- "trackbytecodeloc", "true"
- ))
- .withClassFileSource(new ClassFileSource() {
- @Override
- public void informAnalysisRelativePathDetail(String usePath, String classFilePath) {
- }
-
- @Override
- public Collection<String> addJar(String jarPath) {
- return null;
- }
-
- @Override
- public String getPossiblyRenamedPath(String path) {
- return path;
- }
-
- @Override
- public Pair<byte[], String> getClassFileContent(String path) throws IOException {
- ZipEntry zipEntry = inputZip.getEntry(path);
-
- if (zipEntry == null) {
- throw new FileNotFoundException(path);
- }
-
- try (InputStream inputStream = inputZip.getInputStream(zipEntry)) {
- return Pair.make(inputStream.readAllBytes(), path);
- }
- }
- })
- .withOutputSink(new OutputSinkFactory() {
- @Override
- public List<SinkClass> getSupportedSinks(SinkType sinkType, Collection<SinkClass> available) {
- return switch (sinkType) {
- case PROGRESS -> Collections.singletonList(SinkClass.STRING);
- case JAVA -> Collections.singletonList(SinkClass.DECOMPILED);
- default -> Collections.emptyList();
- };
- }
-
- @SuppressWarnings("unchecked")
- @Override
- public <T> Sink<T> getSink(SinkType sinkType, SinkClass sinkClass) {
- return switch (sinkType) {
- case PROGRESS -> (p) -> project.getLogger().debug((String) p);
- case JAVA -> (Sink<T>) decompiledSink(jos, addedDirectories);
- case EXCEPTION -> (e) -> project.getLogger().error((String) e);
- default -> null;
- };
- }
- })
- .build();
-
- List<String> classes = Collections.list(inputZip.entries()).stream()
- .map(ZipEntry::getName)
- .filter(input -> input.endsWith(".class"))
- .collect(Collectors.toList());
-
- ExecutorService executorService = Executors.newFixedThreadPool(metaData.numberOfThreads());
- List<Future<?>> futures = new LinkedList<>();
-
- for (String clazz : classes) {
- futures.add(executorService.submit(() -> {
- loggerMap.computeIfAbsent(Thread.currentThread().getId(), createLogger).progress(clazz);
- driver.analyse(Collections.singletonList(clazz));
- }));
- }
-
- for (Future<?> future : futures) {
- future.get();
- }
- } catch (IOException | InterruptedException | ExecutionException e) {
- throw new RuntimeException("Failed to decompile", e);
- } finally {
- loggerMap.forEach((threadId, progressLogger) -> progressLogger.completed());
- }
- }
-
- private static OutputSinkFactory.Sink<SinkReturns.Decompiled> decompiledSink(JarOutputStream jos, Set<String> addedDirectories) {
- return decompiled -> {
- String filename = decompiled.getPackageName().replace('.', '/');
- if (!filename.isEmpty()) filename += "/";
- filename += decompiled.getClassName() + ".java";
-
- byte[] data = decompiled.getJava().getBytes(Charsets.UTF_8);
-
- writeToJar(filename, data, jos, addedDirectories);
- };
- }
-
- // TODO move to task queue?
- private static synchronized void writeToJar(String filename, byte[] data, JarOutputStream jos, Set<String> addedDirectories) {
- 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 {
- jos.putNextEntry(entry);
- jos.closeEntry();
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
- }
-
- JarEntry entry = new JarEntry(filename);
- entry.setTime(new Date().getTime());
- entry.setSize(data.length);
-
- try {
- jos.putNextEntry(entry);
- jos.write(data);
- jos.closeEntry();
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-}
diff --git a/src/main/java/net/fabricmc/loom/decompilers/cfr/LoomCFRDecompiler.java b/src/main/java/net/fabricmc/loom/decompilers/cfr/LoomCFRDecompiler.java
new file mode 100644
index 00000000..bbfb0be2
--- /dev/null
+++ b/src/main/java/net/fabricmc/loom/decompilers/cfr/LoomCFRDecompiler.java
@@ -0,0 +1,148 @@
+/*
+ * 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.nio.file.Paths;
+import java.util.Collections;
+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;
+import net.fabricmc.loom.decompilers.LineNumberRemapper;
+
+public class LoomCFRDecompiler implements LoomDecompiler {
+ private static final Map<String, String> DECOMPILE_OPTIONS = Map.of(
+ "renameillegalidents", "true",
+ "trackbytecodeloc", "true",
+ "comments", "false"
+ );
+
+ @Override
+ public String name() {
+ return "Cfr";
+ }
+
+ @Override
+ public void decompile(Path compiledJar, Path sourcesDestination, Path linemapDestination, DecompilationMetadata metaData) {
+ final String path = compiledJar.toAbsolutePath().toString();
+ final Options options = OptionsImpl.getFactory().create(DECOMPILE_OPTIONS);
+
+ 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("%s\t%d\t%d\n".formatted(name, maxLine, maxLineDest));
+ writer.write(builder.toString());
+ writer.write("\n");
+ }
+ } catch (IOException e) {
+ throw new UncheckedIOException("Failed to write line map", e);
+ }
+ }
+
+ // A test main class to make it quicker/easier to debug with minimal jars
+ public static void main(String[] args) throws IOException {
+ LoomCFRDecompiler decompiler = new LoomCFRDecompiler();
+
+ Path lineMap = Paths.get("linemap.txt");
+
+ decompiler.decompile(Paths.get("input.jar"),
+ Paths.get("output-sources.jar"),
+ lineMap,
+ new DecompilationMetadata(4, null, Collections.emptyList(), null)
+ );
+
+ LineNumberRemapper lineNumberRemapper = new LineNumberRemapper();
+ lineNumberRemapper.readMappings(lineMap.toFile());
+ lineNumberRemapper.process(null, Paths.get("input.jar"), Paths.get("output.jar"));
+ }
+}