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 
17 package dalvik.system;
18 
19 import android.system.ErrnoException;
20 import android.system.StructStat;
21 import java.io.File;
22 import java.io.IOException;
23 import java.net.MalformedURLException;
24 import java.net.URL;
25 import java.util.ArrayList;
26 import java.util.Arrays;
27 import java.util.Collections;
28 import java.util.Enumeration;
29 import java.util.List;
30 import java.util.zip.ZipEntry;
31 import libcore.io.IoUtils;
32 import libcore.io.Libcore;
33 import libcore.io.ClassPathURLStreamHandler;
34 
35 import 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      */
DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory)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 
toString()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      */
getNativeLibraryDirectories()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      */
addDexPath(String dexPath, File optimizedDirectory)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      */
splitDexPath(String path)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      */
splitPaths(String searchPath, boolean directoriesOnly)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      */
makeDexElements(List<File> files, File optimizedDirectory, List<IOException> suppressedExceptions, ClassLoader loader)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      */
makePathElements(List<File> files, List<IOException> suppressedExceptions, ClassLoader loader)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      */
makePathElements(List<File> files, File optimizedDirectory, List<IOException> suppressedExceptions)279     private static Element[] makePathElements(List<File> files, File optimizedDirectory,
280                                               List<IOException> suppressedExceptions) {
281         return makeElements(files, optimizedDirectory, suppressedExceptions, false, null);
282     }
283 
makeElements(List<File> files, File optimizedDirectory, List<IOException> suppressedExceptions, boolean ignoreDexFiles, ClassLoader loader)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      */
loadDexFile(File file, File optimizedDirectory, ClassLoader loader, Element[] elements)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      */
optimizedPathFor(File path, File optimizedDirectory)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      */
findClass(String name, List<Throwable> suppressed)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      */
findResource(String name)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      */
findResources(String name)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      */
findLibrary(String libraryName)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 
Element(File dir, boolean isDirectory, File zip, DexFile dexFile)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 
toString()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 
maybeInit()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 
findNativeLibrary(String name)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 
findResource(String name)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