mirror of
https://github.com/revanced/smali.git
synced 2025-04-29 22:24:26 +02:00
Refactor how classpath loading works
This commit is contained in:
parent
4c77ad7617
commit
31ad2bc100
@ -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<String> classPathDirectories = Lists.newArrayList(".");
|
||||
public List<String> 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<String> 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);
|
||||
}
|
||||
}
|
||||
|
@ -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<String> 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<String> lines = StringWrapper.wrapStringOnBreaks(registerInfoHelp,
|
||||
ConsoleUtil.getConsoleWidth());
|
||||
for (String line : lines) {
|
||||
|
@ -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<String> entries;
|
||||
try {
|
||||
entries = DexFileFactory.getAllDexEntries(file);
|
||||
MultiDexContainer<? extends DexBackedDexFile> container =
|
||||
DexFileFactory.loadDexContainer(file, Opcodes.forApi(15));
|
||||
entries = container.getDexEntryNames();
|
||||
} catch (IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
|
@ -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<? extends DexBackedDexFile> loadAllDexFiles(
|
||||
public static MultiDexContainer<? extends DexBackedDexFile> loadDexContainer(
|
||||
@Nonnull File file, @Nonnull final Opcodes opcodes) throws IOException {
|
||||
if (!file.exists()) {
|
||||
throw new DexFileNotFoundException("%s does not exist", file.getName());
|
||||
@ -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<DexBackedDexFile>() {
|
||||
@Override public Iterator<DexBackedDexFile> iterator() {
|
||||
final Enumeration<? extends ZipEntry> entries = finalZipFile.entries();
|
||||
return new AbstractIterator<DexBackedDexFile>() {
|
||||
@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<String> getAllDexEntries(@Nonnull File file) throws IOException {
|
||||
if (!file.exists()) {
|
||||
throw new DexFileNotFoundException("%s does not exist", file.getName());
|
||||
}
|
||||
|
||||
List<String> 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<? extends ZipEntry> 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<? extends DexBackedDexFile> dexContainer;
|
||||
|
||||
@Nonnull
|
||||
protected abstract List<String> getEntryNames();
|
||||
|
||||
@Nonnull
|
||||
protected abstract String getFilename();
|
||||
public DexEntryFinder(@Nonnull String filename,
|
||||
@Nonnull MultiDexContainer<? extends DexBackedDexFile> dexContainer) {
|
||||
this.filename = filename;
|
||||
this.dexContainer = dexContainer;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public DexBackedDexFile findEntry(@Nonnull String targetEntry, boolean exactMatch) throws IOException {
|
||||
if (exactMatch) {
|
||||
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<DexBackedDexFile> fullEntries = Lists.newArrayList();
|
||||
List<String> partialMatches = Lists.newArrayList();
|
||||
List<DexBackedDexFile> 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<DexBackedDexFile> {
|
||||
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<String> getDexEntryNames() throws IOException {
|
||||
return ImmutableList.of(entryName);
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
@Nullable @Override public DexBackedDexFile getEntry(@Nonnull String entryName) throws IOException {
|
||||
if (entryName.equals(this.entryName)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<String> classPathDirs,
|
||||
@Nullable Iterable<String> bootClassPathEntries,
|
||||
@Nonnull Iterable<String> extraClassPathEntries, @Nonnull DexFile dexFile,
|
||||
int api, boolean checkPackagePrivateAccess)
|
||||
throws IOException {
|
||||
List<ClassProvider> classProviders = Lists.newArrayList();
|
||||
if (bootClassPathEntries == null) {
|
||||
bootClassPathEntries = getDefaultDeviceBootClassPath(dexFile, api);
|
||||
}
|
||||
for (String entry: Iterables.concat(bootClassPathEntries, extraClassPathEntries)) {
|
||||
List<File> 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<? extends DexBackedDexFile> 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<File> {
|
||||
@Nonnull private List<String> 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<reversePathComponents.size(); i++) {
|
||||
if (file == null) {
|
||||
return i;
|
||||
}
|
||||
if (!file.getName().equals(reversePathComponents.get(i))) {
|
||||
return i;
|
||||
}
|
||||
file = file.getParentFile();
|
||||
}
|
||||
return reversePathComponents.size();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ye olde recursive file search.
|
||||
*
|
||||
* Searches for all files (not directories!) named "name".
|
||||
*
|
||||
* It attempts to detect filesystem loops via File.getCanonicalPath, and will not recurse path maxDepth directories.
|
||||
*/
|
||||
@Nonnull
|
||||
private static List<File> findFiles(@Nonnull File dir, @Nonnull String name, int maxDepth) throws IOException {
|
||||
List<File> files = Lists.newArrayList();
|
||||
Set<String> 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<File> result, @Nonnull Set<String> 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<OdexedFieldInstructionMapper> fieldInstructionMapperSupplier = Suppliers.memoize(
|
||||
new Supplier<OdexedFieldInstructionMapper>() {
|
||||
@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<String> 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<String> classPathDirs;
|
||||
private final Opcodes opcodes;
|
||||
|
||||
private final Set<File> loadedFiles = Sets.newHashSet();
|
||||
private final List<ClassProvider> 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<String> bootClassPathDirs, @Nonnull List<String> bootClassPathEntries,
|
||||
@Nonnull List<String> 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<String> bootClassPathDirs, @Nonnull List<String> extraClassPathEntries,
|
||||
@Nonnull DexFile dexFile, int apiLevel)
|
||||
throws IOException {
|
||||
this(bootClassPathDirs, null, extraClassPathEntries, dexFile, apiLevel);
|
||||
}
|
||||
|
||||
private ClassPathResolver(@Nonnull List<String> bootClassPathDirs, @Nullable List<String> bootClassPathEntries,
|
||||
@Nonnull List<String> 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<? extends MultiDexFile> 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<ClassProvider> 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<String> 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<pathComponents.size(); i++) {
|
||||
String partialPath = pathJoiner.join(pathComponents.subList(i, pathComponents.size()));
|
||||
File entryFile = new File(directoryFile, partialPath);
|
||||
if (entryFile.exists() && entryFile.isFile()) {
|
||||
loadEntry(entryFile, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new NotFoundException("Could not find classpath entry %s", entry);
|
||||
}
|
||||
|
||||
private void loadEntry(@Nonnull File entryFile, boolean loadOatDependencies)
|
||||
throws IOException, NoDexException {
|
||||
if (loadedFiles.contains(entryFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
MultiDexContainer<? extends DexBackedDexFile> container;
|
||||
try {
|
||||
container = DexFileFactory.loadDexContainer(entryFile, opcodes);
|
||||
} catch (UnsupportedFileTypeException ex) {
|
||||
throw new ResolveException(ex);
|
||||
}
|
||||
|
||||
List<String> 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<String> 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<String> splitDevicePath(@Nonnull String path) {
|
||||
return Lists.newArrayList(Splitter.on('/').split(path));
|
||||
}
|
||||
|
||||
private void loadOatDependencies(@Nonnull File directory, @Nonnull List<String> 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<String> 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
|
||||
}
|
||||
}
|
@ -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++) {
|
||||
|
@ -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<OatDexFile> {
|
||||
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<OatDexFile> iterator() {
|
||||
return new Iterator<OatDexFile>() {
|
||||
int index = 0;
|
||||
int offset = oatHeader.getDexListStart();
|
||||
|
||||
@Override public boolean hasNext() {
|
||||
return index < size();
|
||||
return Iterators.transform(new DexEntryIterator(), new Function<DexEntry, OatDexFile>() {
|
||||
@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<String> getDexEntryNames() throws IOException {
|
||||
return new AbstractForwardSequentialList<String>() {
|
||||
@Override public int size() {
|
||||
return oatHeader.getDexFileCount();
|
||||
}
|
||||
|
||||
@Nonnull @Override public Iterator<String> iterator() {
|
||||
return Iterators.transform(new DexEntryIterator(), new Function<DexEntry, String>() {
|
||||
@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<DexEntry> {
|
||||
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() {}
|
||||
}
|
||||
|
||||
}
|
@ -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<ZipDexFile>, 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<String> getDexEntryNames() throws IOException {
|
||||
List<String> entryNames = Lists.newArrayList();
|
||||
Enumeration<? extends ZipEntry> 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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<T extends DexFile> {
|
||||
/**
|
||||
* @return A list of the names of dex entries in this container
|
||||
*/
|
||||
@Nonnull List<String> 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<? extends MultiDexFile> getContainer();
|
||||
}
|
||||
}
|
@ -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<String, DexBackedDexFile> 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<String, DexBackedDexFile> 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<String, DexBackedDexFile> entries) {
|
||||
this.fileName = fileName;
|
||||
this.entries = entries;
|
||||
public static class TestMultiDexContainer implements MultiDexContainer<DexBackedDexFile> {
|
||||
@Nonnull private final Map<String, DexBackedDexFile> entries;
|
||||
|
||||
public TestMultiDexContainer(@Nonnull Map<String, DexBackedDexFile> entries) {
|
||||
this.entries = entries;
|
||||
}
|
||||
|
||||
@Nonnull @Override public List<String> getDexEntryNames() throws IOException {
|
||||
List<String> entryNames = Lists.newArrayList();
|
||||
|
||||
for (Entry<String, DexBackedDexFile> 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<String> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -70,8 +70,9 @@ public class CustomMethodInlineTableTest {
|
||||
|
||||
DexFile dexFile = new ImmutableDexFile(Opcodes.forApi(19), ImmutableList.of(classDef));
|
||||
|
||||
ClassPath classPath = ClassPath.loadClassPath(ImmutableList.<String>of(),
|
||||
ImmutableList.<String>of(), ImmutableList.<String>of(), dexFile, 15, false);
|
||||
ClassPathResolver resolver = new ClassPathResolver(ImmutableList.<String>of(),
|
||||
ImmutableList.<String>of(), ImmutableList.<String>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.<String>of(),
|
||||
ImmutableList.<String>of(), ImmutableList.<String>of(), dexFile, 15, false);
|
||||
ClassPathResolver resolver = new ClassPathResolver(ImmutableList.<String>of(),
|
||||
ImmutableList.<String>of(), ImmutableList.<String>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.<String>of(),
|
||||
ImmutableList.<String>of(), ImmutableList.<String>of(), dexFile, 15, false);
|
||||
ClassPathResolver resolver = new ClassPathResolver(ImmutableList.<String>of(),
|
||||
ImmutableList.<String>of(), ImmutableList.<String>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);
|
||||
|
||||
|
@ -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<String> basePath = getPathComponents(canonicalBaseFile);
|
||||
List<String> pathToRelativize = getPathComponents(canonicalFileToRelativize);
|
||||
@ -108,7 +98,7 @@ public class PathUtil {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static List<String> getPathComponents(File file) {
|
||||
private static List<String> getPathComponents(File file) {
|
||||
ArrayList<String> path = new ArrayList<String>();
|
||||
|
||||
while (file != null) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user