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