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 */ 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