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