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