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