diff options
Diffstat (limited to 'src/main/java/net/fabricmc/loom/decompilers/cfr')
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")); + } +} |