1 /* 2 * Copyright 2012, 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; 33 34 import com.google.common.base.Joiner; 35 import com.google.common.collect.ImmutableList; 36 import com.google.common.collect.Lists; 37 import com.google.common.io.ByteStreams; 38 import com.google.common.io.Files; 39 import org.jf.dexlib2.dexbacked.DexBackedDexFile; 40 import org.jf.dexlib2.dexbacked.DexBackedDexFile.NotADexFile; 41 import org.jf.dexlib2.dexbacked.DexBackedOdexFile; 42 import org.jf.dexlib2.dexbacked.OatFile; 43 import org.jf.dexlib2.dexbacked.OatFile.NotAnOatFileException; 44 import org.jf.dexlib2.dexbacked.OatFile.OatDexFile; 45 import org.jf.dexlib2.dexbacked.OatFile.VdexProvider; 46 import org.jf.dexlib2.dexbacked.ZipDexContainer; 47 import org.jf.dexlib2.dexbacked.ZipDexContainer.NotAZipFileException; 48 import org.jf.dexlib2.iface.DexFile; 49 import org.jf.dexlib2.iface.MultiDexContainer; 50 import org.jf.dexlib2.writer.pool.DexPool; 51 import org.jf.util.ExceptionWithContext; 52 53 import javax.annotation.Nonnull; 54 import javax.annotation.Nullable; 55 import java.io.*; 56 import java.util.List; 57 58 public final class DexFileFactory { 59 60 @Nonnull loadDexFile(@onnull String path, @Nullable Opcodes opcodes)61 public static DexBackedDexFile loadDexFile(@Nonnull String path, @Nullable Opcodes opcodes) throws IOException { 62 return loadDexFile(new File(path), opcodes); 63 } 64 65 /** 66 * Loads a dex/apk/odex/oat file. 67 * 68 * For oat files with multiple dex files, the first will be opened. For zip/apk files, the "classes.dex" entry 69 * will be opened. 70 * 71 * @param file The file to open 72 * @param opcodes The set of opcodes to use 73 * @return A DexBackedDexFile for the given file 74 * 75 * @throws UnsupportedOatVersionException If file refers to an unsupported oat file 76 * @throws DexFileNotFoundException If file does not exist, if file is a zip file but does not have a "classes.dex" 77 * entry, or if file is an oat file that has no dex entries. 78 * @throws UnsupportedFileTypeException If file is not a valid dex/zip/odex/oat file, or if the "classes.dex" entry 79 * in a zip file is not a valid dex file 80 */ 81 @Nonnull loadDexFile(@onnull File file, @Nullable Opcodes opcodes)82 public static DexBackedDexFile loadDexFile(@Nonnull File file, @Nullable Opcodes opcodes) throws IOException { 83 if (!file.exists()) { 84 throw new DexFileNotFoundException("%s does not exist", file.getName()); 85 } 86 87 try { 88 ZipDexContainer container = new ZipDexContainer(file, opcodes); 89 return new DexEntryFinder(file.getPath(), container).findEntry("classes.dex", true); 90 } catch (NotAZipFileException ex) { 91 // eat it and continue 92 } 93 94 InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); 95 try { 96 try { 97 return DexBackedDexFile.fromInputStream(opcodes, inputStream); 98 } catch (DexBackedDexFile.NotADexFile ex) { 99 // just eat it 100 } 101 102 try { 103 return DexBackedOdexFile.fromInputStream(opcodes, inputStream); 104 } catch (DexBackedOdexFile.NotAnOdexFile ex) { 105 // just eat it 106 } 107 108 // Note: DexBackedDexFile.fromInputStream and DexBackedOdexFile.fromInputStream will reset inputStream 109 // back to the same position, if they fails 110 111 OatFile oatFile = null; 112 try { 113 oatFile = OatFile.fromInputStream(inputStream, new FilenameVdexProvider(file)); 114 } catch (NotAnOatFileException ex) { 115 // just eat it 116 } 117 118 if (oatFile != null) { 119 if (oatFile.isSupportedVersion() == OatFile.UNSUPPORTED) { 120 throw new UnsupportedOatVersionException(oatFile); 121 } 122 123 List<OatDexFile> oatDexFiles = oatFile.getDexFiles(); 124 125 if (oatDexFiles.size() == 0) { 126 throw new DexFileNotFoundException("Oat file %s contains no dex files", file.getName()); 127 } 128 129 return oatDexFiles.get(0); 130 } 131 } finally { 132 inputStream.close(); 133 } 134 135 throw new UnsupportedFileTypeException("%s is not an apk, dex, odex or oat file.", file.getPath()); 136 } 137 138 /** 139 * Loads a dex entry from a container format (zip/oat) 140 * 141 * This has two modes of operation, depending on the exactMatch parameter. When exactMatch is true, it will only 142 * load an entry whose name exactly matches that provided by the dexEntry parameter. 143 * 144 * When exactMatch is false, then it will search for any entry that dexEntry is a path suffix of. "path suffix" 145 * meaning all the path components in dexEntry must fully match the corresponding path components in the entry name, 146 * but some path components at the beginning of entry name can be missing. 147 * 148 * For example, if an oat file contains a "/system/framework/framework.jar:classes2.dex" entry, then the following 149 * will match (not an exhaustive list): 150 * 151 * "/system/framework/framework.jar:classes2.dex" 152 * "system/framework/framework.jar:classes2.dex" 153 * "framework/framework.jar:classes2.dex" 154 * "framework.jar:classes2.dex" 155 * "classes2.dex" 156 * 157 * Note that partial path components specifically don't match. So something like "work/framework.jar:classes2.dex" 158 * would not match. 159 * 160 * If dexEntry contains an initial slash, it will be ignored for purposes of this suffix match -- but not when 161 * performing an exact match. 162 * 163 * If multiple entries match the given dexEntry, a MultipleMatchingDexEntriesException will be thrown 164 * 165 * @param file The container file. This must be either a zip (apk) file or an oat file. 166 * @param dexEntry The name of the entry to load. This can either be the exact entry name, if exactMatch is true, 167 * or it can be a path suffix. 168 * @param exactMatch If true, dexE 169 * @param opcodes The set of opcodes to use 170 * @return A DexBackedDexFile for the given entry 171 * 172 * @throws UnsupportedOatVersionException If file refers to an unsupported oat file 173 * @throws DexFileNotFoundException If the file does not exist, or if no matching entry could be found 174 * @throws UnsupportedFileTypeException If file is not a valid zip/oat file, or if the matching entry is not a 175 * valid dex file 176 * @throws MultipleMatchingDexEntriesException If multiple entries match the given dexEntry 177 */ loadDexEntry(@onnull File file, @Nonnull String dexEntry, boolean exactMatch, @Nullable Opcodes opcodes)178 public static DexBackedDexFile loadDexEntry(@Nonnull File file, @Nonnull String dexEntry, 179 boolean exactMatch, @Nullable Opcodes opcodes) throws IOException { 180 if (!file.exists()) { 181 throw new DexFileNotFoundException("Container file %s does not exist", file.getName()); 182 } 183 184 try { 185 ZipDexContainer container = new ZipDexContainer(file, opcodes); 186 return new DexEntryFinder(file.getPath(), container).findEntry(dexEntry, exactMatch); 187 } catch (NotAZipFileException ex) { 188 // eat it and continue 189 } 190 191 InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); 192 try { 193 OatFile oatFile = null; 194 try { 195 oatFile = OatFile.fromInputStream(inputStream, new FilenameVdexProvider(file)); 196 } catch (NotAnOatFileException ex) { 197 // just eat it 198 } 199 200 if (oatFile != null) { 201 if (oatFile.isSupportedVersion() == OatFile.UNSUPPORTED) { 202 throw new UnsupportedOatVersionException(oatFile); 203 } 204 205 List<OatDexFile> oatDexFiles = oatFile.getDexFiles(); 206 207 if (oatDexFiles.size() == 0) { 208 throw new DexFileNotFoundException("Oat file %s contains no dex files", file.getName()); 209 } 210 211 return new DexEntryFinder(file.getPath(), oatFile).findEntry(dexEntry, exactMatch); 212 } 213 } finally { 214 inputStream.close(); 215 } 216 217 throw new UnsupportedFileTypeException("%s is not an apk or oat file.", file.getPath()); 218 } 219 220 /** 221 * Loads a file containing 1 or more dex files 222 * 223 * If the given file is a dex or odex file, it will return a MultiDexContainer containing that single entry. 224 * Otherwise, for an oat or zip file, it will return an OatFile or ZipDexContainer respectively. 225 * 226 * @param file The file to open 227 * @param opcodes The set of opcodes to use 228 * @return A MultiDexContainer 229 * @throws DexFileNotFoundException If the given file does not exist 230 * @throws UnsupportedFileTypeException If the given file is not a valid dex/zip/odex/oat file 231 */ loadDexContainer( @onnull File file, @Nullable final Opcodes opcodes)232 public static MultiDexContainer<? extends DexBackedDexFile> loadDexContainer( 233 @Nonnull File file, @Nullable final Opcodes opcodes) throws IOException { 234 if (!file.exists()) { 235 throw new DexFileNotFoundException("%s does not exist", file.getName()); 236 } 237 238 ZipDexContainer zipDexContainer = new ZipDexContainer(file, opcodes); 239 if (zipDexContainer.isZipFile()) { 240 return zipDexContainer; 241 } 242 243 InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); 244 try { 245 try { 246 DexBackedDexFile dexFile = DexBackedDexFile.fromInputStream(opcodes, inputStream); 247 return new SingletonMultiDexContainer(file.getPath(), dexFile); 248 } catch (DexBackedDexFile.NotADexFile ex) { 249 // just eat it 250 } 251 252 try { 253 DexBackedOdexFile odexFile = DexBackedOdexFile.fromInputStream(opcodes, inputStream); 254 return new SingletonMultiDexContainer(file.getPath(), odexFile); 255 } catch (DexBackedOdexFile.NotAnOdexFile ex) { 256 // just eat it 257 } 258 259 // Note: DexBackedDexFile.fromInputStream and DexBackedOdexFile.fromInputStream will reset inputStream 260 // back to the same position, if they fails 261 262 OatFile oatFile = null; 263 try { 264 oatFile = OatFile.fromInputStream(inputStream, new FilenameVdexProvider(file)); 265 } catch (NotAnOatFileException ex) { 266 // just eat it 267 } 268 269 if (oatFile != null) { 270 // TODO: we should support loading earlier oat files, just not deodexing them 271 if (oatFile.isSupportedVersion() == OatFile.UNSUPPORTED) { 272 throw new UnsupportedOatVersionException(oatFile); 273 } 274 return oatFile; 275 } 276 } finally { 277 inputStream.close(); 278 } 279 280 throw new UnsupportedFileTypeException("%s is not an apk, dex, odex or oat file.", file.getPath()); 281 } 282 283 /** 284 * Writes a DexFile out to disk 285 * 286 * @param path The path to write the dex file to 287 * @param dexFile a DexFile to write 288 */ writeDexFile(@onnull String path, @Nonnull DexFile dexFile)289 public static void writeDexFile(@Nonnull String path, @Nonnull DexFile dexFile) throws IOException { 290 DexPool.writeTo(path, dexFile); 291 } 292 DexFileFactory()293 private DexFileFactory() {} 294 295 public static class DexFileNotFoundException extends ExceptionWithContext { DexFileNotFoundException(@ullable String message, Object... formatArgs)296 public DexFileNotFoundException(@Nullable String message, Object... formatArgs) { 297 super(message, formatArgs); 298 } 299 } 300 301 public static class UnsupportedOatVersionException extends ExceptionWithContext { 302 @Nonnull public final OatFile oatFile; 303 UnsupportedOatVersionException(@onnull OatFile oatFile)304 public UnsupportedOatVersionException(@Nonnull OatFile oatFile) { 305 super("Unsupported oat version: %d", oatFile.getOatVersion()); 306 this.oatFile = oatFile; 307 } 308 } 309 310 public static class MultipleMatchingDexEntriesException extends ExceptionWithContext { MultipleMatchingDexEntriesException(@onnull String message, Object... formatArgs)311 public MultipleMatchingDexEntriesException(@Nonnull String message, Object... formatArgs) { 312 super(String.format(message, formatArgs)); 313 } 314 } 315 316 public static class UnsupportedFileTypeException extends ExceptionWithContext { UnsupportedFileTypeException(@onnull String message, Object... formatArgs)317 public UnsupportedFileTypeException(@Nonnull String message, Object... formatArgs) { 318 super(String.format(message, formatArgs)); 319 } 320 } 321 322 /** 323 * Matches two entries fully, ignoring any initial slash, if any 324 */ fullEntryMatch(@onnull String entry, @Nonnull String targetEntry)325 private static boolean fullEntryMatch(@Nonnull String entry, @Nonnull String targetEntry) { 326 if (entry.equals(targetEntry)) { 327 return true; 328 } 329 330 if (entry.charAt(0) == '/') { 331 entry = entry.substring(1); 332 } 333 334 if (targetEntry.charAt(0) == '/') { 335 targetEntry = targetEntry.substring(1); 336 } 337 338 return entry.equals(targetEntry); 339 } 340 341 /** 342 * Performs a partial match against entry and targetEntry. 343 * 344 * This is considered a partial match if targetEntry is a suffix of entry, and if the suffix starts 345 * on a path "part" (ignoring the initial separator, if any). Both '/' and ':' are considered separators for this. 346 * 347 * So entry="/blah/blah/something.dex" and targetEntry="lah/something.dex" shouldn't match, but 348 * both targetEntry="blah/something.dex" and "/blah/something.dex" should match. 349 */ partialEntryMatch(String entry, String targetEntry)350 private static boolean partialEntryMatch(String entry, String targetEntry) { 351 if (entry.equals(targetEntry)) { 352 return true; 353 } 354 355 if (!entry.endsWith(targetEntry)) { 356 return false; 357 } 358 359 // Make sure the first matching part is a full entry. We don't want to match "/blah/blah/something.dex" with 360 // "lah/something.dex", but both "/blah/something.dex" and "blah/something.dex" should match 361 char precedingChar = entry.charAt(entry.length() - targetEntry.length() - 1); 362 char firstTargetChar = targetEntry.charAt(0); 363 // This is a device path, so we should always use the linux separator '/', rather than the current platform's 364 // separator 365 return firstTargetChar == ':' || firstTargetChar == '/' || precedingChar == ':' || precedingChar == '/'; 366 } 367 368 protected static class DexEntryFinder { 369 private final String filename; 370 private final MultiDexContainer<? extends DexBackedDexFile> dexContainer; 371 DexEntryFinder(@onnull String filename, @Nonnull MultiDexContainer<? extends DexBackedDexFile> dexContainer)372 public DexEntryFinder(@Nonnull String filename, 373 @Nonnull MultiDexContainer<? extends DexBackedDexFile> dexContainer) { 374 this.filename = filename; 375 this.dexContainer = dexContainer; 376 } 377 378 @Nonnull findEntry(@onnull String targetEntry, boolean exactMatch)379 public DexBackedDexFile findEntry(@Nonnull String targetEntry, boolean exactMatch) throws IOException { 380 if (exactMatch) { 381 try { 382 DexBackedDexFile dexFile = dexContainer.getEntry(targetEntry); 383 if (dexFile == null) { 384 throw new DexFileNotFoundException("Could not find entry %s in %s.", targetEntry, filename); 385 } 386 return dexFile; 387 } catch (NotADexFile ex) { 388 throw new UnsupportedFileTypeException("Entry %s in %s is not a dex file", targetEntry, filename); 389 } 390 } 391 392 // find all full and partial matches 393 List<String> fullMatches = Lists.newArrayList(); 394 List<DexBackedDexFile> fullEntries = Lists.newArrayList(); 395 List<String> partialMatches = Lists.newArrayList(); 396 List<DexBackedDexFile> partialEntries = Lists.newArrayList(); 397 for (String entry: dexContainer.getDexEntryNames()) { 398 if (fullEntryMatch(entry, targetEntry)) { 399 // We want to grab all full matches, regardless of whether they're actually a dex file. 400 fullMatches.add(entry); 401 fullEntries.add(dexContainer.getEntry(entry)); 402 } else if (partialEntryMatch(entry, targetEntry)) { 403 partialMatches.add(entry); 404 partialEntries.add(dexContainer.getEntry(entry)); 405 } 406 } 407 408 // full matches always take priority 409 if (fullEntries.size() == 1) { 410 try { 411 DexBackedDexFile dexFile = fullEntries.get(0); 412 assert dexFile != null; 413 return dexFile; 414 } catch (NotADexFile ex) { 415 throw new UnsupportedFileTypeException("Entry %s in %s is not a dex file", 416 fullMatches.get(0), filename); 417 } 418 } 419 if (fullEntries.size() > 1) { 420 // This should be quite rare. This would only happen if an oat file has two entries that differ 421 // only by an initial path separator. e.g. "/blah/blah.dex" and "blah/blah.dex" 422 throw new MultipleMatchingDexEntriesException(String.format( 423 "Multiple entries in %s match %s: %s", filename, targetEntry, 424 Joiner.on(", ").join(fullMatches))); 425 } 426 427 if (partialEntries.size() == 0) { 428 throw new DexFileNotFoundException("Could not find a dex entry in %s matching %s", 429 filename, targetEntry); 430 } 431 if (partialEntries.size() > 1) { 432 throw new MultipleMatchingDexEntriesException(String.format( 433 "Multiple dex entries in %s match %s: %s", filename, targetEntry, 434 Joiner.on(", ").join(partialMatches))); 435 } 436 return partialEntries.get(0); 437 } 438 } 439 440 private static class SingletonMultiDexContainer implements MultiDexContainer<DexBackedDexFile> { 441 private final String entryName; 442 private final DexBackedDexFile dexFile; 443 SingletonMultiDexContainer(@onnull String entryName, @Nonnull DexBackedDexFile dexFile)444 public SingletonMultiDexContainer(@Nonnull String entryName, @Nonnull DexBackedDexFile dexFile) { 445 this.entryName = entryName; 446 this.dexFile = dexFile; 447 } 448 getDexEntryNames()449 @Nonnull @Override public List<String> getDexEntryNames() throws IOException { 450 return ImmutableList.of(entryName); 451 } 452 getEntry(@onnull String entryName)453 @Nullable @Override public DexBackedDexFile getEntry(@Nonnull String entryName) throws IOException { 454 if (entryName.equals(this.entryName)) { 455 return dexFile; 456 } 457 return null; 458 } 459 } 460 461 public static class FilenameVdexProvider implements VdexProvider { 462 private final File vdexFile; 463 464 @Nullable 465 private byte[] buf = null; 466 private boolean loadedVdex = false; 467 FilenameVdexProvider(File oatFile)468 public FilenameVdexProvider(File oatFile) { 469 File oatParent = oatFile.getAbsoluteFile().getParentFile(); 470 String baseName = Files.getNameWithoutExtension(oatFile.getAbsolutePath()); 471 vdexFile = new File(oatParent, baseName + ".vdex"); 472 } 473 getVdex()474 @Nullable @Override public byte[] getVdex() { 475 if (!loadedVdex) { 476 if (vdexFile.exists()) { 477 try { 478 buf = ByteStreams.toByteArray(new FileInputStream(vdexFile)); 479 } catch (FileNotFoundException e) { 480 buf = null; 481 } catch (IOException ex) { 482 throw new RuntimeException(ex); 483 } 484 } 485 loadedVdex = true; 486 } 487 488 return buf; 489 } 490 } 491 } 492