aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJesse Wilson <jessewilson@google.com>2012-01-11 15:33:57 -0500
committerJesse Wilson <jessewilson@google.com>2012-01-11 15:34:48 -0500
commit73cfa4498f640e0915b95fc806db4a0d54172fe8 (patch)
tree4dc6220954bfe41a209a232a48b12a8b6e039c8b
parentb4fdb175545f178c642194bc43a3fad31af3f0e9 (diff)
downloaddexmaker-73cfa4498f640e0915b95fc806db4a0d54172fe8.tar.gz
Adopt Hugo Hudson's AppDataDirGuesser in the core DexMaker. It's far too cumbersome to rely on frameworks to include such heuristics.
-rw-r--r--src/main/java/com/google/dexmaker/AppDataDirGuesser.java85
-rw-r--r--src/main/java/com/google/dexmaker/DexMaker.java48
-rw-r--r--src/main/java/com/google/dexmaker/stock/ProxyBuilder.java19
-rw-r--r--src/test/java/com/google/dexmaker/AppDataDirGuesserTest.java98
4 files changed, 236 insertions, 14 deletions
diff --git a/src/main/java/com/google/dexmaker/AppDataDirGuesser.java b/src/main/java/com/google/dexmaker/AppDataDirGuesser.java
new file mode 100644
index 0000000..2492ea0
--- /dev/null
+++ b/src/main/java/com/google/dexmaker/AppDataDirGuesser.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.dexmaker;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Uses heuristics to guess the application's private data directory.
+ */
+class AppDataDirGuesser {
+ public File guess() {
+ try {
+ ClassLoader classLoader = guessSuitableClassLoader();
+ // Check that we have an instance of the PathClassLoader.
+ Class<?> clazz = Class.forName("dalvik.system.PathClassLoader");
+ clazz.cast(classLoader);
+ // Use the toString() method to calculate the data directory.
+ String pathFromThisClassLoader = getPathFromThisClassLoader(classLoader);
+ File[] results = guessPath(pathFromThisClassLoader);
+ if (results.length > 0) {
+ return results[0];
+ }
+ } catch (ClassCastException ignored) {
+ } catch (ClassNotFoundException ignored) {
+ }
+ return null;
+ }
+
+ private ClassLoader guessSuitableClassLoader() {
+ return AppDataDirGuesser.class.getClassLoader();
+ }
+
+ private String getPathFromThisClassLoader(ClassLoader classLoader) {
+ // Parsing toString() method: yuck. But no other way to get the path.
+ // Strip out the bit between angle brackets, that's our path.
+ String result = classLoader.toString();
+ int index = result.lastIndexOf('[');
+ result = (index == -1) ? result : result.substring(index + 1);
+ index = result.indexOf(']');
+ return (index == -1) ? result : result.substring(0, index);
+ }
+
+ File[] guessPath(String input) {
+ List<File> results = new ArrayList<File>();
+ for (String potential : input.split(":")) {
+ if (!potential.startsWith("/data/app/")) {
+ continue;
+ }
+ int start = "/data/app/".length();
+ int end = potential.lastIndexOf(".apk");
+ if (end != potential.length() - 4) {
+ continue;
+ }
+ int dash = potential.indexOf("-");
+ if (dash != -1) {
+ end = dash;
+ }
+ File file = new File("/data/data/" + potential.substring(start, end) + "/cache");
+ if (isWriteableDirectory(file)) {
+ results.add(file);
+ }
+ }
+ return results.toArray(new File[results.size()]);
+ }
+
+ boolean isWriteableDirectory(File file) {
+ return file.isDirectory() && file.canWrite();
+ }
+}
diff --git a/src/main/java/com/google/dexmaker/DexMaker.java b/src/main/java/com/google/dexmaker/DexMaker.java
index 3566fb6..ae59740 100644
--- a/src/main/java/com/google/dexmaker/DexMaker.java
+++ b/src/main/java/com/google/dexmaker/DexMaker.java
@@ -326,20 +326,42 @@ public final class DexMaker {
/**
* Generates a dex file and loads its types into the current process.
*
- * <p>All parameters are optional; you may pass {@code null} and suitable
- * defaults will be used.
+ * <h3>Picking a dex cache directory</h3>
+ * The {@code dexCache} should be an application-private directory. If
+ * you pass a world-writable directory like {@code /sdcard} a malicious app
+ * could inject code into your process. Most applications should use this:
+ * <pre> {@code
*
- * <p>If you opt to provide your own {@code dexDir}, take care to ensure
- * that it is not world-writable, otherwise a malicious app may be able
- * to inject code into your process. A suitable parameter is:
- * {@code getApplicationContext().getDir("dx", Context.MODE_PRIVATE); }
+ * File dexCache = getApplicationContext().getDir("dx", Context.MODE_PRIVATE);
+ * }</pre>
+ * If the {@code dexCache} is null, this method will consult the {@code
+ * dexmaker.dexcache} system property. If that exists, it will be used for
+ * the dex cache. If it doesn't exist, this method will attempt to guess
+ * the application's private data directory as a last resort. If that fails,
+ * this method will fail with an unchecked exception. You can avoid the
+ * exception by either providing a non-null value or setting the system
+ * property.
*
- * @param parent the parent ClassLoader to be used when loading
- * our generated types
- * @param dexDir the destination directory where generated and
- * optimized dex files will be written.
+ * @param parent the parent ClassLoader to be used when loading our
+ * generated types
+ * @param dexCache the destination directory where generated and optimized
+ * dex files will be written. If null, this class will try to guess the
+ * application's private data dir.
*/
- public ClassLoader generateAndLoad(ClassLoader parent, File dexDir) throws IOException {
+ public ClassLoader generateAndLoad(ClassLoader parent, File dexCache) throws IOException {
+ if (dexCache == null) {
+ String property = System.getProperty("dexmaker.dexcache");
+ if (property != null) {
+ dexCache = new File(property);
+ } else {
+ dexCache = new AppDataDirGuesser().guess();
+ if (dexCache == null) {
+ throw new IllegalArgumentException("dexcache == null (and no default could be"
+ + " found; consider setting the 'dexmaker.dexcache' system property)");
+ }
+ }
+ }
+
byte[] dex = generate();
/*
@@ -349,7 +371,7 @@ public final class DexMaker {
*
* TODO: load the dex from memory where supported.
*/
- File result = File.createTempFile("Generated", ".jar", dexDir);
+ File result = File.createTempFile("Generated", ".jar", dexCache);
result.deleteOnExit();
JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(result));
jarOut.putNextEntry(new JarEntry(DexFormat.DEX_IN_JAR_NAME));
@@ -359,7 +381,7 @@ public final class DexMaker {
try {
return (ClassLoader) Class.forName("dalvik.system.DexClassLoader")
.getConstructor(String.class, String.class, String.class, ClassLoader.class)
- .newInstance(result.getPath(), dexDir.getAbsolutePath(), null, parent);
+ .newInstance(result.getPath(), dexCache.getAbsolutePath(), null, parent);
} catch (ClassNotFoundException e) {
throw new UnsupportedOperationException("load() requires a Dalvik VM", e);
} catch (InvocationTargetException e) {
diff --git a/src/main/java/com/google/dexmaker/stock/ProxyBuilder.java b/src/main/java/com/google/dexmaker/stock/ProxyBuilder.java
index f5b9c39..ad702e4 100644
--- a/src/main/java/com/google/dexmaker/stock/ProxyBuilder.java
+++ b/src/main/java/com/google/dexmaker/stock/ProxyBuilder.java
@@ -126,7 +126,6 @@ public final class ProxyBuilder<T> {
= Collections.synchronizedMap(new HashMap<Class<?>, Class<?>>());
private final Class<T> baseClass;
- // TODO: make DexMaker do the defaulting here
private ClassLoader parentClassLoader = ProxyBuilder.class.getClassLoader();
private InvocationHandler handler;
private File dexCache;
@@ -156,6 +155,11 @@ public final class ProxyBuilder<T> {
return this;
}
+ /**
+ * Sets the directory where executable code is stored. See {@link
+ * DexMaker#generateAndLoad DexMaker.generateAndLoad()} for guidance on
+ * choosing a secure location for the dex cache.
+ */
public ProxyBuilder<T> dexCache(File dexCache) {
this.dexCache = dexCache;
return this;
@@ -310,6 +314,19 @@ public final class ProxyBuilder<T> {
}
}
+ /**
+ * Returns true if {@code c} is a proxy class created by this builder.
+ */
+ public static boolean isProxyClass(Class<?> c) {
+ // TODO: use a marker interface instead?
+ try {
+ c.getDeclaredField(FIELD_NAME_HANDLER);
+ return true;
+ } catch (NoSuchFieldException e) {
+ return false;
+ }
+ }
+
private static <T, G extends T> void generateCodeForAllMethods(DexMaker dexMaker,
TypeId<G> generatedType, Method[] methodsToProxy, TypeId<T> superclassType) {
TypeId<InvocationHandler> handlerType = TypeId.get(InvocationHandler.class);
diff --git a/src/test/java/com/google/dexmaker/AppDataDirGuesserTest.java b/src/test/java/com/google/dexmaker/AppDataDirGuesserTest.java
new file mode 100644
index 0000000..5c92f34
--- /dev/null
+++ b/src/test/java/com/google/dexmaker/AppDataDirGuesserTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.dexmaker;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import junit.framework.TestCase;
+
+public final class AppDataDirGuesserTest extends TestCase {
+ public void testGuessCacheDir_SimpleExample() {
+ guessCacheDirFor("/data/app/a.b.c.apk").shouldGive("/data/data/a.b.c/cache");
+ guessCacheDirFor("/data/app/a.b.c.tests.apk").shouldGive("/data/data/a.b.c.tests/cache");
+ }
+
+ public void testGuessCacheDir_MultipleResultsSeparatedByColon() {
+ guessCacheDirFor("/data/app/a.b.c.apk:/data/app/d.e.f.apk")
+ .shouldGive("/data/data/a.b.c/cache", "/data/data/d.e.f/cache");
+ }
+
+ public void testGuessCacheDir_NotWriteableSkipped() {
+ guessCacheDirFor("/data/app/a.b.c.apk:/data/app/d.e.f.apk")
+ .withNonWriteable("/data/data/a.b.c/cache")
+ .shouldGive("/data/data/d.e.f/cache");
+ }
+
+ public void testGuessCacheDir_StripHyphenatedSuffixes() {
+ guessCacheDirFor("/data/app/a.b.c-2.apk").shouldGive("/data/data/a.b.c/cache");
+ }
+
+ public void testGuessCacheDir_LeadingAndTrailingColonsIgnored() {
+ guessCacheDirFor("/data/app/a.b.c.apk:asdf:").shouldGive("/data/data/a.b.c/cache");
+ guessCacheDirFor(":asdf:/data/app/a.b.c.apk").shouldGive("/data/data/a.b.c/cache");
+ }
+
+ public void testGuessCacheDir_InvalidInputsGiveEmptyArray() {
+ guessCacheDirFor("").shouldGive();
+ }
+
+ public void testGuessCacheDir_JarsIgnored() {
+ guessCacheDirFor("/data/app/a.b.c.jar").shouldGive();
+ guessCacheDirFor("/system/framework/android.test.runner.jar").shouldGive();
+ }
+
+ public void testGuessCacheDir_RealWorldExample() {
+ String realPath = "/system/framework/android.test.runner.jar:" +
+ "/data/app/com.google.android.voicesearch.tests-2.apk:" +
+ "/data/app/com.google.android.voicesearch-1.apk";
+ guessCacheDirFor(realPath)
+ .withNonWriteable("/data/data/com.google.android.voicesearch.tests/cache")
+ .shouldGive("/data/data/com.google.android.voicesearch/cache");
+ }
+
+ private interface TestCondition {
+ TestCondition withNonWriteable(String... files);
+ void shouldGive(String... files);
+ }
+
+ private TestCondition guessCacheDirFor(final String path) {
+ final Set<String> notWriteable = new HashSet<String>();
+ return new TestCondition() {
+ public void shouldGive(String... files) {
+ AppDataDirGuesser guesser = new AppDataDirGuesser() {
+ @Override
+ public boolean isWriteableDirectory(File file) {
+ return !notWriteable.contains(file.getAbsolutePath());
+ }
+ };
+ File[] results = guesser.guessPath(path);
+ assertNotNull("Null results for " + path, results);
+ assertEquals("Bad lengths for " + path, files.length, results.length);
+ for (int i = 0; i < files.length; ++i) {
+ assertEquals("Element " + i, new File(files[i]), results[i]);
+ }
+ }
+
+ public TestCondition withNonWriteable(String... files) {
+ notWriteable.addAll(Arrays.asList(files));
+ return this;
+ }
+ };
+ }
+}