soot.util.cfgcmd.AltClassLoader Maven / Gradle / Ivy
package soot.util.cfgcmd;
/*-
* #%L
* Soot - a J*va Optimization Framework
* %%
* Copyright (C) 2003 John Jorgensen
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 2.1 of the
* License, or (at your option) any later version.
*
* 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
* GNU General Lesser Public License for more details.
*
* You should have received a copy of the GNU General Lesser Public
* License along with this program. If not, see
* .
* #L%
*/
import java.io.File;
import java.io.FileInputStream;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import soot.G;
import soot.Singletons;
/**
*
* A {@link ClassLoader} that loads specified classes from a different class path than that given by the value of
* java.class.path
in {@link System#getProperties()}.
*
*
*
* This class is part of Soot's test infrastructure. It allows loading multiple implementations of a class with a given name,
* and was written to compare different implementations of Soot's CFG representations.
*
*/
public class AltClassLoader extends ClassLoader {
private static final Logger logger = LoggerFactory.getLogger(AltClassLoader.class);
private final static boolean DEBUG = false;
private String[] locations; // Locations in the alternate
// classpath.
private final Map> alreadyFound = new HashMap>(); // Maps from already loaded
// classnames to their
// Class objects.
private final Map nameToMangledName = new HashMap();// Maps from the names
// of classes to be
// loaded from the alternate
// classpath to mangled
// names to use for them.
private final Map mangledNameToName = new HashMap();// Maps from the mangled names
// of classes back to their
// original names.
/**
* Constructs an AltClassLoader
for inclusion in Soot's global variable manager, {@link G}.
*
* @param g
* guarantees that the constructor may only be called from {@link Singletons}.
*/
public AltClassLoader(Singletons.Global g) {
}
/**
* Returns the single instance of AltClassLoader
, which loads classes from the classpath set by the most
* recent call to its {@link #setAltClassPath}.
*
* @return Soot's AltClassLoader
.
*/
public static AltClassLoader v() {
return G.v().soot_util_cfgcmd_AltClassLoader();
}
/**
* Sets the list of locations in the alternate classpath.
*
* @param classPath
* A list of directories and jar files to search for class files, delimited by {@link File#pathSeparator}.
*/
public void setAltClassPath(String altClassPath) {
List locationList = new LinkedList();
for (StringTokenizer tokens = new StringTokenizer(altClassPath, File.pathSeparator, false); tokens.hasMoreTokens();) {
String location = tokens.nextToken();
locationList.add(location);
}
locations = new String[locationList.size()];
locations = locationList.toArray(locations);
}
/**
* Specifies the set of class names that the AltClassLoader
should load from the alternate classpath instead
* of the regular classpath.
*
* @param classNames[]
* an array containing the names of classes to be loaded from the AltClassLoader.
*/
public void setAltClasses(String[] classNames) {
nameToMangledName.clear();
for (String origName : classNames) {
String mangledName = mangleName(origName);
nameToMangledName.put(origName, mangledName);
mangledNameToName.put(mangledName, origName);
}
}
/**
* Mangles a classname so that it will not be found on the system classpath by our parent class loader, even if there is a
* class with the original name there. We use a crude heuristic to do this that happens to work with the names we have
* needed to mangle to date. The heuristic requires that origName
include at least two dots (i.e., the class
* must be in a package, where the package name has at least two components). More sophisticated possibilities certainly
* exist, but they would require more thorough parsing of the class file.
*
* @param origName
* the name to be mangled.
* @return the mangled name.
* @throws IllegalArgumentException
* if origName
is not amenable to our crude mangling.
*/
private static String mangleName(String origName) throws IllegalArgumentException {
final char dot = '.';
final char dotReplacement = '_';
StringBuffer mangledName = new StringBuffer(origName);
int replacements = 0;
int lastDot = origName.lastIndexOf(dot);
for (int nextDot = lastDot; (nextDot = origName.lastIndexOf(dot, nextDot - 1)) >= 0;) {
mangledName.setCharAt(nextDot, dotReplacement);
replacements++;
}
if (replacements <= 0) {
throw new IllegalArgumentException(
"AltClassLoader.mangleName()'s crude classname mangling cannot deal with " + origName);
}
return mangledName.toString();
}
/**
*
* Loads a class from either the regular classpath, or the alternate classpath, depending on whether it looks like we have
* already mangled its name.
*
*
*
* This method follows the steps provided by
* Ken McCrary's ClasssLoader
* tutorial.
*
*
* @param maybeMangledName
* A string from which the desired class's name can be determined. It may have been mangled by
* {@link AltClassLoader#loadClass(String) AltClassLoader.loadClass()} so that the regular
* ClassLoader
to which we are delegating won't load the class from the regular classpath.
* @return the loaded class.
* @throws ClassNotFoundException
* if the class cannot be loaded.
*
*/
protected Class findClass(String maybeMangledName) throws ClassNotFoundException {
if (DEBUG) {
logger.debug("AltClassLoader.findClass(" + maybeMangledName + ')');
}
Class result = alreadyFound.get(maybeMangledName);
if (result != null) {
return result;
}
String name = mangledNameToName.get(maybeMangledName);
if (name == null) {
name = maybeMangledName;
}
String pathTail = "/" + name.replace('.', File.separatorChar) + ".class";
for (String element : locations) {
String path = element + pathTail;
try {
FileInputStream stream = new FileInputStream(path);
byte[] classBytes = new byte[stream.available()];
stream.read(classBytes);
replaceAltClassNames(classBytes);
result = defineClass(maybeMangledName, classBytes, 0, classBytes.length);
alreadyFound.put(maybeMangledName, result);
stream.close();
return result;
} catch (java.io.IOException e) {
// Try the next location.
} catch (ClassFormatError e) {
if (DEBUG) {
logger.error(e.getMessage(), e);
}
// Try the next location.
}
}
throw new ClassNotFoundException("Unable to find class" + name + " in alternate classpath");
}
/**
*
* Loads a class, from the alternate classpath if the class's name has been included in the list of alternate classes with
* {@link #setAltClasses(String[]) setAltClasses()}, from the regular system classpath otherwise. When a alternate class is
* loaded, its references to other alternate classes are also resolved to the alternate classpath.
*
* @param name
* the name of the class to load.
* @return the loaded class.
* @throws ClassNotFoundException
* if the class cannot be loaded.
*/
public Class loadClass(String name) throws ClassNotFoundException {
if (DEBUG) {
logger.debug("AltClassLoader.loadClass(" + name + ")");
}
String nameForParent = nameToMangledName.get(name);
if (nameForParent == null) {
// This is not an alternate class
nameForParent = name;
}
if (DEBUG) {
logger.debug("AltClassLoader.loadClass asking parent for " + nameForParent);
}
return super.loadClass(nameForParent, false);
}
/**
* Replaces any occurrences in classBytes
of classnames to be loaded from the alternate class path with the
* corresponding mangled names. Of course we should really parse the class pool properly, since the simple-minded, brute
* force replacment done here could produce problems with some combinations of classnames and class contents. But we've got
* away with this so far!
*/
private void replaceAltClassNames(byte[] classBytes) {
for (Map.Entry entry : nameToMangledName.entrySet()) {
String origName = entry.getKey();
origName = origName.replace('.', '/');
String mangledName = entry.getValue();
mangledName = mangledName.replace('.', '/');
findAndReplace(classBytes, stringToUtf8Pattern(origName), stringToUtf8Pattern(mangledName));
findAndReplace(classBytes, stringToTypeStringPattern(origName), stringToTypeStringPattern(mangledName));
}
}
/**
* Returns the bytes that correspond to a CONSTANT_Utf8 constant pool entry containing the passed string.
*/
private static byte[] stringToUtf8Pattern(String s) {
byte[] origBytes = s.getBytes();
int length = origBytes.length;
final byte CONSTANT_Utf8 = 1;
byte[] result = new byte[length + 3];
result[0] = CONSTANT_Utf8;
result[1] = (byte) (length & 0xff00);
result[2] = (byte) (length & 0x00ff);
for (int i = 0; i < length; i++) {
result[i + 3] = origBytes[i];
}
return result;
}
/**
* Returns the bytes that correspond to a type signature string containing the passed string.
*/
private static byte[] stringToTypeStringPattern(String s) {
byte[] origBytes = s.getBytes();
int length = origBytes.length;
byte[] result = new byte[length + 2];
result[0] = (byte) 'L';
for (int i = 0; i < length; i++) {
result[i + 1] = origBytes[i];
}
result[length + 1] = (byte) ';';
return result;
}
/**
* Replaces all occurrences of the pattern
in text
with replacement
.
*
* @throws IllegalArgumentException
* if the lengths of text
and replacement
differ.
*/
private static void findAndReplace(byte[] text, byte[] pattern, byte[] replacement) throws IllegalArgumentException {
int patternLength = pattern.length;
if (patternLength != replacement.length) {
throw new IllegalArgumentException("findAndReplace(): The lengths of the pattern and replacement must match.");
}
int match = 0;
while ((match = findMatch(text, pattern, match)) >= 0) {
replace(text, replacement, match);
match += patternLength;
}
}
/**
* A naive string-searching algorithm for finding a pattern in a byte array.
*
* @param text
* the array to search in.
* @param pattern
* the string of bytes to search for.
* @param start
* the first position in text to search (0-based).
* @return the index in text where the first occurrence of pattern
in text
after
* start
begins. Returns -1 if pattern
does not occur in text
after
* start
.
*/
private static int findMatch(byte[] text, byte[] pattern, int start) {
int textLength = text.length;
int patternLength = pattern.length;
nextBase: for (int base = start; base < textLength; base++) {
for (int t = base, p = 0; p < patternLength; t++, p++) {
if (text[t] != pattern[p]) {
continue nextBase;
}
}
return base;
}
return -1;
}
/**
* Replace the replacement.length
bytes in text
starting at start
with the bytes in
* replacement
.
*
* @throws ArrayIndexOutOfBounds
* if there are not replacement.length
remaining after text[start]
.
*/
private static void replace(byte[] text, byte[] replacement, int start) {
for (int t = start, p = 0; p < replacement.length; t++, p++) {
text[t] = replacement[p];
}
}
/**
*
* A main() entry for basic unit testing.
*
*
*
* Usage: path class ...
*
*/
public static void main(String[] argv) throws ClassNotFoundException {
AltClassLoader.v().setAltClassPath(argv[0]);
for (int i = 1; i < argv.length; i++) {
AltClassLoader.v().setAltClasses(new String[] { argv[i] });
logger.debug("main() loadClass(" + argv[i] + ")");
AltClassLoader.v().loadClass(argv[i]);
}
}
}