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