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