DexPathList.java revision 1a796cbc5dfb263511f2f4e5213adbc1d396a813
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
17package dalvik.system;
18
19import android.system.ErrnoException;
20import android.system.StructStat;
21import java.io.File;
22import java.io.FilterInputStream;
23import java.io.IOException;
24import java.io.InputStream;
25import java.net.JarURLConnection;
26import java.net.MalformedURLException;
27import java.net.URI;
28import java.net.URL;
29import java.net.URLConnection;
30import java.net.URLStreamHandler;
31import java.util.ArrayList;
32import java.util.Arrays;
33import java.util.Collections;
34import java.util.Enumeration;
35import java.util.List;
36import java.util.jar.JarFile;
37import java.util.zip.ZipEntry;
38import java.util.zip.ZipFile;
39import libcore.io.IoUtils;
40import libcore.io.Libcore;
41import static android.system.OsConstants.*;
42
43/**
44 * A pair of lists of entries, associated with a {@code ClassLoader}.
45 * One of the lists is a dex/resource path — typically referred
46 * to as a "class path" — list, and the other names directories
47 * containing native code libraries. Class path entries may be any of:
48 * a {@code .jar} or {@code .zip} file containing an optional
49 * top-level {@code classes.dex} file as well as arbitrary resources,
50 * or a plain {@code .dex} file (with no possibility of associated
51 * resources).
52 *
53 * <p>This class also contains methods to use these lists to look up
54 * classes and resources.</p>
55 */
56/*package*/ final class DexPathList {
57    private static final String DEX_SUFFIX = ".dex";
58
59    /** class definition context */
60    private final ClassLoader definingContext;
61
62    /** List of dexfiles. */
63    private final List<File> dexFiles;
64
65    /**
66     * List of dex/resource (class path) elements.
67     * Should be called pathElements, but the Facebook app uses reflection
68     * to modify 'dexElements' (http://b/7726934).
69     */
70    private final Element[] dexElements;
71
72    /** List of native library directories. */
73    private final List<File> nativeLibraryDirectories;
74
75    /**
76     * Exceptions thrown during creation of the dexElements list.
77     */
78    private final IOException[] dexElementsSuppressedExceptions;
79
80    /**
81     * Constructs an instance.
82     *
83     * @param definingContext the context in which any as-yet unresolved
84     * classes should be defined
85     * @param dexPath list of dex/resource path elements, separated by
86     * {@code File.pathSeparator}
87     * @param libraryPath list of native library directory path elements,
88     * separated by {@code File.pathSeparator}
89     * @param optimizedDirectory directory where optimized {@code .dex} files
90     * should be found and written to, or {@code null} to use the default
91     * system directory for same
92     */
93    public DexPathList(ClassLoader definingContext, String dexPath,
94            String libraryPath, File optimizedDirectory) {
95        if (definingContext == null) {
96            throw new NullPointerException("definingContext == null");
97        }
98
99        if (dexPath == null) {
100            throw new NullPointerException("dexPath == null");
101        }
102
103        if (optimizedDirectory != null) {
104            if (!optimizedDirectory.exists())  {
105                throw new IllegalArgumentException(
106                        "optimizedDirectory doesn't exist: "
107                        + optimizedDirectory);
108            }
109
110            if (!(optimizedDirectory.canRead()
111                            && optimizedDirectory.canWrite())) {
112                throw new IllegalArgumentException(
113                        "optimizedDirectory not readable/writable: "
114                        + optimizedDirectory);
115            }
116        }
117
118        this.definingContext = definingContext;
119        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
120        // save dexPath for BaseDexClassLoader
121        this.dexFiles = splitDexPath(dexPath);
122        this.dexElements = makeDexElements(dexFiles, optimizedDirectory,
123                                           suppressedExceptions);
124        if (suppressedExceptions.size() > 0) {
125            this.dexElementsSuppressedExceptions =
126                suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
127        } else {
128            dexElementsSuppressedExceptions = null;
129        }
130        this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
131    }
132
133    @Override public String toString() {
134        File[] nativeLibraryDirectoriesArray =
135                nativeLibraryDirectories.toArray(new File[nativeLibraryDirectories.size()]);
136
137        return "DexPathList[" + Arrays.toString(dexElements) +
138            ",nativeLibraryDirectories=" + Arrays.toString(nativeLibraryDirectoriesArray) + "]";
139    }
140
141    /**
142     * For BaseDexClassLoader.getLdLibraryPath.
143     */
144    public List<File> getNativeLibraryDirectories() {
145        return nativeLibraryDirectories;
146    }
147
148    /**
149     * For BaseDexClassLoader.getDexPath.
150     */
151    public List<File> getDexFiles() {
152        return dexFiles;
153    }
154
155    /**
156     * Splits the given dex path string into elements using the path
157     * separator, pruning out any elements that do not refer to existing
158     * and readable files. (That is, directories are not included in the
159     * result.)
160     */
161    private static List<File> splitDexPath(String path) {
162        return splitPaths(path, null, false);
163    }
164
165    /**
166     * Splits the given library directory path string into elements
167     * using the path separator ({@code File.pathSeparator}, which
168     * defaults to {@code ":"} on Android, appending on the elements
169     * from the system library path, and pruning out any elements that
170     * do not refer to existing and readable directories.
171     */
172    private static List<File> splitLibraryPath(String path) {
173        // Native libraries may exist in both the system and
174        // application library paths, and we use this search order:
175        //
176        //   1. this class loader's library path for application libraries
177        //   2. the VM's library path from the system property for system libraries
178        //
179        // This order was reversed prior to Gingerbread; see http://b/2933456.
180        return splitPaths(path, System.getProperty("java.library.path"), true);
181    }
182
183    /**
184     * Splits the given path strings into file elements using the path
185     * separator, combining the results and filtering out elements
186     * that don't exist, aren't readable, or aren't either a regular
187     * file or a directory (as specified). Either string may be empty
188     * or {@code null}, in which case it is ignored. If both strings
189     * are empty or {@code null}, or all elements get pruned out, then
190     * this returns a zero-element list.
191     */
192    private static List<File> splitPaths(String path1, String path2, boolean wantDirectories) {
193        List<File> result = new ArrayList<File>();
194
195        splitAndAdd(path1, wantDirectories, result);
196        splitAndAdd(path2, wantDirectories, result);
197        return result;
198    }
199
200    /**
201     * Helper for {@link #splitPaths}, which does the actual splitting
202     * and filtering and adding to a result.
203     */
204    private static void splitAndAdd(String searchPath, boolean directoriesOnly,
205            List<File> resultList) {
206        if (searchPath == null) {
207            return;
208        }
209        for (String path : searchPath.split(":")) {
210            try {
211                StructStat sb = Libcore.os.stat(path);
212                if (!directoriesOnly || S_ISDIR(sb.st_mode)) {
213                    resultList.add(new File(path));
214                }
215            } catch (ErrnoException ignored) {
216            }
217        }
218    }
219
220    /**
221     * Makes an array of dex/resource path elements, one per element of
222     * the given array.
223     */
224    private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
225                                             List<IOException> suppressedExceptions) {
226        ArrayList<Element> elements = new ArrayList<Element>();
227        /*
228         * Open all files and load the (direct or contained) dex files
229         * up front.
230         */
231        for (File file : files) {
232            File zip = null;
233            DexFile dex = null;
234            String name = file.getName();
235
236            if (file.isDirectory()) {
237                // We support directories for looking up resources.
238                // This is only useful for running libcore tests.
239                elements.add(new Element(file, true, null, null));
240            } else if (file.isFile()){
241                if (name.endsWith(DEX_SUFFIX)) {
242                    // Raw dex file (not inside a zip/jar).
243                    try {
244                        dex = loadDexFile(file, optimizedDirectory);
245                    } catch (IOException ex) {
246                        System.logE("Unable to load dex file: " + file, ex);
247                    }
248                } else {
249                    zip = file;
250
251                    try {
252                        dex = loadDexFile(file, optimizedDirectory);
253                    } catch (IOException suppressed) {
254                        /*
255                         * IOException might get thrown "legitimately" by the DexFile constructor if
256                         * the zip file turns out to be resource-only (that is, no classes.dex file
257                         * in it).
258                         * Let dex == null and hang on to the exception to add to the tea-leaves for
259                         * when findClass returns null.
260                         */
261                        suppressedExceptions.add(suppressed);
262                    }
263                }
264            } else {
265                System.logW("ClassLoader referenced unknown path: " + file);
266            }
267
268            if ((zip != null) || (dex != null)) {
269                elements.add(new Element(file, false, zip, dex));
270            }
271        }
272
273        return elements.toArray(new Element[elements.size()]);
274    }
275
276    /**
277     * Constructs a {@code DexFile} instance, as appropriate depending
278     * on whether {@code optimizedDirectory} is {@code null}.
279     */
280    private static DexFile loadDexFile(File file, File optimizedDirectory)
281            throws IOException {
282        if (optimizedDirectory == null) {
283            return new DexFile(file);
284        } else {
285            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
286            return DexFile.loadDex(file.getPath(), optimizedPath, 0);
287        }
288    }
289
290    /**
291     * Converts a dex/jar file path and an output directory to an
292     * output file path for an associated optimized dex file.
293     */
294    private static String optimizedPathFor(File path,
295            File optimizedDirectory) {
296        /*
297         * Get the filename component of the path, and replace the
298         * suffix with ".dex" if that's not already the suffix.
299         *
300         * We don't want to use ".odex", because the build system uses
301         * that for files that are paired with resource-only jar
302         * files. If the VM can assume that there's no classes.dex in
303         * the matching jar, it doesn't need to open the jar to check
304         * for updated dependencies, providing a slight performance
305         * boost at startup. The use of ".dex" here matches the use on
306         * files in /data/dalvik-cache.
307         */
308        String fileName = path.getName();
309        if (!fileName.endsWith(DEX_SUFFIX)) {
310            int lastDot = fileName.lastIndexOf(".");
311            if (lastDot < 0) {
312                fileName += DEX_SUFFIX;
313            } else {
314                StringBuilder sb = new StringBuilder(lastDot + 4);
315                sb.append(fileName, 0, lastDot);
316                sb.append(DEX_SUFFIX);
317                fileName = sb.toString();
318            }
319        }
320
321        File result = new File(optimizedDirectory, fileName);
322        return result.getPath();
323    }
324
325    /**
326     * Finds the named class in one of the dex files pointed at by
327     * this instance. This will find the one in the earliest listed
328     * path element. If the class is found but has not yet been
329     * defined, then this method will define it in the defining
330     * context that this instance was constructed with.
331     *
332     * @param name of class to find
333     * @param suppressed exceptions encountered whilst finding the class
334     * @return the named class or {@code null} if the class is not
335     * found in any of the dex files
336     */
337    public Class findClass(String name, List<Throwable> suppressed) {
338        for (Element element : dexElements) {
339            DexFile dex = element.dexFile;
340
341            if (dex != null) {
342                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
343                if (clazz != null) {
344                    return clazz;
345                }
346            }
347        }
348        if (dexElementsSuppressedExceptions != null) {
349            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
350        }
351        return null;
352    }
353
354    /**
355     * Finds the named resource in one of the zip/jar files pointed at
356     * by this instance. This will find the one in the earliest listed
357     * path element.
358     *
359     * @return a URL to the named resource or {@code null} if the
360     * resource is not found in any of the zip/jar files
361     */
362    public URL findResource(String name) {
363        for (Element element : dexElements) {
364            URL url = element.findResource(name);
365            if (url != null) {
366                return url;
367            }
368        }
369
370        return null;
371    }
372
373    /**
374     * Finds all the resources with the given name, returning an
375     * enumeration of them. If there are no resources with the given
376     * name, then this method returns an empty enumeration.
377     */
378    public Enumeration<URL> findResources(String name) {
379        ArrayList<URL> result = new ArrayList<URL>();
380
381        for (Element element : dexElements) {
382            URL url = element.findResource(name);
383            if (url != null) {
384                result.add(url);
385            }
386        }
387
388        return Collections.enumeration(result);
389    }
390
391    /**
392     * Finds the named native code library on any of the library
393     * directories pointed at by this instance. This will find the
394     * one in the earliest listed directory, ignoring any that are not
395     * readable regular files.
396     *
397     * @return the complete path to the library or {@code null} if no
398     * library was found
399     */
400    public String findLibrary(String libraryName) {
401        String fileName = System.mapLibraryName(libraryName);
402        for (File directory : nativeLibraryDirectories) {
403            String path = new File(directory, fileName).getPath();
404            if (IoUtils.canOpenReadOnly(path)) {
405                return path;
406            }
407        }
408        return null;
409    }
410
411    /**
412     * Element of the dex/resource file path
413     */
414    /*package*/ static class Element {
415        private final File file;
416        private final boolean isDirectory;
417        private final File zip;
418        private final DexFile dexFile;
419
420        private ZipFile zipFile;
421        private URLStreamHandler urlHandler;
422        private boolean initialized;
423
424        public Element(File file, boolean isDirectory, File zip, DexFile dexFile) {
425            this.file = file;
426            this.isDirectory = isDirectory;
427            this.zip = zip;
428            this.dexFile = dexFile;
429        }
430
431        @Override public String toString() {
432            if (isDirectory) {
433                return "directory \"" + file + "\"";
434            } else if (zip != null) {
435                return "zip file \"" + zip + "\"";
436            } else {
437                return "dex file \"" + dexFile + "\"";
438            }
439        }
440
441        public synchronized void maybeInit() {
442            if (initialized) {
443                return;
444            }
445
446            initialized = true;
447
448            if (isDirectory || zip == null) {
449                return;
450            }
451
452            try {
453                zipFile = new ZipFile(zip);
454                urlHandler = new ElementURLStreamHandler(zipFile);
455            } catch (IOException ioe) {
456                /*
457                 * Note: ZipException (a subclass of IOException)
458                 * might get thrown by the ZipFile constructor
459                 * (e.g. if the file isn't actually a zip/jar
460                 * file).
461                 */
462                System.logE("Unable to open zip file: " + file, ioe);
463                zipFile = null;
464            }
465        }
466
467        public URL findResource(String name) {
468            maybeInit();
469
470            // We support directories so we can run tests and/or legacy code
471            // that uses Class.getResource.
472            if (isDirectory) {
473                File resourceFile = new File(file, name);
474                if (resourceFile.exists()) {
475                    try {
476                        return resourceFile.toURI().toURL();
477                    } catch (MalformedURLException ex) {
478                        throw new RuntimeException(ex);
479                    }
480                }
481            }
482
483            if (zipFile == null || zipFile.getEntry(name) == null) {
484                /*
485                 * Either this element has no zip/jar file (first
486                 * clause), or the zip/jar file doesn't have an entry
487                 * for the given name (second clause).
488                 */
489                return null;
490            }
491
492            try {
493                /*
494                 * File.toURI() is compliant with RFC 1738 in
495                 * always creating absolute path names. If we
496                 * construct the URL by concatenating strings, we
497                 * might end up with illegal URLs for relative
498                 * names.
499                 */
500                URI fileUri = file.toURI();
501                return new URL("jar", null, -1, fileUri.toString() + "!/" + name, urlHandler);
502            } catch (MalformedURLException ex) {
503                throw new RuntimeException(ex);
504            }
505        }
506
507        /**
508         * URLStreamHandler for an Element. Avoids the need to open a .jar file again to read
509         * resources.
510         */
511        private static class ElementURLStreamHandler extends URLStreamHandler {
512
513            private final ZipFile zipFile;
514
515            public ElementURLStreamHandler(ZipFile zipFile) {
516                this.zipFile = zipFile;
517            }
518
519            @Override
520            protected URLConnection openConnection(URL url) throws IOException {
521                return new ElementJarURLConnection(url, zipFile);
522            }
523        }
524
525        /**
526         * A JarURLConnection that is backed by a ZipFile held open by an {@link Element}. For
527         * backwards compatibility it extends JarURLConnection even though it's not actually backed
528         * by a {@link JarFile}.
529         */
530        private static class ElementJarURLConnection extends JarURLConnection {
531
532            private final ZipFile zipFile;
533            private final ZipEntry zipEntry;
534
535            private InputStream jarInput;
536            private boolean closed;
537            private JarFile jarFile;
538
539            public ElementJarURLConnection(URL url, ZipFile zipFile) throws MalformedURLException {
540                super(url);
541                this.zipFile = zipFile;
542                this.zipEntry = zipFile.getEntry(getEntryName());
543                if (zipEntry == null) {
544                    throw new MalformedURLException(
545                            "URL does not correspond to an entry in the zip file. URL=" + url
546                                    + ", zipfile=" + zipFile.getName());
547                }
548            }
549
550            @Override
551            public void connect() {
552                connected = true;
553            }
554
555            @Override
556            public JarFile getJarFile() throws IOException {
557                // This is expensive because we only pretend that we wrap a JarFile.
558                if (jarFile == null) {
559                    jarFile = new JarFile(zipFile.getName());
560                }
561                return jarFile;
562            }
563
564            @Override
565            public InputStream getInputStream() throws IOException {
566                if (closed) {
567                    throw new IllegalStateException("JarURLConnection InputStream has been closed");
568                }
569                connect();
570                if (jarInput != null) {
571                    return jarInput;
572                }
573                return jarInput = new FilterInputStream(zipFile.getInputStream(zipEntry)) {
574                    @Override
575                    public void close() throws IOException {
576                        super.close();
577                        closed = true;
578                    }
579                };
580            }
581
582            /**
583             * Returns the content type of the entry based on the name of the entry. Returns
584             * non-null results ("content/unknown" for unknown types).
585             *
586             * @return the content type
587             */
588            @Override
589            public String getContentType() {
590                String cType = guessContentTypeFromName(getEntryName());
591                if (cType == null) {
592                    cType = "content/unknown";
593                }
594                return cType;
595            }
596
597            @Override
598            public int getContentLength() {
599                connect();
600                return (int) zipEntry.getSize();
601            }
602        }
603    }
604}
605