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