613 lines
18 KiB
Java
613 lines
18 KiB
Java
/*******************************************************************************
|
|
* Copyright (c) 2002,2006 IBM Corporation.
|
|
* All rights reserved. This program and the accompanying materials
|
|
* are made available under the terms of the Eclipse Public License v1.0
|
|
* which accompanies this distribution, and is available at
|
|
* http://www.eclipse.org/legal/epl-v10.html
|
|
*
|
|
* Contributors:
|
|
* IBM Corporation - initial API and implementation
|
|
*******************************************************************************/
|
|
package com.ibm.wala.shrikeBT.tools;
|
|
|
|
import java.io.BufferedInputStream;
|
|
import java.io.BufferedOutputStream;
|
|
import java.io.File;
|
|
import java.io.FileFilter;
|
|
import java.io.FileInputStream;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.OutputStream;
|
|
import java.util.ArrayList;
|
|
import java.util.BitSet;
|
|
import java.util.Enumeration;
|
|
import java.util.HashSet;
|
|
import java.util.jar.JarEntry;
|
|
import java.util.jar.JarFile;
|
|
import java.util.jar.JarOutputStream;
|
|
import java.util.zip.ZipEntry;
|
|
|
|
import com.ibm.wala.shrikeBT.analysis.ClassHierarchyProvider;
|
|
|
|
/**
|
|
* This class provides functionality for performing offline instrumentation. It is subclassed with class-toolkit-specific
|
|
* functionality.
|
|
*/
|
|
public abstract class OfflineInstrumenterBase {
|
|
private int inputIndex;
|
|
|
|
final private HashSet<String> entryNames = new HashSet<>();
|
|
|
|
final private ArrayList<Input> inputs = new ArrayList<>();
|
|
|
|
final private BitSet ignoringInputs = new BitSet();
|
|
|
|
private File outputFile;
|
|
|
|
private boolean passUnmodifiedClasses = false;
|
|
|
|
private JarOutputStream outputJar;
|
|
|
|
private JarFile cachedJar;
|
|
|
|
private File cachedJarFile;
|
|
|
|
private ManifestBuilder manifestBuilder;
|
|
|
|
protected ClassHierarchyProvider cha;
|
|
|
|
/**
|
|
* This installs a ManifestBuilder callback that this class will notify whenever an entry has been added to the output zip file.
|
|
*/
|
|
public void setManifestBuilder(ManifestBuilder mb) {
|
|
manifestBuilder = mb;
|
|
}
|
|
|
|
/**
|
|
* Thiscallback is notified whenever an entry has been added to the output zip file.
|
|
*/
|
|
public static interface ManifestBuilder {
|
|
public void addEntry(ZipEntry ze);
|
|
}
|
|
|
|
/**
|
|
* This class represents a resource which can be opened and read; either a file or a JAR entry.
|
|
*/
|
|
abstract class Input {
|
|
private String className;
|
|
|
|
/**
|
|
* Tell us what the classname is supposed to be, if it's a class file.
|
|
*/
|
|
public final void setClassName(String c) {
|
|
className = c.intern();
|
|
}
|
|
|
|
/**
|
|
* Returns the classname if it has been set.
|
|
*/
|
|
public final String getClassName() {
|
|
return className;
|
|
}
|
|
|
|
/**
|
|
* get name of resource used for input
|
|
*/
|
|
public abstract String getInputName();
|
|
|
|
/**
|
|
* Open the resource for reading as a stream.
|
|
*/
|
|
public abstract InputStream open() throws IOException;
|
|
|
|
/**
|
|
* @return true if this resource represents a class, false otherwise
|
|
*/
|
|
public boolean isClass() {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This class represents a JAR file entry. It might or might not be a class; we support non-class JAR resources so that we can
|
|
* copy them to the output JAR if the client requests that.
|
|
*/
|
|
final class JarInput extends Input {
|
|
final private File file;
|
|
|
|
final private String name;
|
|
|
|
/**
|
|
* Select a particular entry from a JAR file on disk.
|
|
*/
|
|
public JarInput(File f, String je) {
|
|
file = f;
|
|
name = je;
|
|
}
|
|
|
|
@Override
|
|
@SuppressWarnings("resource")
|
|
public InputStream open() throws IOException {
|
|
JarFile cachedJar = openCachedJar(file);
|
|
return cachedJar.getInputStream(cachedJar.getEntry(name));
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return file.getPath() + "#" + name;
|
|
}
|
|
|
|
@Override
|
|
public boolean isClass() {
|
|
return name.endsWith(".class");
|
|
}
|
|
|
|
@Override
|
|
public String getInputName() {
|
|
return name;
|
|
}
|
|
|
|
/**
|
|
* Get the underlying ZipEntry corresponding to this resource.
|
|
*/
|
|
@SuppressWarnings("resource")
|
|
public ZipEntry getEntry() throws IOException {
|
|
JarFile cachedJar = openCachedJar(file);
|
|
return cachedJar.getEntry(name);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Open a JAR/ZIP file. This routine caches the last JAR file opened to save effort when the same file is accessed again and
|
|
* again. DO NOT close the file returned by this routine until you've finished with this OfflineInstrumente completely. Also, this
|
|
* JarFile will be closed the next time someone calls openCachedJar.
|
|
*/
|
|
private JarFile openCachedJar(File file) throws IOException {
|
|
if (cachedJarFile != null && cachedJarFile.equals(file)) {
|
|
return cachedJar;
|
|
} else {
|
|
if (cachedJar != null) {
|
|
cachedJar.close();
|
|
}
|
|
cachedJarFile = file;
|
|
cachedJar = new JarFile(file, false);
|
|
return cachedJar;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This class represents a plain old class file in the filesystem. Non-class file resources are not supported.
|
|
*/
|
|
final class ClassInput extends Input {
|
|
final private File file;
|
|
final private File baseDirectory;
|
|
|
|
public ClassInput(File baseDirectory, File f) {
|
|
file = f;
|
|
this.baseDirectory = baseDirectory;
|
|
}
|
|
|
|
@Override
|
|
public InputStream open() throws IOException {
|
|
return new FileInputStream(file);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return file.getPath();
|
|
}
|
|
|
|
@Override
|
|
public String getInputName() {
|
|
int base = baseDirectory.getPath().length() + 1;
|
|
return file.getPath().substring(base);
|
|
}
|
|
}
|
|
|
|
protected OfflineInstrumenterBase() {
|
|
|
|
}
|
|
|
|
public void setClassHierarchyProvider(ClassHierarchyProvider cha) {
|
|
this.cha = cha;
|
|
}
|
|
|
|
/**
|
|
* Set the file in which instrumented classes will be deposited.
|
|
*/
|
|
final public void setOutputJar(File f) {
|
|
outputFile = f;
|
|
}
|
|
|
|
/**
|
|
* Indicate whether classes which are not modified will be put into the output jar anyway.
|
|
*/
|
|
final public void setPassUnmodifiedClasses(boolean pass) {
|
|
passUnmodifiedClasses = pass;
|
|
}
|
|
|
|
/**
|
|
* Add a JAR file containing source classes to instrument.
|
|
*/
|
|
final public void addInputJar(File f) throws IOException {
|
|
try (final JarFile jf = new JarFile(f, false)) {
|
|
for (Enumeration<JarEntry> e = jf.entries(); e.hasMoreElements();) {
|
|
JarEntry entry = e.nextElement();
|
|
String name = entry.getName();
|
|
inputs.add(new JarInput(f, name));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a JAR entry containing a source class to instrument.
|
|
*/
|
|
final public void addInputJarEntry(File f, String name) {
|
|
inputs.add(new JarInput(f, name));
|
|
}
|
|
|
|
/**
|
|
* Add a class file containing a source class to instrument.
|
|
*/
|
|
final public void addInputClass(File baseDirectory, File f) {
|
|
inputs.add(new ClassInput(baseDirectory, f));
|
|
}
|
|
|
|
/**
|
|
* Add a directory containing class files to instrument. All subdirectories are also scanned.
|
|
*
|
|
* @throws IllegalArgumentException if d is null
|
|
*/
|
|
final public void addInputDirectory(File baseDirectory, File d) throws IOException, IllegalArgumentException {
|
|
if (d == null) {
|
|
throw new IllegalArgumentException("d is null");
|
|
}
|
|
File[] fs = d.listFiles(new FileFilter() {
|
|
@Override
|
|
public boolean accept(File f) {
|
|
return f.isDirectory() || f.getName().endsWith(".class");
|
|
}
|
|
});
|
|
if (fs == null) {
|
|
throw new IllegalArgumentException("bad directory " + d.getAbsolutePath());
|
|
}
|
|
for (int i = 0; i < fs.length; i++) {
|
|
File f = fs[i];
|
|
if (f.isDirectory()) {
|
|
addInputDirectory(baseDirectory, f);
|
|
} else {
|
|
addInputClass(baseDirectory, f);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add something to instrument --- the name of a JAR file, a class file, a directory or an entry within a jar file (as
|
|
* filename#entryname). If we can't identify it, nothing is added and we return false.
|
|
*
|
|
* @throws IllegalArgumentException if a is null
|
|
*/
|
|
final public boolean addInputElement(File baseDirectory, String a) throws IOException {
|
|
if (a == null) {
|
|
throw new IllegalArgumentException("a is null");
|
|
}
|
|
try {
|
|
int poundIndex = a.indexOf('#');
|
|
if (poundIndex > 0) {
|
|
addInputJarEntry(new File(a.substring(0, poundIndex)), a.substring(poundIndex + 1));
|
|
return true;
|
|
}
|
|
File f = new File(a);
|
|
if (f.isDirectory()) {
|
|
addInputDirectory(baseDirectory, f);
|
|
return true;
|
|
} else if (f.exists()) {
|
|
if (a.endsWith(".class")) {
|
|
addInputClass(baseDirectory, f);
|
|
return true;
|
|
} else if (a.endsWith(".jar") || a.endsWith(".zip")) {
|
|
addInputJar(new File(a));
|
|
return true;
|
|
}
|
|
}
|
|
} catch (IOException ex) {
|
|
throw new IOException("Error reading input element '" + a + "': " + ex.getMessage());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Parse an argument list to find elements to instrument and the name of the output file. The "-o filename" option selects the
|
|
* output JAR file name. Any other argument not starting with "-" is added to the list of elements to instrument, if it appears to
|
|
* be the name of a class file, JAR file, or directory. If any argument starting with "--" is encountered, the rest of the
|
|
* command-line is considered leftover
|
|
*
|
|
* @return the arguments that were not understood
|
|
* @throws IllegalArgumentException if args == null
|
|
*/
|
|
final public String[] parseStandardArgs(String[] args) throws IllegalArgumentException, IOException {
|
|
if (args == null) {
|
|
throw new IllegalArgumentException("args == null");
|
|
}
|
|
ArrayList<String> leftover = new ArrayList<>();
|
|
|
|
for (int i = 0; i < args.length; i++) {
|
|
String a = args[i];
|
|
if (a == null) {
|
|
throw new IllegalArgumentException("args[" + i + "] is null");
|
|
}
|
|
if (a.equals("-o") && i + 1 < args.length) {
|
|
setOutputJar(new File(args[i + 1]));
|
|
i++;
|
|
continue;
|
|
} else if (!a.startsWith("-")) {
|
|
if (addInputElement(new File(a), a)) {
|
|
continue;
|
|
}
|
|
} else if (a.startsWith("--")) {
|
|
for (int j = i; j < args.length; j++) {
|
|
leftover.add(args[j]);
|
|
}
|
|
break;
|
|
}
|
|
leftover.add(a);
|
|
}
|
|
|
|
String[] r = new String[leftover.size()];
|
|
leftover.toArray(r);
|
|
return r;
|
|
}
|
|
|
|
/**
|
|
* @return the number of source classes to be instrumented
|
|
*/
|
|
final public int getNumInputClasses() {
|
|
return inputs.size();
|
|
}
|
|
|
|
/**
|
|
* Start traversing the source class list from the beginning.
|
|
*/
|
|
final public void beginTraversal() {
|
|
inputIndex = 0;
|
|
}
|
|
|
|
protected abstract Object makeClassFromStream(String inputName, BufferedInputStream s) throws IOException;
|
|
|
|
protected abstract String getClassName(Object cl);
|
|
|
|
protected abstract void writeClassTo(Object cl, Object mods, OutputStream s) throws IOException;
|
|
|
|
final protected Object internalNextClass() throws IOException {
|
|
while (true) {
|
|
if (inputIndex >= inputs.size()) {
|
|
return null;
|
|
} else {
|
|
Input in = inputs.get(inputIndex);
|
|
inputIndex++;
|
|
if (ignoringInputs.get(inputIndex - 1) || !in.isClass()) {
|
|
continue;
|
|
}
|
|
try (final BufferedInputStream s = new BufferedInputStream(in.open())) {
|
|
Object r = makeClassFromStream(in.getInputName(), s);
|
|
String name = getClassName(r);
|
|
in.setClassName(name);
|
|
return r;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static String toEntryName(String className) {
|
|
return className.replace('.', '/') + ".class";
|
|
}
|
|
|
|
/**
|
|
* Get the name of the resource containing the last class returned. This is either a file name (e.g., "com/ibm/Main.class"), or a
|
|
* JAR entry name (e.g., "apps/app.jar#com/ibm/Main.class").
|
|
*
|
|
* @return the resource name, or null if no class has been returned yet
|
|
*/
|
|
final public String getLastClassResourceName() {
|
|
if (inputIndex < 1) {
|
|
return null;
|
|
} else {
|
|
Input in = inputs.get(inputIndex - 1);
|
|
return in.toString();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the File we are storing classes into.
|
|
*/
|
|
final public File getOutputFile() {
|
|
return outputFile;
|
|
}
|
|
|
|
final protected boolean internalOutputModifiedClass(Object cf, String name, Object mods) throws IOException {
|
|
makeOutputJar();
|
|
if (entryNames.contains(name)) {
|
|
return false;
|
|
} else {
|
|
putNextEntry(new ZipEntry(name));
|
|
BufferedOutputStream s = new BufferedOutputStream(outputJar);
|
|
writeClassTo(cf, mods, s);
|
|
s.flush();
|
|
outputJar.closeEntry();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the JAR Comment for the output JAR.
|
|
*/
|
|
final public void setJARComment(String comment) throws IOException, IllegalStateException {
|
|
makeOutputJar();
|
|
outputJar.setComment(comment);
|
|
}
|
|
|
|
final void makeOutputJar() throws IOException, IllegalStateException {
|
|
if (outputJar == null) {
|
|
if (outputFile == null) {
|
|
throw new IllegalStateException("Output file was not set");
|
|
}
|
|
|
|
@SuppressWarnings("resource")
|
|
final FileOutputStream out = new FileOutputStream(outputFile);
|
|
outputJar = new JarOutputStream(out);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Skip the last class returned in every future traversal of the class list.
|
|
*/
|
|
final public void setIgnore(boolean ignore) throws IllegalArgumentException {
|
|
if (inputIndex == 0) {
|
|
throw new IllegalArgumentException("Must get a class before ignoring it");
|
|
}
|
|
|
|
ignoringInputs.set(inputIndex - 1);
|
|
}
|
|
|
|
private static byte[] cachedBuf;
|
|
|
|
private static synchronized byte[] makeBuf() {
|
|
if (cachedBuf != null) {
|
|
byte[] r = cachedBuf;
|
|
cachedBuf = null;
|
|
return r;
|
|
} else {
|
|
return new byte[60000];
|
|
}
|
|
}
|
|
|
|
private static synchronized void releaseBuf(byte[] buf) {
|
|
cachedBuf = buf;
|
|
}
|
|
|
|
public static void copyStream(InputStream in, OutputStream out) throws IllegalArgumentException, IOException {
|
|
if (in == null) {
|
|
throw new IllegalArgumentException("in == null");
|
|
}
|
|
byte[] buf = makeBuf();
|
|
try {
|
|
while (true) {
|
|
int read = in.read(buf);
|
|
if (read < 0) {
|
|
return;
|
|
}
|
|
out.write(buf, 0, read);
|
|
}
|
|
} finally {
|
|
releaseBuf(buf);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a raw ZipEntry to the output JAR. Call endOutputJarEntry() when you're done.
|
|
*
|
|
* @return the OutputStream to be used to write the entry contents
|
|
*/
|
|
final public OutputStream addOutputJarEntry(ZipEntry ze) throws IOException, IllegalStateException {
|
|
if (outputJar == null) {
|
|
throw new IllegalStateException("output jar is null");
|
|
}
|
|
putNextEntry(ze);
|
|
return outputJar;
|
|
}
|
|
|
|
/**
|
|
* Complete and flush the entry initiated by addOutputJarEntry.
|
|
*/
|
|
final public void endOutputJarEntry() throws IOException, IllegalStateException {
|
|
if (outputJar == null) {
|
|
throw new IllegalStateException("output jar is null");
|
|
}
|
|
outputJar.closeEntry();
|
|
}
|
|
|
|
/**
|
|
* Call this to copy any unmodified classes to the output. This is called automatically by close(); you should only call this if
|
|
* you want to write an entry to the JAR file *after* the unmodified classes. This will only ever be called once per output JAR.
|
|
*/
|
|
final public void writeUnmodifiedClasses() throws IOException, IllegalStateException {
|
|
passUnmodifiedClasses = false;
|
|
makeOutputJar();
|
|
for (int i = 0; i < inputs.size(); i++) {
|
|
Input in = inputs.get(i);
|
|
if (!in.isClass()) {
|
|
if (in instanceof JarInput) {
|
|
JarInput jin = (JarInput) in;
|
|
ZipEntry entry = jin.getEntry();
|
|
try (final InputStream s = jin.open()) {
|
|
ZipEntry newEntry = new ZipEntry(entry.getName());
|
|
newEntry.setComment(entry.getComment());
|
|
newEntry.setExtra(entry.getExtra());
|
|
newEntry.setTime(entry.getTime());
|
|
putNextEntry(newEntry);
|
|
copyStream(s, outputJar);
|
|
outputJar.closeEntry();
|
|
}
|
|
} else {
|
|
throw new Error("Unknown non-class input: " + in);
|
|
}
|
|
} else {
|
|
String name = in.getClassName();
|
|
if (name == null) {
|
|
try (final BufferedInputStream s = new BufferedInputStream(in.open(), 65536)) {
|
|
Object cl = makeClassFromStream(in.getInputName(), s);
|
|
String entryName = toEntryName(getClassName(cl));
|
|
if (!entryNames.contains(entryName)) {
|
|
putNextEntry(new ZipEntry(entryName));
|
|
BufferedOutputStream clOut = new BufferedOutputStream(outputJar);
|
|
writeClassTo(cl, null, clOut);
|
|
clOut.flush();
|
|
outputJar.closeEntry();
|
|
}
|
|
}
|
|
} else {
|
|
String entryName = toEntryName(name);
|
|
if (!entryNames.contains(entryName)) {
|
|
try (final BufferedInputStream s = new BufferedInputStream(in.open())) {
|
|
putNextEntry(new ZipEntry(entryName));
|
|
BufferedOutputStream clOut = new BufferedOutputStream(outputJar);
|
|
copyStream(s, clOut);
|
|
clOut.flush();
|
|
outputJar.closeEntry();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Call this when you're done modifying classes.
|
|
*/
|
|
final public void close() throws IOException, IllegalStateException {
|
|
if (passUnmodifiedClasses) {
|
|
writeUnmodifiedClasses();
|
|
}
|
|
|
|
if (outputJar != null) {
|
|
outputJar.close();
|
|
}
|
|
|
|
if (cachedJar != null) {
|
|
cachedJar.close();
|
|
}
|
|
}
|
|
|
|
private void putNextEntry(ZipEntry newEntry) throws IOException, IllegalStateException {
|
|
if (outputJar == null) {
|
|
throw new IllegalStateException();
|
|
}
|
|
outputJar.putNextEntry(newEntry);
|
|
entryNames.add(newEntry.getName());
|
|
if (manifestBuilder != null) {
|
|
manifestBuilder.addEntry(newEntry);
|
|
}
|
|
}
|
|
}
|