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