Skip to content

Commit

Permalink
feature: add support for Jar and class files analysis with JarLaunche…
Browse files Browse the repository at this point in the history
…r (decompile then build AST) (#2455)
  • Loading branch information
nharrand authored and monperrus committed Sep 27, 2018
1 parent a1e8ee9 commit 409f519
Show file tree
Hide file tree
Showing 11 changed files with 414 additions and 3 deletions.
3 changes: 3 additions & 0 deletions doc/first_analysis_processor.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,6 @@ $ java -classpath /path/to/binary/of/your/processor.jar:spoon-core-{{site.spoon_
2. Specify your processors in fully qualified name (here `processors.CatchProcessor`).
{{site.data.alerts.end}}

## Bytecode analysis

Note that spoon also supports the analysis of bytecode through decompilation. See [javadoc](http://spoon.gforge.inria.fr/mvnsites/spoon-core/apidocs/spoon/JarLauncher.html).
24 changes: 24 additions & 0 deletions doc/launcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,30 @@ CtModel model = launcher.getModel();
```
To avoid invoking maven over and over to build a classpath that has not changed, it is stored in a file `spoon.classpath.tmp` (or depending on the scope `spoon.classpath-app.tmp` or `spoon.classpath-test.tmp`) in the same folder as the `pom.xml`. This classpath will be refreshed is the file is deleted or if it has not been modified since 1h.

## The JarLauncher class

The Spoon `JarLauncher` ([JavaDoc](http://spoon.gforge.inria.fr/mvnsites/spoon-core/apidocs/spoon/JarLauncher.html)) is used to create the AST model from a jar.
It automatically decompiles class files contained in the jar and analyzes them.
If a pom file corresponding to the jar is provided, it will be used to build the classpath containing all dependencies.

```java
//More constructors are available, check the JavaDOc for more information.
JarLauncher launcher = JarLauncher("<path_to_jar>", "<path_to_output_src_dir>", "<path_to_pom>");
launcher.buildModel();
CtModel model = launcher.getModel();
```

Note that the default decompiler [CFR](http://www.benf.org/other/cfr/) can be changed by providing an instance implementing `spoon.decompiler.Decompiler` as a parameter.

```java
JarLauncher launcher = new JarLauncher("<path_to_jar>", "<path_to_output_src_dir>", "<path_to_pom>", new Decompiler() {
@Override
public void decompile(String jarPath) {
//Custom decompiler call
}
});
```

## About the classpath

Spoon analyzes source code. However, this source code may refer to libraries (as a field, parameter, or method return type). There are two cases:
Expand Down
16 changes: 15 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@
<java.test.version>1.8</java.test.version>
<runtime.log>target/velocity.log</runtime.log>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<version.cfr>0.132.0</version.cfr>
</properties>

<distributionManagement>
Expand All @@ -203,13 +204,19 @@
</site>
</distributionManagement>

<!-- This repository is actually needed for revapi to compare against the last Spoon Snapshot -->
<repositories>
<!-- This repository is actually needed for revapi to compare against the last Spoon Snapshot -->
<repository>
<id>maven.inria.fr-snapshot</id>
<name>Maven Repository for Spoon Snapshots</name>
<url>http://maven.inria.fr/artifactory/spoon-public-snapshot</url>
</repository>
<!-- This repository is needed for cfr -->
<repository>
<id>inria</id>
<name>triskell-public-release</name>
<url>http://maven.inria.fr/artifactory/triskell-public-release</url>
</repository>
</repositories>

<dependencies>
Expand Down Expand Up @@ -295,6 +302,13 @@
<artifactId>maven-invoker</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<!-- released on maven.inria.fr -->
<groupId>org.benf</groupId>
<artifactId>cfr</artifactId>
<!-- 0.132 is the version of July 2018 -->
<version>0.132.0</version>
</dependency>
</dependencies>

<build>
Expand Down
168 changes: 168 additions & 0 deletions src/main/java/spoon/JarLauncher.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/**
* 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;

import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import spoon.decompiler.CFRDecompiler;
import spoon.decompiler.Decompiler;
import spoon.support.Experimental;
import spoon.support.compiler.SpoonPom;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;

import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

@Experimental
public class JarLauncher extends Launcher {
File pom;
File jar;
File decompiledRoot;
File decompiledSrc;
Decompiler decompiler;
boolean decompile = false;

/**
* JarLauncher basic constructor. Uses the defauld Decompiler (CFR)
*
* @param jarPath path to the jar to be analyzed
*/
public JarLauncher(String jarPath) {
this(jarPath, null, (String) null);
}


/**
* JarLauncher basic constructor. Uses the defauld Decompiler (CFR)
*
* @param jarPath path to the jar to be analyzed
* @param decompiledSrcPath path to directory where decompiled source will be outputted
*/
public JarLauncher(String jarPath, String decompiledSrcPath) {
this(jarPath, decompiledSrcPath, (String) null);
}

/**
* JarLauncher basic constructor. Uses the defauld Decompiler (CFR)
*
* @param jarPath path to the jar to be analyzed
* @param decompiledSrcPath path to directory where decompiled source will be outputted
* @param pom path to pom associated with the jar to be analyzed
*/
public JarLauncher(String jarPath, String decompiledSrcPath, String pom) {
this(jarPath, decompiledSrcPath, pom, null);
}

/**
* JarLauncher basic constructor. Uses the defauld Decompiler (CFR)
*
* @param jarPath path to the jar to be analyzed
* @param decompiledSrcPath path to directory where decompiled source will be outputted
* @param decompiler Instance implementing {@link spoon.decompiler.Decompiler} to be used
*/
public JarLauncher(String jarPath, String decompiledSrcPath, Decompiler decompiler) {
this(jarPath, decompiledSrcPath, null, decompiler);
}

/**
* JarLauncher constructor. Uses the defauld Decompiler (CFR)
*
* @param jarPath path to the jar to be analyzed
* @param decompiledSrcPath path to directory where decompiled source will be outputted
* @param pom path to pom associated with the jar to be analyzed
* @param decompiler Instance implementing {@link spoon.decompiler.Decompiler} to be used
*/
public JarLauncher(String jarPath, String decompiledSrcPath, String pom, Decompiler decompiler) {
this.decompiler = decompiler;
if (decompiledSrcPath == null) {
decompiledSrcPath = System.getProperty("java.io.tmpdir") + System.getProperty("file.separator") + "spoon-tmp";
decompile = true;
}
this.decompiledRoot = new File(decompiledSrcPath);
if (decompiledRoot.exists() && !decompiledRoot.canWrite()) {
throw new SpoonException("Dir " + decompiledRoot.getPath() + " already exists and is not deletable.");
} else if (decompiledRoot.exists() && decompile) {
decompiledRoot.delete();
}
if (!decompiledRoot.exists()) {
decompiledRoot.mkdirs();
decompile = true;
}
decompiledSrc = new File(decompiledRoot, "src/main/java");
if (!decompiledSrc.exists()) {
decompiledSrc.mkdirs();
decompile = true;
}

if (decompiler == null) {
this.decompiler = getDefaultDecompiler();
}

jar = new File(jarPath);
if (!jar.exists() || !jar.isFile()) {
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;
}
init(pom);
}

private void init(String pomPath) {
//We call the decompiler only if jar has changed since last decompilation.
if (decompile) {
decompiler.decompile(jar.getAbsolutePath());
}


if (pomPath != null) {
File srcPom = new File(pomPath);
if (!srcPom.exists() || !srcPom.isFile()) {
throw new SpoonException("Pom " + srcPom.getPath() + "not found.");
}
try {
pom = new File(decompiledRoot, "pom.xml");
Files.copy(srcPom.toPath(), pom.toPath(), REPLACE_EXISTING);
} catch (IOException e) {
throw new SpoonException("Unable to write " + pom.getPath());
}
try {
SpoonPom pomModel = new SpoonPom(pom.getPath(), null, MavenLauncher.SOURCE_TYPE.APP_SOURCE, getEnvironment());
if (pomModel == null) {
throw new SpoonException("Unable to create the model, pom not found?");
}
getEnvironment().setComplianceLevel(pomModel.getSourceVersion());
String[] classpath = pomModel.buildClassPath(null, MavenLauncher.SOURCE_TYPE.APP_SOURCE, LOGGER, false);
// dependencies
this.getModelBuilder().setSourceClasspath(classpath);
} catch (IOException | XmlPullParserException e) {
throw new SpoonException("Failed to read classpath file.");
}
addInputResource(decompiledSrc.getAbsolutePath());
} else {
addInputResource(decompiledSrc.getAbsolutePath());
}
}

protected Decompiler getDefaultDecompiler() {
return new CFRDecompiler(decompiledSrc);
}

}
37 changes: 37 additions & 0 deletions src/main/java/spoon/decompiler/CFRDecompiler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* 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 java.io.File;

import org.benf.cfr.reader.Main;
import spoon.support.Experimental;

@Experimental
public class CFRDecompiler implements Decompiler {

File outputDir;

public CFRDecompiler(File outputDir) {
this.outputDir = outputDir;
}

@Override
public void decompile(String jarPath) {
Main.main(new String[]{jarPath, "--outputdir", outputDir.getPath()});
}
}
31 changes: 31 additions & 0 deletions src/main/java/spoon/decompiler/Decompiler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* 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 spoon.support.Experimental;

@Experimental
public interface Decompiler {

/**
* Sets the output directory for source generated.
*
* @param jarPath
* Path to jar to be analyzed.
*/
void decompile(String jarPath);
}
2 changes: 0 additions & 2 deletions src/main/java/spoon/support/compiler/SpoonPom.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ public class SpoonPom implements SpoonResource {
/**
* Extract the information from the pom
* @param path the path to the pom
* @return the extracted model
* @throws IOException when the file does not exist
* @throws XmlPullParserException when the file is corrupted
*/
Expand All @@ -83,7 +82,6 @@ public SpoonPom(String path, MavenLauncher.SOURCE_TYPE sourceType, Environment e
* Extract the information from the pom
* @param path the path to the pom
* @param parent the parent pom
* @return the extracted model
* @throws IOException when the file does not exist
* @throws XmlPullParserException when the file is corrupted
*/
Expand Down
60 changes: 60 additions & 0 deletions src/test/java/spoon/JarLauncherTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* 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;

import org.junit.Ignore;
import org.junit.Test;
import spoon.reflect.CtModel;
import spoon.reflect.code.CtLocalVariable;
import spoon.reflect.code.CtTry;
import spoon.reflect.declaration.CtConstructor;

import java.io.File;

import static org.junit.Assert.*;

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");
JarLauncher launcher = new JarLauncher(jar.getAbsolutePath(), null, pom.getAbsolutePath());
launcher.getEnvironment().setAutoImports(true);
launcher.buildModel();
CtModel model = launcher.getModel();
assertEquals(model.getAllTypes().size(), 5);
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);
assertNotNull(var.getType().getTypeDeclaration());
}

@Test
public void testJarLauncherNoPom() {
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);
launcher.getEnvironment().setAutoImports(true);
launcher.buildModel();
CtModel model = launcher.getModel();
assertEquals(model.getAllTypes().size(),5);
}

}
Loading

0 comments on commit 409f519

Please sign in to comment.