1 /* 2 * Copyright 2016, Google Inc. 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are 7 * met: 8 * 9 * Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * Redistributions in binary form must reproduce the above 12 * copyright notice, this list of conditions and the following disclaimer 13 * in the documentation and/or other materials provided with the 14 * distribution. 15 * Neither the name of Google Inc. nor the names of its 16 * contributors may be used to endorse or promote products derived from 17 * this software without specific prior written permission. 18 * 19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 */ 31 32 package org.jf.dexlib2.analysis; 33 34 import com.beust.jcommander.internal.Sets; 35 import com.google.common.base.Joiner; 36 import com.google.common.base.Splitter; 37 import com.google.common.collect.Lists; 38 import org.jf.dexlib2.DexFileFactory; 39 import org.jf.dexlib2.DexFileFactory.UnsupportedFileTypeException; 40 import org.jf.dexlib2.Opcodes; 41 import org.jf.dexlib2.dexbacked.DexBackedDexFile; 42 import org.jf.dexlib2.dexbacked.DexBackedOdexFile; 43 import org.jf.dexlib2.dexbacked.OatFile; 44 import org.jf.dexlib2.dexbacked.OatFile.OatDexFile; 45 import org.jf.dexlib2.iface.DexFile; 46 import org.jf.dexlib2.iface.MultiDexContainer; 47 import org.jf.dexlib2.iface.MultiDexContainer.MultiDexFile; 48 49 import javax.annotation.Nonnull; 50 import javax.annotation.Nullable; 51 import java.io.File; 52 import java.io.IOException; 53 import java.util.List; 54 import java.util.Set; 55 56 public class ClassPathResolver { 57 private final Iterable<String> classPathDirs; 58 private final Opcodes opcodes; 59 60 private final Set<File> loadedFiles = Sets.newHashSet(); 61 private final List<ClassProvider> classProviders = Lists.newArrayList(); 62 63 /** 64 * Constructs a new ClassPathResolver using a specified list of bootclasspath entries 65 * 66 * @param bootClassPathDirs A list of directories to search for boot classpath entries. Can be empty if all boot 67 * classpath entries are specified as local paths 68 * @param bootClassPathEntries A list of boot classpath entries to load. These can either be local paths, or 69 * device paths (e.g. "/system/framework/framework.jar"). The entry will be interpreted 70 * first as a local path. If not found as a local path, it will be interpreted as a 71 * partial or absolute device path, and will be searched for in bootClassPathDirs 72 * @param extraClassPathEntries A list of additional classpath entries to load. Can be empty. All entries must be 73 * local paths. Device paths are not supported. 74 * @param dexFile The dex file that the classpath will be used to analyze 75 * @throws IOException If any IOException occurs 76 * @throws ResolveException If any classpath entries cannot be loaded for some reason 77 * 78 * If null, a default bootclasspath is used, 79 * depending on the the file type of dexFile and the api level. If empty, no boot 80 * classpath entries will be loaded 81 */ 82 public ClassPathResolver(@Nonnull List<String> bootClassPathDirs, @Nonnull List<String> bootClassPathEntries, 83 @Nonnull List<String> extraClassPathEntries, @Nonnull DexFile dexFile) 84 throws IOException { 85 this(bootClassPathDirs, bootClassPathEntries, extraClassPathEntries, dexFile, true); 86 } 87 88 /** 89 * Constructs a new ClassPathResolver using a default list of bootclasspath entries 90 * 91 * @param bootClassPathDirs A list of directories to search for boot classpath entries 92 * @param extraClassPathEntries A list of additional classpath entries to load. Can be empty. All entries must be 93 * local paths. Device paths are not supported. 94 * @param dexFile The dex file that the classpath will be used to analyze 95 * @throws IOException If any IOException occurs 96 * @throws ResolveException If any classpath entries cannot be loaded for some reason 97 * 98 * If null, a default bootclasspath is used, 99 * depending on the the file type of dexFile and the api level. If empty, no boot 100 * classpath entries will be loaded 101 */ 102 public ClassPathResolver(@Nonnull List<String> bootClassPathDirs, @Nonnull List<String> extraClassPathEntries, 103 @Nonnull DexFile dexFile) 104 throws IOException { 105 this(bootClassPathDirs, null, extraClassPathEntries, dexFile, true); 106 } 107 108 private ClassPathResolver(@Nonnull List<String> bootClassPathDirs, @Nullable List<String> bootClassPathEntries, 109 @Nonnull List<String> extraClassPathEntries, @Nonnull DexFile dexFile, boolean unused) 110 throws IOException { 111 this.classPathDirs = bootClassPathDirs; 112 opcodes = dexFile.getOpcodes(); 113 114 if (bootClassPathEntries == null) { 115 bootClassPathEntries = getDefaultBootClassPath(dexFile, opcodes.api); 116 } 117 118 for (String entry : bootClassPathEntries) { 119 try { 120 loadLocalOrDeviceBootClassPathEntry(entry); 121 } catch (NoDexException ex) { 122 if (entry.endsWith(".jar")) { 123 String odexEntry = entry.substring(0, entry.length() - 4) + ".odex"; 124 try { 125 loadLocalOrDeviceBootClassPathEntry(odexEntry); 126 } catch (NoDexException ex2) { 127 throw new ResolveException("Neither %s nor %s contain a dex file", entry, odexEntry); 128 } catch (NotFoundException ex2) { 129 throw new ResolveException(ex); 130 } 131 } else { 132 throw new ResolveException(ex); 133 } 134 } catch (NotFoundException ex) { 135 if (entry.endsWith(".odex")) { 136 String jarEntry = entry.substring(0, entry.length() - 5) + ".jar"; 137 try { 138 loadLocalOrDeviceBootClassPathEntry(jarEntry); 139 } catch (NoDexException ex2) { 140 throw new ResolveException("Neither %s nor %s contain a dex file", entry, jarEntry); 141 } catch (NotFoundException ex2) { 142 throw new ResolveException(ex); 143 } 144 } else { 145 throw new ResolveException(ex); 146 } 147 } 148 } 149 150 for (String entry: extraClassPathEntries) { 151 // extra classpath entries must be specified using a local path, so we don't need to do the search through 152 // bootClassPathDirs 153 try { 154 loadLocalClassPathEntry(entry); 155 } catch (NoDexException ex) { 156 throw new ResolveException(ex); 157 } 158 } 159 160 if (dexFile instanceof MultiDexContainer.MultiDexFile) { 161 MultiDexContainer<? extends MultiDexFile> container = ((MultiDexFile)dexFile).getContainer(); 162 for (String entry: container.getDexEntryNames()) { 163 classProviders.add(new DexClassProvider(container.getEntry(entry))); 164 } 165 } else { 166 classProviders.add(new DexClassProvider(dexFile)); 167 } 168 } 169 170 @Nonnull 171 public List<ClassProvider> getResolvedClassProviders() { 172 return classProviders; 173 } 174 175 private boolean loadLocalClassPathEntry(@Nonnull String entry) throws NoDexException, IOException { 176 File entryFile = new File(entry); 177 if (entryFile.exists() && entryFile.isFile()) { 178 try { 179 loadEntry(entryFile, true); 180 return true; 181 } catch (UnsupportedFileTypeException ex) { 182 throw new ResolveException(ex, "Couldn't load classpath entry %s", entry); 183 } 184 } 185 return false; 186 } 187 188 private void loadLocalOrDeviceBootClassPathEntry(@Nonnull String entry) 189 throws IOException, NoDexException, NotFoundException { 190 // first, see if the entry is a valid local path 191 if (loadLocalClassPathEntry(entry)) { 192 return; 193 } 194 195 // It's not a local path, so let's try to resolve it as a device path, relative to one of the provided 196 // directories 197 List<String> pathComponents = splitDevicePath(entry); 198 Joiner pathJoiner = Joiner.on(File.pathSeparatorChar); 199 200 for (String directory: classPathDirs) { 201 File directoryFile = new File(directory); 202 if (!directoryFile.exists()) { 203 continue; 204 } 205 206 for (int i=0; i<pathComponents.size(); i++) { 207 String partialPath = pathJoiner.join(pathComponents.subList(i, pathComponents.size())); 208 File entryFile = new File(directoryFile, partialPath); 209 if (entryFile.exists() && entryFile.isFile()) { 210 loadEntry(entryFile, true); 211 return; 212 } 213 } 214 } 215 216 throw new NotFoundException("Could not find classpath entry %s", entry); 217 } 218 219 private void loadEntry(@Nonnull File entryFile, boolean loadOatDependencies) 220 throws IOException, NoDexException { 221 if (loadedFiles.contains(entryFile)) { 222 return; 223 } 224 225 MultiDexContainer<? extends DexBackedDexFile> container; 226 try { 227 container = DexFileFactory.loadDexContainer(entryFile, opcodes); 228 } catch (UnsupportedFileTypeException ex) { 229 throw new ResolveException(ex); 230 } 231 232 List<String> entryNames = container.getDexEntryNames(); 233 234 if (entryNames.size() == 0) { 235 throw new NoDexException("%s contains no dex file", entryFile); 236 } 237 238 loadedFiles.add(entryFile); 239 240 for (String entryName: entryNames) { 241 classProviders.add(new DexClassProvider(container.getEntry(entryName))); 242 } 243 244 if (loadOatDependencies && container instanceof OatFile) { 245 List<String> oatDependencies = ((OatFile)container).getBootClassPath(); 246 if (!oatDependencies.isEmpty()) { 247 try { 248 loadOatDependencies(entryFile.getParentFile(), oatDependencies); 249 } catch (NotFoundException ex) { 250 throw new ResolveException(ex, "Error while loading oat file %s", entryFile); 251 } catch (NoDexException ex) { 252 throw new ResolveException(ex, "Error while loading dependencies for oat file %s", entryFile); 253 } 254 } 255 } 256 } 257 258 @Nonnull 259 private static List<String> splitDevicePath(@Nonnull String path) { 260 return Lists.newArrayList(Splitter.on('/').split(path)); 261 } 262 263 private void loadOatDependencies(@Nonnull File directory, @Nonnull List<String> oatDependencies) 264 throws IOException, NoDexException, NotFoundException { 265 // We assume that all oat dependencies are located in the same directory as the oat file 266 for (String oatDependency: oatDependencies) { 267 String oatDependencyName = getFilenameForOatDependency(oatDependency); 268 File file = new File(directory, oatDependencyName); 269 if (!file.exists()) { 270 throw new NotFoundException("Cannot find dependency %s in %s", oatDependencyName, directory); 271 } 272 273 loadEntry(file, false); 274 } 275 } 276 277 @Nonnull 278 private String getFilenameForOatDependency(String oatDependency) { 279 int index = oatDependency.lastIndexOf('/'); 280 281 String dependencyLeaf = oatDependency.substring(index+1); 282 if (dependencyLeaf.endsWith(".art")) { 283 return dependencyLeaf.substring(0, dependencyLeaf.length() - 4) + ".oat"; 284 } 285 return dependencyLeaf; 286 } 287 288 private static class NotFoundException extends Exception { 289 public NotFoundException(String message, Object... formatArgs) { 290 super(String.format(message, formatArgs)); 291 } 292 } 293 294 private static class NoDexException extends Exception { 295 public NoDexException(String message, Object... formatArgs) { 296 super(String.format(message, formatArgs)); 297 } 298 } 299 300 /** 301 * An error that occurred while resolving the classpath 302 */ 303 public static class ResolveException extends RuntimeException { 304 public ResolveException (String message, Object... formatArgs) { 305 super(String.format(message, formatArgs)); 306 } 307 308 public ResolveException (Throwable cause) { 309 super(cause); 310 } 311 312 public ResolveException (Throwable cause, String message, Object... formatArgs) { 313 super(String.format(message, formatArgs), cause); 314 } 315 } 316 317 /** 318 * Returns the default boot class path for the given dex file and api level. 319 */ 320 @Nonnull 321 private static List<String> getDefaultBootClassPath(@Nonnull DexFile dexFile, int apiLevel) { 322 if (dexFile instanceof OatFile.OatDexFile) { 323 List<String> bcp = ((OatDexFile)dexFile).getContainer().getBootClassPath(); 324 if (!bcp.isEmpty()) { 325 for (int i=0; i<bcp.size(); i++) { 326 String entry = bcp.get(i); 327 if (entry.endsWith(".art")) { 328 bcp.set(i, entry.substring(0, entry.length() - 4) + ".oat"); 329 } 330 } 331 return bcp; 332 } 333 return Lists.newArrayList("boot.oat"); 334 } 335 336 if (dexFile instanceof DexBackedOdexFile) { 337 return ((DexBackedOdexFile)dexFile).getDependencies(); 338 } 339 340 if (apiLevel <= 8) { 341 return Lists.newArrayList( 342 "/system/framework/core.jar", 343 "/system/framework/ext.jar", 344 "/system/framework/framework.jar", 345 "/system/framework/android.policy.jar", 346 "/system/framework/services.jar"); 347 } else if (apiLevel <= 11) { 348 return Lists.newArrayList( 349 "/system/framework/core.jar", 350 "/system/framework/bouncycastle.jar", 351 "/system/framework/ext.jar", 352 "/system/framework/framework.jar", 353 "/system/framework/android.policy.jar", 354 "/system/framework/services.jar", 355 "/system/framework/core-junit.jar"); 356 } else if (apiLevel <= 13) { 357 return Lists.newArrayList( 358 "/system/framework/core.jar", 359 "/system/framework/apache-xml.jar", 360 "/system/framework/bouncycastle.jar", 361 "/system/framework/ext.jar", 362 "/system/framework/framework.jar", 363 "/system/framework/android.policy.jar", 364 "/system/framework/services.jar", 365 "/system/framework/core-junit.jar"); 366 } else if (apiLevel <= 15) { 367 return Lists.newArrayList( 368 "/system/framework/core.jar", 369 "/system/framework/core-junit.jar", 370 "/system/framework/bouncycastle.jar", 371 "/system/framework/ext.jar", 372 "/system/framework/framework.jar", 373 "/system/framework/android.policy.jar", 374 "/system/framework/services.jar", 375 "/system/framework/apache-xml.jar", 376 "/system/framework/filterfw.jar"); 377 } else if (apiLevel <= 17) { 378 // this is correct as of api 17/4.2.2 379 return Lists.newArrayList( 380 "/system/framework/core.jar", 381 "/system/framework/core-junit.jar", 382 "/system/framework/bouncycastle.jar", 383 "/system/framework/ext.jar", 384 "/system/framework/framework.jar", 385 "/system/framework/telephony-common.jar", 386 "/system/framework/mms-common.jar", 387 "/system/framework/android.policy.jar", 388 "/system/framework/services.jar", 389 "/system/framework/apache-xml.jar"); 390 } else if (apiLevel <= 18) { 391 return Lists.newArrayList( 392 "/system/framework/core.jar", 393 "/system/framework/core-junit.jar", 394 "/system/framework/bouncycastle.jar", 395 "/system/framework/ext.jar", 396 "/system/framework/framework.jar", 397 "/system/framework/telephony-common.jar", 398 "/system/framework/voip-common.jar", 399 "/system/framework/mms-common.jar", 400 "/system/framework/android.policy.jar", 401 "/system/framework/services.jar", 402 "/system/framework/apache-xml.jar"); 403 } else if (apiLevel <= 19) { 404 return Lists.newArrayList( 405 "/system/framework/core.jar", 406 "/system/framework/conscrypt.jar", 407 "/system/framework/core-junit.jar", 408 "/system/framework/bouncycastle.jar", 409 "/system/framework/ext.jar", 410 "/system/framework/framework.jar", 411 "/system/framework/framework2.jar", 412 "/system/framework/telephony-common.jar", 413 "/system/framework/voip-common.jar", 414 "/system/framework/mms-common.jar", 415 "/system/framework/android.policy.jar", 416 "/system/framework/services.jar", 417 "/system/framework/apache-xml.jar", 418 "/system/framework/webviewchromium.jar"); 419 } else if (apiLevel <= 22) { 420 return Lists.newArrayList( 421 "/system/framework/core-libart.jar", 422 "/system/framework/conscrypt.jar", 423 "/system/framework/okhttp.jar", 424 "/system/framework/core-junit.jar", 425 "/system/framework/bouncycastle.jar", 426 "/system/framework/ext.jar", 427 "/system/framework/framework.jar", 428 "/system/framework/telephony-common.jar", 429 "/system/framework/voip-common.jar", 430 "/system/framework/ims-common.jar", 431 "/system/framework/mms-common.jar", 432 "/system/framework/android.policy.jar", 433 "/system/framework/apache-xml.jar"); 434 } else if (apiLevel <= 23) { 435 return Lists.newArrayList( 436 "/system/framework/core-libart.jar", 437 "/system/framework/conscrypt.jar", 438 "/system/framework/okhttp.jar", 439 "/system/framework/core-junit.jar", 440 "/system/framework/bouncycastle.jar", 441 "/system/framework/ext.jar", 442 "/system/framework/framework.jar", 443 "/system/framework/telephony-common.jar", 444 "/system/framework/voip-common.jar", 445 "/system/framework/ims-common.jar", 446 "/system/framework/apache-xml.jar", 447 "/system/framework/org.apache.http.legacy.boot.jar"); 448 } else /*if (apiLevel <= 24)*/ { 449 return Lists.newArrayList( 450 "/system/framework/core-oj.jar", 451 "/system/framework/core-libart.jar", 452 "/system/framework/conscrypt.jar", 453 "/system/framework/okhttp.jar", 454 "/system/framework/core-junit.jar", 455 "/system/framework/bouncycastle.jar", 456 "/system/framework/ext.jar", 457 "/system/framework/framework.jar", 458 "/system/framework/telephony-common.jar", 459 "/system/framework/voip-common.jar", 460 "/system/framework/ims-common.jar", 461 "/system/framework/apache-xml.jar", 462 "/system/framework/org.apache.http.legacy.boot.jar"); 463 } 464 } 465 } 466