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