Home | History | Annotate | Download | only in system
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package dalvik.system;
     18 
     19 import android.system.ErrnoException;
     20 import android.system.StructStat;
     21 import java.io.File;
     22 import java.io.IOException;
     23 import java.net.MalformedURLException;
     24 import java.net.URL;
     25 import java.util.ArrayList;
     26 import java.util.Arrays;
     27 import java.util.Collections;
     28 import java.util.Enumeration;
     29 import java.util.List;
     30 import java.util.zip.ZipFile;
     31 import libcore.io.IoUtils;
     32 import libcore.io.Libcore;
     33 import static android.system.OsConstants.*;
     34 
     35 /**
     36  * A pair of lists of entries, associated with a {@code ClassLoader}.
     37  * One of the lists is a dex/resource path — typically referred
     38  * to as a "class path" — list, and the other names directories
     39  * containing native code libraries. Class path entries may be any of:
     40  * a {@code .jar} or {@code .zip} file containing an optional
     41  * top-level {@code classes.dex} file as well as arbitrary resources,
     42  * or a plain {@code .dex} file (with no possibility of associated
     43  * resources).
     44  *
     45  * <p>This class also contains methods to use these lists to look up
     46  * classes and resources.</p>
     47  */
     48 /*package*/ final class DexPathList {
     49     private static final String DEX_SUFFIX = ".dex";
     50 
     51     /** class definition context */
     52     private final ClassLoader definingContext;
     53 
     54     /**
     55      * List of dex/resource (class path) elements.
     56      * Should be called pathElements, but the Facebook app uses reflection
     57      * to modify 'dexElements' (http://b/7726934).
     58      */
     59     private final Element[] dexElements;
     60 
     61     /** List of native library directories. */
     62     private final File[] nativeLibraryDirectories;
     63 
     64     /**
     65      * Exceptions thrown during creation of the dexElements list.
     66      */
     67     private final IOException[] dexElementsSuppressedExceptions;
     68 
     69     /**
     70      * Constructs an instance.
     71      *
     72      * @param definingContext the context in which any as-yet unresolved
     73      * classes should be defined
     74      * @param dexPath list of dex/resource path elements, separated by
     75      * {@code File.pathSeparator}
     76      * @param libraryPath list of native library directory path elements,
     77      * separated by {@code File.pathSeparator}
     78      * @param optimizedDirectory directory where optimized {@code .dex} files
     79      * should be found and written to, or {@code null} to use the default
     80      * system directory for same
     81      */
     82     public DexPathList(ClassLoader definingContext, String dexPath,
     83             String libraryPath, File optimizedDirectory) {
     84         if (definingContext == null) {
     85             throw new NullPointerException("definingContext == null");
     86         }
     87 
     88         if (dexPath == null) {
     89             throw new NullPointerException("dexPath == null");
     90         }
     91 
     92         if (optimizedDirectory != null) {
     93             if (!optimizedDirectory.exists())  {
     94                 throw new IllegalArgumentException(
     95                         "optimizedDirectory doesn't exist: "
     96                         + optimizedDirectory);
     97             }
     98 
     99             if (!(optimizedDirectory.canRead()
    100                             && optimizedDirectory.canWrite())) {
    101                 throw new IllegalArgumentException(
    102                         "optimizedDirectory not readable/writable: "
    103                         + optimizedDirectory);
    104             }
    105         }
    106 
    107         this.definingContext = definingContext;
    108         ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
    109         this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
    110                                            suppressedExceptions);
    111         if (suppressedExceptions.size() > 0) {
    112             this.dexElementsSuppressedExceptions =
    113                 suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
    114         } else {
    115             dexElementsSuppressedExceptions = null;
    116         }
    117         this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
    118     }
    119 
    120     @Override public String toString() {
    121         return "DexPathList[" + Arrays.toString(dexElements) +
    122             ",nativeLibraryDirectories=" + Arrays.toString(nativeLibraryDirectories) + "]";
    123     }
    124 
    125     /**
    126      * For BaseDexClassLoader.getLdLibraryPath.
    127      */
    128     public File[] getNativeLibraryDirectories() {
    129         return nativeLibraryDirectories;
    130     }
    131 
    132     /**
    133      * Splits the given dex path string into elements using the path
    134      * separator, pruning out any elements that do not refer to existing
    135      * and readable files. (That is, directories are not included in the
    136      * result.)
    137      */
    138     private static ArrayList<File> splitDexPath(String path) {
    139         return splitPaths(path, null, false);
    140     }
    141 
    142     /**
    143      * Splits the given library directory path string into elements
    144      * using the path separator ({@code File.pathSeparator}, which
    145      * defaults to {@code ":"} on Android, appending on the elements
    146      * from the system library path, and pruning out any elements that
    147      * do not refer to existing and readable directories.
    148      */
    149     private static File[] splitLibraryPath(String path) {
    150         // Native libraries may exist in both the system and
    151         // application library paths, and we use this search order:
    152         //
    153         //   1. this class loader's library path for application libraries
    154         //   2. the VM's library path from the system property for system libraries
    155         //
    156         // This order was reversed prior to Gingerbread; see http://b/2933456.
    157         ArrayList<File> result = splitPaths(path, System.getProperty("java.library.path"), true);
    158         return result.toArray(new File[result.size()]);
    159     }
    160 
    161     /**
    162      * Splits the given path strings into file elements using the path
    163      * separator, combining the results and filtering out elements
    164      * that don't exist, aren't readable, or aren't either a regular
    165      * file or a directory (as specified). Either string may be empty
    166      * or {@code null}, in which case it is ignored. If both strings
    167      * are empty or {@code null}, or all elements get pruned out, then
    168      * this returns a zero-element list.
    169      */
    170     private static ArrayList<File> splitPaths(String path1, String path2,
    171             boolean wantDirectories) {
    172         ArrayList<File> result = new ArrayList<File>();
    173 
    174         splitAndAdd(path1, wantDirectories, result);
    175         splitAndAdd(path2, wantDirectories, result);
    176         return result;
    177     }
    178 
    179     /**
    180      * Helper for {@link #splitPaths}, which does the actual splitting
    181      * and filtering and adding to a result.
    182      */
    183     private static void splitAndAdd(String searchPath, boolean directoriesOnly,
    184             ArrayList<File> resultList) {
    185         if (searchPath == null) {
    186             return;
    187         }
    188         for (String path : searchPath.split(":")) {
    189             try {
    190                 StructStat sb = Libcore.os.stat(path);
    191                 if (!directoriesOnly || S_ISDIR(sb.st_mode)) {
    192                     resultList.add(new File(path));
    193                 }
    194             } catch (ErrnoException ignored) {
    195             }
    196         }
    197     }
    198 
    199     /**
    200      * Makes an array of dex/resource path elements, one per element of
    201      * the given array.
    202      */
    203     private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
    204                                              ArrayList<IOException> suppressedExceptions) {
    205         ArrayList<Element> elements = new ArrayList<Element>();
    206         /*
    207          * Open all files and load the (direct or contained) dex files
    208          * up front.
    209          */
    210         for (File file : files) {
    211             File zip = null;
    212             DexFile dex = null;
    213             String name = file.getName();
    214 
    215             if (file.isDirectory()) {
    216                 // We support directories for looking up resources.
    217                 // This is only useful for running libcore tests.
    218                 elements.add(new Element(file, true, null, null));
    219             } else if (file.isFile()){
    220                 if (name.endsWith(DEX_SUFFIX)) {
    221                     // Raw dex file (not inside a zip/jar).
    222                     try {
    223                         dex = loadDexFile(file, optimizedDirectory);
    224                     } catch (IOException ex) {
    225                         System.logE("Unable to load dex file: " + file, ex);
    226                     }
    227                 } else {
    228                     zip = file;
    229 
    230                     try {
    231                         dex = loadDexFile(file, optimizedDirectory);
    232                     } catch (IOException suppressed) {
    233                         /*
    234                          * IOException might get thrown "legitimately" by the DexFile constructor if
    235                          * the zip file turns out to be resource-only (that is, no classes.dex file
    236                          * in it).
    237                          * Let dex == null and hang on to the exception to add to the tea-leaves for
    238                          * when findClass returns null.
    239                          */
    240                         suppressedExceptions.add(suppressed);
    241                     }
    242                 }
    243             } else {
    244                 System.logW("ClassLoader referenced unknown path: " + file);
    245             }
    246 
    247             if ((zip != null) || (dex != null)) {
    248                 elements.add(new Element(file, false, zip, dex));
    249             }
    250         }
    251 
    252         return elements.toArray(new Element[elements.size()]);
    253     }
    254 
    255     /**
    256      * Constructs a {@code DexFile} instance, as appropriate depending
    257      * on whether {@code optimizedDirectory} is {@code null}.
    258      */
    259     private static DexFile loadDexFile(File file, File optimizedDirectory)
    260             throws IOException {
    261         if (optimizedDirectory == null) {
    262             return new DexFile(file);
    263         } else {
    264             String optimizedPath = optimizedPathFor(file, optimizedDirectory);
    265             return DexFile.loadDex(file.getPath(), optimizedPath, 0);
    266         }
    267     }
    268 
    269     /**
    270      * Converts a dex/jar file path and an output directory to an
    271      * output file path for an associated optimized dex file.
    272      */
    273     private static String optimizedPathFor(File path,
    274             File optimizedDirectory) {
    275         /*
    276          * Get the filename component of the path, and replace the
    277          * suffix with ".dex" if that's not already the suffix.
    278          *
    279          * We don't want to use ".odex", because the build system uses
    280          * that for files that are paired with resource-only jar
    281          * files. If the VM can assume that there's no classes.dex in
    282          * the matching jar, it doesn't need to open the jar to check
    283          * for updated dependencies, providing a slight performance
    284          * boost at startup. The use of ".dex" here matches the use on
    285          * files in /data/dalvik-cache.
    286          */
    287         String fileName = path.getName();
    288         if (!fileName.endsWith(DEX_SUFFIX)) {
    289             int lastDot = fileName.lastIndexOf(".");
    290             if (lastDot < 0) {
    291                 fileName += DEX_SUFFIX;
    292             } else {
    293                 StringBuilder sb = new StringBuilder(lastDot + 4);
    294                 sb.append(fileName, 0, lastDot);
    295                 sb.append(DEX_SUFFIX);
    296                 fileName = sb.toString();
    297             }
    298         }
    299 
    300         File result = new File(optimizedDirectory, fileName);
    301         return result.getPath();
    302     }
    303 
    304     /**
    305      * Finds the named class in one of the dex files pointed at by
    306      * this instance. This will find the one in the earliest listed
    307      * path element. If the class is found but has not yet been
    308      * defined, then this method will define it in the defining
    309      * context that this instance was constructed with.
    310      *
    311      * @param name of class to find
    312      * @param suppressed exceptions encountered whilst finding the class
    313      * @return the named class or {@code null} if the class is not
    314      * found in any of the dex files
    315      */
    316     public Class findClass(String name, List<Throwable> suppressed) {
    317         for (Element element : dexElements) {
    318             DexFile dex = element.dexFile;
    319 
    320             if (dex != null) {
    321                 Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
    322                 if (clazz != null) {
    323                     return clazz;
    324                 }
    325             }
    326         }
    327         if (dexElementsSuppressedExceptions != null) {
    328             suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    329         }
    330         return null;
    331     }
    332 
    333     /**
    334      * Finds the named resource in one of the zip/jar files pointed at
    335      * by this instance. This will find the one in the earliest listed
    336      * path element.
    337      *
    338      * @return a URL to the named resource or {@code null} if the
    339      * resource is not found in any of the zip/jar files
    340      */
    341     public URL findResource(String name) {
    342         for (Element element : dexElements) {
    343             URL url = element.findResource(name);
    344             if (url != null) {
    345                 return url;
    346             }
    347         }
    348 
    349         return null;
    350     }
    351 
    352     /**
    353      * Finds all the resources with the given name, returning an
    354      * enumeration of them. If there are no resources with the given
    355      * name, then this method returns an empty enumeration.
    356      */
    357     public Enumeration<URL> findResources(String name) {
    358         ArrayList<URL> result = new ArrayList<URL>();
    359 
    360         for (Element element : dexElements) {
    361             URL url = element.findResource(name);
    362             if (url != null) {
    363                 result.add(url);
    364             }
    365         }
    366 
    367         return Collections.enumeration(result);
    368     }
    369 
    370     /**
    371      * Finds the named native code library on any of the library
    372      * directories pointed at by this instance. This will find the
    373      * one in the earliest listed directory, ignoring any that are not
    374      * readable regular files.
    375      *
    376      * @return the complete path to the library or {@code null} if no
    377      * library was found
    378      */
    379     public String findLibrary(String libraryName) {
    380         String fileName = System.mapLibraryName(libraryName);
    381         for (File directory : nativeLibraryDirectories) {
    382             String path = new File(directory, fileName).getPath();
    383             if (IoUtils.canOpenReadOnly(path)) {
    384                 return path;
    385             }
    386         }
    387         return null;
    388     }
    389 
    390     /**
    391      * Element of the dex/resource file path
    392      */
    393     /*package*/ static class Element {
    394         private final File file;
    395         private final boolean isDirectory;
    396         private final File zip;
    397         private final DexFile dexFile;
    398 
    399         private ZipFile zipFile;
    400         private boolean initialized;
    401 
    402         public Element(File file, boolean isDirectory, File zip, DexFile dexFile) {
    403             this.file = file;
    404             this.isDirectory = isDirectory;
    405             this.zip = zip;
    406             this.dexFile = dexFile;
    407         }
    408 
    409         @Override public String toString() {
    410             if (isDirectory) {
    411                 return "directory \"" + file + "\"";
    412             } else if (zip != null) {
    413                 return "zip file \"" + zip + "\"";
    414             } else {
    415                 return "dex file \"" + dexFile + "\"";
    416             }
    417         }
    418 
    419         public synchronized void maybeInit() {
    420             if (initialized) {
    421                 return;
    422             }
    423 
    424             initialized = true;
    425 
    426             if (isDirectory || zip == null) {
    427                 return;
    428             }
    429 
    430             try {
    431                 zipFile = new ZipFile(zip);
    432             } catch (IOException ioe) {
    433                 /*
    434                  * Note: ZipException (a subclass of IOException)
    435                  * might get thrown by the ZipFile constructor
    436                  * (e.g. if the file isn't actually a zip/jar
    437                  * file).
    438                  */
    439                 System.logE("Unable to open zip file: " + file, ioe);
    440                 zipFile = null;
    441             }
    442         }
    443 
    444         public URL findResource(String name) {
    445             maybeInit();
    446 
    447             // We support directories so we can run tests and/or legacy code
    448             // that uses Class.getResource.
    449             if (isDirectory) {
    450                 File resourceFile = new File(file, name);
    451                 if (resourceFile.exists()) {
    452                     try {
    453                         return resourceFile.toURI().toURL();
    454                     } catch (MalformedURLException ex) {
    455                         throw new RuntimeException(ex);
    456                     }
    457                 }
    458             }
    459 
    460             if (zipFile == null || zipFile.getEntry(name) == null) {
    461                 /*
    462                  * Either this element has no zip/jar file (first
    463                  * clause), or the zip/jar file doesn't have an entry
    464                  * for the given name (second clause).
    465                  */
    466                 return null;
    467             }
    468 
    469             try {
    470                 /*
    471                  * File.toURL() is compliant with RFC 1738 in
    472                  * always creating absolute path names. If we
    473                  * construct the URL by concatenating strings, we
    474                  * might end up with illegal URLs for relative
    475                  * names.
    476                  */
    477                 return new URL("jar:" + file.toURL() + "!/" + name);
    478             } catch (MalformedURLException ex) {
    479                 throw new RuntimeException(ex);
    480             }
    481         }
    482     }
    483 }
    484