Refactor DexFileFactory and implement new syntax for dex entries

This commit is contained in:
Ben Gruver 2016-08-30 21:15:03 -07:00
parent 41a5b4953c
commit 3587c6f2a6
9 changed files with 636 additions and 197 deletions

View File

@ -32,9 +32,10 @@
package org.jf.baksmali; package org.jf.baksmali;
import com.beust.jcommander.JCommander; import com.beust.jcommander.JCommander;
import com.google.common.base.Strings;
import org.jf.dexlib2.DexFileFactory; import org.jf.dexlib2.DexFileFactory;
import org.jf.dexlib2.Opcodes;
import org.jf.dexlib2.dexbacked.DexBackedDexFile; import org.jf.dexlib2.dexbacked.DexBackedDexFile;
import org.jf.dexlib2.dexbacked.OatFile;
import org.jf.util.jcommander.Command; import org.jf.util.jcommander.Command;
import javax.annotation.Nonnull; 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. * 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 * In some cases, the input file can contain multiple dex files. If this is the case, you can refer to a specific
* can additionally consist of a colon followed by a specific dex entry to load. * 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 apiLevel The api level to load the dex file with
* @param experimentalOpcodes whether experimental opcodes should be allowed * @param experimentalOpcodes whether experimental opcodes should be allowed
* @return The loaded DexBackedDexFile * @return The loaded DexBackedDexFile
*/ */
@Nonnull @Nonnull
protected DexBackedDexFile loadDexFile(@Nonnull String input, int apiLevel, boolean experimentalOpcodes) { protected DexBackedDexFile loadDexFile(@Nonnull String input, int apiLevel, boolean experimentalOpcodes) {
File dexFileFile = new File(input); File file = new File(input);
String dexFileEntry = null;
int previousIndex = input.length(); while (file != null && !file.exists()) {
while (!dexFileFile.exists()) { file = file.getParentFile();
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;
}
} }
if (!dexFileFile.exists()) { if (file == null || !file.exists() || file.isDirectory()) {
System.err.println("Can't find the file " + input); System.err.println("Can't find file: " + input);
System.exit(1); System.exit(1);
} }
if (!dexFileFile.exists()) { File dexFile = file;
int colonIndex = input.lastIndexOf(':'); String dexEntry = null;
if (dexFile.getPath().length() < input.length()) {
if (colonIndex >= 0) { dexEntry = input.substring(dexFile.getPath().length() + 1);
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);
}
} }
try { if (!Strings.isNullOrEmpty(dexEntry)) {
return DexFileFactory.loadDexFile(dexFileFile, dexFileEntry, apiLevel, experimentalOpcodes); boolean exactMatch = false;
} catch (DexFileFactory.MultipleDexFilesException ex) { if (dexEntry.length() > 2 && dexEntry.charAt(0) == '"' && dexEntry.charAt(dexEntry.length() - 1) == '"') {
System.err.println(String.format("%s is an oat file that contains multiple dex files. You must specify " + dexEntry = dexEntry.substring(1, dexEntry.length() - 1);
"which one to load. E.g. To load the \"core.dex\" entry from boot.oat, you should use " + exactMatch = true;
"\"boot.oat:core.dex\"", dexFileFile));
System.err.println("Valid entries include:");
for (OatFile.OatDexFile oatDexFile : ex.oatFile.getDexFiles()) {
System.err.println(oatDexFile.filename);
} }
} catch (IOException ex) {
throw new RuntimeException(ex);
}
// execution can never actually reach here try {
throw new IllegalStateException(); 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);
}
}
} }
} }

View File

@ -35,12 +35,10 @@ import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters; import com.beust.jcommander.Parameters;
import com.beust.jcommander.validators.PositiveInteger; import com.beust.jcommander.validators.PositiveInteger;
import org.jf.dexlib2.DexFileFactory;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import org.jf.dexlib2.analysis.ClassPath; import org.jf.dexlib2.analysis.ClassPath;
import org.jf.dexlib2.dexbacked.DexBackedDexFile; import org.jf.dexlib2.dexbacked.DexBackedDexFile;
import org.jf.dexlib2.dexbacked.OatFile;
import org.jf.dexlib2.iface.DexFile; import org.jf.dexlib2.iface.DexFile;
import org.jf.dexlib2.util.SyntheticAccessorResolver; import org.jf.dexlib2.util.SyntheticAccessorResolver;
import org.jf.util.StringWrapper; import org.jf.util.StringWrapper;
@ -50,7 +48,6 @@ import org.jf.util.jcommander.ExtendedParameters;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -187,40 +184,7 @@ public class DisassembleCommand extends DexInputCommand {
} }
String input = inputList.get(0); String input = inputList.get(0);
File dexFileFile = new File(input); DexBackedDexFile dexFile = loadDexFile(input, 15, false);
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);
}
if (showDeodexWarning() && dexFile.hasOdexOpcodes()) { if (showDeodexWarning() && dexFile.hasOdexOpcodes()) {
StringWrapper.printWrappedString(System.err, StringWrapper.printWrappedString(System.err,

View File

@ -85,7 +85,7 @@ public class AnalysisTest {
public void runTest(String test, boolean registerInfo) throws IOException, URISyntaxException { public void runTest(String test, boolean registerInfo) throws IOException, URISyntaxException {
String dexFilePath = String.format("%s%sclasses.dex", test, File.separatorChar); 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(); BaksmaliOptions options = new BaksmaliOptions();
if (registerInfo) { if (registerInfo) {

View File

@ -101,6 +101,7 @@ subprojects {
guava: 'com.google.guava:guava:18.0', guava: 'com.google.guava:guava:18.0',
findbugs: 'com.google.code.findbugs:jsr305:1.3.9', findbugs: 'com.google.code.findbugs:jsr305:1.3.9',
junit: 'junit:junit:4.6', junit: 'junit:junit:4.6',
mockito: 'org.mockito:mockito-core:1.+',
antlr_runtime: 'org.antlr:antlr-runtime:3.5.2', antlr_runtime: 'org.antlr:antlr-runtime:3.5.2',
antlr: 'org.antlr:antlr:3.5.2', antlr: 'org.antlr:antlr:3.5.2',
stringtemplate: 'org.antlr:stringtemplate:3.2.1', stringtemplate: 'org.antlr:stringtemplate:3.2.1',

View File

@ -51,6 +51,7 @@ dependencies {
compile depends.guava compile depends.guava
testCompile depends.junit testCompile depends.junit
testCompile depends.mockito
accessorTestGenerator project('accessorTestGenerator') accessorTestGenerator project('accessorTestGenerator')

View File

@ -31,9 +31,10 @@
package org.jf.dexlib2; package org.jf.dexlib2;
import com.google.common.base.MoreObjects; import com.google.common.base.Joiner;
import com.google.common.io.ByteStreams; import com.google.common.collect.Lists;
import org.jf.dexlib2.dexbacked.DexBackedDexFile; import org.jf.dexlib2.dexbacked.DexBackedDexFile;
import org.jf.dexlib2.dexbacked.DexBackedDexFile.NotADexFile;
import org.jf.dexlib2.dexbacked.DexBackedOdexFile; import org.jf.dexlib2.dexbacked.DexBackedOdexFile;
import org.jf.dexlib2.dexbacked.OatFile; import org.jf.dexlib2.dexbacked.OatFile;
import org.jf.dexlib2.dexbacked.OatFile.NotAnOatFileException; import org.jf.dexlib2.dexbacked.OatFile.NotAnOatFileException;
@ -45,81 +46,65 @@ import org.jf.util.ExceptionWithContext;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.io.*; import java.io.*;
import java.util.Enumeration;
import java.util.List; import java.util.List;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipFile; import java.util.zip.ZipFile;
public final class DexFileFactory { public final class DexFileFactory {
@Nonnull @Nonnull
public static DexBackedDexFile loadDexFile(@Nonnull String path, int api) throws IOException { public static DexBackedDexFile loadDexFile(@Nonnull String path) throws IOException {
return loadDexFile(path, api, false); return loadDexFile(new File(path), Opcodes.forApi(15));
} }
@Nonnull @Nonnull
public static DexBackedDexFile loadDexFile(@Nonnull String path, int api, boolean experimental) public static DexBackedDexFile loadDexFile(@Nonnull String path, @Nonnull Opcodes opcodes) throws IOException {
throws IOException { return loadDexFile(new File(path), opcodes);
return loadDexFile(new File(path), "classes.dex", Opcodes.forApi(api, experimental));
} }
@Nonnull @Nonnull
public static DexBackedDexFile loadDexFile(@Nonnull File dexFile, int api) throws IOException { public static DexBackedDexFile loadDexFile(@Nonnull File file) throws IOException {
return loadDexFile(dexFile, api, false); 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 @Nonnull
public static DexBackedDexFile loadDexFile(@Nonnull File dexFile, int api, boolean experimental) public static DexBackedDexFile loadDexFile(@Nonnull File file, @Nonnull Opcodes opcodes) throws IOException {
throws IOException { if (!file.exists()) {
return loadDexFile(dexFile, null, Opcodes.forApi(api, experimental)); 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; ZipFile zipFile = null;
boolean isZipFile = false;
try { try {
zipFile = new ZipFile(dexFile); zipFile = new ZipFile(file);
// 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);
} catch (IOException ex) { } catch (IOException ex) {
// don't continue on if we know it's a zip file // ignore and continue
if (isZipFile) { }
throw ex;
} if (zipFile != null) {
} finally { try {
if (zipFile != null) { return new ZipDexEntryFinder(zipFile, opcodes).findEntry("classes.dex", true);
try { } finally {
zipFile.close(); zipFile.close();
} catch (IOException ex) {
// just eat it
}
} }
} }
InputStream inputStream = new BufferedInputStream(new FileInputStream(dexFile)); InputStream inputStream = new BufferedInputStream(new FileInputStream(file));
try { try {
try { try {
return DexBackedDexFile.fromInputStream(opcodes, inputStream); return DexBackedDexFile.fromInputStream(opcodes, inputStream);
@ -127,14 +112,15 @@ public final class DexFileFactory {
// just eat it // just eat it
} }
// Note: DexBackedDexFile.fromInputStream will reset inputStream back to the same position, if it fails
try { try {
return DexBackedOdexFile.fromInputStream(opcodes, inputStream); return DexBackedOdexFile.fromInputStream(opcodes, inputStream);
} catch (DexBackedOdexFile.NotAnOdexFile ex) { } catch (DexBackedOdexFile.NotAnOdexFile ex) {
// just eat it // just eat it
} }
// Note: DexBackedDexFile.fromInputStream and DexBackedOdexFile.fromInputStream will reset inputStream
// back to the same position, if they fails
OatFile oatFile = null; OatFile oatFile = null;
try { try {
oatFile = OatFile.fromInputStream(inputStream); oatFile = OatFile.fromInputStream(inputStream);
@ -150,71 +136,127 @@ public final class DexFileFactory {
List<OatDexFile> oatDexFiles = oatFile.getDexFiles(); List<OatDexFile> oatDexFiles = oatFile.getDexFiles();
if (oatDexFiles.size() == 0) { 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) { return oatDexFiles.get(0);
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);
}
} }
} finally { } finally {
inputStream.close(); 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<OatDexFile> 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 { public static void writeDexFile(@Nonnull String path, @Nonnull DexFile dexFile) throws IOException {
DexPool.writeTo(path, dexFile); DexPool.writeTo(path, dexFile);
} }
private DexFileFactory() {} private DexFileFactory() {}
public static class DexFileNotFound extends ExceptionWithContext { public static class DexFileNotFoundException extends ExceptionWithContext {
public DexFileNotFound(@Nullable Throwable cause) { public DexFileNotFoundException(@Nullable String message, Object... formatArgs) {
super(cause);
}
public DexFileNotFound(@Nullable Throwable cause, @Nullable String message, Object... formatArgs) {
super(cause, message, formatArgs);
}
public DexFileNotFound(@Nullable String message, Object... formatArgs) {
super(message, 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 { public static class UnsupportedOatVersionException extends ExceptionWithContext {
@Nonnull public final OatFile oatFile; @Nonnull public final OatFile oatFile;
@ -223,4 +265,212 @@ public final class DexFileFactory {
this.oatFile = oatFile; 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<String> 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<String> fullMatches = Lists.newArrayList();
List<DexBackedDexFile> fullEntries = Lists.newArrayList();
List<String> partialMatches = Lists.newArrayList();
List<DexBackedDexFile> 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<String> getEntryNames() {
List<String> entries = Lists.newArrayList();
Enumeration<? extends ZipEntry> 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<String> getEntryNames() {
List<String> entries = Lists.newArrayList();
for (OatDexFile oatDexFile: oatFile.getDexFiles()) {
entries.add(oatDexFile.filename);
}
return entries;
}
@Nonnull @Override protected String getFilename() {
return fileName;
}
}
} }

View File

@ -40,7 +40,6 @@ import com.google.common.collect.*;
import com.google.common.io.Files; import com.google.common.io.Files;
import com.google.common.primitives.Ints; import com.google.common.primitives.Ints;
import org.jf.dexlib2.DexFileFactory; import org.jf.dexlib2.DexFileFactory;
import org.jf.dexlib2.DexFileFactory.MultipleDexFilesException;
import org.jf.dexlib2.Opcodes; import org.jf.dexlib2.Opcodes;
import org.jf.dexlib2.analysis.reflection.ReflectionClassDef; import org.jf.dexlib2.analysis.reflection.ReflectionClassDef;
import org.jf.dexlib2.dexbacked.DexBackedOdexFile; import org.jf.dexlib2.dexbacked.DexBackedOdexFile;
@ -231,14 +230,16 @@ public class ClassPath {
} }
File bestMatch = Collections.max(files, new ClassPathEntryComparator(entry)); File bestMatch = Collections.max(files, new ClassPathEntryComparator(entry));
try { DexFile entryDexFile = DexFileFactory.loadDexFile(bestMatch, Opcodes.forApi(api, experimental));
DexFile entryDexFile = DexFileFactory.loadDexFile(bestMatch, api, experimental); classProviders.add(new DexClassProvider(entryDexFile));
classProviders.add(new DexClassProvider(entryDexFile)); // TODO: DexFileFactory.loadAllDexFiles?
/*try {
} catch (MultipleDexFilesException ex) { } catch (MultipleDexFilesException ex) {
for (DexFile entryDexFile: ex.oatFile.getDexFiles()) { for (DexFile entryDexFile: ex.oatFile.getDexFiles()) {
classProviders.add(new DexClassProvider(entryDexFile)); classProviders.add(new DexClassProvider(entryDexFile));
} }
} }*/
} }
int oatVersion = -1; int oatVersion = -1;
@ -329,12 +330,9 @@ public class ClassPath {
} else { } else {
if (name.equals(child.getName())) { if (name.equals(child.getName())) {
try { try {
DexFileFactory.loadDexFile(child, 15); DexFileFactory.loadDexFile(child);
} catch (ExceptionWithContext ex) { } catch (ExceptionWithContext ex) {
if (!(ex instanceof MultipleDexFilesException)) { continue;
// Don't add it to the results if it can't be loaded
continue;
}
} }
result.add(child); result.add(child);
} }

View File

@ -79,7 +79,7 @@ public class AccessorTest {
public void testAccessors() throws IOException { public void testAccessors() throws IOException {
URL url = AccessorTest.class.getClassLoader().getResource("accessorTest.dex"); URL url = AccessorTest.class.getClassLoader().getResource("accessorTest.dex");
Assert.assertNotNull(url); 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()); SyntheticAccessorResolver sar = new SyntheticAccessorResolver(f.getOpcodes(), f.getClasses());

View File

@ -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<String, DexBackedDexFile> 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<String, DexBackedDexFile> 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<String, DexBackedDexFile> 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<String, DexBackedDexFile> 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<String, DexBackedDexFile> entries;
public TestDexEntryFinder(@Nonnull String fileName, @Nonnull Map<String, DexBackedDexFile> 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<String> getEntryNames() {
return Lists.newArrayList(entries.keySet());
}
@Nonnull @Override protected String getFilename() {
return fileName;
}
}
}
}