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