| /* |
| * [The "BSD licence"] |
| * Copyright (c) 2010 Ben Gruver (JesusFreke) |
| * All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * 3. The name of the author may not be used to endorse or promote products |
| * derived from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR |
| * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
| * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. |
| * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, |
| * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
| * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
| * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| package org.jf.dexlib.Code.Analysis; |
| |
| import org.jf.dexlib.*; |
| import org.jf.dexlib.Util.AccessFlags; |
| import org.jf.dexlib.Util.ExceptionWithContext; |
| import org.jf.dexlib.Util.SparseArray; |
| |
| import javax.annotation.Nonnull; |
| import javax.annotation.Nullable; |
| import java.io.File; |
| import java.util.*; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import static org.jf.dexlib.ClassDataItem.EncodedField; |
| import static org.jf.dexlib.ClassDataItem.EncodedMethod; |
| |
| public class ClassPath { |
| private static ClassPath theClassPath = null; |
| |
| /** |
| * The current version of dalvik in master(AOSP) has a slight change to the way the |
| * virtual tables are computed. This should be set to true to use the new logic. |
| * TODO: set this based on api level, once it's present in a released version of Android |
| */ |
| private boolean checkPackagePrivateAccess; |
| |
| private final HashMap<String, ClassDef> classDefs; |
| protected ClassDef javaLangObjectClassDef; //cached ClassDef for Ljava/lang/Object; |
| |
| // Contains the classes that we haven't loaded yet |
| private HashMap<String, UnresolvedClassInfo> unloadedClasses; |
| |
| private static final Pattern dalvikCacheOdexPattern = Pattern.compile("@([^@]+)@classes.dex$"); |
| |
| /** |
| * Initialize the class path using the dependencies from an odex file |
| * @param classPathDirs The directories to search for boot class path files |
| * @param extraBootClassPathEntries any extra entries that should be added after the entries that are read |
| * from the odex file |
| * @param dexFilePath The path of the dex file (used for error reporting purposes only) |
| * @param dexFile The DexFile to load - it must represents an odex file |
| */ |
| public static void InitializeClassPathFromOdex(String[] classPathDirs, String[] extraBootClassPathEntries, |
| String dexFilePath, DexFile dexFile, |
| boolean checkPackagePrivateAccess) { |
| if (!dexFile.isOdex()) { |
| throw new ExceptionWithContext("Cannot use InitialiazeClassPathFromOdex with a non-odex DexFile"); |
| } |
| |
| if (theClassPath != null) { |
| throw new ExceptionWithContext("Cannot initialize ClassPath multiple times"); |
| } |
| |
| OdexDependencies odexDependencies = dexFile.getOdexDependencies(); |
| |
| String[] bootClassPath = new String[odexDependencies.getDependencyCount()]; |
| for (int i=0; i<bootClassPath.length; i++) { |
| String dependency = odexDependencies.getDependency(i); |
| |
| if (dependency.endsWith(".odex")) { |
| int slashIndex = dependency.lastIndexOf("/"); |
| |
| if (slashIndex != -1) { |
| dependency = dependency.substring(slashIndex+1); |
| } |
| } else if (dependency.endsWith("@classes.dex")) { |
| Matcher m = dalvikCacheOdexPattern.matcher(dependency); |
| |
| if (!m.find()) { |
| throw new ExceptionWithContext(String.format("Cannot parse dependency value %s", dependency)); |
| } |
| |
| dependency = m.group(1); |
| } else { |
| throw new ExceptionWithContext(String.format("Cannot parse dependency value %s", dependency)); |
| } |
| |
| bootClassPath[i] = dependency; |
| } |
| |
| theClassPath = new ClassPath(); |
| theClassPath.initClassPath(classPathDirs, bootClassPath, extraBootClassPathEntries, dexFilePath, dexFile, |
| checkPackagePrivateAccess); |
| } |
| |
| /** |
| * Initialize the class path using the given boot class path entries |
| * @param classPathDirs The directories to search for boot class path files |
| * @param bootClassPath A list of the boot class path entries to search for and load |
| * @param dexFilePath The path of the dex file (used for error reporting purposes only) |
| * @param dexFile the DexFile to load |
| * classes |
| */ |
| public static void InitializeClassPath(String[] classPathDirs, String[] bootClassPath, |
| String[] extraBootClassPathEntries, String dexFilePath, DexFile dexFile, |
| boolean checkPackagePrivateAccess) { |
| if (theClassPath != null) { |
| throw new ExceptionWithContext("Cannot initialize ClassPath multiple times"); |
| } |
| |
| theClassPath = new ClassPath(); |
| theClassPath.initClassPath(classPathDirs, bootClassPath, extraBootClassPathEntries, dexFilePath, dexFile, |
| checkPackagePrivateAccess); |
| } |
| |
| private ClassPath() { |
| classDefs = new HashMap<String, ClassDef>(); |
| } |
| |
| private void initClassPath(String[] classPathDirs, String[] bootClassPath, String[] extraBootClassPathEntries, |
| String dexFilePath, DexFile dexFile, boolean checkPackagePrivateAccess) { |
| this.checkPackagePrivateAccess = checkPackagePrivateAccess; |
| unloadedClasses = new LinkedHashMap<String, UnresolvedClassInfo>(); |
| |
| if (bootClassPath != null) { |
| for (String bootClassPathEntry: bootClassPath) { |
| loadBootClassPath(classPathDirs, bootClassPathEntry); |
| } |
| } |
| |
| if (extraBootClassPathEntries != null) { |
| for (String bootClassPathEntry: extraBootClassPathEntries) { |
| loadBootClassPath(classPathDirs, bootClassPathEntry); |
| } |
| } |
| |
| if (dexFile != null) { |
| loadDexFile(dexFilePath, dexFile); |
| } |
| |
| javaLangObjectClassDef = getClassDef("Ljava/lang/Object;", false); |
| |
| for (String primitiveType: new String[]{"Z", "B", "S", "C", "I", "J", "F", "D"}) { |
| ClassDef classDef = new PrimitiveClassDef(primitiveType); |
| classDefs.put(primitiveType, classDef); |
| } |
| } |
| |
| private void loadBootClassPath(String[] classPathDirs, String bootClassPathEntry) { |
| for (String classPathDir: classPathDirs) { |
| File file = null; |
| DexFile dexFile = null; |
| |
| int extIndex = bootClassPathEntry.lastIndexOf("."); |
| |
| String baseEntry; |
| if (extIndex == -1) { |
| baseEntry = bootClassPathEntry; |
| } else { |
| baseEntry = bootClassPathEntry.substring(0, extIndex); |
| } |
| |
| for (String ext: new String[]{"", ".odex", ".jar", ".apk", ".zip"}) { |
| if (ext.length() == 0) { |
| file = new File(classPathDir, bootClassPathEntry); |
| } else { |
| file = new File(classPathDir, baseEntry + ext); |
| } |
| |
| if (file.exists()) { |
| if (!file.canRead()) { |
| System.err.println(String.format("warning: cannot open %s for reading. Will continue " + |
| "looking.", file.getPath())); |
| continue; |
| } |
| |
| try { |
| dexFile = new DexFile(file, false, true); |
| } catch (DexFile.NoClassesDexException ex) { |
| continue; |
| } catch (Exception ex) { |
| throw ExceptionWithContext.withContext(ex, "Error while reading boot class path entry \"" + |
| bootClassPathEntry + "\"."); |
| } |
| } |
| } |
| if (dexFile == null) { |
| continue; |
| } |
| |
| try { |
| loadDexFile(file.getPath(), dexFile); |
| } catch (Exception ex) { |
| throw ExceptionWithContext.withContext(ex, |
| String.format("Error while loading boot classpath entry %s", bootClassPathEntry)); |
| } |
| return; |
| } |
| throw new ExceptionWithContext(String.format("Cannot locate boot class path file %s", bootClassPathEntry)); |
| } |
| |
| private void loadDexFile(String dexFilePath, DexFile dexFile) { |
| for (ClassDefItem classDefItem: dexFile.ClassDefsSection.getItems()) { |
| try { |
| UnresolvedClassInfo unresolvedClassInfo = new UnresolvedClassInfo(dexFilePath, classDefItem); |
| |
| if (!unloadedClasses.containsKey(unresolvedClassInfo.classType)) { |
| unloadedClasses.put(unresolvedClassInfo.classType, unresolvedClassInfo); |
| } |
| } catch (Exception ex) { |
| throw ExceptionWithContext.withContext(ex, String.format("Error while loading class %s", |
| classDefItem.getClassType().getTypeDescriptor())); |
| } |
| } |
| } |
| |
| /** |
| * This method loads the given class (and any dependent classes, as needed), removing them from unloadedClasses |
| * @param classType the class to load |
| * @return the newly loaded ClassDef object for the given class, or null if the class cannot be found |
| */ |
| @Nullable |
| private static ClassDef loadClassDef(String classType) { |
| ClassDef classDef = null; |
| |
| UnresolvedClassInfo classInfo = theClassPath.unloadedClasses.get(classType); |
| if (classInfo == null) { |
| return null; |
| } |
| |
| try { |
| classDef = new ClassDef(classInfo); |
| theClassPath.classDefs.put(classDef.classType, classDef); |
| } catch (Exception ex) { |
| throw ExceptionWithContext.withContext(ex, String.format("Error while loading class %s from file %s", |
| classInfo.classType, classInfo.dexFilePath)); |
| } |
| theClassPath.unloadedClasses.remove(classType); |
| |
| return classDef; |
| } |
| |
| @Nonnull |
| public static ClassDef getClassDef(String classType, boolean createUnresolvedClassDef) { |
| ClassDef classDef = theClassPath.classDefs.get(classType); |
| if (classDef == null) { |
| //if it's an array class, try to create it |
| if (classType.charAt(0) == '[') { |
| return theClassPath.createArrayClassDef(classType); |
| } else { |
| try { |
| classDef = loadClassDef(classType); |
| if (classDef == null) { |
| throw new ExceptionWithContext( |
| String.format("Could not find definition for class %s", classType)); |
| } |
| } catch (Exception ex) { |
| RuntimeException exWithContext = ExceptionWithContext.withContext(ex, |
| String.format("Error while loading ClassPath class %s", classType)); |
| if (createUnresolvedClassDef) { |
| //TODO: add warning message |
| return theClassPath.createUnresolvedClassDef(classType); |
| } else { |
| throw exWithContext; |
| } |
| } |
| } |
| } |
| return classDef; |
| } |
| |
| public static ClassDef getClassDef(String classType) { |
| return getClassDef(classType, true); |
| } |
| |
| public static ClassDef getClassDef(TypeIdItem classType) { |
| return getClassDef(classType.getTypeDescriptor()); |
| } |
| |
| public static ClassDef getClassDef(TypeIdItem classType, boolean creatUnresolvedClassDef) { |
| return getClassDef(classType.getTypeDescriptor(), creatUnresolvedClassDef); |
| } |
| |
| //256 [ characters |
| private static final String arrayPrefix = "[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[" + |
| "[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[" + |
| "[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[["; |
| private static ClassDef getArrayClassDefByElementClassAndDimension(ClassDef classDef, int arrayDimension) { |
| return getClassDef(arrayPrefix.substring(256 - arrayDimension) + classDef.classType); |
| } |
| |
| private static ClassDef unresolvedObjectClassDef = null; |
| public static ClassDef getUnresolvedObjectClassDef() { |
| if (unresolvedObjectClassDef == null) { |
| unresolvedObjectClassDef = new UnresolvedClassDef("Ljava/lang/Object;"); |
| } |
| return unresolvedObjectClassDef; |
| } |
| |
| private ClassDef createUnresolvedClassDef(String classType) { |
| assert classType.charAt(0) == 'L'; |
| |
| UnresolvedClassDef unresolvedClassDef = new UnresolvedClassDef(classType); |
| classDefs.put(classType, unresolvedClassDef); |
| return unresolvedClassDef; |
| } |
| |
| private ClassDef createArrayClassDef(String arrayClassName) { |
| assert arrayClassName != null; |
| assert arrayClassName.charAt(0) == '['; |
| |
| ArrayClassDef arrayClassDef = new ArrayClassDef(arrayClassName); |
| if (arrayClassDef.elementClass == null) { |
| return null; |
| } |
| |
| classDefs.put(arrayClassName, arrayClassDef); |
| return arrayClassDef; |
| } |
| |
| public static ClassDef getCommonSuperclass(ClassDef class1, ClassDef class2) { |
| if (class1 == class2) { |
| return class1; |
| } |
| |
| if (class1 == null) { |
| return class2; |
| } |
| |
| if (class2 == null) { |
| return class1; |
| } |
| |
| //TODO: do we want to handle primitive types here? I don't think so.. (if not, add assert) |
| |
| if (class2.isInterface) { |
| if (class1.implementsInterface(class2)) { |
| return class2; |
| } |
| return theClassPath.javaLangObjectClassDef; |
| } |
| |
| if (class1.isInterface) { |
| if (class2.implementsInterface(class1)) { |
| return class1; |
| } |
| return theClassPath.javaLangObjectClassDef; |
| } |
| |
| if (class1 instanceof ArrayClassDef && class2 instanceof ArrayClassDef) { |
| return getCommonArraySuperclass((ArrayClassDef)class1, (ArrayClassDef)class2); |
| } |
| |
| //we've got two non-array reference types. Find the class depth of each, and then move up the longer one |
| //so that both classes are at the same class depth, and then move each class up until they match |
| |
| //we don't strictly need to keep track of the class depth separately, but it's probably slightly faster |
| //to do so, rather than calling getClassDepth() many times |
| int class1Depth = class1.getClassDepth(); |
| int class2Depth = class2.getClassDepth(); |
| |
| while (class1Depth > class2Depth) { |
| class1 = class1.superclass; |
| class1Depth--; |
| } |
| |
| while (class2Depth > class1Depth) { |
| class2 = class2.superclass; |
| class2Depth--; |
| } |
| |
| while (class1Depth > 0) { |
| if (class1 == class2) { |
| return class1; |
| } |
| class1 = class1.superclass; |
| class1Depth--; |
| class2 = class2.superclass; |
| class2Depth--; |
| } |
| |
| return class1; |
| } |
| |
| private static ClassDef getCommonArraySuperclass(ArrayClassDef class1, ArrayClassDef class2) { |
| assert class1 != class2; |
| |
| //If one of the arrays is a primitive array, then the only option is to return java.lang.Object |
| //TODO: might it be possible to merge something like int[] and short[] into int[]? (I don't think so..) |
| if (class1.elementClass instanceof PrimitiveClassDef || class2.elementClass instanceof PrimitiveClassDef) { |
| return theClassPath.javaLangObjectClassDef; |
| } |
| |
| //if the two arrays have the same number of dimensions, then we should return an array class with the |
| //same number of dimensions, for the common superclass of the 2 element classes |
| if (class1.arrayDimensions == class2.arrayDimensions) { |
| ClassDef commonElementClass; |
| if (class1.elementClass instanceof UnresolvedClassDef || |
| class2.elementClass instanceof UnresolvedClassDef) { |
| commonElementClass = ClassPath.getUnresolvedObjectClassDef(); |
| } else { |
| commonElementClass = getCommonSuperclass(class1.elementClass, class2.elementClass); |
| } |
| return getArrayClassDefByElementClassAndDimension(commonElementClass, class1.arrayDimensions); |
| } |
| |
| //something like String[][][] and String[][] should be merged to Object[][] |
| //this also holds when the element classes aren't the same (but are both reference types) |
| int dimensions = Math.min(class1.arrayDimensions, class2.arrayDimensions); |
| return getArrayClassDefByElementClassAndDimension(theClassPath.javaLangObjectClassDef, dimensions); |
| } |
| |
| public static class ArrayClassDef extends ClassDef { |
| private final ClassDef elementClass; |
| private final int arrayDimensions; |
| |
| protected ArrayClassDef(String arrayClassType) { |
| super(arrayClassType, ClassDef.ArrayClassDef); |
| assert arrayClassType.charAt(0) == '['; |
| |
| int i=0; |
| while (arrayClassType.charAt(i) == '[') i++; |
| |
| String elementClassType = arrayClassType.substring(i); |
| |
| if (i>256) { |
| throw new ExceptionWithContext("Error while creating array class for element type " + elementClassType + |
| " with " + i + " dimensions. The maximum number of dimensions is 256"); |
| } |
| |
| try { |
| elementClass = ClassPath.getClassDef(arrayClassType.substring(i)); |
| } catch (Exception ex) { |
| throw ExceptionWithContext.withContext(ex, "Error while creating array class " + arrayClassType); |
| } |
| arrayDimensions = i; |
| } |
| |
| /** |
| * Returns the "base" element class of the array. |
| * |
| * For example, for a multi-dimensional array of strings ([[Ljava/lang/String;), this method would return |
| * Ljava/lang/String; |
| * @return the "base" element class of the array |
| */ |
| public ClassDef getBaseElementClass() { |
| return elementClass; |
| } |
| |
| /** |
| * Returns the "immediate" element class of the array. |
| * |
| * For example, for a multi-dimensional array of stings with 2 dimensions ([[Ljava/lang/String;), this method |
| * would return [Ljava/lang/String; |
| * @return the immediate element class of the array |
| */ |
| public ClassDef getImmediateElementClass() { |
| if (arrayDimensions == 1) { |
| return elementClass; |
| } |
| return getArrayClassDefByElementClassAndDimension(elementClass, arrayDimensions - 1); |
| } |
| |
| public int getArrayDimensions() { |
| return arrayDimensions; |
| } |
| |
| @Override |
| public boolean extendsClass(ClassDef superclassDef) { |
| if (!(superclassDef instanceof ArrayClassDef)) { |
| if (superclassDef == ClassPath.theClassPath.javaLangObjectClassDef) { |
| return true; |
| } else if (superclassDef.isInterface) { |
| return this.implementsInterface(superclassDef); |
| } |
| return false; |
| } |
| |
| ArrayClassDef arraySuperclassDef = (ArrayClassDef)superclassDef; |
| if (this.arrayDimensions == arraySuperclassDef.arrayDimensions) { |
| ClassDef baseElementClass = arraySuperclassDef.getBaseElementClass(); |
| |
| if (baseElementClass.isInterface) { |
| return true; |
| } |
| |
| return baseElementClass.extendsClass(arraySuperclassDef.getBaseElementClass()); |
| } else if (this.arrayDimensions > arraySuperclassDef.arrayDimensions) { |
| ClassDef baseElementClass = arraySuperclassDef.getBaseElementClass(); |
| if (baseElementClass.isInterface) { |
| return true; |
| } |
| |
| if (baseElementClass == ClassPath.theClassPath.javaLangObjectClassDef) { |
| return true; |
| } |
| return false; |
| } |
| return false; |
| } |
| } |
| |
| public static class PrimitiveClassDef extends ClassDef { |
| protected PrimitiveClassDef(String primitiveClassType) { |
| super(primitiveClassType, ClassDef.PrimitiveClassDef); |
| assert primitiveClassType.charAt(0) != 'L' && primitiveClassType.charAt(0) != '['; |
| } |
| } |
| |
| public static class UnresolvedClassDef extends ClassDef { |
| protected UnresolvedClassDef(String unresolvedClassDef) { |
| super(unresolvedClassDef, ClassDef.UnresolvedClassDef); |
| assert unresolvedClassDef.charAt(0) == 'L'; |
| } |
| |
| protected ValidationException unresolvedValidationException() { |
| return new ValidationException(String.format("class %s cannot be resolved.", this.getClassType())); |
| } |
| |
| public ClassDef getSuperclass() { |
| return theClassPath.javaLangObjectClassDef; |
| } |
| |
| public int getClassDepth() { |
| throw unresolvedValidationException(); |
| } |
| |
| public boolean isInterface() { |
| throw unresolvedValidationException(); |
| } |
| |
| public boolean extendsClass(ClassDef superclassDef) { |
| if (superclassDef != theClassPath.javaLangObjectClassDef && superclassDef != this) { |
| throw unresolvedValidationException(); |
| } |
| return true; |
| } |
| |
| public boolean implementsInterface(ClassDef interfaceDef) { |
| throw unresolvedValidationException(); |
| } |
| |
| public boolean hasVirtualMethod(String method) { |
| if (!super.hasVirtualMethod(method)) { |
| throw unresolvedValidationException(); |
| } |
| return true; |
| } |
| } |
| |
| public static class FieldDef { |
| public final String definingClass; |
| public final String name; |
| public final String type; |
| |
| public FieldDef(String definingClass, String name, String type) { |
| this.definingClass = definingClass; |
| this.name = name; |
| this.type = type; |
| } |
| } |
| |
| public static class ClassDef implements Comparable<ClassDef> { |
| private final String classType; |
| private final ClassDef superclass; |
| /** |
| * This is a list of all of the interfaces that a class implements, either directly or indirectly. It includes |
| * all interfaces implemented by the superclass, and all super-interfaces of any implemented interface. The |
| * intention is to make it easier to determine whether the class implements a given interface or not. |
| */ |
| private final TreeSet<ClassDef> implementedInterfaces; |
| |
| private final boolean isInterface; |
| |
| private final int classDepth; |
| |
| // classes can only be public or package-private. Internally, any private/protected inner class is actually |
| // package-private. |
| private final boolean isPublic; |
| |
| private final VirtualMethod[] vtable; |
| |
| //this maps a method name of the form method(III)Ljava/lang/String; to an integer |
| //If the value is non-negative, it is a vtable index |
| //If it is -1, it is a non-static direct method, |
| //If it is -2, it is a static method |
| private final HashMap<String, Integer> methodLookup; |
| |
| private final SparseArray<FieldDef> instanceFields; |
| |
| public final static int ArrayClassDef = 0; |
| public final static int PrimitiveClassDef = 1; |
| public final static int UnresolvedClassDef = 2; |
| |
| private final static int DirectMethod = -1; |
| private final static int StaticMethod = -2; |
| |
| /** |
| * The following fields are used only during the initial loading of classes, and are set to null afterwards |
| * TODO: free these |
| */ |
| |
| //This is only the virtual methods that this class declares itself. |
| private VirtualMethod[] virtualMethods; |
| //this is a list of all the interfaces that the class implements directory, or any super interfaces of those |
| //interfaces. It is generated in such a way that it is ordered in the same way as dalvik's ClassObject.iftable, |
| private LinkedHashMap<String, ClassDef> interfaceTable; |
| |
| /** |
| * This constructor is used for the ArrayClassDef, PrimitiveClassDef and UnresolvedClassDef subclasses |
| * @param classType the class type |
| * @param classFlavor one of ArrayClassDef, PrimitiveClassDef or UnresolvedClassDef |
| */ |
| protected ClassDef(String classType, int classFlavor) { |
| if (classFlavor == ArrayClassDef) { |
| assert classType.charAt(0) == '['; |
| this.classType = classType; |
| this.superclass = ClassPath.theClassPath.javaLangObjectClassDef; |
| implementedInterfaces = new TreeSet<ClassDef>(); |
| implementedInterfaces.add(ClassPath.getClassDef("Ljava/lang/Cloneable;")); |
| implementedInterfaces.add(ClassPath.getClassDef("Ljava/io/Serializable;")); |
| isInterface = false; |
| isPublic = true; |
| |
| vtable = superclass.vtable; |
| methodLookup = superclass.methodLookup; |
| |
| instanceFields = superclass.instanceFields; |
| classDepth = 1; //1 off from java.lang.Object |
| |
| virtualMethods = null; |
| interfaceTable = null; |
| } else if (classFlavor == PrimitiveClassDef) { |
| //primitive type |
| assert classType.charAt(0) != '[' && classType.charAt(0) != 'L'; |
| |
| this.classType = classType; |
| this.superclass = null; |
| implementedInterfaces = null; |
| isInterface = false; |
| isPublic = true; |
| vtable = null; |
| methodLookup = null; |
| instanceFields = null; |
| classDepth = 0; //TODO: maybe use -1 to indicate not applicable? |
| |
| virtualMethods = null; |
| interfaceTable = null; |
| } else /*if (classFlavor == UnresolvedClassDef)*/ { |
| assert classType.charAt(0) == 'L'; |
| this.classType = classType; |
| this.superclass = ClassPath.getClassDef("Ljava/lang/Object;"); |
| implementedInterfaces = new TreeSet<ClassDef>(); |
| isInterface = false; |
| isPublic = true; |
| |
| vtable = superclass.vtable; |
| methodLookup = superclass.methodLookup; |
| |
| instanceFields = superclass.instanceFields; |
| classDepth = 1; //1 off from java.lang.Object |
| |
| virtualMethods = null; |
| interfaceTable = null; |
| } |
| } |
| |
| protected ClassDef(UnresolvedClassInfo classInfo) { |
| classType = classInfo.classType; |
| isPublic = classInfo.isPublic; |
| isInterface = classInfo.isInterface; |
| |
| superclass = loadSuperclass(classInfo); |
| if (superclass == null) { |
| classDepth = 0; |
| } else { |
| classDepth = superclass.classDepth + 1; |
| } |
| |
| implementedInterfaces = loadAllImplementedInterfaces(classInfo); |
| |
| //TODO: we can probably get away with only creating the interface table for interface types |
| interfaceTable = loadInterfaceTable(classInfo); |
| virtualMethods = classInfo.virtualMethods; |
| vtable = loadVtable(classInfo); |
| |
| int directMethodCount = 0; |
| if (classInfo.directMethods != null) { |
| directMethodCount = classInfo.directMethods.length; |
| } |
| methodLookup = new HashMap<String, Integer>((int)Math.ceil(((vtable.length + directMethodCount)/ .7f)), .75f); |
| for (int i=0; i<vtable.length; i++) { |
| methodLookup.put(vtable[i].method, i); |
| } |
| if (directMethodCount > 0) { |
| for (int i=0; i<classInfo.directMethods.length; i++) { |
| if (classInfo.staticMethods[i]) { |
| methodLookup.put(classInfo.directMethods[i], StaticMethod); |
| } else { |
| methodLookup.put(classInfo.directMethods[i], DirectMethod); |
| } |
| } |
| } |
| |
| instanceFields = loadFields(classInfo); |
| } |
| |
| public String getClassType() { |
| return classType; |
| } |
| |
| public ClassDef getSuperclass() { |
| return superclass; |
| } |
| |
| public int getClassDepth() { |
| return classDepth; |
| } |
| |
| public boolean isInterface() { |
| return this.isInterface; |
| } |
| |
| public boolean isPublic() { |
| return this.isPublic; |
| } |
| |
| public boolean extendsClass(ClassDef superclassDef) { |
| if (superclassDef == null) { |
| return false; |
| } |
| |
| if (this == superclassDef) { |
| return true; |
| } |
| |
| if (superclassDef instanceof UnresolvedClassDef) { |
| throw ((UnresolvedClassDef)superclassDef).unresolvedValidationException(); |
| } |
| |
| int superclassDepth = superclassDef.classDepth; |
| ClassDef ancestor = this; |
| while (ancestor.classDepth > superclassDepth) { |
| ancestor = ancestor.getSuperclass(); |
| } |
| |
| return ancestor == superclassDef; |
| } |
| |
| /** |
| * Returns true if this class implements the given interface. This searches the interfaces that this class |
| * directly implements, any interface implemented by this class's superclasses, and any super-interface of |
| * any of these interfaces. |
| * @param interfaceDef the interface |
| * @return true if this class implements the given interface |
| */ |
| public boolean implementsInterface(ClassDef interfaceDef) { |
| assert !(interfaceDef instanceof UnresolvedClassDef); |
| return implementedInterfaces.contains(interfaceDef); |
| } |
| |
| public boolean hasVirtualMethod(String method) { |
| Integer val = methodLookup.get(method); |
| if (val == null || val < 0) { |
| return false; |
| } |
| return true; |
| } |
| |
| public int getMethodType(String method) { |
| Integer val = methodLookup.get(method); |
| if (val == null) { |
| return -1; |
| } |
| if (val >= 0) { |
| return DeodexUtil.Virtual; |
| } |
| if (val == DirectMethod) { |
| return DeodexUtil.Direct; |
| } |
| if (val == StaticMethod) { |
| return DeodexUtil.Static; |
| } |
| throw new RuntimeException("Unexpected method type"); |
| } |
| |
| public FieldDef getInstanceField(int fieldOffset) { |
| return this.instanceFields.get(fieldOffset, null); |
| } |
| |
| public String getVirtualMethod(int vtableIndex) { |
| if (vtableIndex < 0 || vtableIndex >= vtable.length) { |
| return null; |
| } |
| return this.vtable[vtableIndex].method; |
| } |
| |
| private void swap(byte[] fieldTypes, FieldDef[] fields, int position1, int position2) { |
| byte tempType = fieldTypes[position1]; |
| fieldTypes[position1] = fieldTypes[position2]; |
| fieldTypes[position2] = tempType; |
| |
| FieldDef tempField = fields[position1]; |
| fields[position1] = fields[position2]; |
| fields[position2] = tempField; |
| } |
| |
| private ClassDef loadSuperclass(UnresolvedClassInfo classInfo) { |
| if (classInfo.classType.equals("Ljava/lang/Object;")) { |
| if (classInfo.superclassType != null) { |
| throw new ExceptionWithContext("Invalid superclass " + |
| classInfo.superclassType + " for Ljava/lang/Object;. " + |
| "The Object class cannot have a superclass"); |
| } |
| return null; |
| } else { |
| String superclassType = classInfo.superclassType; |
| if (superclassType == null) { |
| throw new ExceptionWithContext(classInfo.classType + " has no superclass"); |
| } |
| |
| ClassDef superclass; |
| try { |
| superclass = ClassPath.getClassDef(superclassType); |
| } catch (Exception ex) { |
| throw ExceptionWithContext.withContext(ex, |
| String.format("Could not find superclass %s", superclassType)); |
| } |
| |
| if (!isInterface && superclass.isInterface) { |
| throw new ValidationException("Class " + classType + " has the interface " + superclass.classType + |
| " as its superclass"); |
| } |
| if (isInterface && !superclass.isInterface && superclass != |
| ClassPath.theClassPath.javaLangObjectClassDef) { |
| throw new ValidationException("Interface " + classType + " has the non-interface class " + |
| superclass.classType + " as its superclass"); |
| } |
| |
| return superclass; |
| } |
| } |
| |
| private TreeSet<ClassDef> loadAllImplementedInterfaces(UnresolvedClassInfo classInfo) { |
| assert classType != null; |
| assert classType.equals("Ljava/lang/Object;") || superclass != null; |
| assert classInfo != null; |
| |
| TreeSet<ClassDef> implementedInterfaceSet = new TreeSet<ClassDef>(); |
| |
| if (superclass != null) { |
| for (ClassDef interfaceDef: superclass.implementedInterfaces) { |
| implementedInterfaceSet.add(interfaceDef); |
| } |
| } |
| |
| |
| if (classInfo.interfaces != null) { |
| for (String interfaceType: classInfo.interfaces) { |
| ClassDef interfaceDef; |
| try { |
| interfaceDef = ClassPath.getClassDef(interfaceType); |
| } catch (Exception ex) { |
| throw ExceptionWithContext.withContext(ex, |
| String.format("Could not find interface %s", interfaceType)); |
| } |
| assert interfaceDef.isInterface(); |
| implementedInterfaceSet.add(interfaceDef); |
| |
| interfaceDef = interfaceDef.getSuperclass(); |
| while (!interfaceDef.getClassType().equals("Ljava/lang/Object;")) { |
| assert interfaceDef.isInterface(); |
| implementedInterfaceSet.add(interfaceDef); |
| interfaceDef = interfaceDef.getSuperclass(); |
| } |
| } |
| } |
| |
| return implementedInterfaceSet; |
| } |
| |
| private LinkedHashMap<String, ClassDef> loadInterfaceTable(UnresolvedClassInfo classInfo) { |
| if (classInfo.interfaces == null) { |
| return null; |
| } |
| |
| LinkedHashMap<String, ClassDef> interfaceTable = new LinkedHashMap<String, ClassDef>(); |
| |
| for (String interfaceType: classInfo.interfaces) { |
| if (!interfaceTable.containsKey(interfaceType)) { |
| ClassDef interfaceDef; |
| try { |
| interfaceDef = ClassPath.getClassDef(interfaceType); |
| } catch (Exception ex) { |
| throw ExceptionWithContext.withContext(ex, |
| String.format("Could not find interface %s", interfaceType)); |
| } |
| interfaceTable.put(interfaceType, interfaceDef); |
| |
| if (interfaceDef.interfaceTable != null) { |
| for (ClassDef superInterface: interfaceDef.interfaceTable.values()) { |
| if (!interfaceTable.containsKey(superInterface.classType)) { |
| interfaceTable.put(superInterface.classType, superInterface); |
| } |
| } |
| } |
| } |
| } |
| |
| return interfaceTable; |
| } |
| |
| //TODO: check the case when we have a package private method that overrides an interface method |
| private VirtualMethod[] loadVtable(UnresolvedClassInfo classInfo) { |
| //TODO: it might be useful to keep track of which class's implementation is used for each virtual method. In other words, associate the implementing class type with each vtable entry |
| List<VirtualMethod> virtualMethodList = new LinkedList<VirtualMethod>(); |
| |
| //copy the virtual methods from the superclass |
| int methodIndex = 0; |
| if (superclass != null) { |
| for (int i=0; i<superclass.vtable.length; i++) { |
| virtualMethodList.add(superclass.vtable[i]); |
| } |
| |
| assert superclass.instanceFields != null; |
| } |
| |
| |
| //iterate over the virtual methods in the current class, and only add them when we don't already have the |
| //method (i.e. if it was implemented by the superclass) |
| if (!this.isInterface) { |
| if (classInfo.virtualMethods != null) { |
| addToVtable(classInfo.virtualMethods, virtualMethodList); |
| } |
| |
| if (interfaceTable != null) { |
| for (ClassDef interfaceDef: interfaceTable.values()) { |
| if (interfaceDef.virtualMethods == null) { |
| continue; |
| } |
| |
| addToVtable(interfaceDef.virtualMethods, virtualMethodList); |
| } |
| } |
| } |
| |
| VirtualMethod[] vtable = new VirtualMethod[virtualMethodList.size()]; |
| for (int i=0; i<virtualMethodList.size(); i++) { |
| vtable[i] = virtualMethodList.get(i); |
| } |
| |
| return vtable; |
| } |
| |
| private void addToVtable(VirtualMethod[] localMethods, List<VirtualMethod> vtable) { |
| for (VirtualMethod virtualMethod: localMethods) { |
| boolean found = false; |
| for (int i=0; i<vtable.size(); i++) { |
| VirtualMethod superMethod = vtable.get(i); |
| if (superMethod.method.equals(virtualMethod.method)) { |
| if (!ClassPath.theClassPath.checkPackagePrivateAccess || this.canAccess(superMethod)) { |
| found = true; |
| vtable.set(i, virtualMethod); |
| break; |
| } |
| } |
| } |
| if (!found) { |
| vtable.add(virtualMethod); |
| } |
| } |
| } |
| |
| private boolean canAccess(VirtualMethod virtualMethod) { |
| if (!virtualMethod.isPackagePrivate) { |
| return true; |
| } |
| |
| String otherPackage = getPackage(virtualMethod.containingClass); |
| String ourPackage = getPackage(this.classType); |
| return otherPackage.equals(ourPackage); |
| } |
| |
| private String getPackage(String classType) { |
| int lastSlash = classType.lastIndexOf('/'); |
| if (lastSlash < 0) { |
| return ""; |
| } |
| return classType.substring(1, lastSlash); |
| } |
| |
| private int getNextFieldOffset() { |
| if (instanceFields == null || instanceFields.size() == 0) { |
| return 8; |
| } |
| |
| int lastItemIndex = instanceFields.size()-1; |
| int fieldOffset = instanceFields.keyAt(lastItemIndex); |
| FieldDef lastField = instanceFields.valueAt(lastItemIndex); |
| |
| switch (lastField.type.charAt(0)) { |
| case 'J': |
| case 'D': |
| return fieldOffset + 8; |
| default: |
| return fieldOffset + 4; |
| } |
| } |
| |
| private SparseArray<FieldDef> loadFields(UnresolvedClassInfo classInfo) { |
| //This is a bit of an "involved" operation. We need to follow the same algorithm that dalvik uses to |
| //arrange fields, so that we end up with the same field offsets (which is needed for deodexing). |
| //See mydroid/dalvik/vm/oo/Class.c - computeFieldOffsets() |
| |
| final byte REFERENCE = 0; |
| final byte WIDE = 1; |
| final byte OTHER = 2; |
| |
| FieldDef[] fields = null; |
| //the "type" for each field in fields. 0=reference,1=wide,2=other |
| byte[] fieldTypes = null; |
| |
| if (classInfo.instanceFields != null) { |
| fields = new FieldDef[classInfo.instanceFields.length]; |
| fieldTypes = new byte[fields.length]; |
| |
| for (int i=0; i<fields.length; i++) { |
| String[] fieldInfo = classInfo.instanceFields[i]; |
| |
| String fieldName = fieldInfo[0]; |
| String fieldType = fieldInfo[1]; |
| |
| fieldTypes[i] = getFieldType(fieldType); |
| fields[i] = new FieldDef(classInfo.classType, fieldName, fieldType); |
| } |
| } |
| |
| if (fields == null) { |
| fields = new FieldDef[0]; |
| fieldTypes = new byte[0]; |
| } |
| |
| //The first operation is to move all of the reference fields to the front. To do this, find the first |
| //non-reference field, then find the last reference field, swap them and repeat |
| int back = fields.length - 1; |
| int front; |
| for (front = 0; front<fields.length; front++) { |
| if (fieldTypes[front] != REFERENCE) { |
| while (back > front) { |
| if (fieldTypes[back] == REFERENCE) { |
| swap(fieldTypes, fields, front, back--); |
| break; |
| } |
| back--; |
| } |
| } |
| |
| if (fieldTypes[front] != REFERENCE) { |
| break; |
| } |
| } |
| |
| |
| int startFieldOffset = 8; |
| if (this.superclass != null) { |
| startFieldOffset = this.superclass.getNextFieldOffset(); |
| } |
| |
| int fieldIndexMod; |
| if ((startFieldOffset % 8) == 0) { |
| fieldIndexMod = 0; |
| } else { |
| fieldIndexMod = 1; |
| } |
| |
| //next, we need to group all the wide fields after the reference fields. But the wide fields have to be |
| //8-byte aligned. If we're on an odd field index, we need to insert a 32-bit field. If the next field |
| //is already a 32-bit field, use that. Otherwise, find the first 32-bit field from the end and swap it in. |
| //If there are no 32-bit fields, do nothing for now. We'll add padding when calculating the field offsets |
| if (front < fields.length && (front % 2) != fieldIndexMod) { |
| if (fieldTypes[front] == WIDE) { |
| //we need to swap in a 32-bit field, so the wide fields will be correctly aligned |
| back = fields.length - 1; |
| while (back > front) { |
| if (fieldTypes[back] == OTHER) { |
| swap(fieldTypes, fields, front++, back); |
| break; |
| } |
| back--; |
| } |
| } else { |
| //there's already a 32-bit field here that we can use |
| front++; |
| } |
| } |
| |
| //do the swap thing for wide fields |
| back = fields.length - 1; |
| for (; front<fields.length; front++) { |
| if (fieldTypes[front] != WIDE) { |
| while (back > front) { |
| if (fieldTypes[back] == WIDE) { |
| swap(fieldTypes, fields, front, back--); |
| break; |
| } |
| back--; |
| } |
| } |
| |
| if (fieldTypes[front] != WIDE) { |
| break; |
| } |
| } |
| |
| int superFieldCount = 0; |
| if (superclass != null) { |
| superFieldCount = superclass.instanceFields.size(); |
| } |
| |
| //now the fields are in the correct order. Add them to the SparseArray and lookup, and calculate the offsets |
| int totalFieldCount = superFieldCount + fields.length; |
| SparseArray<FieldDef> instanceFields = new SparseArray<FieldDef>(totalFieldCount); |
| |
| int fieldOffset; |
| |
| if (superclass != null && superFieldCount > 0) { |
| for (int i=0; i<superFieldCount; i++) { |
| instanceFields.append(superclass.instanceFields.keyAt(i), superclass.instanceFields.valueAt(i)); |
| } |
| |
| fieldOffset = instanceFields.keyAt(superFieldCount-1); |
| |
| FieldDef lastSuperField = superclass.instanceFields.valueAt(superFieldCount-1); |
| char fieldType = lastSuperField.type.charAt(0); |
| if (fieldType == 'J' || fieldType == 'D') { |
| fieldOffset += 8; |
| } else { |
| fieldOffset += 4; |
| } |
| } else { |
| //the field values start at 8 bytes into the DataObject dalvik structure |
| fieldOffset = 8; |
| } |
| |
| boolean gotDouble = false; |
| for (int i=0; i<fields.length; i++) { |
| FieldDef field = fields[i]; |
| |
| //add padding to align the wide fields, if needed |
| if (fieldTypes[i] == WIDE && !gotDouble) { |
| if (!gotDouble) { |
| if (fieldOffset % 8 != 0) { |
| assert fieldOffset % 8 == 4; |
| fieldOffset += 4; |
| } |
| gotDouble = true; |
| } |
| } |
| |
| instanceFields.append(fieldOffset, field); |
| if (fieldTypes[i] == WIDE) { |
| fieldOffset += 8; |
| } else { |
| fieldOffset += 4; |
| } |
| } |
| return instanceFields; |
| } |
| |
| private byte getFieldType(String fieldType) { |
| switch (fieldType.charAt(0)) { |
| case '[': |
| case 'L': |
| return 0; //REFERENCE |
| case 'J': |
| case 'D': |
| return 1; //WIDE |
| default: |
| return 2; //OTHER |
| } |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) return true; |
| if (!(o instanceof ClassDef)) return false; |
| |
| ClassDef classDef = (ClassDef) o; |
| |
| return classType.equals(classDef.classType); |
| } |
| |
| @Override |
| public int hashCode() { |
| return classType.hashCode(); |
| } |
| |
| public int compareTo(ClassDef classDef) { |
| return classType.compareTo(classDef.classType); |
| } |
| } |
| |
| private static class VirtualMethod { |
| public String containingClass; |
| public String method; |
| public boolean isPackagePrivate; |
| } |
| |
| /** |
| * This aggregates the basic information about a class in an easy-to-use format, without requiring references |
| * to any other class. |
| */ |
| private static class UnresolvedClassInfo { |
| public final String dexFilePath; |
| public final String classType; |
| public final boolean isPublic; |
| public final boolean isInterface; |
| public final String superclassType; |
| public final String[] interfaces; |
| public final boolean[] staticMethods; |
| public final String[] directMethods; |
| public final VirtualMethod[] virtualMethods; |
| public final String[][] instanceFields; |
| |
| public UnresolvedClassInfo(String dexFilePath, ClassDefItem classDefItem) { |
| this.dexFilePath = dexFilePath; |
| |
| classType = classDefItem.getClassType().getTypeDescriptor(); |
| |
| isPublic = (classDefItem.getAccessFlags() & AccessFlags.PUBLIC.getValue()) != 0; |
| isInterface = (classDefItem.getAccessFlags() & AccessFlags.INTERFACE.getValue()) != 0; |
| |
| TypeIdItem superclassType = classDefItem.getSuperclass(); |
| if (superclassType == null) { |
| this.superclassType = null; |
| } else { |
| this.superclassType = superclassType.getTypeDescriptor(); |
| } |
| |
| interfaces = loadInterfaces(classDefItem); |
| |
| ClassDataItem classDataItem = classDefItem.getClassData(); |
| if (classDataItem != null) { |
| boolean[][] _staticMethods = new boolean[1][]; |
| directMethods = loadDirectMethods(classDataItem, _staticMethods); |
| staticMethods = _staticMethods[0]; |
| virtualMethods = loadVirtualMethods(classDataItem); |
| instanceFields = loadInstanceFields(classDataItem); |
| } else { |
| staticMethods = null; |
| directMethods = null; |
| virtualMethods = null; |
| instanceFields = null; |
| } |
| } |
| |
| private String[] loadInterfaces(ClassDefItem classDefItem) { |
| TypeListItem typeList = classDefItem.getInterfaces(); |
| if (typeList != null) { |
| List<TypeIdItem> types = typeList.getTypes(); |
| if (types != null && types.size() > 0) { |
| String[] interfaces = new String[types.size()]; |
| for (int i=0; i<interfaces.length; i++) { |
| interfaces[i] = types.get(i).getTypeDescriptor(); |
| } |
| return interfaces; |
| } |
| } |
| return null; |
| } |
| |
| private String[] loadDirectMethods(ClassDataItem classDataItem, boolean[][] _staticMethods) { |
| List<EncodedMethod> encodedMethods = classDataItem.getDirectMethods(); |
| if (encodedMethods.size() > 0) { |
| boolean[] staticMethods = new boolean[encodedMethods.size()]; |
| String[] directMethods = new String[encodedMethods.size()]; |
| |
| for (int i=0; i<encodedMethods.size(); i++) { |
| EncodedMethod encodedMethod = encodedMethods.get(i); |
| |
| if ((encodedMethod.accessFlags & AccessFlags.STATIC.getValue()) != 0) { |
| staticMethods[i] = true; |
| } |
| directMethods[i] = encodedMethod.method.getShortMethodString(); |
| } |
| _staticMethods[0] = staticMethods; |
| return directMethods; |
| } |
| return null; |
| } |
| |
| private VirtualMethod[] loadVirtualMethods(ClassDataItem classDataItem) { |
| List<EncodedMethod> encodedMethods = classDataItem.getVirtualMethods(); |
| if (encodedMethods.size() > 0) { |
| VirtualMethod[] virtualMethods = new VirtualMethod[encodedMethods.size()]; |
| for (int i=0; i<encodedMethods.size(); i++) { |
| virtualMethods[i] = new VirtualMethod(); |
| EncodedMethod encodedMethod = encodedMethods.get(i); |
| |
| virtualMethods[i].isPackagePrivate = methodIsPackagePrivate(encodedMethod.accessFlags); |
| virtualMethods[i].containingClass = classDataItem.getParentType().getTypeDescriptor(); |
| virtualMethods[i].method = encodedMethods.get(i).method.getShortMethodString(); |
| } |
| return virtualMethods; |
| } |
| return null; |
| } |
| |
| private static boolean methodIsPackagePrivate(int accessFlags) { |
| return (accessFlags & (AccessFlags.PRIVATE.getValue() | |
| AccessFlags.PROTECTED.getValue() | |
| AccessFlags.PUBLIC.getValue())) == 0; |
| } |
| |
| private String[][] loadInstanceFields(ClassDataItem classDataItem) { |
| List<EncodedField> encodedFields = classDataItem.getInstanceFields(); |
| if (encodedFields.size() > 0) { |
| String[][] instanceFields = new String[encodedFields.size()][2]; |
| for (int i=0; i<encodedFields.size(); i++) { |
| EncodedField encodedField = encodedFields.get(i); |
| instanceFields[i][0] = encodedField.field.getFieldName().getStringValue(); |
| instanceFields[i][1] = encodedField.field.getFieldType().getTypeDescriptor(); |
| } |
| return instanceFields; |
| } |
| return null; |
| } |
| } |
| } |