| /* |
| * Copyright 2012, Google Inc. |
| * All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following disclaimer |
| * in the documentation and/or other materials provided with the |
| * distribution. |
| * * Neither the name of Google Inc. nor the names of its |
| * contributors may be used to endorse or promote products derived from |
| * this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| package org.jf.dexlib2; |
| |
| import com.google.common.base.Joiner; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Lists; |
| import com.google.common.io.ByteStreams; |
| import com.google.common.io.Files; |
| import org.jf.dexlib2.dexbacked.DexBackedDexFile; |
| import org.jf.dexlib2.dexbacked.DexBackedDexFile.NotADexFile; |
| import org.jf.dexlib2.dexbacked.DexBackedOdexFile; |
| import org.jf.dexlib2.dexbacked.OatFile; |
| import org.jf.dexlib2.dexbacked.OatFile.NotAnOatFileException; |
| import org.jf.dexlib2.dexbacked.OatFile.OatDexFile; |
| import org.jf.dexlib2.dexbacked.OatFile.VdexProvider; |
| import org.jf.dexlib2.dexbacked.ZipDexContainer; |
| import org.jf.dexlib2.dexbacked.ZipDexContainer.NotAZipFileException; |
| import org.jf.dexlib2.iface.DexFile; |
| import org.jf.dexlib2.iface.MultiDexContainer; |
| import org.jf.dexlib2.writer.pool.DexPool; |
| import org.jf.util.ExceptionWithContext; |
| |
| import javax.annotation.Nonnull; |
| import javax.annotation.Nullable; |
| import java.io.*; |
| import java.util.List; |
| |
| public final class DexFileFactory { |
| |
| @Nonnull |
| public static DexBackedDexFile loadDexFile(@Nonnull String path, @Nonnull Opcodes opcodes) throws IOException { |
| return loadDexFile(new File(path), opcodes); |
| } |
| |
| /** |
| * Loads a dex/apk/odex/oat file. |
| * |
| * For oat files with multiple dex files, the first will be opened. For zip/apk files, the "classes.dex" entry |
| * will be opened. |
| * |
| * @param file The file to open |
| * @param opcodes The set of opcodes to use |
| * @return A DexBackedDexFile for the given file |
| * |
| * @throws UnsupportedOatVersionException If file refers to an unsupported oat file |
| * @throws DexFileNotFoundException If file does not exist, if file is a zip file but does not have a "classes.dex" |
| * entry, or if file is an oat file that has no dex entries. |
| * @throws UnsupportedFileTypeException If file is not a valid dex/zip/odex/oat file, or if the "classes.dex" entry |
| * in a zip file is not a valid dex file |
| */ |
| @Nonnull |
| public static DexBackedDexFile loadDexFile(@Nonnull File file, @Nonnull Opcodes opcodes) throws IOException { |
| if (!file.exists()) { |
| throw new DexFileNotFoundException("%s does not exist", file.getName()); |
| } |
| |
| try { |
| ZipDexContainer container = new ZipDexContainer(file, opcodes); |
| return new DexEntryFinder(file.getPath(), container).findEntry("classes.dex", true); |
| } catch (NotAZipFileException ex) { |
| // eat it and continue |
| } |
| |
| InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); |
| try { |
| try { |
| return DexBackedDexFile.fromInputStream(opcodes, inputStream); |
| } catch (DexBackedDexFile.NotADexFile ex) { |
| // just eat it |
| } |
| |
| try { |
| return DexBackedOdexFile.fromInputStream(opcodes, inputStream); |
| } catch (DexBackedOdexFile.NotAnOdexFile ex) { |
| // just eat it |
| } |
| |
| // Note: DexBackedDexFile.fromInputStream and DexBackedOdexFile.fromInputStream will reset inputStream |
| // back to the same position, if they fails |
| |
| OatFile oatFile = null; |
| try { |
| oatFile = OatFile.fromInputStream(inputStream, new FilenameVdexProvider(file)); |
| } catch (NotAnOatFileException ex) { |
| // just eat it |
| } |
| |
| if (oatFile != null) { |
| if (oatFile.isSupportedVersion() == OatFile.UNSUPPORTED) { |
| throw new UnsupportedOatVersionException(oatFile); |
| } |
| |
| List<OatDexFile> oatDexFiles = oatFile.getDexFiles(); |
| |
| if (oatDexFiles.size() == 0) { |
| throw new DexFileNotFoundException("Oat file %s contains no dex files", file.getName()); |
| } |
| |
| return oatDexFiles.get(0); |
| } |
| } finally { |
| inputStream.close(); |
| } |
| |
| throw new UnsupportedFileTypeException("%s is not an apk, dex, odex or oat file.", file.getPath()); |
| } |
| |
| /** |
| * Loads a dex entry from a container format (zip/oat) |
| * |
| * This has two modes of operation, depending on the exactMatch parameter. When exactMatch is true, it will only |
| * load an entry whose name exactly matches that provided by the dexEntry parameter. |
| * |
| * When exactMatch is false, then it will search for any entry that dexEntry is a path suffix of. "path suffix" |
| * meaning all the path components in dexEntry must fully match the corresponding path components in the entry name, |
| * but some path components at the beginning of entry name can be missing. |
| * |
| * For example, if an oat file contains a "/system/framework/framework.jar:classes2.dex" entry, then the following |
| * will match (not an exhaustive list): |
| * |
| * "/system/framework/framework.jar:classes2.dex" |
| * "system/framework/framework.jar:classes2.dex" |
| * "framework/framework.jar:classes2.dex" |
| * "framework.jar:classes2.dex" |
| * "classes2.dex" |
| * |
| * Note that partial path components specifically don't match. So something like "work/framework.jar:classes2.dex" |
| * would not match. |
| * |
| * If dexEntry contains an initial slash, it will be ignored for purposes of this suffix match -- but not when |
| * performing an exact match. |
| * |
| * If multiple entries match the given dexEntry, a MultipleMatchingDexEntriesException will be thrown |
| * |
| * @param file The container file. This must be either a zip (apk) file or an oat file. |
| * @param dexEntry The name of the entry to load. This can either be the exact entry name, if exactMatch is true, |
| * or it can be a path suffix. |
| * @param exactMatch If true, dexE |
| * @param opcodes The set of opcodes to use |
| * @return A DexBackedDexFile for the given entry |
| * |
| * @throws UnsupportedOatVersionException If file refers to an unsupported oat file |
| * @throws DexFileNotFoundException If the file does not exist, or if no matching entry could be found |
| * @throws UnsupportedFileTypeException If file is not a valid zip/oat file, or if the matching entry is not a |
| * valid dex file |
| * @throws MultipleMatchingDexEntriesException If multiple entries match the given dexEntry |
| */ |
| public static DexBackedDexFile loadDexEntry(@Nonnull File file, @Nonnull String dexEntry, |
| boolean exactMatch, @Nonnull Opcodes opcodes) throws IOException { |
| if (!file.exists()) { |
| throw new DexFileNotFoundException("Container file %s does not exist", file.getName()); |
| } |
| |
| try { |
| ZipDexContainer container = new ZipDexContainer(file, opcodes); |
| return new DexEntryFinder(file.getPath(), container).findEntry(dexEntry, exactMatch); |
| } catch (NotAZipFileException ex) { |
| // eat it and continue |
| } |
| |
| InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); |
| try { |
| OatFile oatFile = null; |
| try { |
| oatFile = OatFile.fromInputStream(inputStream, new FilenameVdexProvider(file)); |
| } catch (NotAnOatFileException ex) { |
| // just eat it |
| } |
| |
| if (oatFile != null) { |
| if (oatFile.isSupportedVersion() == OatFile.UNSUPPORTED) { |
| throw new UnsupportedOatVersionException(oatFile); |
| } |
| |
| List<OatDexFile> oatDexFiles = oatFile.getDexFiles(); |
| |
| if (oatDexFiles.size() == 0) { |
| throw new DexFileNotFoundException("Oat file %s contains no dex files", file.getName()); |
| } |
| |
| return new DexEntryFinder(file.getPath(), oatFile).findEntry(dexEntry, exactMatch); |
| } |
| } finally { |
| inputStream.close(); |
| } |
| |
| throw new UnsupportedFileTypeException("%s is not an apk or oat file.", file.getPath()); |
| } |
| |
| /** |
| * Loads a file containing 1 or more dex files |
| * |
| * If the given file is a dex or odex file, it will return a MultiDexContainer containing that single entry. |
| * Otherwise, for an oat or zip file, it will return an OatFile or ZipDexContainer respectively. |
| * |
| * @param file The file to open |
| * @param opcodes The set of opcodes to use |
| * @return A MultiDexContainer |
| * @throws DexFileNotFoundException If the given file does not exist |
| * @throws UnsupportedFileTypeException If the given file is not a valid dex/zip/odex/oat file |
| */ |
| public static MultiDexContainer<? extends DexBackedDexFile> loadDexContainer( |
| @Nonnull File file, @Nonnull final Opcodes opcodes) throws IOException { |
| if (!file.exists()) { |
| throw new DexFileNotFoundException("%s does not exist", file.getName()); |
| } |
| |
| ZipDexContainer zipDexContainer = new ZipDexContainer(file, opcodes); |
| if (zipDexContainer.isZipFile()) { |
| return zipDexContainer; |
| } |
| |
| InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); |
| try { |
| try { |
| DexBackedDexFile dexFile = DexBackedDexFile.fromInputStream(opcodes, inputStream); |
| return new SingletonMultiDexContainer(file.getPath(), dexFile); |
| } catch (DexBackedDexFile.NotADexFile ex) { |
| // just eat it |
| } |
| |
| try { |
| DexBackedOdexFile odexFile = DexBackedOdexFile.fromInputStream(opcodes, inputStream); |
| return new SingletonMultiDexContainer(file.getPath(), odexFile); |
| } catch (DexBackedOdexFile.NotAnOdexFile ex) { |
| // just eat it |
| } |
| |
| // Note: DexBackedDexFile.fromInputStream and DexBackedOdexFile.fromInputStream will reset inputStream |
| // back to the same position, if they fails |
| |
| OatFile oatFile = null; |
| try { |
| oatFile = OatFile.fromInputStream(inputStream, new FilenameVdexProvider(file)); |
| } catch (NotAnOatFileException ex) { |
| // just eat it |
| } |
| |
| if (oatFile != null) { |
| // TODO: we should support loading earlier oat files, just not deodexing them |
| if (oatFile.isSupportedVersion() == OatFile.UNSUPPORTED) { |
| throw new UnsupportedOatVersionException(oatFile); |
| } |
| return oatFile; |
| } |
| } finally { |
| inputStream.close(); |
| } |
| |
| throw new UnsupportedFileTypeException("%s is not an apk, dex, odex or oat file.", file.getPath()); |
| } |
| |
| /** |
| * Writes a DexFile out to disk |
| * |
| * @param path The path to write the dex file to |
| * @param dexFile a DexFile to write |
| */ |
| public static void writeDexFile(@Nonnull String path, @Nonnull DexFile dexFile) throws IOException { |
| DexPool.writeTo(path, dexFile); |
| } |
| |
| private DexFileFactory() {} |
| |
| public static class DexFileNotFoundException extends ExceptionWithContext { |
| public DexFileNotFoundException(@Nullable String message, Object... formatArgs) { |
| super(message, formatArgs); |
| } |
| } |
| |
| public static class UnsupportedOatVersionException extends ExceptionWithContext { |
| @Nonnull public final OatFile oatFile; |
| |
| public UnsupportedOatVersionException(@Nonnull OatFile oatFile) { |
| super("Unsupported oat version: %d", oatFile.getOatVersion()); |
| this.oatFile = oatFile; |
| } |
| } |
| |
| public static class MultipleMatchingDexEntriesException extends ExceptionWithContext { |
| public MultipleMatchingDexEntriesException(@Nonnull String message, Object... formatArgs) { |
| super(String.format(message, formatArgs)); |
| } |
| } |
| |
| public static class UnsupportedFileTypeException extends ExceptionWithContext { |
| public UnsupportedFileTypeException(@Nonnull String message, Object... formatArgs) { |
| super(String.format(message, formatArgs)); |
| } |
| } |
| |
| /** |
| * Matches two entries fully, ignoring any initial slash, if any |
| */ |
| private static boolean fullEntryMatch(@Nonnull String entry, @Nonnull String targetEntry) { |
| if (entry.equals(targetEntry)) { |
| return true; |
| } |
| |
| if (entry.charAt(0) == '/') { |
| entry = entry.substring(1); |
| } |
| |
| if (targetEntry.charAt(0) == '/') { |
| targetEntry = targetEntry.substring(1); |
| } |
| |
| return entry.equals(targetEntry); |
| } |
| |
| /** |
| * Performs a partial match against entry and targetEntry. |
| * |
| * This is considered a partial match if targetEntry is a suffix of entry, and if the suffix starts |
| * on a path "part" (ignoring the initial separator, if any). Both '/' and ':' are considered separators for this. |
| * |
| * So entry="/blah/blah/something.dex" and targetEntry="lah/something.dex" shouldn't match, but |
| * both targetEntry="blah/something.dex" and "/blah/something.dex" should match. |
| */ |
| private static boolean partialEntryMatch(String entry, String targetEntry) { |
| if (entry.equals(targetEntry)) { |
| return true; |
| } |
| |
| if (!entry.endsWith(targetEntry)) { |
| return false; |
| } |
| |
| // Make sure the first matching part is a full entry. We don't want to match "/blah/blah/something.dex" with |
| // "lah/something.dex", but both "/blah/something.dex" and "blah/something.dex" should match |
| char precedingChar = entry.charAt(entry.length() - targetEntry.length() - 1); |
| char firstTargetChar = targetEntry.charAt(0); |
| // This is a device path, so we should always use the linux separator '/', rather than the current platform's |
| // separator |
| return firstTargetChar == ':' || firstTargetChar == '/' || precedingChar == ':' || precedingChar == '/'; |
| } |
| |
| protected static class DexEntryFinder { |
| private final String filename; |
| private final MultiDexContainer<? extends DexBackedDexFile> dexContainer; |
| |
| public DexEntryFinder(@Nonnull String filename, |
| @Nonnull MultiDexContainer<? extends DexBackedDexFile> dexContainer) { |
| this.filename = filename; |
| this.dexContainer = dexContainer; |
| } |
| |
| @Nonnull |
| public DexBackedDexFile findEntry(@Nonnull String targetEntry, boolean exactMatch) throws IOException { |
| if (exactMatch) { |
| try { |
| DexBackedDexFile dexFile = dexContainer.getEntry(targetEntry); |
| if (dexFile == null) { |
| throw new DexFileNotFoundException("Could not find entry %s in %s.", targetEntry, filename); |
| } |
| return dexFile; |
| } catch (NotADexFile ex) { |
| throw new UnsupportedFileTypeException("Entry %s in %s is not a dex file", targetEntry, filename); |
| } |
| } |
| |
| // find all full and partial matches |
| List<String> fullMatches = Lists.newArrayList(); |
| List<DexBackedDexFile> fullEntries = Lists.newArrayList(); |
| List<String> partialMatches = Lists.newArrayList(); |
| List<DexBackedDexFile> partialEntries = Lists.newArrayList(); |
| for (String entry: dexContainer.getDexEntryNames()) { |
| if (fullEntryMatch(entry, targetEntry)) { |
| // We want to grab all full matches, regardless of whether they're actually a dex file. |
| fullMatches.add(entry); |
| fullEntries.add(dexContainer.getEntry(entry)); |
| } else if (partialEntryMatch(entry, targetEntry)) { |
| partialMatches.add(entry); |
| partialEntries.add(dexContainer.getEntry(entry)); |
| } |
| } |
| |
| // full matches always take priority |
| if (fullEntries.size() == 1) { |
| try { |
| DexBackedDexFile dexFile = fullEntries.get(0); |
| assert dexFile != null; |
| return dexFile; |
| } catch (NotADexFile ex) { |
| throw new UnsupportedFileTypeException("Entry %s in %s is not a dex file", |
| fullMatches.get(0), filename); |
| } |
| } |
| if (fullEntries.size() > 1) { |
| // This should be quite rare. This would only happen if an oat file has two entries that differ |
| // only by an initial path separator. e.g. "/blah/blah.dex" and "blah/blah.dex" |
| throw new MultipleMatchingDexEntriesException(String.format( |
| "Multiple entries in %s match %s: %s", filename, targetEntry, |
| Joiner.on(", ").join(fullMatches))); |
| } |
| |
| if (partialEntries.size() == 0) { |
| throw new DexFileNotFoundException("Could not find a dex entry in %s matching %s", |
| filename, targetEntry); |
| } |
| if (partialEntries.size() > 1) { |
| throw new MultipleMatchingDexEntriesException(String.format( |
| "Multiple dex entries in %s match %s: %s", filename, targetEntry, |
| Joiner.on(", ").join(partialMatches))); |
| } |
| return partialEntries.get(0); |
| } |
| } |
| |
| private static class SingletonMultiDexContainer implements MultiDexContainer<DexBackedDexFile> { |
| private final String entryName; |
| private final DexBackedDexFile dexFile; |
| |
| public SingletonMultiDexContainer(@Nonnull String entryName, @Nonnull DexBackedDexFile dexFile) { |
| this.entryName = entryName; |
| this.dexFile = dexFile; |
| } |
| |
| @Nonnull @Override public List<String> getDexEntryNames() throws IOException { |
| return ImmutableList.of(entryName); |
| } |
| |
| @Nullable @Override public DexBackedDexFile getEntry(@Nonnull String entryName) throws IOException { |
| if (entryName.equals(this.entryName)) { |
| return dexFile; |
| } |
| return null; |
| } |
| |
| @Nonnull @Override public Opcodes getOpcodes() { |
| return dexFile.getOpcodes(); |
| } |
| } |
| |
| public static class FilenameVdexProvider implements VdexProvider { |
| private final File vdexFile; |
| |
| @Nullable |
| private byte[] buf = null; |
| private boolean loadedVdex = false; |
| |
| public FilenameVdexProvider(File oatFile) { |
| File oatParent = oatFile.getAbsoluteFile().getParentFile(); |
| String baseName = Files.getNameWithoutExtension(oatFile.getAbsolutePath()); |
| vdexFile = new File(oatParent, baseName + ".vdex"); |
| } |
| |
| @Nullable @Override public byte[] getVdex() { |
| if (!loadedVdex) { |
| if (vdexFile.exists()) { |
| try { |
| buf = ByteStreams.toByteArray(new FileInputStream(vdexFile)); |
| } catch (FileNotFoundException e) { |
| buf = null; |
| } catch (IOException ex) { |
| throw new RuntimeException(ex); |
| } |
| } |
| loadedVdex = true; |
| } |
| |
| return buf; |
| } |
| } |
| } |