From 3587c6f2a61876a104aad328910578be63f91fc5 Mon Sep 17 00:00:00 2001 From: Ben Gruver Date: Tue, 30 Aug 2016 21:15:03 -0700 Subject: [PATCH] Refactor DexFileFactory and implement new syntax for dex entries --- .../java/org/jf/baksmali/DexInputCommand.java | 105 ++-- .../org/jf/baksmali/DisassembleCommand.java | 38 +- .../java/org/jf/baksmali/AnalysisTest.java | 2 +- build.gradle | 1 + dexlib2/build.gradle | 1 + .../java/org/jf/dexlib2/DexFileFactory.java | 456 ++++++++++++++---- .../org/jf/dexlib2/analysis/ClassPath.java | 18 +- .../java/org/jf/dexlib2/AccessorTest.java | 2 +- .../org/jf/dexlib2/DexEntryFinderTest.java | 210 ++++++++ 9 files changed, 636 insertions(+), 197 deletions(-) create mode 100644 dexlib2/src/test/java/org/jf/dexlib2/DexEntryFinderTest.java diff --git a/baksmali/src/main/java/org/jf/baksmali/DexInputCommand.java b/baksmali/src/main/java/org/jf/baksmali/DexInputCommand.java index 207dbe53..9ed9354c 100644 --- a/baksmali/src/main/java/org/jf/baksmali/DexInputCommand.java +++ b/baksmali/src/main/java/org/jf/baksmali/DexInputCommand.java @@ -32,9 +32,10 @@ package org.jf.baksmali; import com.beust.jcommander.JCommander; +import com.google.common.base.Strings; import org.jf.dexlib2.DexFileFactory; +import org.jf.dexlib2.Opcodes; import org.jf.dexlib2.dexbacked.DexBackedDexFile; -import org.jf.dexlib2.dexbacked.OatFile; import org.jf.util.jcommander.Command; import javax.annotation.Nonnull; @@ -55,65 +56,79 @@ public abstract class DexInputCommand extends Command { /** * Parses a dex file input from the user and loads the given dex file. * - * @param input The name of a dex, apk, odex or oat file. For apk or oat files with multiple dex files, the input - * can additionally consist of a colon followed by a specific dex entry to load. + * In some cases, the input file can contain multiple dex files. If this is the case, you can refer to a specific + * dex file with a slash, followed by the entry name, optionally in quotes. + * + * If the entry name is enclosed in quotes, then it will strip the first and last quote and look for an entry with + * exactly that name. Otherwise, it will perform a partial filename match against the entry to find any candidates. + * If there is a single matching candidate, it will be used. Otherwise, an error will be generated. + * + * For example, to refer to the "/system/framework/framework.jar:classes2.dex" entry within the + * "framework/arm/framework.oat" oat file, you could use any of: + * + * framework/arm/framework.oat/"/system/framework/framework.jar:classes2.dex" + * framework/arm/framework.oat/system/framework/framework.jar:classes2.dex + * framework/arm/framework.oat/framework/framework.jar:classes2.dex + * framework/arm/framework.oat/framework.jar:classes2.dex + * framework/arm/framework.oat/classes2.dex + * + * The last option is the easiest, but only works if the oat file doesn't contain another entry with the + * "classes2.dex" name. e.g. "/system/framework/blah.jar:classes2.dex" + * + * It's technically possible (although unlikely) for an oat file to contain 2 entries like: + * /system/framework/framework.jar:classes2.dex + * system/framework/framework.jar:classes2.dex + * + * In this case, the "framework/arm/framework.oat/system/framework/framework.jar:classes2.dex" syntax will generate + * an error because both entries match the partial entry name. Instead, you could use the following for the + * first and second entry respectively: + * + * framework/arm/framework.oat/"/system/framework/framework.jar:classes2.dex" + * framework/arm/framework.oat/"system/framework/framework.jar:classes2.dex" + * + * @param input The name of a dex, apk, odex or oat file/entry. * @param apiLevel The api level to load the dex file with * @param experimentalOpcodes whether experimental opcodes should be allowed * @return The loaded DexBackedDexFile */ @Nonnull protected DexBackedDexFile loadDexFile(@Nonnull String input, int apiLevel, boolean experimentalOpcodes) { - File dexFileFile = new File(input); - String dexFileEntry = null; + File file = new File(input); - int previousIndex = input.length(); - while (!dexFileFile.exists()) { - int colonIndex = input.lastIndexOf(':', previousIndex - 1); - - if (colonIndex >= 0) { - dexFileFile = new File(input.substring(0, colonIndex)); - dexFileEntry = input.substring(colonIndex + 1); - previousIndex = colonIndex; - } else { - break; - } + while (file != null && !file.exists()) { + file = file.getParentFile(); } - if (!dexFileFile.exists()) { - System.err.println("Can't find the file " + input); + if (file == null || !file.exists() || file.isDirectory()) { + System.err.println("Can't find file: " + input); System.exit(1); } - if (!dexFileFile.exists()) { - int colonIndex = input.lastIndexOf(':'); - - if (colonIndex >= 0) { - dexFileFile = new File(input.substring(0, colonIndex)); - dexFileEntry = input.substring(colonIndex + 1); - } - - if (!dexFileFile.exists()) { - System.err.println("Can't find the file " + input); - System.exit(1); - } + File dexFile = file; + String dexEntry = null; + if (dexFile.getPath().length() < input.length()) { + dexEntry = input.substring(dexFile.getPath().length() + 1); } - try { - return DexFileFactory.loadDexFile(dexFileFile, dexFileEntry, apiLevel, experimentalOpcodes); - } catch (DexFileFactory.MultipleDexFilesException ex) { - System.err.println(String.format("%s is an oat file that contains multiple dex files. You must specify " + - "which one to load. E.g. To load the \"core.dex\" entry from boot.oat, you should use " + - "\"boot.oat:core.dex\"", dexFileFile)); - System.err.println("Valid entries include:"); - - for (OatFile.OatDexFile oatDexFile : ex.oatFile.getDexFiles()) { - System.err.println(oatDexFile.filename); + if (!Strings.isNullOrEmpty(dexEntry)) { + boolean exactMatch = false; + if (dexEntry.length() > 2 && dexEntry.charAt(0) == '"' && dexEntry.charAt(dexEntry.length() - 1) == '"') { + dexEntry = dexEntry.substring(1, dexEntry.length() - 1); + exactMatch = true; } - } catch (IOException ex) { - throw new RuntimeException(ex); - } - // execution can never actually reach here - throw new IllegalStateException(); + try { + return DexFileFactory.loadDexEntry(dexFile, dexEntry, exactMatch, + Opcodes.forApi(apiLevel, experimentalOpcodes)); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } else { + try { + return DexFileFactory.loadDexFile(dexFile, Opcodes.forApi(apiLevel, experimentalOpcodes)); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } } } diff --git a/baksmali/src/main/java/org/jf/baksmali/DisassembleCommand.java b/baksmali/src/main/java/org/jf/baksmali/DisassembleCommand.java index 2e6b7b35..2764feeb 100644 --- a/baksmali/src/main/java/org/jf/baksmali/DisassembleCommand.java +++ b/baksmali/src/main/java/org/jf/baksmali/DisassembleCommand.java @@ -35,12 +35,10 @@ import com.beust.jcommander.JCommander; import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameters; import com.beust.jcommander.validators.PositiveInteger; -import org.jf.dexlib2.DexFileFactory; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.jf.dexlib2.analysis.ClassPath; import org.jf.dexlib2.dexbacked.DexBackedDexFile; -import org.jf.dexlib2.dexbacked.OatFile; import org.jf.dexlib2.iface.DexFile; import org.jf.dexlib2.util.SyntheticAccessorResolver; import org.jf.util.StringWrapper; @@ -50,7 +48,6 @@ import org.jf.util.jcommander.ExtendedParameters; import javax.annotation.Nonnull; import java.io.File; -import java.io.IOException; import java.util.List; import java.util.Map; @@ -187,40 +184,7 @@ public class DisassembleCommand extends DexInputCommand { } String input = inputList.get(0); - File dexFileFile = new File(input); - String dexFileEntry = null; - if (!dexFileFile.exists()) { - String filename = dexFileFile.getName(); - int colonIndex = filename.indexOf(':'); - - if (colonIndex >= 0) { - dexFileFile = new File(dexFileFile.getParent(), filename.substring(0, colonIndex)); - dexFileEntry = filename.substring(colonIndex + 1); - } - - if (!dexFileFile.exists()) { - System.err.println("Can't find the file " + input); - System.exit(1); - } - } - - //Read in and parse the dex file - DexBackedDexFile dexFile = null; - try { - dexFile = DexFileFactory.loadDexFile(dexFileFile, dexFileEntry, apiLevel, experimentalOpcodes); - } catch (DexFileFactory.MultipleDexFilesException ex) { - System.err.println(String.format("%s is an oat file that contains multiple dex files. You must specify " + - "which one to load. E.g. To load the \"classes2.dex\" entry from blah.apk, you should use " + - "\"blah.apk:classes2.dex\"", dexFileFile)); - System.err.println("Valid entries include:"); - - for (OatFile.OatDexFile oatDexFile : ex.oatFile.getDexFiles()) { - System.err.println(oatDexFile.filename); - } - System.exit(1); - } catch (IOException ex) { - throw new RuntimeException(ex); - } + DexBackedDexFile dexFile = loadDexFile(input, 15, false); if (showDeodexWarning() && dexFile.hasOdexOpcodes()) { StringWrapper.printWrappedString(System.err, diff --git a/baksmali/src/test/java/org/jf/baksmali/AnalysisTest.java b/baksmali/src/test/java/org/jf/baksmali/AnalysisTest.java index 9a2a7549..9d652e2f 100644 --- a/baksmali/src/test/java/org/jf/baksmali/AnalysisTest.java +++ b/baksmali/src/test/java/org/jf/baksmali/AnalysisTest.java @@ -85,7 +85,7 @@ public class AnalysisTest { public void runTest(String test, boolean registerInfo) throws IOException, URISyntaxException { String dexFilePath = String.format("%s%sclasses.dex", test, File.separatorChar); - DexFile dexFile = DexFileFactory.loadDexFile(findResource(dexFilePath), 15, false); + DexFile dexFile = DexFileFactory.loadDexFile(findResource(dexFilePath)); BaksmaliOptions options = new BaksmaliOptions(); if (registerInfo) { diff --git a/build.gradle b/build.gradle index 8d1b2f34..c9c8beb1 100644 --- a/build.gradle +++ b/build.gradle @@ -101,6 +101,7 @@ subprojects { guava: 'com.google.guava:guava:18.0', findbugs: 'com.google.code.findbugs:jsr305:1.3.9', junit: 'junit:junit:4.6', + mockito: 'org.mockito:mockito-core:1.+', antlr_runtime: 'org.antlr:antlr-runtime:3.5.2', antlr: 'org.antlr:antlr:3.5.2', stringtemplate: 'org.antlr:stringtemplate:3.2.1', diff --git a/dexlib2/build.gradle b/dexlib2/build.gradle index 8fbe5ffe..422d2c31 100644 --- a/dexlib2/build.gradle +++ b/dexlib2/build.gradle @@ -51,6 +51,7 @@ dependencies { compile depends.guava testCompile depends.junit + testCompile depends.mockito accessorTestGenerator project('accessorTestGenerator') diff --git a/dexlib2/src/main/java/org/jf/dexlib2/DexFileFactory.java b/dexlib2/src/main/java/org/jf/dexlib2/DexFileFactory.java index 60488ba2..06233334 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/DexFileFactory.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/DexFileFactory.java @@ -31,9 +31,10 @@ package org.jf.dexlib2; -import com.google.common.base.MoreObjects; -import com.google.common.io.ByteStreams; +import com.google.common.base.Joiner; +import com.google.common.collect.Lists; 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; @@ -45,81 +46,65 @@ import org.jf.util.ExceptionWithContext; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.*; +import java.util.Enumeration; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; public final class DexFileFactory { @Nonnull - public static DexBackedDexFile loadDexFile(@Nonnull String path, int api) throws IOException { - return loadDexFile(path, api, false); + public static DexBackedDexFile loadDexFile(@Nonnull String path) throws IOException { + return loadDexFile(new File(path), Opcodes.forApi(15)); } @Nonnull - public static DexBackedDexFile loadDexFile(@Nonnull String path, int api, boolean experimental) - throws IOException { - return loadDexFile(new File(path), "classes.dex", Opcodes.forApi(api, experimental)); + public static DexBackedDexFile loadDexFile(@Nonnull String path, @Nonnull Opcodes opcodes) throws IOException { + return loadDexFile(new File(path), opcodes); } @Nonnull - public static DexBackedDexFile loadDexFile(@Nonnull File dexFile, int api) throws IOException { - return loadDexFile(dexFile, api, false); + public static DexBackedDexFile loadDexFile(@Nonnull File file) throws IOException { + return loadDexFile(file, Opcodes.forApi(15)); } + /** + * 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 dexFile, int api, boolean experimental) - throws IOException { - return loadDexFile(dexFile, null, Opcodes.forApi(api, experimental)); - } + public static DexBackedDexFile loadDexFile(@Nonnull File file, @Nonnull Opcodes opcodes) throws IOException { + if (!file.exists()) { + throw new DexFileNotFoundException("%s does not exist", file.getName()); + } - @Nonnull - public static DexBackedDexFile loadDexFile(@Nonnull File dexFile, @Nullable String dexEntry, int api, - boolean experimental) throws IOException { - return loadDexFile(dexFile, dexEntry, Opcodes.forApi(api, experimental)); - } - - @Nonnull - public static DexBackedDexFile loadDexFile(@Nonnull File dexFile, @Nullable String dexEntry, - @Nonnull Opcodes opcodes) throws IOException { ZipFile zipFile = null; - boolean isZipFile = false; try { - zipFile = new ZipFile(dexFile); - // if we get here, it's safe to assume we have a zip file - isZipFile = true; - - String zipEntryName = MoreObjects.firstNonNull(dexEntry, "classes.dex"); - ZipEntry zipEntry = zipFile.getEntry(zipEntryName); - if (zipEntry == null) { - throw new DexFileNotFound("zip file %s does not contain a %s file", dexFile.getName(), zipEntryName); - } - long fileLength = zipEntry.getSize(); - if (fileLength < 40) { - throw new ExceptionWithContext("The %s file in %s is too small to be a valid dex file", - zipEntryName, dexFile.getName()); - } else if (fileLength > Integer.MAX_VALUE) { - throw new ExceptionWithContext("The %s file in %s is too large to read in", - zipEntryName, dexFile.getName()); - } - byte[] dexBytes = new byte[(int)fileLength]; - ByteStreams.readFully(zipFile.getInputStream(zipEntry), dexBytes); - return new DexBackedDexFile(opcodes, dexBytes); + zipFile = new ZipFile(file); } catch (IOException ex) { - // don't continue on if we know it's a zip file - if (isZipFile) { - throw ex; - } - } finally { - if (zipFile != null) { - try { - zipFile.close(); - } catch (IOException ex) { - // just eat it - } + // ignore and continue + } + + if (zipFile != null) { + try { + return new ZipDexEntryFinder(zipFile, opcodes).findEntry("classes.dex", true); + } finally { + zipFile.close(); } } - InputStream inputStream = new BufferedInputStream(new FileInputStream(dexFile)); + InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); try { try { return DexBackedDexFile.fromInputStream(opcodes, inputStream); @@ -127,14 +112,15 @@ public final class DexFileFactory { // just eat it } - // Note: DexBackedDexFile.fromInputStream will reset inputStream back to the same position, if it fails - 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); @@ -150,71 +136,127 @@ public final class DexFileFactory { List oatDexFiles = oatFile.getDexFiles(); if (oatDexFiles.size() == 0) { - throw new DexFileNotFound("Oat file %s contains no dex files", dexFile.getName()); + throw new DexFileNotFoundException("Oat file %s contains no dex files", file.getName()); } - if (dexEntry == null) { - if (oatDexFiles.size() > 1) { - throw new MultipleDexFilesException(oatFile); - } - return oatDexFiles.get(0); - } else { - // first check for an exact match - for (OatDexFile oatDexFile : oatFile.getDexFiles()) { - if (oatDexFile.filename.equals(dexEntry)) { - return oatDexFile; - } - } - - if (!dexEntry.contains("/")) { - for (OatDexFile oatDexFile : oatFile.getDexFiles()) { - File oatEntryFile = new File(oatDexFile.filename); - if (oatEntryFile.getName().equals(dexEntry)) { - return oatDexFile; - } - } - } - - throw new DexFileNotFound("oat file %s does not contain a dex file named %s", - dexFile.getName(), dexEntry); - } + return oatDexFiles.get(0); } } finally { inputStream.close(); } - throw new ExceptionWithContext("%s is not an apk, dex, odex or oat file.", dexFile.getPath()); + 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()); + } + + ZipFile zipFile = null; + try { + zipFile = new ZipFile(file); + } catch (IOException ex) { + // ignore and continue + } + + if (zipFile != null) { + try { + return new ZipDexEntryFinder(zipFile, opcodes).findEntry(dexEntry, exactMatch); + } finally { + zipFile.close(); + } + } + + InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); + try { + OatFile oatFile = null; + try { + oatFile = OatFile.fromInputStream(inputStream); + } catch (NotAnOatFileException ex) { + // just eat it + } + + if (oatFile != null) { + if (oatFile.isSupportedVersion() == OatFile.UNSUPPORTED) { + throw new UnsupportedOatVersionException(oatFile); + } + + List oatDexFiles = oatFile.getDexFiles(); + + if (oatDexFiles.size() == 0) { + throw new DexFileNotFoundException("Oat file %s contains no dex files", file.getName()); + } + + return new OatDexEntryFinder(file.getPath(), oatFile).findEntry(dexEntry, exactMatch); + } + } finally { + inputStream.close(); + } + + throw new UnsupportedFileTypeException("%s is not an apk 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 + * @throws IOException + */ public static void writeDexFile(@Nonnull String path, @Nonnull DexFile dexFile) throws IOException { DexPool.writeTo(path, dexFile); } private DexFileFactory() {} - public static class DexFileNotFound extends ExceptionWithContext { - public DexFileNotFound(@Nullable Throwable cause) { - super(cause); - } - - public DexFileNotFound(@Nullable Throwable cause, @Nullable String message, Object... formatArgs) { - super(cause, message, formatArgs); - } - - public DexFileNotFound(@Nullable String message, Object... formatArgs) { + public static class DexFileNotFoundException extends ExceptionWithContext { + public DexFileNotFoundException(@Nullable String message, Object... formatArgs) { super(message, formatArgs); } } - public static class MultipleDexFilesException extends ExceptionWithContext { - @Nonnull public final OatFile oatFile; - - public MultipleDexFilesException(@Nonnull OatFile oatFile) { - super("Oat file has multiple dex files."); - this.oatFile = oatFile; - } - } - public static class UnsupportedOatVersionException extends ExceptionWithContext { @Nonnull public final OatFile oatFile; @@ -223,4 +265,212 @@ public final class DexFileFactory { 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 abstract static class DexEntryFinder { + @Nullable + protected abstract DexBackedDexFile getEntry(@Nonnull String entry) throws IOException; + + @Nonnull + protected abstract List getEntryNames(); + + @Nonnull + protected abstract String getFilename(); + + @Nonnull + public DexBackedDexFile findEntry(@Nonnull String targetEntry, boolean exactMatch) throws IOException { + if (exactMatch) { + DexBackedDexFile dexFile = getEntry(targetEntry); + if (dexFile == null) { + if (getEntryNames().contains(targetEntry)) { + throw new UnsupportedFileTypeException("Entry %s in %s is not a dex file", targetEntry, + getFilename()); + } else { + throw new DexFileNotFoundException("Could not find %s in %s.", targetEntry, getFilename()); + } + } + return dexFile; + } + + // find all full and partial matches + List fullMatches = Lists.newArrayList(); + List fullEntries = Lists.newArrayList(); + List partialMatches = Lists.newArrayList(); + List partialEntries = Lists.newArrayList(); + for (String entry: getEntryNames()) { + 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(getEntry(entry)); + } else if (partialEntryMatch(entry, targetEntry)) { + DexBackedDexFile dexFile = getEntry(entry); + // We only want to grab a partial match if it is actually a dex file. + if (dexFile != null) { + partialMatches.add(entry); + partialEntries.add(dexFile); + } + } + } + + // full matches always take priority + if (fullEntries.size() == 1) { + DexBackedDexFile dexFile = fullEntries.get(0); + if (dexFile == null) { + throw new UnsupportedFileTypeException("Entry %s in %s is not a dex file", + fullMatches.get(0), getFilename()); + } + return dexFile; + } + 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", getFilename(), targetEntry, + Joiner.on(", ").join(fullMatches))); + } + + if (partialEntries.size() == 0) { + throw new DexFileNotFoundException("Could not find a dex entry in %s matching %s", + getFilename(), targetEntry); + } + if (partialEntries.size() > 1) { + throw new MultipleMatchingDexEntriesException(String.format( + "Multiple dex entries in %s match %s: %s", getFilename(), targetEntry, + Joiner.on(", ").join(partialMatches))); + } + return partialEntries.get(0); + } + } + + private static class ZipDexEntryFinder extends DexEntryFinder { + @Nonnull private final ZipFile zipFile; + @Nonnull private final Opcodes opcodes; + + public ZipDexEntryFinder(@Nonnull ZipFile zipFile, @Nonnull Opcodes opcodes) { + this.zipFile = zipFile; + this.opcodes = opcodes; + } + + @Nullable @Override protected DexBackedDexFile getEntry(@Nonnull String entry) throws IOException { + ZipEntry zipEntry = zipFile.getEntry(entry); + + InputStream stream = null; + try { + stream = zipFile.getInputStream(zipEntry); + return DexBackedDexFile.fromInputStream(opcodes, stream); + } catch (NotADexFile ex) { + return null; + } finally { + if (stream != null) { + stream.close(); + } + } + } + + @Nonnull @Override protected List getEntryNames() { + List entries = Lists.newArrayList(); + Enumeration entriesEnumeration = zipFile.entries(); + + while (entriesEnumeration.hasMoreElements()) { + ZipEntry entry = entriesEnumeration.nextElement(); + entries.add(entry.getName()); + } + + return entries; + } + + @Nonnull @Override protected String getFilename() { + return zipFile.getName(); + } + } + + private static class OatDexEntryFinder extends DexEntryFinder { + @Nonnull private final String fileName; + @Nonnull private final OatFile oatFile; + + public OatDexEntryFinder(@Nonnull String fileName, @Nonnull OatFile oatFile) { + this.fileName = fileName; + this.oatFile = oatFile; + } + + @Nullable @Override protected DexBackedDexFile getEntry(@Nonnull String entry) throws IOException { + for (OatDexFile dexFile: oatFile.getDexFiles()) { + if (dexFile.filename.equals(entry)) { + return dexFile; + } + } + return null; + } + + @Nonnull @Override protected List getEntryNames() { + List entries = Lists.newArrayList(); + + for (OatDexFile oatDexFile: oatFile.getDexFiles()) { + entries.add(oatDexFile.filename); + } + + return entries; + } + + @Nonnull @Override protected String getFilename() { + return fileName; + } + } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPath.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPath.java index 05cf4f8f..bc4d6a16 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPath.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPath.java @@ -40,7 +40,6 @@ import com.google.common.collect.*; import com.google.common.io.Files; import com.google.common.primitives.Ints; import org.jf.dexlib2.DexFileFactory; -import org.jf.dexlib2.DexFileFactory.MultipleDexFilesException; import org.jf.dexlib2.Opcodes; import org.jf.dexlib2.analysis.reflection.ReflectionClassDef; import org.jf.dexlib2.dexbacked.DexBackedOdexFile; @@ -231,14 +230,16 @@ public class ClassPath { } File bestMatch = Collections.max(files, new ClassPathEntryComparator(entry)); - try { - DexFile entryDexFile = DexFileFactory.loadDexFile(bestMatch, api, experimental); - classProviders.add(new DexClassProvider(entryDexFile)); + DexFile entryDexFile = DexFileFactory.loadDexFile(bestMatch, Opcodes.forApi(api, experimental)); + classProviders.add(new DexClassProvider(entryDexFile)); + // TODO: DexFileFactory.loadAllDexFiles? + /*try { + } catch (MultipleDexFilesException ex) { for (DexFile entryDexFile: ex.oatFile.getDexFiles()) { classProviders.add(new DexClassProvider(entryDexFile)); } - } + }*/ } int oatVersion = -1; @@ -329,12 +330,9 @@ public class ClassPath { } else { if (name.equals(child.getName())) { try { - DexFileFactory.loadDexFile(child, 15); + DexFileFactory.loadDexFile(child); } catch (ExceptionWithContext ex) { - if (!(ex instanceof MultipleDexFilesException)) { - // Don't add it to the results if it can't be loaded - continue; - } + continue; } result.add(child); } diff --git a/dexlib2/src/test/java/org/jf/dexlib2/AccessorTest.java b/dexlib2/src/test/java/org/jf/dexlib2/AccessorTest.java index 4c8f85bf..ced2d972 100644 --- a/dexlib2/src/test/java/org/jf/dexlib2/AccessorTest.java +++ b/dexlib2/src/test/java/org/jf/dexlib2/AccessorTest.java @@ -79,7 +79,7 @@ public class AccessorTest { public void testAccessors() throws IOException { URL url = AccessorTest.class.getClassLoader().getResource("accessorTest.dex"); Assert.assertNotNull(url); - DexFile f = DexFileFactory.loadDexFile(url.getFile(), 15, false); + DexFile f = DexFileFactory.loadDexFile(url.getFile()); SyntheticAccessorResolver sar = new SyntheticAccessorResolver(f.getOpcodes(), f.getClasses()); diff --git a/dexlib2/src/test/java/org/jf/dexlib2/DexEntryFinderTest.java b/dexlib2/src/test/java/org/jf/dexlib2/DexEntryFinderTest.java new file mode 100644 index 00000000..d7a82b34 --- /dev/null +++ b/dexlib2/src/test/java/org/jf/dexlib2/DexEntryFinderTest.java @@ -0,0 +1,210 @@ +/* + * Copyright 2016, 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.beust.jcommander.internal.Maps; +import com.google.common.collect.Lists; +import org.jf.dexlib2.DexEntryFinderTest.TestDexFileFactory.TestDexEntryFinder; +import org.jf.dexlib2.DexFileFactory.DexEntryFinder; +import org.jf.dexlib2.DexFileFactory.DexFileNotFoundException; +import org.jf.dexlib2.DexFileFactory.MultipleMatchingDexEntriesException; +import org.jf.dexlib2.DexFileFactory.UnsupportedFileTypeException; +import org.jf.dexlib2.dexbacked.DexBackedDexFile; +import org.junit.Assert; +import org.junit.Test; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static org.mockito.Mockito.mock; + +public class DexEntryFinderTest { + + + @Test + public void testNormalStuff() throws Exception { + Map entries = Maps.newHashMap(); + DexBackedDexFile dexFile1 = mock(DexBackedDexFile.class); + entries.put("/system/framework/framework.jar", dexFile1); + DexBackedDexFile dexFile2 = mock(DexBackedDexFile.class); + entries.put("/system/framework/framework.jar:classes2.dex", dexFile2); + TestDexEntryFinder testFinder = new TestDexEntryFinder("blah.oat", entries); + + Assert.assertEquals(dexFile1, testFinder.findEntry("/system/framework/framework.jar", true)); + + assertEntryNotFound(testFinder, "system/framework/framework.jar", true); + assertEntryNotFound(testFinder, "/framework/framework.jar", true); + assertEntryNotFound(testFinder, "framework/framework.jar", true); + assertEntryNotFound(testFinder, "/framework.jar", true); + assertEntryNotFound(testFinder, "framework.jar", true); + + Assert.assertEquals(dexFile1, testFinder.findEntry("system/framework/framework.jar", false)); + Assert.assertEquals(dexFile1, testFinder.findEntry("/framework/framework.jar", false)); + Assert.assertEquals(dexFile1, testFinder.findEntry("framework/framework.jar", false)); + Assert.assertEquals(dexFile1, testFinder.findEntry("/framework.jar", false)); + Assert.assertEquals(dexFile1, testFinder.findEntry("framework.jar", false)); + + assertEntryNotFound(testFinder, "ystem/framework/framework.jar", false); + assertEntryNotFound(testFinder, "ssystem/framework/framework.jar", false); + assertEntryNotFound(testFinder, "ramework/framework.jar", false); + assertEntryNotFound(testFinder, "ramework.jar", false); + assertEntryNotFound(testFinder, "framework", false); + + Assert.assertEquals(dexFile2, testFinder.findEntry("/system/framework/framework.jar:classes2.dex", true)); + + assertEntryNotFound(testFinder, "system/framework/framework.jar:classes2.dex", true); + assertEntryNotFound(testFinder, "framework.jar:classes2.dex", true); + assertEntryNotFound(testFinder, "classes2.dex", true); + + Assert.assertEquals(dexFile2, testFinder.findEntry("system/framework/framework.jar:classes2.dex", false)); + Assert.assertEquals(dexFile2, testFinder.findEntry("/framework/framework.jar:classes2.dex", false)); + Assert.assertEquals(dexFile2, testFinder.findEntry("framework/framework.jar:classes2.dex", false)); + Assert.assertEquals(dexFile2, testFinder.findEntry("/framework.jar:classes2.dex", false)); + Assert.assertEquals(dexFile2, testFinder.findEntry("framework.jar:classes2.dex", false)); + Assert.assertEquals(dexFile2, testFinder.findEntry(":classes2.dex", false)); + Assert.assertEquals(dexFile2, testFinder.findEntry("classes2.dex", false)); + + assertEntryNotFound(testFinder, "ystem/framework/framework.jar:classes2.dex", false); + assertEntryNotFound(testFinder, "ramework.jar:classes2.dex", false); + assertEntryNotFound(testFinder, "lasses2.dex", false); + assertEntryNotFound(testFinder, "classes2", false); + } + + @Test + public void testSimilarEntries() throws Exception { + Map entries = Maps.newHashMap(); + DexBackedDexFile dexFile1 = mock(DexBackedDexFile.class); + entries.put("/system/framework/framework.jar", dexFile1); + DexBackedDexFile dexFile2 = mock(DexBackedDexFile.class); + entries.put("system/framework/framework.jar", dexFile2); + TestDexEntryFinder testFinder = new TestDexEntryFinder("blah.oat", entries); + + Assert.assertEquals(dexFile1, testFinder.findEntry("/system/framework/framework.jar", true)); + Assert.assertEquals(dexFile2, testFinder.findEntry("system/framework/framework.jar", true)); + + assertMultipleMatchingEntries(testFinder, "/system/framework/framework.jar"); + assertMultipleMatchingEntries(testFinder, "system/framework/framework.jar"); + + assertMultipleMatchingEntries(testFinder, "/framework/framework.jar"); + assertMultipleMatchingEntries(testFinder, "framework/framework.jar"); + assertMultipleMatchingEntries(testFinder, "/framework.jar"); + assertMultipleMatchingEntries(testFinder, "framework.jar"); + } + + @Test + public void testMatchingSuffix() throws Exception { + Map entries = Maps.newHashMap(); + DexBackedDexFile dexFile1 = mock(DexBackedDexFile.class); + entries.put("/system/framework/framework.jar", dexFile1); + DexBackedDexFile dexFile2 = mock(DexBackedDexFile.class); + entries.put("/framework/framework.jar", dexFile2); + TestDexEntryFinder testFinder = new TestDexEntryFinder("blah.oat", entries); + + Assert.assertEquals(dexFile1, testFinder.findEntry("/system/framework/framework.jar", true)); + Assert.assertEquals(dexFile2, testFinder.findEntry("/framework/framework.jar", true)); + + Assert.assertEquals(dexFile2, testFinder.findEntry("/framework/framework.jar", false)); + Assert.assertEquals(dexFile2, testFinder.findEntry("framework/framework.jar", false)); + + assertMultipleMatchingEntries(testFinder, "/framework.jar"); + assertMultipleMatchingEntries(testFinder, "framework.jar"); + } + + @Test + public void testNonDexEntries() throws Exception { + Map entries = Maps.newHashMap(); + DexBackedDexFile dexFile1 = mock(DexBackedDexFile.class); + entries.put("classes.dex", dexFile1); + entries.put("/blah/classes.dex", null); + TestDexEntryFinder testFinder = new TestDexEntryFinder("blah.oat", entries); + + Assert.assertEquals(dexFile1, testFinder.findEntry("classes.dex", true)); + Assert.assertEquals(dexFile1, testFinder.findEntry("classes.dex", false)); + + assertUnsupportedFileType(testFinder, "/blah/classes.dex", true); + assertUnsupportedFileType(testFinder, "/blah/classes.dex", false); + } + + private void assertEntryNotFound(DexEntryFinder finder, String entry, boolean exactMatch) throws IOException { + try { + finder.findEntry(entry, exactMatch); + Assert.fail(); + } catch (DexFileNotFoundException ex) { + // expected exception + } + } + + private void assertMultipleMatchingEntries(DexEntryFinder finder, String entry) throws IOException { + try { + finder.findEntry(entry, false); + Assert.fail(); + } catch (MultipleMatchingDexEntriesException ex) { + // expected exception + } + } + + private void assertUnsupportedFileType(DexEntryFinder finder, String entry, boolean exactMatch) throws IOException { + try { + finder.findEntry(entry, exactMatch); + Assert.fail(); + } catch (UnsupportedFileTypeException ex) { + // expected exception + } + } + + public static class TestDexFileFactory { + public static class TestDexEntryFinder extends DexEntryFinder { + @Nonnull private final String fileName; + @Nonnull private final Map entries; + + public TestDexEntryFinder(@Nonnull String fileName, @Nonnull Map entries) { + this.fileName = fileName; + this.entries = entries; + } + + @Nullable @Override protected DexBackedDexFile getEntry(@Nonnull String entry) throws IOException { + return entries.get(entry); + } + + @Nonnull @Override protected List getEntryNames() { + return Lists.newArrayList(entries.keySet()); + } + + @Nonnull @Override protected String getFilename() { + return fileName; + } + } + } +}