Home | History | Annotate | Download | only in analysis
      1 /*
      2  * Copyright 2016, Google Inc.
      3  * All rights reserved.
      4  *
      5  * Redistribution and use in source and binary forms, with or without
      6  * modification, are permitted provided that the following conditions are
      7  * met:
      8  *
      9  * Redistributions of source code must retain the above copyright
     10  * notice, this list of conditions and the following disclaimer.
     11  * Redistributions in binary form must reproduce the above
     12  * copyright notice, this list of conditions and the following disclaimer
     13  * in the documentation and/or other materials provided with the
     14  * distribution.
     15  * Neither the name of Google Inc. nor the names of its
     16  * contributors may be used to endorse or promote products derived from
     17  * this software without specific prior written permission.
     18  *
     19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     20  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     21  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     22  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     23  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     24  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     25  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     26  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     27  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     28  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     29  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     30  */
     31 
     32 package org.jf.dexlib2.analysis;
     33 
     34 import com.beust.jcommander.internal.Sets;
     35 import com.google.common.base.Joiner;
     36 import com.google.common.base.Splitter;
     37 import com.google.common.collect.Lists;
     38 import org.jf.dexlib2.DexFileFactory;
     39 import org.jf.dexlib2.DexFileFactory.UnsupportedFileTypeException;
     40 import org.jf.dexlib2.Opcodes;
     41 import org.jf.dexlib2.dexbacked.DexBackedDexFile;
     42 import org.jf.dexlib2.dexbacked.DexBackedOdexFile;
     43 import org.jf.dexlib2.dexbacked.OatFile;
     44 import org.jf.dexlib2.dexbacked.OatFile.OatDexFile;
     45 import org.jf.dexlib2.iface.DexFile;
     46 import org.jf.dexlib2.iface.MultiDexContainer;
     47 import org.jf.dexlib2.iface.MultiDexContainer.MultiDexFile;
     48 
     49 import javax.annotation.Nonnull;
     50 import javax.annotation.Nullable;
     51 import java.io.File;
     52 import java.io.IOException;
     53 import java.util.List;
     54 import java.util.Set;
     55 
     56 public class ClassPathResolver {
     57     private final Iterable<String> classPathDirs;
     58     private final Opcodes opcodes;
     59 
     60     private final Set<File> loadedFiles = Sets.newHashSet();
     61     private final List<ClassProvider> classProviders = Lists.newArrayList();
     62 
     63     /**
     64      * Constructs a new ClassPathResolver using a specified list of bootclasspath entries
     65      *
     66      * @param bootClassPathDirs A list of directories to search for boot classpath entries. Can be empty if all boot
     67      *                          classpath entries are specified as local paths
     68      * @param bootClassPathEntries A list of boot classpath entries to load. These can either be local paths, or
     69      *                             device paths (e.g. "/system/framework/framework.jar"). The entry will be interpreted
     70      *                             first as a local path. If not found as a local path, it will be interpreted as a
     71      *                             partial or absolute device path, and will be searched for in bootClassPathDirs
     72      * @param extraClassPathEntries A list of additional classpath entries to load. Can be empty. All entries must be
     73      *                              local paths. Device paths are not supported.
     74      * @param dexFile The dex file that the classpath will be used to analyze
     75      * @throws IOException If any IOException occurs
     76      * @throws ResolveException If any classpath entries cannot be loaded for some reason
     77      *
     78      *  If null, a default bootclasspath is used,
     79      *                             depending on the the file type of dexFile and the api level. If empty, no boot
     80      *                             classpath entries will be loaded
     81      */
     82     public ClassPathResolver(@Nonnull List<String> bootClassPathDirs, @Nonnull List<String> bootClassPathEntries,
     83                              @Nonnull List<String> extraClassPathEntries, @Nonnull DexFile dexFile)
     84             throws IOException {
     85         this(bootClassPathDirs, bootClassPathEntries, extraClassPathEntries, dexFile, true);
     86     }
     87 
     88     /**
     89      * Constructs a new ClassPathResolver using a default list of bootclasspath entries
     90      *
     91      * @param bootClassPathDirs A list of directories to search for boot classpath entries
     92      * @param extraClassPathEntries A list of additional classpath entries to load. Can be empty. All entries must be
     93      *                              local paths. Device paths are not supported.
     94      * @param dexFile The dex file that the classpath will be used to analyze
     95      * @throws IOException If any IOException occurs
     96      * @throws ResolveException If any classpath entries cannot be loaded for some reason
     97      *
     98      *  If null, a default bootclasspath is used,
     99      *                             depending on the the file type of dexFile and the api level. If empty, no boot
    100      *                             classpath entries will be loaded
    101      */
    102     public ClassPathResolver(@Nonnull List<String> bootClassPathDirs, @Nonnull List<String> extraClassPathEntries,
    103                              @Nonnull DexFile dexFile)
    104             throws IOException {
    105         this(bootClassPathDirs, null, extraClassPathEntries, dexFile, true);
    106     }
    107 
    108     private ClassPathResolver(@Nonnull List<String> bootClassPathDirs, @Nullable List<String> bootClassPathEntries,
    109                               @Nonnull List<String> extraClassPathEntries, @Nonnull DexFile dexFile, boolean unused)
    110             throws IOException {
    111         this.classPathDirs = bootClassPathDirs;
    112         opcodes = dexFile.getOpcodes();
    113 
    114         if (bootClassPathEntries == null) {
    115             bootClassPathEntries = getDefaultBootClassPath(dexFile, opcodes.api);
    116         }
    117 
    118         for (String entry : bootClassPathEntries) {
    119             try {
    120                 loadLocalOrDeviceBootClassPathEntry(entry);
    121             } catch (NoDexException ex) {
    122                 if (entry.endsWith(".jar")) {
    123                     String odexEntry = entry.substring(0, entry.length() - 4) + ".odex";
    124                     try {
    125                         loadLocalOrDeviceBootClassPathEntry(odexEntry);
    126                     } catch (NoDexException ex2) {
    127                         throw new ResolveException("Neither %s nor %s contain a dex file", entry, odexEntry);
    128                     } catch (NotFoundException ex2) {
    129                         throw new ResolveException(ex);
    130                     }
    131                 } else {
    132                     throw new ResolveException(ex);
    133                 }
    134             } catch (NotFoundException ex) {
    135                 if (entry.endsWith(".odex")) {
    136                     String jarEntry = entry.substring(0, entry.length() - 5) + ".jar";
    137                     try {
    138                         loadLocalOrDeviceBootClassPathEntry(jarEntry);
    139                         } catch (NoDexException ex2) {
    140                         throw new ResolveException("Neither %s nor %s contain a dex file", entry, jarEntry);
    141                     } catch (NotFoundException ex2) {
    142                         throw new ResolveException(ex);
    143                     }
    144                 } else {
    145                     throw new ResolveException(ex);
    146                 }
    147             }
    148         }
    149 
    150         for (String entry: extraClassPathEntries) {
    151             // extra classpath entries must be specified using a local path, so we don't need to do the search through
    152             // bootClassPathDirs
    153             try {
    154                 loadLocalClassPathEntry(entry);
    155             } catch (NoDexException ex) {
    156                 throw new ResolveException(ex);
    157             }
    158         }
    159 
    160         if (dexFile instanceof MultiDexContainer.MultiDexFile) {
    161             MultiDexContainer<? extends MultiDexFile> container = ((MultiDexFile)dexFile).getContainer();
    162             for (String entry: container.getDexEntryNames()) {
    163                 classProviders.add(new DexClassProvider(container.getEntry(entry)));
    164             }
    165         } else {
    166             classProviders.add(new DexClassProvider(dexFile));
    167         }
    168     }
    169 
    170     @Nonnull
    171     public List<ClassProvider> getResolvedClassProviders() {
    172         return classProviders;
    173     }
    174 
    175     private boolean loadLocalClassPathEntry(@Nonnull String entry) throws NoDexException, IOException {
    176         File entryFile = new File(entry);
    177         if (entryFile.exists() && entryFile.isFile()) {
    178             try {
    179                 loadEntry(entryFile, true);
    180                 return true;
    181             } catch (UnsupportedFileTypeException ex) {
    182                 throw new ResolveException(ex, "Couldn't load classpath entry %s", entry);
    183             }
    184         }
    185         return false;
    186     }
    187 
    188     private void loadLocalOrDeviceBootClassPathEntry(@Nonnull String entry)
    189             throws IOException, NoDexException, NotFoundException {
    190         // first, see if the entry is a valid local path
    191         if (loadLocalClassPathEntry(entry)) {
    192             return;
    193         }
    194 
    195         // It's not a local path, so let's try to resolve it as a device path, relative to one of the provided
    196         // directories
    197         List<String> pathComponents = splitDevicePath(entry);
    198         Joiner pathJoiner = Joiner.on(File.pathSeparatorChar);
    199 
    200         for (String directory: classPathDirs) {
    201             File directoryFile = new File(directory);
    202             if (!directoryFile.exists()) {
    203                 continue;
    204             }
    205 
    206             for (int i=0; i<pathComponents.size(); i++) {
    207                 String partialPath = pathJoiner.join(pathComponents.subList(i, pathComponents.size()));
    208                 File entryFile = new File(directoryFile, partialPath);
    209                 if (entryFile.exists() && entryFile.isFile()) {
    210                     loadEntry(entryFile, true);
    211                     return;
    212                 }
    213             }
    214         }
    215 
    216         throw new NotFoundException("Could not find classpath entry %s", entry);
    217     }
    218 
    219     private void loadEntry(@Nonnull File entryFile, boolean loadOatDependencies)
    220             throws IOException, NoDexException {
    221         if (loadedFiles.contains(entryFile)) {
    222             return;
    223         }
    224 
    225         MultiDexContainer<? extends DexBackedDexFile> container;
    226         try {
    227             container = DexFileFactory.loadDexContainer(entryFile, opcodes);
    228         } catch (UnsupportedFileTypeException ex) {
    229             throw new ResolveException(ex);
    230         }
    231 
    232         List<String> entryNames = container.getDexEntryNames();
    233 
    234         if (entryNames.size() == 0) {
    235             throw new NoDexException("%s contains no dex file", entryFile);
    236         }
    237 
    238         loadedFiles.add(entryFile);
    239 
    240         for (String entryName: entryNames) {
    241             classProviders.add(new DexClassProvider(container.getEntry(entryName)));
    242         }
    243 
    244         if (loadOatDependencies && container instanceof OatFile) {
    245             List<String> oatDependencies = ((OatFile)container).getBootClassPath();
    246             if (!oatDependencies.isEmpty()) {
    247                 try {
    248                     loadOatDependencies(entryFile.getParentFile(), oatDependencies);
    249                 } catch (NotFoundException ex) {
    250                     throw new ResolveException(ex, "Error while loading oat file %s", entryFile);
    251                 } catch (NoDexException ex) {
    252                     throw new ResolveException(ex, "Error while loading dependencies for oat file %s", entryFile);
    253                 }
    254             }
    255         }
    256     }
    257 
    258     @Nonnull
    259     private static List<String> splitDevicePath(@Nonnull String path) {
    260         return Lists.newArrayList(Splitter.on('/').split(path));
    261     }
    262 
    263     private void loadOatDependencies(@Nonnull File directory, @Nonnull List<String> oatDependencies)
    264             throws IOException, NoDexException, NotFoundException {
    265         // We assume that all oat dependencies are located in the same directory as the oat file
    266         for (String oatDependency: oatDependencies) {
    267             String oatDependencyName = getFilenameForOatDependency(oatDependency);
    268             File file = new File(directory, oatDependencyName);
    269             if (!file.exists()) {
    270                 throw new NotFoundException("Cannot find dependency %s in %s", oatDependencyName, directory);
    271             }
    272 
    273             loadEntry(file, false);
    274         }
    275     }
    276 
    277     @Nonnull
    278     private String getFilenameForOatDependency(String oatDependency) {
    279         int index = oatDependency.lastIndexOf('/');
    280 
    281         String dependencyLeaf = oatDependency.substring(index+1);
    282         if (dependencyLeaf.endsWith(".art")) {
    283             return dependencyLeaf.substring(0, dependencyLeaf.length() - 4) + ".oat";
    284         }
    285         return dependencyLeaf;
    286     }
    287 
    288     private static class NotFoundException extends Exception {
    289         public NotFoundException(String message, Object... formatArgs) {
    290             super(String.format(message, formatArgs));
    291         }
    292     }
    293 
    294     private static class NoDexException extends Exception {
    295         public NoDexException(String message, Object... formatArgs) {
    296             super(String.format(message, formatArgs));
    297         }
    298     }
    299 
    300     /**
    301      * An error that occurred while resolving the classpath
    302      */
    303     public static class ResolveException extends RuntimeException {
    304         public ResolveException (String message, Object... formatArgs) {
    305             super(String.format(message, formatArgs));
    306         }
    307 
    308         public ResolveException (Throwable cause) {
    309             super(cause);
    310         }
    311 
    312         public ResolveException (Throwable cause, String message, Object... formatArgs) {
    313             super(String.format(message, formatArgs), cause);
    314         }
    315     }
    316 
    317     /**
    318      * Returns the default boot class path for the given dex file and api level.
    319      */
    320     @Nonnull
    321     private static List<String> getDefaultBootClassPath(@Nonnull DexFile dexFile, int apiLevel) {
    322         if (dexFile instanceof OatFile.OatDexFile) {
    323             List<String> bcp = ((OatDexFile)dexFile).getContainer().getBootClassPath();
    324             if (!bcp.isEmpty()) {
    325                 for (int i=0; i<bcp.size(); i++) {
    326                     String entry = bcp.get(i);
    327                     if (entry.endsWith(".art")) {
    328                         bcp.set(i, entry.substring(0, entry.length() - 4) + ".oat");
    329                     }
    330                 }
    331                 return bcp;
    332             }
    333             return Lists.newArrayList("boot.oat");
    334         }
    335 
    336         if (dexFile instanceof DexBackedOdexFile) {
    337             return ((DexBackedOdexFile)dexFile).getDependencies();
    338         }
    339 
    340         if (apiLevel <= 8) {
    341             return Lists.newArrayList(
    342                     "/system/framework/core.jar",
    343                     "/system/framework/ext.jar",
    344                     "/system/framework/framework.jar",
    345                     "/system/framework/android.policy.jar",
    346                     "/system/framework/services.jar");
    347         } else if (apiLevel <= 11) {
    348             return Lists.newArrayList(
    349                     "/system/framework/core.jar",
    350                     "/system/framework/bouncycastle.jar",
    351                     "/system/framework/ext.jar",
    352                     "/system/framework/framework.jar",
    353                     "/system/framework/android.policy.jar",
    354                     "/system/framework/services.jar",
    355                     "/system/framework/core-junit.jar");
    356         } else if (apiLevel <= 13) {
    357             return Lists.newArrayList(
    358                     "/system/framework/core.jar",
    359                     "/system/framework/apache-xml.jar",
    360                     "/system/framework/bouncycastle.jar",
    361                     "/system/framework/ext.jar",
    362                     "/system/framework/framework.jar",
    363                     "/system/framework/android.policy.jar",
    364                     "/system/framework/services.jar",
    365                     "/system/framework/core-junit.jar");
    366         } else if (apiLevel <= 15) {
    367             return Lists.newArrayList(
    368                     "/system/framework/core.jar",
    369                     "/system/framework/core-junit.jar",
    370                     "/system/framework/bouncycastle.jar",
    371                     "/system/framework/ext.jar",
    372                     "/system/framework/framework.jar",
    373                     "/system/framework/android.policy.jar",
    374                     "/system/framework/services.jar",
    375                     "/system/framework/apache-xml.jar",
    376                     "/system/framework/filterfw.jar");
    377         } else if (apiLevel <= 17) {
    378             // this is correct as of api 17/4.2.2
    379             return Lists.newArrayList(
    380                     "/system/framework/core.jar",
    381                     "/system/framework/core-junit.jar",
    382                     "/system/framework/bouncycastle.jar",
    383                     "/system/framework/ext.jar",
    384                     "/system/framework/framework.jar",
    385                     "/system/framework/telephony-common.jar",
    386                     "/system/framework/mms-common.jar",
    387                     "/system/framework/android.policy.jar",
    388                     "/system/framework/services.jar",
    389                     "/system/framework/apache-xml.jar");
    390         } else if (apiLevel <= 18) {
    391             return Lists.newArrayList(
    392                     "/system/framework/core.jar",
    393                     "/system/framework/core-junit.jar",
    394                     "/system/framework/bouncycastle.jar",
    395                     "/system/framework/ext.jar",
    396                     "/system/framework/framework.jar",
    397                     "/system/framework/telephony-common.jar",
    398                     "/system/framework/voip-common.jar",
    399                     "/system/framework/mms-common.jar",
    400                     "/system/framework/android.policy.jar",
    401                     "/system/framework/services.jar",
    402                     "/system/framework/apache-xml.jar");
    403         } else if (apiLevel <= 19) {
    404             return Lists.newArrayList(
    405                     "/system/framework/core.jar",
    406                     "/system/framework/conscrypt.jar",
    407                     "/system/framework/core-junit.jar",
    408                     "/system/framework/bouncycastle.jar",
    409                     "/system/framework/ext.jar",
    410                     "/system/framework/framework.jar",
    411                     "/system/framework/framework2.jar",
    412                     "/system/framework/telephony-common.jar",
    413                     "/system/framework/voip-common.jar",
    414                     "/system/framework/mms-common.jar",
    415                     "/system/framework/android.policy.jar",
    416                     "/system/framework/services.jar",
    417                     "/system/framework/apache-xml.jar",
    418                     "/system/framework/webviewchromium.jar");
    419         } else if (apiLevel <= 22) {
    420             return Lists.newArrayList(
    421                     "/system/framework/core-libart.jar",
    422                     "/system/framework/conscrypt.jar",
    423                     "/system/framework/okhttp.jar",
    424                     "/system/framework/core-junit.jar",
    425                     "/system/framework/bouncycastle.jar",
    426                     "/system/framework/ext.jar",
    427                     "/system/framework/framework.jar",
    428                     "/system/framework/telephony-common.jar",
    429                     "/system/framework/voip-common.jar",
    430                     "/system/framework/ims-common.jar",
    431                     "/system/framework/mms-common.jar",
    432                     "/system/framework/android.policy.jar",
    433                     "/system/framework/apache-xml.jar");
    434         } else if (apiLevel <= 23) {
    435             return Lists.newArrayList(
    436                     "/system/framework/core-libart.jar",
    437                     "/system/framework/conscrypt.jar",
    438                     "/system/framework/okhttp.jar",
    439                     "/system/framework/core-junit.jar",
    440                     "/system/framework/bouncycastle.jar",
    441                     "/system/framework/ext.jar",
    442                     "/system/framework/framework.jar",
    443                     "/system/framework/telephony-common.jar",
    444                     "/system/framework/voip-common.jar",
    445                     "/system/framework/ims-common.jar",
    446                     "/system/framework/apache-xml.jar",
    447                     "/system/framework/org.apache.http.legacy.boot.jar");
    448         } else /*if (apiLevel <= 24)*/ {
    449             return Lists.newArrayList(
    450                     "/system/framework/core-oj.jar",
    451                     "/system/framework/core-libart.jar",
    452                     "/system/framework/conscrypt.jar",
    453                     "/system/framework/okhttp.jar",
    454                     "/system/framework/core-junit.jar",
    455                     "/system/framework/bouncycastle.jar",
    456                     "/system/framework/ext.jar",
    457                     "/system/framework/framework.jar",
    458                     "/system/framework/telephony-common.jar",
    459                     "/system/framework/voip-common.jar",
    460                     "/system/framework/ims-common.jar",
    461                     "/system/framework/apache-xml.jar",
    462                     "/system/framework/org.apache.http.legacy.boot.jar");
    463         }
    464     }
    465 }
    466