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