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