diff options
author | Jesse Wilson <jessewilson@google.com> | 2012-01-11 15:33:57 -0500 |
---|---|---|
committer | Jesse Wilson <jessewilson@google.com> | 2012-01-11 15:34:48 -0500 |
commit | 73cfa4498f640e0915b95fc806db4a0d54172fe8 (patch) | |
tree | 4dc6220954bfe41a209a232a48b12a8b6e039c8b | |
parent | b4fdb175545f178c642194bc43a3fad31af3f0e9 (diff) | |
download | dexmaker-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.
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; + } + }; + } +} |