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