diff --git a/spoon-decompiler/pom.xml b/spoon-decompiler/pom.xml index 09eddf773f8..6094c27503f 100644 --- a/spoon-decompiler/pom.xml +++ b/spoon-decompiler/pom.xml @@ -51,6 +51,13 @@ cfr 0.146 + + + org.bitbucket.mstrobel + procyon-compilertools + 0.5.36 + + diff --git a/spoon-decompiler/src/main/java/spoon/JarLauncher.java b/spoon-decompiler/src/main/java/spoon/JarLauncher.java index abbcec27ff1..6486bc6eb40 100644 --- a/spoon-decompiler/src/main/java/spoon/JarLauncher.java +++ b/spoon-decompiler/src/main/java/spoon/JarLauncher.java @@ -125,10 +125,8 @@ public JarLauncher(String jarPath, String decompiledSrcPath, String pom, Decompi throw new SpoonException("Jar " + jar.getPath() + " not found."); } - //We call the decompiler only if jar has changed since last decompilation. - if (jar.lastModified() > decompiledSrc.lastModified()) { - decompile = true; - } + + decompile = true; init(pom); } diff --git a/spoon-decompiler/src/main/java/spoon/decompiler/FernflowerDecompiler.java b/spoon-decompiler/src/main/java/spoon/decompiler/FernflowerDecompiler.java index 19569ce4381..306ca610bf7 100644 --- a/spoon-decompiler/src/main/java/spoon/decompiler/FernflowerDecompiler.java +++ b/spoon-decompiler/src/main/java/spoon/decompiler/FernflowerDecompiler.java @@ -18,6 +18,7 @@ import org.jetbrains.java.decompiler.main.decompiler.ConsoleDecompiler; +@Deprecated public class FernflowerDecompiler implements Decompiler { @Override diff --git a/spoon-decompiler/src/main/java/spoon/decompiler/ProcyonDecompiler.java b/spoon-decompiler/src/main/java/spoon/decompiler/ProcyonDecompiler.java new file mode 100644 index 00000000000..60873903b2e --- /dev/null +++ b/spoon-decompiler/src/main/java/spoon/decompiler/ProcyonDecompiler.java @@ -0,0 +1,295 @@ +/** + * Copyright (C) 2006-2018 INRIA and contributors + * Spoon - http://spoon.gforge.inria.fr/ + * + * This software is governed by the CeCILL-C License under French law and + * abiding by the rules of distribution of free software. You can use, modify + * and/or redistribute the software under the terms of the CeCILL-C license as + * circulated by CEA, CNRS and INRIA at http://www.cecill.info. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ +package spoon.decompiler; + +import com.strobel.assembler.InputTypeLoader; +import com.strobel.assembler.metadata.CompositeTypeLoader; +import com.strobel.assembler.metadata.DeobfuscationUtilities; +import com.strobel.assembler.metadata.IMetadataResolver; +import com.strobel.assembler.metadata.ITypeLoader; +import com.strobel.assembler.metadata.JarTypeLoader; +import com.strobel.assembler.metadata.MetadataParser; +import com.strobel.assembler.metadata.MetadataSystem; +import com.strobel.assembler.metadata.TypeDefinition; +import com.strobel.assembler.metadata.TypeReference; +import com.strobel.core.StringUtilities; +import com.strobel.decompiler.DecompilationOptions; +import com.strobel.decompiler.DecompilerSettings; +import com.strobel.decompiler.PlainTextOutput; +import com.strobel.decompiler.languages.BytecodeLanguage; +import com.strobel.io.PathHelper; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.Charset; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import spoon.support.Experimental; + +@Experimental +public class ProcyonDecompiler implements Decompiler { + + @Override + public void decompile(String inputPath, String outputPath, String[] classpath) { + try { + if (inputPath.endsWith(".jar")) { + decompileJar(inputPath, outputPath); + } else if (inputPath.endsWith(".class")) { + decompileClass(inputPath, outputPath); + } + } catch (Exception e) { + e.printStackTrace(); + } + + } + + private void decompileClass(String path, String outputDir) throws Exception { + final File tempClass = new File(path); + + DecompilerSettings settings = DecompilerSettings.javaDefaults(); + + MetadataSystem metadataSystem = new MetadataSystem(new InputTypeLoader()); + TypeReference type = metadataSystem.lookupType(tempClass + .getCanonicalPath()); + + DecompilationOptions decompilationOptions = new DecompilationOptions(); + decompilationOptions.setSettings(DecompilerSettings.javaDefaults()); + decompilationOptions.setFullDecompilation(true); + + TypeDefinition resolvedType = null; + if (type == null || ((resolvedType = type.resolve()) == null)) { + throw new Exception("Unable to resolve type."); + } + + final Writer writer = createWriter(resolvedType, settings, outputDir); + final boolean writeToFile = writer instanceof FileOutputWriter; + final PlainTextOutput output; + + output = new PlainTextOutput(writer); + + output.setUnicodeOutputEnabled(settings.isUnicodeOutputEnabled()); + + if (settings.getLanguage() instanceof BytecodeLanguage) { + output.setIndentToken(" "); + } + + if (writeToFile) { + System.out.printf("Decompiling %s...\n", resolvedType.getFullName()); + } + + settings.getLanguage().decompileType(resolvedType, output, decompilationOptions); + + writer.flush(); + + if (writeToFile) { + writer.close(); + } + } + + private void decompileJar(String jarFilePath, String outputDir) throws IOException { + DecompilationOptions decompilationOptions = new DecompilationOptions(); + decompilationOptions.setSettings(DecompilerSettings.javaDefaults()); + decompilationOptions.setFullDecompilation(true); + final File jarFile = new File(jarFilePath); + + if (!jarFile.exists()) { + throw new FileNotFoundException("File not found: " + jarFilePath); + } + + final DecompilerSettings settings = decompilationOptions.getSettings(); + settings.setTypeLoader(new InputTypeLoader()); + settings.setExcludeNestedTypes(false); + + + final JarFile jar = new JarFile(jarFile); + final Enumeration entries = jar.entries(); + + final boolean oldShowSyntheticMembers = settings.getShowSyntheticMembers(); + final ITypeLoader oldTypeLoader = settings.getTypeLoader(); + + settings.setShowSyntheticMembers(false); + settings.setTypeLoader(new CompositeTypeLoader(new JarTypeLoader(jar), oldTypeLoader)); + + try { + MetadataSystem metadataSystem = new NoRetryMetadataSystem(settings.getTypeLoader()); + + int classesDecompiled = 0; + + while (entries.hasMoreElements()) { + final JarEntry entry = entries.nextElement(); + final String name = entry.getName(); + + if (!name.endsWith(".class")) { + continue; + } + + final String internalName = StringUtilities.removeRight(name, ".class"); + + try { + decompileType(metadataSystem, internalName, decompilationOptions, outputDir); + + if (++classesDecompiled % 100 == 0) { + metadataSystem = new NoRetryMetadataSystem(settings.getTypeLoader()); + } + } catch (final Throwable t) { + t.printStackTrace(); + } + } + } finally { + settings.setShowSyntheticMembers(oldShowSyntheticMembers); + settings.setTypeLoader(oldTypeLoader); + } + } + + private void decompileType(MetadataSystem metadataSystem, String typeName, DecompilationOptions options, String outputDir) throws IOException { + + final TypeReference type; + final DecompilerSettings settings = options.getSettings(); + + if (typeName.length() == 1) { + // + // Hack to get around classes whose descriptors clash with primitive types. + // + + final MetadataParser parser = new MetadataParser(IMetadataResolver.EMPTY); + final TypeReference reference = parser.parseTypeDescriptor(typeName); + + type = metadataSystem.resolve(reference); + } else { + type = metadataSystem.lookupType(typeName); + } + + final TypeDefinition resolvedType; + + if (type == null || (resolvedType = type.resolve()) == null) { + System.err.printf("!!! ERROR: Failed to load class %s.\n", typeName); + return; + } + + DeobfuscationUtilities.processType(resolvedType); + + if (resolvedType.isNested() || resolvedType.isAnonymous() || resolvedType.isSynthetic()) { + return; + } + + final Writer writer = createWriter(resolvedType, settings, outputDir); + final boolean writeToFile = writer instanceof FileOutputWriter; + final PlainTextOutput output; + + output = new PlainTextOutput(writer); + + output.setUnicodeOutputEnabled(settings.isUnicodeOutputEnabled()); + + if (settings.getLanguage() instanceof BytecodeLanguage) { + output.setIndentToken(" "); + } + + if (writeToFile) { + System.out.printf("Decompiling %s...\n", typeName); + } + + settings.getLanguage().decompileType(resolvedType, output, options); + + writer.flush(); + + if (writeToFile) { + writer.close(); + } + } + + private Writer createWriter(TypeDefinition type, DecompilerSettings settings, String outputDirectory) throws IOException { + + if (StringUtilities.isNullOrWhitespace(outputDirectory)) { + return new OutputStreamWriter( + System.out, + settings.isUnicodeOutputEnabled() ? Charset.forName("UTF-8") + : Charset.defaultCharset() + ); + } + + String outputPath; + String fileName = type.getName() + settings.getLanguage().getFileExtension(); + String packageName = type.getPackageName(); + + if (StringUtilities.isNullOrWhitespace(packageName)) { + outputPath = PathHelper.combine(outputDirectory, fileName); + } else { + outputPath = PathHelper.combine( + outputDirectory, + packageName.replace('.', PathHelper.DirectorySeparator), + fileName + ); + } + + File outputFile = new File(outputPath); + File parentFile = outputFile.getParentFile(); + + if (parentFile != null && !parentFile.exists() && !parentFile.mkdirs()) { + throw new IllegalStateException(String.format("Could not create output directory for file \"%s\".", outputPath)); + } + + if (!outputFile.exists() && !outputFile.createNewFile()) { + throw new IllegalStateException(String.format("Could not create output file \"%s\".", outputPath)); + } + + return new FileOutputWriter(outputFile, settings); + } + + final class FileOutputWriter extends OutputStreamWriter { + private final File file; + + FileOutputWriter(final File file, final DecompilerSettings settings) throws IOException { + super( + new FileOutputStream(file), + settings.isUnicodeOutputEnabled() ? Charset.forName("UTF-8") + : Charset.defaultCharset() + ); + this.file = file; + } + } + + final class NoRetryMetadataSystem extends MetadataSystem { + private final Set _failedTypes = new HashSet<>(); + + NoRetryMetadataSystem(final ITypeLoader typeLoader) { + super(typeLoader); + } + + @Override + protected TypeDefinition resolveType(final String descriptor, final boolean mightBePrimitive) { + if (_failedTypes.contains(descriptor)) { + return null; + } + + final TypeDefinition result = super.resolveType(descriptor, mightBePrimitive); + + if (result == null) { + _failedTypes.add(descriptor); + } + + return result; + } + } +} diff --git a/spoon-decompiler/src/test/java/spoon/JarLauncherTest.java b/spoon-decompiler/src/test/java/spoon/JarLauncherTest.java index f1e9f37d637..fe6055c1109 100644 --- a/spoon-decompiler/src/test/java/spoon/JarLauncherTest.java +++ b/spoon-decompiler/src/test/java/spoon/JarLauncherTest.java @@ -16,15 +16,20 @@ */ package spoon; +import org.apache.commons.io.FileUtils; +import org.junit.Ignore; import org.junit.Test; import spoon.decompiler.CFRDecompiler; +import spoon.decompiler.Decompiler; import spoon.decompiler.FernflowerDecompiler; +import spoon.decompiler.ProcyonDecompiler; import spoon.reflect.CtModel; import spoon.reflect.code.CtLocalVariable; import spoon.reflect.code.CtTry; import spoon.reflect.declaration.CtConstructor; import java.io.File; +import java.io.IOException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -33,38 +38,23 @@ public class JarLauncherTest { @Test - public void testJarLauncher() { - File baseDir = new File("src/test/resources/jarLauncher"); - File pom = new File(baseDir, "pom.xml"); - File jar = new File(baseDir, "helloworld-1.0-SNAPSHOT.jar"); - - File pathToDecompiledRoot = new File(System.getProperty("java.io.tmpdir") + System.getProperty("file.separator") + "spoon-tmp"); - if(pathToDecompiledRoot.exists()) { - pathToDecompiledRoot.delete(); - } - File pathToDecompile = new File(pathToDecompiledRoot,"src/main/java"); - pathToDecompile.mkdirs(); - - JarLauncher launcher = new JarLauncher(jar.getAbsolutePath(), pathToDecompiledRoot.getPath(), pom.getAbsolutePath(), new CFRDecompiler()); - launcher.getEnvironment().setAutoImports(true); - launcher.buildModel(); - CtModel model = launcher.getModel(); - - //contract: all types are decompiled (Sources are produced for each type) - assertEquals(model.getAllTypes().size(), 5); - + public void testJarLauncherWithCFR() throws IOException { + testJarLauncher(new CFRDecompiler()); + } - CtConstructor constructor = (CtConstructor) model.getRootPackage().getFactory().Type().get("se.kth.castor.UseJson").getTypeMembers().get(0); - CtTry tryStmt = (CtTry) constructor.getBody().getStatement(1); - CtLocalVariable var = (CtLocalVariable) tryStmt.getBody().getStatement(0); - //contract: UseJson is correctly decompiled (UseJSON.java contains a local variable declaration) - assertNotNull(var.getType().getTypeDeclaration()); + @Test + public void testJarLauncherWithProcyon() throws IOException { + testJarLauncher(new ProcyonDecompiler()); + } - if(pathToDecompiledRoot.exists()) { - pathToDecompiledRoot.delete(); - } + @Ignore + @Test + public void testJarLauncherWithFernflower() throws IOException { + testJarLauncher(new FernflowerDecompiler()); } + @Ignore + @Test public void testTmpDirDeletion() { File baseDir = new File("src/test/resources/jarLauncher"); File jar = new File(baseDir, "helloworld-1.0-SNAPSHOT.jar"); @@ -87,26 +77,26 @@ public void testTmpDirDeletion() { } - @Test - public void testJarLauncherFernflower() { + public void testJarLauncher(Decompiler decompiler) throws IOException { File baseDir = new File("src/test/resources/jarLauncher"); File pom = new File(baseDir, "pom.xml"); File jar = new File(baseDir, "helloworld-1.0-SNAPSHOT.jar"); File pathToDecompiledRoot = new File(System.getProperty("java.io.tmpdir") + System.getProperty("file.separator") + "spoon-tmp"); if(pathToDecompiledRoot.exists()) { + FileUtils.deleteDirectory(pathToDecompiledRoot); pathToDecompiledRoot.delete(); } File pathToDecompile = new File(pathToDecompiledRoot,"src/main/java"); pathToDecompile.mkdirs(); - JarLauncher launcher = new JarLauncher(jar.getAbsolutePath(), pathToDecompiledRoot.getPath(), pom.getAbsolutePath(), new FernflowerDecompiler()); + JarLauncher launcher = new JarLauncher(jar.getAbsolutePath(), pathToDecompiledRoot.getPath(), pom.getAbsolutePath(),decompiler); launcher.getEnvironment().setAutoImports(true); launcher.buildModel(); CtModel model = launcher.getModel(); //contract: all types are decompiled (Sources are produced for each type) - assertEquals(model.getAllTypes().size(), 5); + assertEquals(5, model.getAllTypes().size()); CtConstructor constructor = (CtConstructor) model.getRootPackage().getFactory().Type().get("se.kth.castor.UseJson").getTypeMembers().get(0); @@ -129,6 +119,18 @@ public void testJarLauncherNoPom() { launcher.getEnvironment().setAutoImports(true); launcher.buildModel(); CtModel model = launcher.getModel(); - assertEquals(model.getAllTypes().size(), 5); + assertEquals(5, model.getAllTypes().size()); + } + + @Ignore + @Test + public void testJarLauncherNoPomFernflower() { + File baseDir = new File("src/test/resources/jarLauncher"); + File jar = new File(baseDir, "helloworld-1.0-SNAPSHOT.jar"); + JarLauncher launcher = new JarLauncher(jar.getAbsolutePath(), null, new FernflowerDecompiler()); + launcher.getEnvironment().setAutoImports(true); + launcher.buildModel(); + CtModel model = launcher.getModel(); + assertEquals(5, model.getAllTypes().size()); } } diff --git a/spoon-decompiler/src/test/java/spoon/decompiler/SpoonClassFileTransformerTest.java b/spoon-decompiler/src/test/java/spoon/decompiler/SpoonClassFileTransformerTest.java index 3b44c5db443..8e31d95af3d 100644 --- a/spoon-decompiler/src/test/java/spoon/decompiler/SpoonClassFileTransformerTest.java +++ b/spoon-decompiler/src/test/java/spoon/decompiler/SpoonClassFileTransformerTest.java @@ -2,6 +2,8 @@ import org.apache.commons.io.FileUtils; import org.junit.Test; +import spoon.reflect.declaration.CtClass; +import spoon.reflect.declaration.CtType; import java.io.File; import java.io.IOException; @@ -58,6 +60,7 @@ public URL getResource(String name) { } } + @Test public void testClassFileTransform() throws ClassNotFoundException, IllegalAccessException, @@ -65,21 +68,65 @@ public void testClassFileTransform() throws ClassNotFoundException, InstantiationException, NoSuchMethodException, IOException { + testClassFileTransform(null); + } + + + @Test + public void testClassFileTransformWithProcyon() throws ClassNotFoundException, + IllegalAccessException, + InvocationTargetException, + InstantiationException, + NoSuchMethodException, + IOException { + testClassFileTransform(new ProcyonDecompiler()); + } + + + @Test + public void testClassFileTransformWithFernflower() throws ClassNotFoundException, + IllegalAccessException, + InvocationTargetException, + InstantiationException, + NoSuchMethodException, + IOException { + testClassFileTransform(new FernflowerDecompiler()); + } + + public void transform(CtType type) { + //Decompiler might create a default constructor with this.transformed=false... or not + //if it does, add a statement at the end of the constructor if not change the default expression + if(type instanceof CtClass) { + if(((CtClass) type).getConstructor().getBody().getStatements().size() <= 1) { + type.getField("transformed").setAssignment(type.getFactory().createCodeSnippetExpression("true")); + } else { + ((CtClass) type).getConstructor().getBody().insertEnd(type.getFactory().createCodeSnippetStatement("this.transformed=true")); + } + } + } + + public void testClassFileTransform(Decompiler decompiler) throws ClassNotFoundException, + IllegalAccessException, + InvocationTargetException, + InstantiationException, + NoSuchMethodException, + IOException { //Setup temporary directories FileUtils.deleteDirectory(TMP_DIR); TMP_DIR.mkdir(); DECOMPILED_DIR.mkdir(); CACHE_DIR.mkdir(); RECOMPILED_DIR.mkdir(); + //type -> type.getField("transformed").setAssignment(type.getFactory().createCodeSnippetExpression("true")), //Create a SpoonClassFileTransformer SpoonClassFileTransformer transformer = new SpoonClassFileTransformer( s -> s.startsWith("se/kth/castor"), - type -> type.getField("transformed").setAssignment(type.getFactory().createCodeSnippetExpression("true")), + type -> transform(type), DECOMPILED_DIR.getPath(), CACHE_DIR.getPath(), RECOMPILED_DIR.getPath(), - null + decompiler ); //Class loaded by cl should be transformed