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