diff --git a/src/main/java/com/heliosdecompiler/transformerapi/common/AbstractResultSaver.java b/src/main/java/com/heliosdecompiler/transformerapi/common/AbstractResultSaver.java index 7f86e20..22b14ef 100644 --- a/src/main/java/com/heliosdecompiler/transformerapi/common/AbstractResultSaver.java +++ b/src/main/java/com/heliosdecompiler/transformerapi/common/AbstractResultSaver.java @@ -35,10 +35,6 @@ protected AbstractResultSaver(DecompilationResult result) { this.result = result; } - protected AbstractResultSaver() { - this(new DecompilationResult()); - } - public Map getResults() { return this.results; } @@ -60,10 +56,17 @@ public void copyFile(String source, String path, String entryName) { public void saveClassFile(String qualifiedName, String content, int[] mapping) { if (mapping != null) { lineNumbers = true; + int maxLineNumber = 0; for (int i = 0; i < mapping.length; i += 2) { int line = mapping[i + 1]; int actualLine = mapping[i]; result.putLineNumber(line, actualLine); + if (actualLine > maxLineNumber) { + maxLineNumber = actualLine; + } + } + if (maxLineNumber > 0) { + result.setMaxLineNumber(maxLineNumber); } } results.put(qualifiedName, content); diff --git a/src/main/java/com/heliosdecompiler/transformerapi/common/BytecodeSourceLinker.java b/src/main/java/com/heliosdecompiler/transformerapi/common/BytecodeSourceLinker.java index b1fae7a..bd08cc5 100644 --- a/src/main/java/com/heliosdecompiler/transformerapi/common/BytecodeSourceLinker.java +++ b/src/main/java/com/heliosdecompiler/transformerapi/common/BytecodeSourceLinker.java @@ -94,7 +94,7 @@ private BytecodeSourceLinker() { * Populate declarations and references after a decompiler has produced source text. */ public static void link(DecompilationResult result, String source, String rootInternalName, Map importantData) { - if (source == null || source.isEmpty() || importantData.isEmpty()) { + if (StringUtils.isEmpty(source) || importantData.isEmpty()) { return; } BytecodeIndex index = BytecodeIndex.of(rootInternalName, importantData); diff --git a/src/main/java/com/heliosdecompiler/transformerapi/decompilers/Decompiler.java b/src/main/java/com/heliosdecompiler/transformerapi/decompilers/Decompiler.java index 4e85629..ed4dacd 100644 --- a/src/main/java/com/heliosdecompiler/transformerapi/decompilers/Decompiler.java +++ b/src/main/java/com/heliosdecompiler/transformerapi/decompilers/Decompiler.java @@ -42,6 +42,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.jar.Attributes; import java.util.jar.Manifest; @@ -212,5 +213,34 @@ protected AbstractDecompiler(String name) { public String getName() { return name; } + + protected static void putLineNumbers(DecompilationResult result, Map lineNumbers) { + Objects.requireNonNull(lineNumbers, "lineNumbers"); + int maxLineNumber = 0; + for (Map.Entry entry : lineNumbers.entrySet()) { + Integer lineNumber = entry.getKey(); + Integer sourceLineNumber = entry.getValue(); + if (lineNumber == null || sourceLineNumber == null) { + continue; + } + result.putLineNumber(lineNumber, sourceLineNumber); + if (sourceLineNumber > maxLineNumber) { + maxLineNumber = sourceLineNumber; + } + } + if (maxLineNumber > 0) { + result.setMaxLineNumber(maxLineNumber); + } + } + + protected static void putIdentityLineNumbers(DecompilationResult result, String source) { + if (StringUtils.isNotBlank(source)) { + int lineCount = source.split("\\R", -1).length; + result.setMaxLineNumber(lineCount); + for (int i = 1; i <= lineCount; i++) { + result.putLineNumber(i, i); + } + } + } } } diff --git a/src/main/java/com/heliosdecompiler/transformerapi/decompilers/cfr/CFRDecompiler.java b/src/main/java/com/heliosdecompiler/transformerapi/decompilers/cfr/CFRDecompiler.java index c8d4fb1..a99a4f4 100644 --- a/src/main/java/com/heliosdecompiler/transformerapi/decompilers/cfr/CFRDecompiler.java +++ b/src/main/java/com/heliosdecompiler/transformerapi/decompilers/cfr/CFRDecompiler.java @@ -61,6 +61,11 @@ public DecompilationResult decompile(Loader loader, String internalName, CFRSett } DecompilationResult decompilationResult = new DecompilationResult(); decompilationResult.setDecompiledOutput(resultCode); + if (!lineMapping.isEmpty()) { + putLineNumbers(decompilationResult, lineMapping); + } else { + putIdentityLineNumbers(decompilationResult, resultCode); + } // CFR emits text only, so link metadata is reconstructed from the source after decompilation. BytecodeSourceLinker.link(decompilationResult, resultCode, classStruct.fullClassName(), classStruct.importantData()); time = stopWatch.getTime(); diff --git a/src/main/java/com/heliosdecompiler/transformerapi/decompilers/fernflower/FernflowerDecompiler.java b/src/main/java/com/heliosdecompiler/transformerapi/decompilers/fernflower/FernflowerDecompiler.java index 2121cf3..78994a7 100644 --- a/src/main/java/com/heliosdecompiler/transformerapi/decompilers/fernflower/FernflowerDecompiler.java +++ b/src/main/java/com/heliosdecompiler/transformerapi/decompilers/fernflower/FernflowerDecompiler.java @@ -50,7 +50,7 @@ public DecompilationResult decompile(Loader loader, String internalName, Fernflo ClassStruct classStruct = readClassAndInnerClasses(loader, internalName); if (!classStruct.importantData().isEmpty()) { IBytecodeProvider provider = new FernflowerBytecodeProvider(classStruct.importantData()); - FernflowerResultSaver saver = new FernflowerResultSaver(); + FernflowerResultSaver saver = new FernflowerResultSaver(decompilationResult); Fernflower baseDecompiler = new Fernflower(provider, saver, settings.getSettings(), new PrintStreamLogger(System.out)); StructContext context; try { @@ -67,6 +67,9 @@ public DecompilationResult decompile(Loader loader, String internalName, Fernflo String key = classStruct.fullClassName(); String source = saver.getResults().get(key); decompilationResult.setDecompiledOutput(source); + if (!saver.hasLineRemapping()) { + putIdentityLineNumbers(decompilationResult, source); + } // Fernflower does not expose token callbacks, so links are synthesized from source and bytecode. BytecodeSourceLinker.link(decompilationResult, source, key, classStruct.importantData()); } diff --git a/src/main/java/com/heliosdecompiler/transformerapi/decompilers/fernflower/FernflowerResultSaver.java b/src/main/java/com/heliosdecompiler/transformerapi/decompilers/fernflower/FernflowerResultSaver.java index dfd7cc1..a5c0615 100644 --- a/src/main/java/com/heliosdecompiler/transformerapi/decompilers/fernflower/FernflowerResultSaver.java +++ b/src/main/java/com/heliosdecompiler/transformerapi/decompilers/fernflower/FernflowerResultSaver.java @@ -22,6 +22,10 @@ public class FernflowerResultSaver extends AbstractResultSaver implements IResultSaver { + public FernflowerResultSaver(jd.core.DecompilationResult result) { + super(result); + } + @Override public void saveClassFile(String path, String qualifiedName, String entryName, String content, int[] mapping) { saveClassFile(qualifiedName, content, mapping); diff --git a/src/main/java/com/heliosdecompiler/transformerapi/decompilers/jadx/JADXDecompiler.java b/src/main/java/com/heliosdecompiler/transformerapi/decompilers/jadx/JADXDecompiler.java index 0d9a777..4ebbf24 100644 --- a/src/main/java/com/heliosdecompiler/transformerapi/decompilers/jadx/JADXDecompiler.java +++ b/src/main/java/com/heliosdecompiler/transformerapi/decompilers/jadx/JADXDecompiler.java @@ -79,7 +79,12 @@ public DecompilationResult decompile(Loader loader, String internalName, JadxArg addLinks(decompilationResult, jadx, codeInfo); // Supplement JADX's native annotations with bytecode-driven links for missed constructor cases. BytecodeSourceLinker.link(decompilationResult, codeInfo.getCodeStr(), internalName, importantData); - codeInfo.getCodeMetadata().getLineMapping().forEach(decompilationResult::putLineNumber); + Map lineMapping = codeInfo.getCodeMetadata().getLineMapping(); + if (lineMapping != null && !lineMapping.isEmpty()) { + putLineNumbers(decompilationResult, lineMapping); + } else { + putIdentityLineNumbers(decompilationResult, codeInfo.getCodeStr()); + } break; } } diff --git a/src/main/java/com/heliosdecompiler/transformerapi/decompilers/procyon/ProcyonDecompiler.java b/src/main/java/com/heliosdecompiler/transformerapi/decompilers/procyon/ProcyonDecompiler.java index 90f9ee7..227f533 100644 --- a/src/main/java/com/heliosdecompiler/transformerapi/decompilers/procyon/ProcyonDecompiler.java +++ b/src/main/java/com/heliosdecompiler/transformerapi/decompilers/procyon/ProcyonDecompiler.java @@ -31,6 +31,8 @@ import com.strobel.decompiler.languages.LineNumberPosition; import com.strobel.decompiler.languages.TypeDecompilationResults; +import jadx.core.utils.Utils; + import java.io.IOException; import java.io.StringWriter; import java.lang.reflect.InvocationTargetException; @@ -68,9 +70,25 @@ private static BytecodeOutputOptions createBytecodeFormattingOptions(final Comma public DecompilationResult decompile(Loader loader, String internalName, CommandLineOptions options) throws IOException { StopWatch stopWatch = StopWatch.createStarted(); Map importantClasses = new HashMap<>(); + DecompilerSettings settings = createSettings(options, importantClasses, loader); + StringWriter stringwriter = new StringWriter(); + DecompilationResult result = new DecompilationResult(); + Map referencesCache = new HashMap<>(); - final DecompilerSettings settings = new DecompilerSettings(); + PlainTextOutput plainTextOutput = new ProcyonLinkProvider(stringwriter, stringwriter, result, internalName, referencesCache); + TypeDecompilationResults typeDecompilationResults = com.strobel.decompiler.Decompiler.decompile(internalName, plainTextOutput, settings); + List lineNumberPositions = typeDecompilationResults.getLineNumberPositions(); + putLineNumberPositions(result, lineNumberPositions); + result.setDecompiledOutput(formatOutput(options, stringwriter.toString(), lineNumberPositions)); + if (Utils.isEmpty(lineNumberPositions)) { + putIdentityLineNumbers(result, result.getDecompiledOutput()); + } + time = stopWatch.getTime(); + return result; + } + private static DecompilerSettings createSettings(CommandLineOptions options, Map importantClasses, Loader loader) { + DecompilerSettings settings = new DecompilerSettings(); settings.setFlattenSwitchBlocks(options.getFlattenSwitchBlocks()); settings.setForceExplicitImports(!options.getCollapseImports()); settings.setForceExplicitTypeArguments(options.getForceExplicitTypeArguments()); @@ -89,44 +107,63 @@ public DecompilationResult decompile(Loader loader, String internalName, Command settings.setForcedCompilerTarget(options.getCompilerTargetOverride()); settings.setTextBlockLineMinimum(options.getTextBlockLineMinimum()); settings.setTypeLoader(new ProcyonFastTypeLoader(importantClasses, loader)); + configureOutputMode(settings, options); + return settings; + } + private static void configureOutputMode(DecompilerSettings settings, CommandLineOptions options) { if (!options.getSuppressBanner()) { settings.setOutputFileHeaderText("\nDecompiled by Procyon v" + Procyon.version() + "\n"); } - if (options.isRawBytecode()) { settings.setLanguage(Languages.bytecode()); settings.setBytecodeOutputOptions(createBytecodeFormattingOptions(options)); - } else if (options.isBytecodeAst()) { + return; + } + if (options.isBytecodeAst()) { settings.setLanguage(options.isUnoptimized() ? Languages.bytecodeAstUnoptimized() : Languages.bytecodeAst()); } + } - StringWriter stringwriter = new StringWriter(); - DecompilationResult result = new DecompilationResult(); - Map referencesCache = new HashMap<>(); - - PlainTextOutput plainTextOutput = new ProcyonLinkProvider(stringwriter, stringwriter, result, internalName, referencesCache); - TypeDecompilationResults typeDecompilationResults = com.strobel.decompiler.Decompiler.decompile(internalName, plainTextOutput, settings); - if (options.getIncludeLineNumbers() || options.getStretchLines()) { - List lineNumberPositions = typeDecompilationResults.getLineNumberPositions(); - EnumSet lineNumberOptions = EnumSet.noneOf(LineNumberOption.class); - - if (options.getIncludeLineNumbers()) { - lineNumberOptions.add(LineNumberOption.LEADING_COMMENTS); + private static void putLineNumberPositions(DecompilationResult result, List lineNumberPositions) { + if (Utils.isEmpty(lineNumberPositions)) { + return; + } + int maxLineNumber = 0; + for (LineNumberPosition lineNumberPosition : lineNumberPositions) { + int emittedLine = lineNumberPosition.getEmittedLine(); + int originalLine = lineNumberPosition.getOriginalLine(); + result.putLineNumber(emittedLine, originalLine); + if (originalLine > maxLineNumber) { + maxLineNumber = originalLine; } + } + if (maxLineNumber > 0) { + result.setMaxLineNumber(maxLineNumber); + } + } - if (options.getStretchLines()) { - lineNumberOptions.add(LineNumberOption.STRETCHED); - } + private static String formatOutput(CommandLineOptions options, String source, List lineNumberPositions) throws IOException { + if (!shouldFormatWithLineNumbers(options)) { + return source; + } + InMemoryLineNumberFormatter lineFormatter = new InMemoryLineNumberFormatter(source, lineNumberPositions, createLineNumberOptions(options)); + return lineFormatter.reformatFile(); + } + + private static boolean shouldFormatWithLineNumbers(CommandLineOptions options) { + return options.getIncludeLineNumbers() || options.getStretchLines(); + } - InMemoryLineNumberFormatter lineFormatter = new InMemoryLineNumberFormatter(stringwriter.toString(), lineNumberPositions, lineNumberOptions); - String sourceWithLineNumbers = lineFormatter.reformatFile(); - result.setDecompiledOutput(sourceWithLineNumbers); - } else { - result.setDecompiledOutput(stringwriter.toString()); + private static EnumSet createLineNumberOptions(CommandLineOptions options) { + EnumSet lineNumberOptions = EnumSet.noneOf(LineNumberOption.class); + if (options.getIncludeLineNumbers()) { + lineNumberOptions.add(LineNumberOption.LEADING_COMMENTS); } - time = stopWatch.getTime(); - return result; + if (options.getStretchLines()) { + lineNumberOptions.add(LineNumberOption.STRETCHED); + } + return lineNumberOptions; } diff --git a/src/main/java/com/heliosdecompiler/transformerapi/decompilers/vineflower/VineflowerDecompiler.java b/src/main/java/com/heliosdecompiler/transformerapi/decompilers/vineflower/VineflowerDecompiler.java index 7a0c8f0..2c6bf48 100644 --- a/src/main/java/com/heliosdecompiler/transformerapi/decompilers/vineflower/VineflowerDecompiler.java +++ b/src/main/java/com/heliosdecompiler/transformerapi/decompilers/vineflower/VineflowerDecompiler.java @@ -63,11 +63,7 @@ public DecompilationResult decompile(Loader loader, String internalName, Vineflo String decompiledOutput = saver.getResults().get(key); decompilationResult.setDecompiledOutput(decompiledOutput); if (!saver.hasLineRemapping()) { - int lineCount = decompiledOutput.split("\n").length; - decompilationResult.setMaxLineNumber(lineCount); - for (int i = 1; i <= lineCount; i++) { - decompilationResult.putLineNumber(i, i); - } + putIdentityLineNumbers(decompilationResult, decompiledOutput); } } time = stopWatch.getTime(); diff --git a/src/main/java/com/heliosdecompiler/transformerapi/decompilers/vineflower/VineflowerSettings.java b/src/main/java/com/heliosdecompiler/transformerapi/decompilers/vineflower/VineflowerSettings.java index 2857e5d..2a9cefd 100644 --- a/src/main/java/com/heliosdecompiler/transformerapi/decompilers/vineflower/VineflowerSettings.java +++ b/src/main/java/com/heliosdecompiler/transformerapi/decompilers/vineflower/VineflowerSettings.java @@ -16,11 +16,14 @@ package com.heliosdecompiler.transformerapi.decompilers.vineflower; -import org.vineflower.java.decompiler.main.extern.IFernflowerPreferences; - import java.util.HashMap; import java.util.Map; +import static org.vineflower.java.decompiler.main.extern.IFernflowerPreferences.BYTECODE_SOURCE_MAPPING; +import static org.vineflower.java.decompiler.main.extern.IFernflowerPreferences.DECOMPILE_GENERIC_SIGNATURES; +import static org.vineflower.java.decompiler.main.extern.IFernflowerPreferences.DUMP_ORIGINAL_LINES; +import static org.vineflower.java.decompiler.main.extern.IFernflowerPreferences.REMOVE_SYNTHETIC; + /** * Represents settings which can be used to configure the particular decompiling session. *

@@ -60,7 +63,11 @@ public Map getSettings() { return this.internalSettings; } + public static Map defaults() { + return Map.of(REMOVE_SYNTHETIC, "1", DECOMPILE_GENERIC_SIGNATURES, "1"); + } + public static Map lineNumbers() { - return Map.of(IFernflowerPreferences.DUMP_ORIGINAL_LINES, "1", IFernflowerPreferences.BYTECODE_SOURCE_MAPPING, "1"); + return Map.of(REMOVE_SYNTHETIC, "1", DECOMPILE_GENERIC_SIGNATURES, "1", DUMP_ORIGINAL_LINES, "1", BYTECODE_SOURCE_MAPPING, "1"); } } diff --git a/src/test/java/com/heliosdecompiler/transformerapi/decompilers/DecompilerLineNumbersTest.java b/src/test/java/com/heliosdecompiler/transformerapi/decompilers/DecompilerLineNumbersTest.java new file mode 100644 index 0000000..f386cc3 --- /dev/null +++ b/src/test/java/com/heliosdecompiler/transformerapi/decompilers/DecompilerLineNumbersTest.java @@ -0,0 +1,96 @@ +package com.heliosdecompiler.transformerapi.decompilers; + +import org.apache.commons.io.IOUtils; +import org.jd.core.v1.util.ZipLoader; +import org.junit.Test; + +import com.heliosdecompiler.transformerapi.StandardTransformers; +import com.heliosdecompiler.transformerapi.StandardTransformers.Decompilers; +import com.heliosdecompiler.transformerapi.common.Loader; +import com.heliosdecompiler.transformerapi.decompilers.cfr.CFRSettings; +import com.heliosdecompiler.transformerapi.decompilers.fernflower.FernflowerSettings; +import com.heliosdecompiler.transformerapi.decompilers.jadx.MapJadxArgs; +import com.heliosdecompiler.transformerapi.decompilers.jd.JDSettings; +import com.heliosdecompiler.transformerapi.decompilers.procyon.MapDecompilerSettings; +import com.heliosdecompiler.transformerapi.decompilers.vineflower.VineflowerSettings; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.TreeMap; + +import jd.core.DecompilationResult; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class DecompilerLineNumbersTest { + + private static final String ROOT_LOCATION = "target/test-classes"; + private static final String PACKAGE_NAME = "com/heliosdecompiler/transformerapi"; + private static final String CLASS_NAME = "TestCompact.class"; + private static final String INTERNAL_NAME = "test/TestCompact"; + + @Test + public void testAllDecompilersPopulateLineNumbersWithLineNumberSettings() throws Exception { + assertProvidesLineNumbers(Decompilers.CFR.decompile(ROOT_LOCATION, PACKAGE_NAME, CLASS_NAME)); + assertProvidesLineNumbers(Decompilers.FERNFLOWER.decompile(ROOT_LOCATION, PACKAGE_NAME, CLASS_NAME)); + assertProvidesLineNumbers(Decompilers.VINEFLOWER.decompile(ROOT_LOCATION, PACKAGE_NAME, CLASS_NAME)); + assertProvidesLineNumbers(Decompilers.JD_CORE_V0.decompile(ROOT_LOCATION, PACKAGE_NAME, CLASS_NAME)); + assertProvidesLineNumbers(Decompilers.JD_CORE_V1.decompile(ROOT_LOCATION, PACKAGE_NAME, CLASS_NAME)); + assertProvidesLineNumbers(Decompilers.PROCYON.decompile(ROOT_LOCATION, PACKAGE_NAME, CLASS_NAME)); + assertProvidesLineNumbers(Decompilers.JADX.decompile(ROOT_LOCATION, PACKAGE_NAME, CLASS_NAME)); + } + + @Test + public void testDecompilersPreserveLineNumbersWithoutExplicitLineNumberSettings() throws Exception { + Loader loader = loader("/test-compact-expand-inline.jar"); + + assertProvidesLineNumbers(StandardTransformers.decompile(loader, INTERNAL_NAME, CFRSettings.defaults(), Decompilers.ENGINE_CFR)); + assertProvidesLineNumbers(StandardTransformers.decompile(loader, INTERNAL_NAME, FernflowerSettings.defaults(), Decompilers.ENGINE_FERNFLOWER)); + assertProvidesLineNumbers(StandardTransformers.decompile(loader, INTERNAL_NAME, VineflowerSettings.defaults(), Decompilers.ENGINE_VINEFLOWER)); + assertProvidesLineNumbers(StandardTransformers.decompile(loader, INTERNAL_NAME, JDSettings.defaults(), Decompilers.ENGINE_JD_CORE_V0)); + assertProvidesLineNumbers(StandardTransformers.decompile(loader, INTERNAL_NAME, JDSettings.defaults(), Decompilers.ENGINE_JD_CORE_V1)); + assertProvidesLineNumbers(StandardTransformers.decompile(loader, INTERNAL_NAME, MapDecompilerSettings.defaults(), Decompilers.ENGINE_PROCYON)); + assertProvidesLineNumbers(StandardTransformers.decompile(loader, INTERNAL_NAME, MapJadxArgs.defaults(), Decompilers.ENGINE_JADX)); + } + + @Test + public void testPutLineNumbersIgnoresNullSourceLines() { + DecompilationResult result = new DecompilationResult(); + TreeMap lineNumbers = new TreeMap<>(); + lineNumbers.put(1, 10); + lineNumbers.put(2, null); + lineNumbers.put(3, 30); + + TestDecompiler.putLineNumbers(result, lineNumbers); + + assertEquals(Integer.valueOf(10), result.getLineNumbers().get(1)); + assertFalse(result.getLineNumbers().containsKey(2)); + assertEquals(Integer.valueOf(30), result.getLineNumbers().get(3)); + assertEquals(30, result.getMaxLineNumber()); + } + + private static void assertProvidesLineNumbers(DecompilationResult result) { + assertNotNull(result.getDecompiledOutput()); + assertFalse(result.getLineNumbers().isEmpty()); + assertTrue(result.getMaxLineNumber() > 0); + assertTrue(result.getLineNumbers().keySet().stream().anyMatch(line -> line != null && line > 0)); + assertTrue(result.getLineNumbers().values().stream().anyMatch(line -> line != null && line > 0)); + } + + private static Loader loader(String resourcePath) throws Exception { + try (InputStream in = DecompilerLineNumbersTest.class.getResourceAsStream(resourcePath)) { + byte[] data = IOUtils.toByteArray(in); + ZipLoader zipLoader = new ZipLoader(new ByteArrayInputStream(data)); + return new Loader(zipLoader::canLoad, zipLoader::load, DecompilerLineNumbersTest.class.getResource(resourcePath).toURI()); + } + } + + private static final class TestDecompiler extends Decompiler.AbstractDecompiler { + private TestDecompiler() { + super("test"); + } + } +}