From 31ad2bc1002784161b318627f32b4df8bcb862e0 Mon Sep 17 00:00:00 2001 From: Ben Gruver Date: Sun, 18 Sep 2016 12:41:46 -0700 Subject: [PATCH] Refactor how classpath loading works --- .../org/jf/baksmali/AnalysisArguments.java | 39 +- .../java/org/jf/baksmali/HelpCommand.java | 61 +++ .../java/org/jf/baksmali/ListDexCommand.java | 7 +- .../java/org/jf/dexlib2/DexFileFactory.java | 292 +++--------- .../org/jf/dexlib2/analysis/ClassPath.java | 315 +------------ .../dexlib2/analysis/ClassPathResolver.java | 432 ++++++++++++++++++ .../dexlib2/dexbacked/DexBackedDexFile.java | 4 +- .../org/jf/dexlib2/dexbacked/OatFile.java | 148 ++++-- .../jf/dexlib2/dexbacked/ZipDexContainer.java | 166 +++++++ .../jf/dexlib2/iface/MultiDexContainer.java | 70 +++ .../org/jf/dexlib2/DexEntryFinderTest.java | 63 ++- .../analysis/CustomMethodInlineTableTest.java | 17 +- util/src/main/java/org/jf/util/PathUtil.java | 12 +- 13 files changed, 995 insertions(+), 631 deletions(-) create mode 100644 dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPathResolver.java create mode 100644 dexlib2/src/main/java/org/jf/dexlib2/dexbacked/ZipDexContainer.java create mode 100644 dexlib2/src/main/java/org/jf/dexlib2/iface/MultiDexContainer.java diff --git a/baksmali/src/main/java/org/jf/baksmali/AnalysisArguments.java b/baksmali/src/main/java/org/jf/baksmali/AnalysisArguments.java index 5198803c..1d1904ef 100644 --- a/baksmali/src/main/java/org/jf/baksmali/AnalysisArguments.java +++ b/baksmali/src/main/java/org/jf/baksmali/AnalysisArguments.java @@ -34,14 +34,19 @@ package org.jf.baksmali; import com.beust.jcommander.Parameter; import com.google.common.collect.Lists; import org.jf.dexlib2.analysis.ClassPath; +import org.jf.dexlib2.analysis.ClassPathResolver; +import org.jf.dexlib2.dexbacked.OatFile.OatDexFile; import org.jf.dexlib2.iface.DexFile; import org.jf.util.jcommander.ColonParameterSplitter; import org.jf.util.jcommander.ExtendedParameter; import javax.annotation.Nonnull; +import java.io.File; import java.io.IOException; import java.util.List; +import static org.jf.dexlib2.analysis.ClassPath.NOT_ART; + public class AnalysisArguments { @Parameter(names = {"-a", "--api"}, description = "The numeric api level of the file being disassembled.") @@ -68,7 +73,7 @@ public class AnalysisArguments { description = "A directory to search for classpath files. This option can be used multiple times to " + "specify multiple directories to search. They will be searched in the order they are provided.") @ExtendedParameter(argumentNames = "dir") - public List classPathDirectories = Lists.newArrayList("."); + public List classPathDirectories = null; public static class CheckPackagePrivateArgument { @Parameter(names = {"--check-package-private-access", "--package-private", "--checkpp", "--pp"}, @@ -77,9 +82,37 @@ public class AnalysisArguments { public boolean checkPackagePrivateAccess = false; } + @Nonnull public ClassPath loadClassPathForDexFile(@Nonnull DexFile dexFile, boolean checkPackagePrivateAccess) throws IOException { - return ClassPath.loadClassPath(classPathDirectories, bootClassPath, classPath, dexFile, apiLevel, - checkPackagePrivateAccess); + ClassPathResolver resolver; + + List filteredClassPathDirectories = Lists.newArrayList(); + if (classPathDirectories != null) { + for (String dir: classPathDirectories) { + File file = new File(dir); + if (!file.exists()) { + System.err.println(String.format("Warning: directory %s does not exist. Ignoring.", dir)); + } else if (!file.isDirectory()) { + System.err.println(String.format("Warning: %s is not a directory. Ignoring.", dir)); + } else { + filteredClassPathDirectories.add(dir); + } + } + } + + if (bootClassPath == null) { + // TODO: we should be able to get the api from the Opcodes object associated with the dexFile.. + // except that the oat version -> api mapping doesn't fully work yet + resolver = new ClassPathResolver(filteredClassPathDirectories, classPath, dexFile, apiLevel); + } else { + resolver = new ClassPathResolver(filteredClassPathDirectories, bootClassPath, classPath, dexFile); + } + + int oatVersion = NOT_ART; + if (dexFile instanceof OatDexFile) { + oatVersion = ((OatDexFile)dexFile).getOatFile().getOatVersion(); + } + return new ClassPath(resolver.getResolvedClassProviders(), checkPackagePrivateAccess, oatVersion); } } diff --git a/baksmali/src/main/java/org/jf/baksmali/HelpCommand.java b/baksmali/src/main/java/org/jf/baksmali/HelpCommand.java index 997d34b4..149ac63d 100644 --- a/baksmali/src/main/java/org/jf/baksmali/HelpCommand.java +++ b/baksmali/src/main/java/org/jf/baksmali/HelpCommand.java @@ -108,6 +108,67 @@ public class HelpCommand extends Command { "around this, you can add double quotes around the entry name to specify an exact entry " + "name. E.g. blah.oat/\"/blah/blah.dex\" or blah.oat/\"blah/blah.dex\" respectively."; + Iterable lines = StringWrapper.wrapStringOnBreaks(registerInfoHelp, + ConsoleUtil.getConsoleWidth()); + for (String line : lines) { + System.out.println(line); + } + } else if (cmd.equals("classpath")) { + printedHelp = true; + String registerInfoHelp = "When deodexing odex/oat files or when using the --register-info " + + "option, baksmali needs to load all classes from the framework files on the device " + + "in order to fully understand the class hierarchy. There are several options that " + + "control how baksmali finds and loads the classpath entries.\n" + + "\n"+ + "L+ devices (ART):\n" + + "When deodexing or disassembling a file from an L+ device using ART, you generally " + + "just need to specify the path to the boot.oat file via the --bootclasspath/-b " + + "parameter. On pre-N devices, the boot.oat file is self-contained and no other files are " + + "needed. In N, boot.oat was split into multiple files. In this case, the other " + + "files should be in the same directory as the boot.oat file, but you still only need to " + + "specify the boot.oat file in the --bootclasspath/-b option. The other files will be " + + "automatically loaded from the same directory.\n" + + "\n" + + "Pre-L devices (dalvik):\n" + + "When deodexing odex files from a pre-L device using dalvik, you " + + "generally just need to specify the path to a directory containing the framework files " + + "from the device via the --classpath-dir/-d option. odex files contain a list of " + + "framework files they depend on and baksmali will search for these dependencies in the " + + "directory that you specify.\n" + + "\n" + + "Dex files don't contain a list of dependencies like odex files, so when disassembling a " + + "dex file using the --register-info option, and using the framework files from a " + + "pre-L device, baksmali will attempt to use a reasonable default list of classpath files " + + "based on the api level set via the -a option. If this default list is incorrect, you " + + "can override the classpath using the --bootclasspath/-b option. This option accepts a " + + "colon separated list of classpath entries. Each entry can be specified in a few " + + "different ways.\n" + + " - A simple filename like \"framework.jar\"\n" + + " - A device path like \"/system/framework/framework.jar\"\n" + + " - A local relative or absolute path like \"/tmp/framework/framework.jar\"\n" + + "When using the first or second formats, you should also specify the directory " + + "containing the framework files via the --classpath-dir/-d option. When using the third " + + "format, this option is not needed.\n" + + "It's worth noting that the second format matches the format used by Android for the " + + "BOOTCLASSPATH environment variable, so you can simply grab the value of that variable " + + "from the device and use it as-is.\n" + + "\n" + + "Examples:\n" + + " For an M device:\n" + + " adb pull /system/framework/arm/boot.oat /tmp/boot.oat\n" + + " baksmali deodex blah.oat -b /tmp/boot.oat\n" + + " For an N+ device:\n" + + " adb pull /system/framework/arm /tmp/framework\n" + + " baksmali deodex blah.oat -b /tmp/framework/boot.oat\n" + + " For a pre-L device:\n" + + " adb pull /system/framework /tmp/framework\n" + + " baksmali deodex blah.odex -d /tmp/framework\n" + + " Using the BOOTCLASSPATH on a pre-L device:\n" + + " adb pull /system/framework /tmp/framework\n" + + " export BOOTCLASSPATH=`adb shell \"echo \\\\$BOOTCLASPATH\"`\n" + + " baksmali disassemble --register-info ARGS,DEST blah.apk -b $BOOTCLASSPATH -d " + + "/tmp/framework"; + Iterable lines = StringWrapper.wrapStringOnBreaks(registerInfoHelp, ConsoleUtil.getConsoleWidth()); for (String line : lines) { diff --git a/baksmali/src/main/java/org/jf/baksmali/ListDexCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListDexCommand.java index 435d554d..dff49f47 100644 --- a/baksmali/src/main/java/org/jf/baksmali/ListDexCommand.java +++ b/baksmali/src/main/java/org/jf/baksmali/ListDexCommand.java @@ -36,6 +36,9 @@ import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameters; import com.google.common.collect.Lists; import org.jf.dexlib2.DexFileFactory; +import org.jf.dexlib2.Opcodes; +import org.jf.dexlib2.dexbacked.DexBackedDexFile; +import org.jf.dexlib2.iface.MultiDexContainer; import org.jf.util.jcommander.Command; import org.jf.util.jcommander.ExtendedParameter; import org.jf.util.jcommander.ExtendedParameters; @@ -85,7 +88,9 @@ public class ListDexCommand extends Command { List entries; try { - entries = DexFileFactory.getAllDexEntries(file); + MultiDexContainer container = + DexFileFactory.loadDexContainer(file, Opcodes.forApi(15)); + entries = container.getDexEntryNames(); } catch (IOException ex) { throw new RuntimeException(ex); } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/DexFileFactory.java b/dexlib2/src/main/java/org/jf/dexlib2/DexFileFactory.java index 81c73102..e088e872 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/DexFileFactory.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/DexFileFactory.java @@ -32,7 +32,7 @@ package org.jf.dexlib2; import com.google.common.base.Joiner; -import com.google.common.collect.AbstractIterator; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import org.jf.dexlib2.dexbacked.DexBackedDexFile; import org.jf.dexlib2.dexbacked.DexBackedDexFile.NotADexFile; @@ -40,18 +40,16 @@ 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.ZipDexContainer; 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.Enumeration; -import java.util.Iterator; import java.util.List; -import java.util.regex.Pattern; -import java.util.zip.ZipEntry; import java.util.zip.ZipFile; public final class DexFileFactory { @@ -100,10 +98,12 @@ public final class DexFileFactory { } if (zipFile != null) { + ZipDexContainer container = new ZipDexContainer(zipFile, opcodes); try { - return new ZipDexEntryFinder(zipFile, opcodes).findEntry("classes.dex", true); + return new DexEntryFinder(file.getPath(), container) + .findEntry("classes.dex", true); } finally { - zipFile.close(); + container.close(); } } @@ -205,10 +205,11 @@ public final class DexFileFactory { } if (zipFile != null) { + ZipDexContainer container = new ZipDexContainer(zipFile, opcodes); try { - return new ZipDexEntryFinder(zipFile, opcodes).findEntry(dexEntry, exactMatch); + return new DexEntryFinder(file.getPath(), container).findEntry(dexEntry, exactMatch); } finally { - zipFile.close(); + container.close(); } } @@ -232,7 +233,7 @@ public final class DexFileFactory { throw new DexFileNotFoundException("Oat file %s contains no dex files", file.getName()); } - return new OatDexEntryFinder(file.getPath(), oatFile).findEntry(dexEntry, exactMatch); + return new DexEntryFinder(file.getPath(), oatFile).findEntry(dexEntry, exactMatch); } } finally { inputStream.close(); @@ -242,20 +243,18 @@ public final class DexFileFactory { } /** - * Loads all dex files from the given file. + * Loads a file containing 1 or more dex files * - * If the given file is a dex or odex file, it will return an iterable with one element. If the given file is - * an oat file, it will return all dex files within the oat file. If the given file is a zip file, it will return - * all dex files matching "classes[0-9]*.dex" + * 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 An iterable of DexBackedDexFiles - * @throws IOException + * @return A MultiDexContainer * @throws DexFileNotFoundException If the given file does not exist - * @throws UnsupportedFileTypeException If the given file is not a zip or oat file + * @throws UnsupportedFileTypeException If the given file is not a valid dex/zip/odex/oat file */ - public static Iterable loadAllDexFiles( + public static MultiDexContainer loadDexContainer( @Nonnull File file, @Nonnull final Opcodes opcodes) throws IOException { if (!file.exists()) { throw new DexFileNotFoundException("%s does not exist", file.getName()); @@ -269,45 +268,21 @@ public final class DexFileFactory { } if (zipFile != null) { - final Pattern dexPattern = Pattern.compile("classes[0-9]*.dex"); - final ZipFile finalZipFile = zipFile; - - return new Iterable() { - @Override public Iterator iterator() { - final Enumeration entries = finalZipFile.entries(); - return new AbstractIterator() { - @Override protected DexBackedDexFile computeNext() { - while (entries.hasMoreElements()) { - ZipEntry zipEntry = entries.nextElement(); - if (dexPattern.matcher(zipEntry.getName()).matches()) { - try { - return loadDexFromZip(finalZipFile, zipEntry, opcodes); - } catch (IOException ex) { - throw new ExceptionWithContext(ex, "Error while reading %s from %s", - zipEntry.getName(), finalZipFile.getName()); - } catch (NotADexFile ex) { - // ignore and continue - } - } - } - endOfData(); - return null; - } - }; - } - }; + return new ZipDexContainer(zipFile, opcodes); } InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); try { try { - return Lists.newArrayList(DexBackedDexFile.fromInputStream(opcodes, inputStream)); + DexBackedDexFile dexFile = DexBackedDexFile.fromInputStream(opcodes, inputStream); + return new SingletonMultiDexContainer(file.getPath(), dexFile); } catch (DexBackedDexFile.NotADexFile ex) { // just eat it } try { - return Lists.newArrayList(DexBackedOdexFile.fromInputStream(opcodes, inputStream)); + DexBackedOdexFile odexFile = DexBackedOdexFile.fromInputStream(opcodes, inputStream); + return new SingletonMultiDexContainer(file.getPath(), odexFile); } catch (DexBackedOdexFile.NotAnOdexFile ex) { // just eat it } @@ -323,11 +298,11 @@ public final class DexFileFactory { } 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.getDexFiles(); + return oatFile; } } finally { inputStream.close(); @@ -336,86 +311,11 @@ public final class DexFileFactory { throw new UnsupportedFileTypeException("%s is not an apk, dex, odex or oat file.", file.getPath()); } - /** - * Gets all dex entries from an oat/zip file. - * - * For zip files, only entries that match classes[0-9]*.dex will be returned. - * - * @param file The file to get dex entries from - - * @return A list of strings contains the dex entry names - * @throws IOException - * @throws DexFileNotFoundException If the given file does not exist - * @throws UnsupportedFileTypeException If the given file is not a zip or oat file - */ - public static List getAllDexEntries(@Nonnull File file) throws IOException { - if (!file.exists()) { - throw new DexFileNotFoundException("%s does not exist", file.getName()); - } - - List entries = Lists.newArrayList(); - Opcodes opcodes = Opcodes.forApi(15); - - ZipFile zipFile = null; - try { - zipFile = new ZipFile(file); - } catch (IOException ex) { - // ignore and continue - } - - if (zipFile != null) { - Pattern dexPattern = Pattern.compile("classes[0-9]*.dex"); - Enumeration zipEntries = zipFile.entries(); - - while (zipEntries.hasMoreElements()) { - ZipEntry zipEntry = zipEntries.nextElement(); - if (dexPattern.matcher(zipEntry.getName()).matches()) { - try { - loadDexFromZip(zipFile, zipEntry, opcodes); - entries.add(zipEntry.getName()); - } catch (IOException ex) { - throw new IOException(String.format("Error while reading %s from %s", - zipEntry.getName(), zipFile.getName()), ex); - }catch (NotADexFile ex) { - // ignore and continue - } - } - } - return entries; - } - - 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); - } - - for (OatDexFile oatDexFile: oatFile.getDexFiles()) { - entries.add(oatDexFile.filename); - } - return entries; - } - } finally { - inputStream.close(); - } - - throw new UnsupportedFileTypeException("%s is not an apk, 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 + * @param dexFile a DexFile to write */ public static void writeDexFile(@Nonnull String path, @Nonnull DexFile dexFile) throws IOException { DexPool.writeTo(path, dexFile); @@ -496,29 +396,28 @@ public final class DexFileFactory { return firstTargetChar == ':' || firstTargetChar == '/' || precedingChar == ':' || precedingChar == '/'; } - protected abstract static class DexEntryFinder { - @Nullable - protected abstract DexBackedDexFile getEntry(@Nonnull String entry) throws IOException; + protected static class DexEntryFinder { + private final String filename; + private final MultiDexContainer dexContainer; - @Nonnull - protected abstract List getEntryNames(); - - @Nonnull - protected abstract String getFilename(); + public DexEntryFinder(@Nonnull String filename, + @Nonnull MultiDexContainer dexContainer) { + this.filename = filename; + this.dexContainer = dexContainer; + } @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()); + 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); } - return dexFile; } // find all full and partial matches @@ -526,130 +425,67 @@ public final class DexFileFactory { List fullEntries = Lists.newArrayList(); List partialMatches = Lists.newArrayList(); List partialEntries = Lists.newArrayList(); - for (String entry: getEntryNames()) { + 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(getEntry(entry)); + fullEntries.add(dexContainer.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); - } + partialMatches.add(entry); + partialEntries.add(dexContainer.getEntry(entry)); } } // full matches always take priority if (fullEntries.size() == 1) { - DexBackedDexFile dexFile = fullEntries.get(0); - if (dexFile == null) { + 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), getFilename()); + fullMatches.get(0), filename); } - 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, + "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", - getFilename(), targetEntry); + filename, targetEntry); } if (partialEntries.size() > 1) { throw new MultipleMatchingDexEntriesException(String.format( - "Multiple dex entries in %s match %s: %s", getFilename(), targetEntry, + "Multiple dex entries in %s match %s: %s", filename, targetEntry, Joiner.on(", ").join(partialMatches))); } return partialEntries.get(0); } } - @Nonnull - private static DexBackedDexFile loadDexFromZip(@Nonnull ZipFile zipFile, @Nonnull ZipEntry zipEntry, - @Nonnull Opcodes opcodes) throws IOException { - InputStream stream; - stream = zipFile.getInputStream(zipEntry); - try { - return DexBackedDexFile.fromInputStream(opcodes, new BufferedInputStream(stream)); - } finally { - if (stream != null) { - stream.close(); - } - } - } + private static class SingletonMultiDexContainer implements MultiDexContainer { + private final String entryName; + private final DexBackedDexFile dexFile; - 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; + public SingletonMultiDexContainer(@Nonnull String entryName, @Nonnull DexBackedDexFile dexFile) { + this.entryName = entryName; + this.dexFile = dexFile; } - @Nullable @Override protected DexBackedDexFile getEntry(@Nonnull String entry) throws IOException { - ZipEntry zipEntry = zipFile.getEntry(entry); - if (zipEntry == null) { - return null; - } - - return loadDexFromZip(zipFile, zipEntry, opcodes); + @Nonnull @Override public List getDexEntryNames() throws IOException { + return ImmutableList.of(entryName); } - @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; - } + @Nullable @Override public DexBackedDexFile getEntry(@Nonnull String entryName) throws IOException { + if (entryName.equals(this.entryName)) { + 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 5073ad76..b534c187 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPath.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPath.java @@ -36,29 +36,18 @@ import com.google.common.base.Suppliers; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; -import com.google.common.collect.*; -import com.google.common.io.Files; -import com.google.common.primitives.Ints; -import org.jf.dexlib2.DexFileFactory; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; import org.jf.dexlib2.Opcodes; import org.jf.dexlib2.analysis.reflection.ReflectionClassDef; -import org.jf.dexlib2.dexbacked.DexBackedDexFile; -import org.jf.dexlib2.dexbacked.DexBackedOdexFile; -import org.jf.dexlib2.dexbacked.OatFile; -import org.jf.dexlib2.dexbacked.OatFile.OatDexFile; import org.jf.dexlib2.iface.ClassDef; -import org.jf.dexlib2.iface.DexFile; import org.jf.dexlib2.immutable.ImmutableDexFile; -import org.jf.util.ExceptionWithContext; -import org.jf.util.PathUtil; import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.Serializable; -import java.util.*; +import java.util.Arrays; +import java.util.List; public class ClassPath { @Nonnull private final TypeProto unknownClass; @@ -165,173 +154,6 @@ public class ClassPath { return checkPackagePrivateAccess; } - - /** - * Creates a ClassPath given a set of user inputs - * - * This performs all the magic in finding the right defaults based on the values provided and what type of dex - * file we have. E.g. choosing the right default bootclasspath if needed, actually locating the files on the - * filesystem, etc. - * - * This is meant to be as forgiving as possible and to generally "do the right thing" based on the given inputs. - * - * @param classPathDirs A list of directories to search for class path entries in. Be sure to include "." to search - * the current working directory, if appropriate. - * @param bootClassPathEntries A list of boot class path entries to load. This can be just the bare filenames, - * relative paths, absolute paths based on the local directory structure, absolute paths - * based on the device directory structure, etc. It can contain paths to - * jar/dex/oat/odex files, or just bare filenames with no extension, etc. - * If non-null and blank, then no entries will be loaded other than dexFile - * If null, it will attempt to use the correct defaults based on the inputs. - * @param extraClassPathEntries Additional class path entries. The same sorts of naming mechanisms as for - * bootClassPathEntries are allowed - * @param dexFile The dex file that will be analyzed. It can be a dex, odex or oat file. - * @param api The api level of the device that these dex files come from. - * @param checkPackagePrivateAccess Whether checkPackagePrivateAccess is needed, enabled for ONLY early API 17 by - * default - * - * @return A ClassPath object - */ - @Nonnull - public static ClassPath loadClassPath(@Nonnull Iterable classPathDirs, - @Nullable Iterable bootClassPathEntries, - @Nonnull Iterable extraClassPathEntries, @Nonnull DexFile dexFile, - int api, boolean checkPackagePrivateAccess) - throws IOException { - List classProviders = Lists.newArrayList(); - if (bootClassPathEntries == null) { - bootClassPathEntries = getDefaultDeviceBootClassPath(dexFile, api); - } - for (String entry: Iterables.concat(bootClassPathEntries, extraClassPathEntries)) { - List files = Lists.newArrayList(); - - for (String extension: new String[] { null, ".apk", ".jar", ".odex", ".oat", ".dex" }) { - String searchEntry = entry; - if (Files.getFileExtension(entry).equals(extension)) { - continue; - } - if (extension != null) { - searchEntry = Files.getNameWithoutExtension(entry) + extension; - } - - for (String dir: classPathDirs) { - files.addAll(findFiles(new File(dir), new File(searchEntry).getName(), 100)); - } - if (files.size() > 0) { - break; - } - } - - if (files.size() == 0) { - throw new FileNotFoundException(String.format("Classpath entry %s could not be found", entry)); - } - - File bestMatch = Collections.max(files, new ClassPathEntryComparator(entry)); - Iterable dexFiles = - DexFileFactory.loadAllDexFiles(bestMatch, Opcodes.forApi(api)); - for (DexFile loadedDexFile: dexFiles) { - classProviders.add(new DexClassProvider(loadedDexFile)); - } - } - - int oatVersion = -1; - if (dexFile instanceof OatDexFile) { - oatVersion = ((OatDexFile)dexFile).getOatVersion(); - } - classProviders.add(new DexClassProvider(dexFile)); - - return new ClassPath(classProviders, checkPackagePrivateAccess, oatVersion); - } - - private static class ClassPathEntryComparator implements Comparator { - @Nonnull private List reversePathComponents; - - public ClassPathEntryComparator(@Nonnull String entry) { - // TODO: will PathUtil.getPathComponents work for unix-style paths while on windows? - this.reversePathComponents = Lists.reverse(PathUtil.getPathComponents(new File(entry))); - } - - @Override public int compare(File file1, File file2) { - int comparison = Ints.compare(countMatchingComponents(file1), countMatchingComponents(file2)); - if (comparison != 0) { - // the path that matches the entry being searched for wins - return comparison; - } - - comparison = Ints.compare(PathUtil.getPathComponents(file1).size(), - PathUtil.getPathComponents(file2).size()); - if (comparison != 0) { - // the path "higher up" (with fewer directories) wins - return comparison * -1; - } - - // otherwise.. just return the first one alphabetically. - return file1.compareTo(file2); - } - - private int countMatchingComponents(File file) { - for (int i=0; i findFiles(@Nonnull File dir, @Nonnull String name, int maxDepth) throws IOException { - List files = Lists.newArrayList(); - Set visitedPaths = Sets.newHashSet(); - - if (!dir.exists()) { - throw new IllegalArgumentException(String.format("Directory %s does not exist", dir.getPath())); - } - if (!dir.isDirectory()) { - throw new IllegalArgumentException(String.format("%s is not a directory", dir.getPath())); - } - - findFiles(files, visitedPaths, dir, name, maxDepth); - return files; - } - - private static void findFiles(@Nonnull List result, @Nonnull Set visitedPaths, @Nonnull File dir, - @Nonnull String name, int maxDepth) throws IOException { - if (maxDepth < 0 || !visitedPaths.add(dir.getCanonicalPath())) { - return; - } - - File[] children = dir.listFiles(); - if (children == null) { - return; - } - for (File child: children) { - if (child.isDirectory()) { - findFiles(result, visitedPaths, child, name, maxDepth-1); - } else { - if (name.equals(child.getName())) { - try { - DexFileFactory.loadDexFile(child); - } catch (ExceptionWithContext ex) { - continue; - } - result.add(child); - } - } - } - } - private final Supplier fieldInstructionMapperSupplier = Suppliers.memoize( new Supplier() { @Override public OdexedFieldInstructionMapper get() { @@ -343,133 +165,4 @@ public class ClassPath { public OdexedFieldInstructionMapper getFieldInstructionMapper() { return fieldInstructionMapperSupplier.get(); } - - /** - * Returns the default boot class path for the given api. This is boot class path that is used for "stock" - * (i.e nexus) images for the given api level, but may not be correct for devices with heavily modified firmware. - */ - @Nonnull - private static List getDefaultDeviceBootClassPath(DexFile dexFile, int apiLevel) { - if (dexFile instanceof OatFile.OatDexFile) { - if (((OatFile.OatDexFile) dexFile).getOatVersion() >= 74) { - return ((OatFile.OatDexFile) dexFile).getOatFile().getBootClassPath(); - } else { - return Lists.newArrayList("boot.oat"); - } - } - - if (dexFile instanceof DexBackedOdexFile) { - return ((DexBackedOdexFile)dexFile).getDependencies(); - } - - if (apiLevel <= 8) { - return Lists.newArrayList( - "/system/framework/core.jar", - "/system/framework/ext.jar", - "/system/framework/framework.jar", - "/system/framework/android.policy.jar", - "/system/framework/services.jar"); - } else if (apiLevel <= 11) { - return Lists.newArrayList( - "/system/framework/core.jar", - "/system/framework/bouncycastle.jar", - "/system/framework/ext.jar", - "/system/framework/framework.jar", - "/system/framework/android.policy.jar", - "/system/framework/services.jar", - "/system/framework/core-junit.jar"); - } else if (apiLevel <= 13) { - return Lists.newArrayList( - "/system/framework/core.jar", - "/system/framework/apache-xml.jar", - "/system/framework/bouncycastle.jar", - "/system/framework/ext.jar", - "/system/framework/framework.jar", - "/system/framework/android.policy.jar", - "/system/framework/services.jar", - "/system/framework/core-junit.jar"); - } else if (apiLevel <= 15) { - return Lists.newArrayList( - "/system/framework/core.jar", - "/system/framework/core-junit.jar", - "/system/framework/bouncycastle.jar", - "/system/framework/ext.jar", - "/system/framework/framework.jar", - "/system/framework/android.policy.jar", - "/system/framework/services.jar", - "/system/framework/apache-xml.jar", - "/system/framework/filterfw.jar"); - } else if (apiLevel <= 17) { - // this is correct as of api 17/4.2.2 - return Lists.newArrayList( - "/system/framework/core.jar", - "/system/framework/core-junit.jar", - "/system/framework/bouncycastle.jar", - "/system/framework/ext.jar", - "/system/framework/framework.jar", - "/system/framework/telephony-common.jar", - "/system/framework/mms-common.jar", - "/system/framework/android.policy.jar", - "/system/framework/services.jar", - "/system/framework/apache-xml.jar"); - } else if (apiLevel <= 18) { - return Lists.newArrayList( - "/system/framework/core.jar", - "/system/framework/core-junit.jar", - "/system/framework/bouncycastle.jar", - "/system/framework/ext.jar", - "/system/framework/framework.jar", - "/system/framework/telephony-common.jar", - "/system/framework/voip-common.jar", - "/system/framework/mms-common.jar", - "/system/framework/android.policy.jar", - "/system/framework/services.jar", - "/system/framework/apache-xml.jar"); - } else if (apiLevel <= 19) { - return Lists.newArrayList( - "/system/framework/core.jar", - "/system/framework/conscrypt.jar", - "/system/framework/core-junit.jar", - "/system/framework/bouncycastle.jar", - "/system/framework/ext.jar", - "/system/framework/framework.jar", - "/system/framework/framework2.jar", - "/system/framework/telephony-common.jar", - "/system/framework/voip-common.jar", - "/system/framework/mms-common.jar", - "/system/framework/android.policy.jar", - "/system/framework/services.jar", - "/system/framework/apache-xml.jar", - "/system/framework/webviewchromium.jar"); - } else if (apiLevel <= 22) { - return Lists.newArrayList( - "/system/framework/core-libart.jar", - "/system/framework/conscrypt.jar", - "/system/framework/okhttp.jar", - "/system/framework/core-junit.jar", - "/system/framework/bouncycastle.jar", - "/system/framework/ext.jar", - "/system/framework/framework.jar", - "/system/framework/telephony-common.jar", - "/system/framework/voip-common.jar", - "/system/framework/ims-common.jar", - "/system/framework/mms-common.jar", - "/system/framework/android.policy.jar", - "/system/framework/apache-xml.jar"); - } else /*if (apiLevel <= 23)*/ { - return Lists.newArrayList( - "/system/framework/core-libart.jar", - "/system/framework/conscrypt.jar", - "/system/framework/okhttp.jar", - "/system/framework/core-junit.jar", - "/system/framework/bouncycastle.jar", - "/system/framework/ext.jar", - "/system/framework/framework.jar", - "/system/framework/telephony-common.jar", - "/system/framework/voip-common.jar", - "/system/framework/ims-common.jar", - "/system/framework/apache-xml.jar", - "/system/framework/org.apache.http.legacy.boot.jar"); - } - } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPathResolver.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPathResolver.java new file mode 100644 index 00000000..f363bc1b --- /dev/null +++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPathResolver.java @@ -0,0 +1,432 @@ +/* + * 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.analysis; + +import com.beust.jcommander.internal.Sets; +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; +import com.google.common.collect.Lists; +import org.jf.dexlib2.DexFileFactory; +import org.jf.dexlib2.DexFileFactory.UnsupportedFileTypeException; +import org.jf.dexlib2.Opcodes; +import org.jf.dexlib2.dexbacked.DexBackedDexFile; +import org.jf.dexlib2.dexbacked.DexBackedOdexFile; +import org.jf.dexlib2.dexbacked.OatFile; +import org.jf.dexlib2.iface.DexFile; +import org.jf.dexlib2.iface.MultiDexContainer; +import org.jf.dexlib2.iface.MultiDexContainer.MultiDexFile; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Set; + +public class ClassPathResolver { + private final Iterable classPathDirs; + private final Opcodes opcodes; + + private final Set loadedFiles = Sets.newHashSet(); + private final List classProviders = Lists.newArrayList(); + + /** + * Constructs a new ClassPathResolver using a specified list of bootclasspath entries + * + * @param bootClassPathDirs A list of directories to search for boot classpath entries. Can be empty if all boot + * classpath entries are specified as local paths + * @param bootClassPathEntries A list of boot classpath entries to load. These can either be local paths, or + * device paths (e.g. "/system/framework/framework.jar"). The entry will be interpreted + * first as a local path. If not found as a local path, it will be interpreted as a + * partial or absolute device path, and will be searched for in bootClassPathDirs + * @param extraClassPathEntries A list of additional classpath entries to load. Can be empty. All entries must be + * local paths. Device paths are not supported. + * @param dexFile The dex file that the classpath will be used to analyze + * @throws IOException If any IOException occurs + * @throws ResolveException If any classpath entries cannot be loaded for some reason + * + * If null, a default bootclasspath is used, + * depending on the the file type of dexFile and the api level. If empty, no boot + * classpath entries will be loaded + */ + public ClassPathResolver(@Nonnull List bootClassPathDirs, @Nonnull List bootClassPathEntries, + @Nonnull List extraClassPathEntries, @Nonnull DexFile dexFile) + throws IOException { + this(bootClassPathDirs, bootClassPathEntries, extraClassPathEntries, dexFile, dexFile.getOpcodes().api); + } + + /** + * Constructs a new ClassPathResolver using a default list of bootclasspath entries + * + * @param bootClassPathDirs A list of directories to search for boot classpath entries + * @param extraClassPathEntries A list of additional classpath entries to load. Can be empty. All entries must be + * local paths. Device paths are not supported. + * @param dexFile The dex file that the classpath will be used to analyze + * @param apiLevel The api level of the device. This is used to select an appropriate set of boot classpath entries. + * @throws IOException If any IOException occurs + * @throws ResolveException If any classpath entries cannot be loaded for some reason + * + * If null, a default bootclasspath is used, + * depending on the the file type of dexFile and the api level. If empty, no boot + * classpath entries will be loaded + */ + public ClassPathResolver(@Nonnull List bootClassPathDirs, @Nonnull List extraClassPathEntries, + @Nonnull DexFile dexFile, int apiLevel) + throws IOException { + this(bootClassPathDirs, null, extraClassPathEntries, dexFile, apiLevel); + } + + private ClassPathResolver(@Nonnull List bootClassPathDirs, @Nullable List bootClassPathEntries, + @Nonnull List extraClassPathEntries, + @Nonnull DexFile dexFile, int apiLevel) + throws IOException { + this.classPathDirs = bootClassPathDirs; + opcodes = dexFile.getOpcodes(); + + if (bootClassPathEntries == null) { + bootClassPathEntries = getDefaultBootClassPath(dexFile, apiLevel); + } + + for (String entry: bootClassPathEntries) { + try { + loadLocalOrDeviceBootClassPathEntry(entry); + } catch (NoDexException ex) { + if (entry.endsWith(".jar")) { + String odexEntry = entry.substring(0, entry.length() - 4) + ".odex"; + try { + loadLocalOrDeviceBootClassPathEntry(odexEntry); + } catch (NoDexException ex2) { + throw new ResolveException("Neither %s nor %s contain a dex file", entry, odexEntry); + } catch (NotFoundException ex2) { + throw new ResolveException(ex); + } + } else { + throw new ResolveException(ex); + } + } catch (NotFoundException ex) { + throw new ResolveException(ex); + } + } + + for (String entry: extraClassPathEntries) { + // extra classpath entries must be specified using a local path, so we don't need to do the search through + // bootClassPathDirs + try { + loadLocalClassPathEntry(entry); + } catch (NoDexException ex) { + throw new ResolveException(ex); + } + } + + if (dexFile instanceof MultiDexContainer.MultiDexFile) { + MultiDexContainer container = ((MultiDexFile)dexFile).getContainer(); + for (String entry: container.getDexEntryNames()) { + classProviders.add(new DexClassProvider(container.getEntry(entry))); + } + } else { + classProviders.add(new DexClassProvider(dexFile)); + } + } + + @Nonnull + public List getResolvedClassProviders() { + return classProviders; + } + + private boolean loadLocalClassPathEntry(@Nonnull String entry) throws NoDexException, IOException { + File entryFile = new File(entry); + if (entryFile.exists() && entryFile.isFile()) { + try { + loadEntry(entryFile, true); + return true; + } catch (UnsupportedFileTypeException ex) { + throw new ResolveException(ex, "Couldn't load classpath entry %s", entry); + } + } + return false; + } + + private void loadLocalOrDeviceBootClassPathEntry(@Nonnull String entry) + throws IOException, NoDexException, NotFoundException { + // first, see if the entry is a valid local path + if (loadLocalClassPathEntry(entry)) { + return; + } + + // It's not a local path, so let's try to resolve it as a device path, relative to one of the provided + // directories + List pathComponents = splitDevicePath(entry); + Joiner pathJoiner = Joiner.on(File.pathSeparatorChar); + + for (String directory: classPathDirs) { + File directoryFile = new File(directory); + if (!directoryFile.exists()) { + // TODO: print a warning in the baksmali frontend before we get here + continue; + } + + for (int i=0; i container; + try { + container = DexFileFactory.loadDexContainer(entryFile, opcodes); + } catch (UnsupportedFileTypeException ex) { + throw new ResolveException(ex); + } + + List entryNames = container.getDexEntryNames(); + + if (entryNames.size() == 0) { + throw new NoDexException("%s contains no dex file"); + } + + loadedFiles.add(entryFile); + + for (String entryName: entryNames) { + classProviders.add(new DexClassProvider(container.getEntry(entryName))); + } + + if (loadOatDependencies && container instanceof OatFile) { + List oatDependencies = ((OatFile)container).getBootClassPath(); + if (!oatDependencies.isEmpty()) { + try { + loadOatDependencies(entryFile.getParentFile(), oatDependencies); + } catch (NotFoundException ex) { + throw new ResolveException(ex, "Error while loading oat file %s", entryFile); + } catch (NoDexException ex) { + throw new ResolveException(ex, "Error while loading dependencies for oat file %s", entryFile); + } + } + } + } + + @Nonnull + private static List splitDevicePath(@Nonnull String path) { + return Lists.newArrayList(Splitter.on('/').split(path)); + } + + private void loadOatDependencies(@Nonnull File directory, @Nonnull List oatDependencies) + throws IOException, NoDexException, NotFoundException { + // We assume that all oat dependencies are located in the same directory as the oat file + for (String oatDependency: oatDependencies) { + String oatDependencyName = getFilenameForOatDependency(oatDependency); + File file = new File(directory, oatDependencyName); + if (!file.exists()) { + throw new NotFoundException("Cannot find dependency %s in %s", oatDependencyName, directory); + } + + loadEntry(file, false); + } + } + + @Nonnull + private String getFilenameForOatDependency(String oatDependency) { + int index = oatDependency.lastIndexOf('/'); + + String dependencyLeaf = oatDependency.substring(index+1); + if (dependencyLeaf.endsWith(".art")) { + return dependencyLeaf.substring(0, dependencyLeaf.length() - 4) + ".oat"; + } + return dependencyLeaf; + } + + private static class NotFoundException extends Exception { + public NotFoundException(String message, Object... formatArgs) { + super(String.format(message, formatArgs)); + } + } + + private static class NoDexException extends Exception { + public NoDexException(String message, Object... formatArgs) { + super(String.format(message, formatArgs)); + } + } + + /** + * An error that occurred while resolving the classpath + */ + public static class ResolveException extends RuntimeException { + public ResolveException (String message, Object... formatArgs) { + super(String.format(message, formatArgs)); + } + + public ResolveException (Throwable cause) { + super(cause); + } + + public ResolveException (Throwable cause, String message, Object... formatArgs) { + super(String.format(message, formatArgs), cause); + } + } + + /** + * Returns the default boot class path for the given dex file and api level. + */ + @Nonnull + private static List getDefaultBootClassPath(@Nonnull DexFile dexFile, int apiLevel) { + if (dexFile instanceof OatFile.OatDexFile) { + return Lists.newArrayList("boot.oat"); + } + + if (dexFile instanceof DexBackedOdexFile) { + return ((DexBackedOdexFile)dexFile).getDependencies(); + } + + if (apiLevel <= 8) { + return Lists.newArrayList( + "/system/framework/core.jar", + "/system/framework/ext.jar", + "/system/framework/framework.jar", + "/system/framework/android.policy.jar", + "/system/framework/services.jar"); + } else if (apiLevel <= 11) { + return Lists.newArrayList( + "/system/framework/core.jar", + "/system/framework/bouncycastle.jar", + "/system/framework/ext.jar", + "/system/framework/framework.jar", + "/system/framework/android.policy.jar", + "/system/framework/services.jar", + "/system/framework/core-junit.jar"); + } else if (apiLevel <= 13) { + return Lists.newArrayList( + "/system/framework/core.jar", + "/system/framework/apache-xml.jar", + "/system/framework/bouncycastle.jar", + "/system/framework/ext.jar", + "/system/framework/framework.jar", + "/system/framework/android.policy.jar", + "/system/framework/services.jar", + "/system/framework/core-junit.jar"); + } else if (apiLevel <= 15) { + return Lists.newArrayList( + "/system/framework/core.jar", + "/system/framework/core-junit.jar", + "/system/framework/bouncycastle.jar", + "/system/framework/ext.jar", + "/system/framework/framework.jar", + "/system/framework/android.policy.jar", + "/system/framework/services.jar", + "/system/framework/apache-xml.jar", + "/system/framework/filterfw.jar"); + } else if (apiLevel <= 17) { + // this is correct as of api 17/4.2.2 + return Lists.newArrayList( + "/system/framework/core.jar", + "/system/framework/core-junit.jar", + "/system/framework/bouncycastle.jar", + "/system/framework/ext.jar", + "/system/framework/framework.jar", + "/system/framework/telephony-common.jar", + "/system/framework/mms-common.jar", + "/system/framework/android.policy.jar", + "/system/framework/services.jar", + "/system/framework/apache-xml.jar"); + } else if (apiLevel <= 18) { + return Lists.newArrayList( + "/system/framework/core.jar", + "/system/framework/core-junit.jar", + "/system/framework/bouncycastle.jar", + "/system/framework/ext.jar", + "/system/framework/framework.jar", + "/system/framework/telephony-common.jar", + "/system/framework/voip-common.jar", + "/system/framework/mms-common.jar", + "/system/framework/android.policy.jar", + "/system/framework/services.jar", + "/system/framework/apache-xml.jar"); + } else if (apiLevel <= 19) { + return Lists.newArrayList( + "/system/framework/core.jar", + "/system/framework/conscrypt.jar", + "/system/framework/core-junit.jar", + "/system/framework/bouncycastle.jar", + "/system/framework/ext.jar", + "/system/framework/framework.jar", + "/system/framework/framework2.jar", + "/system/framework/telephony-common.jar", + "/system/framework/voip-common.jar", + "/system/framework/mms-common.jar", + "/system/framework/android.policy.jar", + "/system/framework/services.jar", + "/system/framework/apache-xml.jar", + "/system/framework/webviewchromium.jar"); + } else if (apiLevel <= 22) { + return Lists.newArrayList( + "/system/framework/core-libart.jar", + "/system/framework/conscrypt.jar", + "/system/framework/okhttp.jar", + "/system/framework/core-junit.jar", + "/system/framework/bouncycastle.jar", + "/system/framework/ext.jar", + "/system/framework/framework.jar", + "/system/framework/telephony-common.jar", + "/system/framework/voip-common.jar", + "/system/framework/ims-common.jar", + "/system/framework/mms-common.jar", + "/system/framework/android.policy.jar", + "/system/framework/apache-xml.jar"); + } else /*if (apiLevel <= 23)*/ { + return Lists.newArrayList( + "/system/framework/core-libart.jar", + "/system/framework/conscrypt.jar", + "/system/framework/okhttp.jar", + "/system/framework/core-junit.jar", + "/system/framework/bouncycastle.jar", + "/system/framework/ext.jar", + "/system/framework/framework.jar", + "/system/framework/telephony-common.jar", + "/system/framework/voip-common.jar", + "/system/framework/ims-common.jar", + "/system/framework/apache-xml.jar", + "/system/framework/org.apache.http.legacy.boot.jar"); + } + // TODO: update for N + } +} diff --git a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedDexFile.java b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedDexFile.java index 5af1e653..8e7127a8 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedDexFile.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedDexFile.java @@ -69,7 +69,7 @@ public class DexBackedDexFile extends BaseDexBuffer implements DexFile { private final int classCount; private final int classStartOffset; - private DexBackedDexFile(@Nonnull Opcodes opcodes, @Nonnull byte[] buf, int offset, boolean verifyMagic) { + protected DexBackedDexFile(@Nonnull Opcodes opcodes, @Nonnull byte[] buf, int offset, boolean verifyMagic) { super(buf, offset); this.opcodes = opcodes; @@ -157,7 +157,7 @@ public class DexBackedDexFile extends BaseDexBuffer implements DexFile { }; } - private static void verifyMagicAndByteOrder(@Nonnull byte[] buf, int offset) { + protected static void verifyMagicAndByteOrder(@Nonnull byte[] buf, int offset) { if (!HeaderItem.verifyMagic(buf, offset)) { StringBuilder sb = new StringBuilder("Invalid magic value:"); for (int i=0; i<8; i++) { diff --git a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/OatFile.java b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/OatFile.java index 25127597..16ee7310 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/OatFile.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/OatFile.java @@ -31,11 +31,15 @@ package org.jf.dexlib2.dexbacked; +import com.google.common.base.Function; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterators; import com.google.common.io.ByteStreams; import org.jf.dexlib2.Opcodes; +import org.jf.dexlib2.dexbacked.OatFile.OatDexFile; import org.jf.dexlib2.dexbacked.OatFile.SymbolTable.Symbol; import org.jf.dexlib2.dexbacked.raw.HeaderItem; +import org.jf.dexlib2.iface.MultiDexContainer; import org.jf.util.AbstractForwardSequentialList; import javax.annotation.Nonnull; @@ -49,7 +53,7 @@ import java.util.Arrays; import java.util.Iterator; import java.util.List; -public class OatFile extends BaseDexBuffer { +public class OatFile extends BaseDexBuffer implements MultiDexContainer { private static final byte[] ELF_MAGIC = new byte[] { 0x7f, 'E', 'L', 'F' }; private static final byte[] OAT_MAGIC = new byte[] { 'o', 'a', 't', '\n' }; private static final int MIN_ELF_HEADER_SIZE = 52; @@ -171,54 +175,44 @@ public class OatFile extends BaseDexBuffer { } @Nonnull @Override public Iterator iterator() { - return new Iterator() { - int index = 0; - int offset = oatHeader.getDexListStart(); - - @Override public boolean hasNext() { - return index < size(); + return Iterators.transform(new DexEntryIterator(), new Function() { + @Nullable @Override public OatDexFile apply(DexEntry dexEntry) { + return dexEntry.getDexFile(); } - - @Override public OatDexFile next() { - int filenameLength = readSmallUint(offset); - offset += 4; - - // TODO: what is the correct character encoding? - String filename = new String(buf, offset, filenameLength, Charset.forName("US-ASCII")); - offset += filenameLength; - - offset += 4; // checksum - - int dexOffset = readSmallUint(offset) + oatHeader.headerOffset; - offset += 4; - - - if (getOatVersion() >= 75) { - offset += 4; // offset to class offsets table - } - if (getOatVersion() >= 73) { - offset += 4; // lookup table offset - } - if (getOatVersion() < 75) { - // prior to 75, the class offsets are included here directly - int classCount = readSmallUint(dexOffset + HeaderItem.CLASS_COUNT_OFFSET); - offset += 4 * classCount; - } - - index++; - - return new OatDexFile(dexOffset, filename); - } - - @Override public void remove() { - throw new UnsupportedOperationException(); - } - }; + }); } }; } - public class OatDexFile extends DexBackedDexFile { + @Nonnull @Override public List getDexEntryNames() throws IOException { + return new AbstractForwardSequentialList() { + @Override public int size() { + return oatHeader.getDexFileCount(); + } + + @Nonnull @Override public Iterator iterator() { + return Iterators.transform(new DexEntryIterator(), new Function() { + @Nullable @Override public String apply(DexEntry dexEntry) { + return dexEntry.entryName; + } + }); + } + }; + } + + @Nullable @Override public OatDexFile getEntry(@Nonnull String entryName) throws IOException { + DexEntryIterator iterator = new DexEntryIterator(); + while (iterator.hasNext()) { + DexEntry entry = iterator.next(); + + if (entry.entryName.equals(entryName)) { + return entry.getDexFile(); + } + } + return null; + } + + public class OatDexFile extends DexBackedDexFile implements MultiDexContainer.MultiDexFile { @Nonnull public final String filename; public OatDexFile(int offset, @Nonnull String filename) { @@ -226,8 +220,12 @@ public class OatFile extends BaseDexBuffer { this.filename = filename; } - public int getOatVersion() { - return OatFile.this.getOatVersion(); + @Nonnull @Override public String getEntryName() { + return filename; + } + + @Nonnull @Override public MultiDexContainer getContainer() { + return OatFile.this; } public OatFile getOatFile() { @@ -540,7 +538,64 @@ public class OatFile extends BaseDexBuffer { return new String(buf, start, end-start, Charset.forName("US-ASCII")); } + } + private class DexEntry { + public final String entryName; + public final int dexOffset; + + public DexEntry(String entryName, int dexOffset) { + this.entryName = entryName; + this.dexOffset = dexOffset; + } + + public OatDexFile getDexFile() { + return new OatDexFile(dexOffset, entryName); + } + } + + private class DexEntryIterator implements Iterator { + int index = 0; + int offset = oatHeader.getDexListStart(); + + @Override public boolean hasNext() { + return index < oatHeader.getDexFileCount(); + } + + @Override public DexEntry next() { + int filenameLength = readSmallUint(offset); + offset += 4; + + // TODO: what is the correct character encoding? + String filename = new String(buf, offset, filenameLength, Charset.forName("US-ASCII")); + offset += filenameLength; + + offset += 4; // checksum + + int dexOffset = readSmallUint(offset) + oatHeader.headerOffset; + offset += 4; + + + if (getOatVersion() >= 75) { + offset += 4; // offset to class offsets table + } + if (getOatVersion() >= 73) { + offset += 4; // lookup table offset + } + if (getOatVersion() < 75) { + // prior to 75, the class offsets are included here directly + int classCount = readSmallUint(dexOffset + HeaderItem.CLASS_COUNT_OFFSET); + offset += 4 * classCount; + } + + index++; + + return new DexEntry(filename, dexOffset); + } + + @Override public void remove() { + throw new UnsupportedOperationException(); + } } public static class InvalidOatFileException extends RuntimeException { @@ -552,4 +607,5 @@ public class OatFile extends BaseDexBuffer { public static class NotAnOatFileException extends RuntimeException { public NotAnOatFileException() {} } + } \ No newline at end of file diff --git a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/ZipDexContainer.java b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/ZipDexContainer.java new file mode 100644 index 00000000..2bffa90e --- /dev/null +++ b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/ZipDexContainer.java @@ -0,0 +1,166 @@ +/* + * 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.dexbacked; + +import com.google.common.collect.Lists; +import com.google.common.io.ByteStreams; +import org.jf.dexlib2.Opcodes; +import org.jf.dexlib2.dexbacked.DexBackedDexFile.NotADexFile; +import org.jf.dexlib2.dexbacked.ZipDexContainer.ZipDexFile; +import org.jf.dexlib2.iface.MultiDexContainer; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.Closeable; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Enumeration; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import static org.jf.dexlib2.dexbacked.DexBackedDexFile.verifyMagicAndByteOrder; + +/** + * Represents a zip file that contains dex files (i.e. an apk or jar file) + */ +public class ZipDexContainer implements MultiDexContainer, Closeable { + + private final ZipFile zipFile; + private final Opcodes opcodes; + + /** + * Constructs a new ZipDexContainer for the given zip file + * + * @param zipFile A Zip + * @param opcodes The Opcodes instance to use when loading dex files from this container + */ + public ZipDexContainer(@Nonnull ZipFile zipFile, @Nonnull Opcodes opcodes) { + this.zipFile = zipFile; + this.opcodes = opcodes; + } + + /** + * Gets a list of the names of dex files in this zip file. + * + * @return A list of the names of dex files in this zip file + */ + @Nonnull @Override public List getDexEntryNames() throws IOException { + List entryNames = Lists.newArrayList(); + Enumeration entriesEnumeration = zipFile.entries(); + + while (entriesEnumeration.hasMoreElements()) { + ZipEntry entry = entriesEnumeration.nextElement(); + + if (!isDex(entry)) { + continue; + } + + entryNames.add(entry.getName()); + } + + return entryNames; + } + + /** + * Loads a dex file from a specific named entry. + * + * @param entryName The name of the entry + * @return A ZipDexFile, or null if there is no entry with the given name + * @throws NotADexFile If the entry isn't a dex file + */ + @Nullable @Override public ZipDexFile getEntry(@Nonnull String entryName) throws IOException { + ZipEntry entry = zipFile.getEntry(entryName); + if (entry == null) { + return null; + } + + return loadEntry(entry); + } + + @Override public void close() throws IOException { + zipFile.close(); + } + + public class ZipDexFile extends DexBackedDexFile implements MultiDexContainer.MultiDexFile { + + private final String entryName; + + protected ZipDexFile(@Nonnull Opcodes opcodes, @Nonnull byte[] buf, @Nonnull String entryName) { + super(opcodes, buf, 0); + this.entryName = entryName; + } + + @Nonnull @Override public String getEntryName() { + return entryName; + } + + @Nonnull @Override public MultiDexContainer getContainer() { + return ZipDexContainer.this; + } + } + + private boolean isDex(@Nonnull ZipEntry zipEntry) throws IOException { + InputStream inputStream = zipFile.getInputStream(zipEntry); + try { + inputStream.mark(44); + byte[] partialHeader = new byte[44]; + try { + ByteStreams.readFully(inputStream, partialHeader); + } catch (EOFException ex) { + throw new NotADexFile("File is too short"); + } + + try { + verifyMagicAndByteOrder(partialHeader, 0); + } catch (NotADexFile ex) { + return false; + } + return true; + } finally { + inputStream.close(); + } + } + + @Nonnull + private ZipDexFile loadEntry(@Nonnull ZipEntry zipEntry) + throws IOException { + InputStream inputStream = zipFile.getInputStream(zipEntry); + try { + byte[] buf = ByteStreams.toByteArray(inputStream); + return new ZipDexFile(opcodes, buf, zipEntry.getName()); + } finally { + inputStream.close(); + } + } +} diff --git a/dexlib2/src/main/java/org/jf/dexlib2/iface/MultiDexContainer.java b/dexlib2/src/main/java/org/jf/dexlib2/iface/MultiDexContainer.java new file mode 100644 index 00000000..251ecdef --- /dev/null +++ b/dexlib2/src/main/java/org/jf/dexlib2/iface/MultiDexContainer.java @@ -0,0 +1,70 @@ +/* + * 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.iface; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.List; + +/** + * This class represents a dex container that can contain multiple, named dex files + */ +public interface MultiDexContainer { + /** + * @return A list of the names of dex entries in this container + */ + @Nonnull List getDexEntryNames() throws IOException; + + /** + * Gets the dex entry with the given name + * + * @param entryName The name of the entry + * @return A DexFile, or null if no entry with that name is found + */ + @Nullable T getEntry(@Nonnull String entryName) throws IOException; + + /** + * This class represents a dex file that is contained in a MultiDexContainer + */ + interface MultiDexFile extends DexFile { + /** + * @return The name of this entry within its container + */ + @Nonnull String getEntryName(); + + /** + * @return The MultiDexContainer that contains this dex file + */ + @Nonnull MultiDexContainer getContainer(); + } +} diff --git a/dexlib2/src/test/java/org/jf/dexlib2/DexEntryFinderTest.java b/dexlib2/src/test/java/org/jf/dexlib2/DexEntryFinderTest.java index d7a82b34..de5b05fa 100644 --- a/dexlib2/src/test/java/org/jf/dexlib2/DexEntryFinderTest.java +++ b/dexlib2/src/test/java/org/jf/dexlib2/DexEntryFinderTest.java @@ -33,12 +33,13 @@ 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.jf.dexlib2.dexbacked.DexBackedDexFile.NotADexFile; +import org.jf.dexlib2.iface.MultiDexContainer; import org.junit.Assert; import org.junit.Test; @@ -47,12 +48,12 @@ import javax.annotation.Nullable; import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import static org.mockito.Mockito.mock; public class DexEntryFinderTest { - @Test public void testNormalStuff() throws Exception { Map entries = Maps.newHashMap(); @@ -60,7 +61,7 @@ public class DexEntryFinderTest { 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); + DexEntryFinder testFinder = new DexEntryFinder("blah.oat", new TestMultiDexContainer(entries)); Assert.assertEquals(dexFile1, testFinder.findEntry("/system/framework/framework.jar", true)); @@ -109,7 +110,7 @@ public class DexEntryFinderTest { 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); + DexEntryFinder testFinder = new DexEntryFinder("blah.oat", new TestMultiDexContainer(entries)); Assert.assertEquals(dexFile1, testFinder.findEntry("/system/framework/framework.jar", true)); Assert.assertEquals(dexFile2, testFinder.findEntry("system/framework/framework.jar", true)); @@ -130,7 +131,7 @@ public class DexEntryFinderTest { 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); + DexEntryFinder testFinder = new DexEntryFinder("blah.oat", new TestMultiDexContainer(entries)); Assert.assertEquals(dexFile1, testFinder.findEntry("/system/framework/framework.jar", true)); Assert.assertEquals(dexFile2, testFinder.findEntry("/framework/framework.jar", true)); @@ -148,13 +149,13 @@ public class DexEntryFinderTest { DexBackedDexFile dexFile1 = mock(DexBackedDexFile.class); entries.put("classes.dex", dexFile1); entries.put("/blah/classes.dex", null); - TestDexEntryFinder testFinder = new TestDexEntryFinder("blah.oat", entries); + DexEntryFinder testFinder = new DexEntryFinder("blah.oat", new TestMultiDexContainer(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); + assertDexFileNotFound(testFinder, "/blah/classes.dex", false); } private void assertEntryNotFound(DexEntryFinder finder, String entry, boolean exactMatch) throws IOException { @@ -184,27 +185,43 @@ public class DexEntryFinderTest { } } - public static class TestDexFileFactory { - public static class TestDexEntryFinder extends DexEntryFinder { - @Nonnull private final String fileName; - @Nonnull private final Map entries; + private void assertDexFileNotFound(DexEntryFinder finder, String entry, boolean exactMatch) throws IOException { + try { + finder.findEntry(entry, exactMatch); + Assert.fail(); + } catch (DexFileNotFoundException ex) { + // expected exception + } + } - public TestDexEntryFinder(@Nonnull String fileName, @Nonnull Map entries) { - this.fileName = fileName; - this.entries = entries; + public static class TestMultiDexContainer implements MultiDexContainer { + @Nonnull private final Map entries; + + public TestMultiDexContainer(@Nonnull Map entries) { + this.entries = entries; + } + + @Nonnull @Override public List getDexEntryNames() throws IOException { + List entryNames = Lists.newArrayList(); + + for (Entry entry: entries.entrySet()) { + if (entry.getValue() != null) { + entryNames.add(entry.getKey()); + } } - @Nullable @Override protected DexBackedDexFile getEntry(@Nonnull String entry) throws IOException { - return entries.get(entry); - } + return entryNames; + } - @Nonnull @Override protected List getEntryNames() { - return Lists.newArrayList(entries.keySet()); - } - - @Nonnull @Override protected String getFilename() { - return fileName; + @Nullable @Override public DexBackedDexFile getEntry(@Nonnull String entryName) throws IOException { + if (entries.containsKey(entryName)) { + DexBackedDexFile entry = entries.get(entryName); + if (entry == null) { + throw new NotADexFile(); + } + return entry; } + return null; } } } diff --git a/dexlib2/src/test/java/org/jf/dexlib2/analysis/CustomMethodInlineTableTest.java b/dexlib2/src/test/java/org/jf/dexlib2/analysis/CustomMethodInlineTableTest.java index 9b4ce128..c780881a 100644 --- a/dexlib2/src/test/java/org/jf/dexlib2/analysis/CustomMethodInlineTableTest.java +++ b/dexlib2/src/test/java/org/jf/dexlib2/analysis/CustomMethodInlineTableTest.java @@ -70,8 +70,9 @@ public class CustomMethodInlineTableTest { DexFile dexFile = new ImmutableDexFile(Opcodes.forApi(19), ImmutableList.of(classDef)); - ClassPath classPath = ClassPath.loadClassPath(ImmutableList.of(), - ImmutableList.of(), ImmutableList.of(), dexFile, 15, false); + ClassPathResolver resolver = new ClassPathResolver(ImmutableList.of(), + ImmutableList.of(), ImmutableList.of(), dexFile); + ClassPath classPath = new ClassPath(resolver.getResolvedClassProviders(), false, ClassPath.NOT_ART); InlineMethodResolver inlineMethodResolver = new CustomInlineMethodResolver(classPath, "Lblah;->blah()V"); MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, inlineMethodResolver, false); @@ -98,8 +99,10 @@ public class CustomMethodInlineTableTest { DexFile dexFile = new ImmutableDexFile(Opcodes.forApi(19), ImmutableList.of(classDef)); - ClassPath classPath = ClassPath.loadClassPath(ImmutableList.of(), - ImmutableList.of(), ImmutableList.of(), dexFile, 15, false); + ClassPathResolver resolver = new ClassPathResolver(ImmutableList.of(), + ImmutableList.of(), ImmutableList.of(), dexFile); + ClassPath classPath = new ClassPath(resolver.getResolvedClassProviders(), false, ClassPath.NOT_ART); + InlineMethodResolver inlineMethodResolver = new CustomInlineMethodResolver(classPath, "Lblah;->blah()V"); MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, inlineMethodResolver, false); @@ -125,8 +128,10 @@ public class CustomMethodInlineTableTest { DexFile dexFile = new ImmutableDexFile(Opcodes.forApi(19), ImmutableList.of(classDef)); - ClassPath classPath = ClassPath.loadClassPath(ImmutableList.of(), - ImmutableList.of(), ImmutableList.of(), dexFile, 15, false); + ClassPathResolver resolver = new ClassPathResolver(ImmutableList.of(), + ImmutableList.of(), ImmutableList.of(), dexFile); + ClassPath classPath = new ClassPath(resolver.getResolvedClassProviders(), false, ClassPath.NOT_ART); + InlineMethodResolver inlineMethodResolver = new CustomInlineMethodResolver(classPath, "Lblah;->blah()V"); MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, inlineMethodResolver, false); diff --git a/util/src/main/java/org/jf/util/PathUtil.java b/util/src/main/java/org/jf/util/PathUtil.java index 93cd01ba..9ba9f301 100644 --- a/util/src/main/java/org/jf/util/PathUtil.java +++ b/util/src/main/java/org/jf/util/PathUtil.java @@ -47,16 +47,6 @@ public class PathUtil { return new File(getRelativeFileInternal(baseFile.getCanonicalFile(), fileToRelativize.getCanonicalFile())); } - public static String getRelativePath(String basePath, String pathToRelativize) throws IOException { - File baseFile = new File(basePath); - if (baseFile.isFile()) { - baseFile = baseFile.getParentFile(); - } - - return getRelativeFileInternal(baseFile.getCanonicalFile(), - new File(pathToRelativize).getCanonicalFile()); - } - static String getRelativeFileInternal(File canonicalBaseFile, File canonicalFileToRelativize) { List basePath = getPathComponents(canonicalBaseFile); List pathToRelativize = getPathComponents(canonicalFileToRelativize); @@ -108,7 +98,7 @@ public class PathUtil { return sb.toString(); } - public static List getPathComponents(File file) { + private static List getPathComponents(File file) { ArrayList path = new ArrayList(); while (file != null) {