diff options
author | Anatol Pomozov <anatol.pomozov@gmail.com> | 2012-01-18 10:37:44 -0800 |
---|---|---|
committer | Anatol Pomozov <anatol.pomozov@gmail.com> | 2012-01-18 10:37:44 -0800 |
commit | f225da938fc7f1fe77813cee6ceb33fea3d19f06 (patch) | |
tree | 6f548a0603b9b8f7df72dc91ac1b89a7b1d74fd9 | |
parent | 01f8aeca8a11dbecc160e1ebe2c92f1dc30c2d44 (diff) | |
parent | 8ec4b1db3afa51730508be7064d2111b723ac2cd (diff) | |
download | dexmaker-f225da938fc7f1fe77813cee6ceb33fea3d19f06.tar.gz |
Merge remote-tracking branch 'upstream/master'
6 files changed, 534 insertions, 63 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/Code.java b/src/main/java/com/google/dexmaker/Code.java index 1562314..4ea5e67 100644 --- a/src/main/java/com/google/dexmaker/Code.java +++ b/src/main/java/com/google/dexmaker/Code.java @@ -587,7 +587,7 @@ public final class Code { } /** - * Copies the value in {@code target} to the static field {@code fieldId}. + * Copies the value in the static field {@code fieldId} to {@code target}. */ public <V> void sget(FieldId<?, V> fieldId, Local<V> target) { addInstruction(new ThrowingCstInsn(Rops.opGetStatic(target.type.ropType), sourcePosition, @@ -782,8 +782,7 @@ public final class Code { } /** - * Assigns {@code target} to the element of {@code array} at index {@code - * index}. + * Assigns the element at {@code index} in {@code array} to {@code target}. */ public void aget(Local<?> target, Local<?> array, Local<Integer> index) { addInstruction(new ThrowingInsn(Rops.opAget(target.type.ropType), sourcePosition, @@ -792,8 +791,7 @@ public final class Code { } /** - * Sets the element at {@code index} in {@code array} the value in {@code - * source}. + * Assigns {@code source} to the element at {@code index} in {@code array}. */ public void aput(Local<?> array, Local<Integer> index, Local<?> source) { addInstruction(new ThrowingInsn(Rops.opAput(source.type.ropType), sourcePosition, 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..639b3dc 100644 --- a/src/main/java/com/google/dexmaker/stock/ProxyBuilder.java +++ b/src/main/java/com/google/dexmaker/stock/ProxyBuilder.java @@ -42,6 +42,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; /** * Creates dynamic proxies of concrete classes. @@ -126,12 +127,12 @@ 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; private Class<?>[] constructorArgTypes = new Class[0]; private Object[] constructorArgValues = new Object[0]; + private Set<Class<?>> interfaces = new HashSet<Class<?>>(); private ProxyBuilder(Class<T> clazz) { baseClass = clazz; @@ -156,10 +157,25 @@ 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; } + + public ProxyBuilder<T> implementing(Class<?>... interfaces) { + for (Class<?> i : interfaces) { + if (!i.isInterface()) { + throw new IllegalArgumentException("Not an interface: " + i.getName()); + } + this.interfaces.add(i); + } + return this; + } public ProxyBuilder<T> constructorArgValues(Object... constructorArgValues) { this.constructorArgValues = constructorArgValues; @@ -186,13 +202,13 @@ public final class ProxyBuilder<T> { check(handler != null, "handler == null"); check(constructorArgTypes.length == constructorArgValues.length, "constructorArgValues.length != constructorArgTypes.length"); - Class<? extends T> proxyClass = getProxyClass(); + Class<? extends T> proxyClass = buildProxyClass(); Constructor<? extends T> constructor; try { constructor = proxyClass.getConstructor(constructorArgTypes); } catch (NoSuchMethodException e) { - // Thrown when the constructor to be called does not exist. - throw new IllegalArgumentException("could not find matching constructor", e); + throw new IllegalArgumentException("No constructor for " + baseClass.getName() + + " with parameter types " + Arrays.toString(constructorArgTypes)); } T result; try { @@ -211,11 +227,15 @@ public final class ProxyBuilder<T> { return result; } - private Class<? extends T> getProxyClass() throws IOException { + // TODO: test coverage for this + // TODO: documentation for this + public Class<? extends T> buildProxyClass() throws IOException { // try the cache to see if we've generated this one before @SuppressWarnings("unchecked") // we only populate the map with matching types Class<? extends T> proxyClass = (Class) generatedProxyClasses.get(baseClass); - if (proxyClass != null && proxyClass.getClassLoader().getParent() == parentClassLoader) { + if (proxyClass != null + && proxyClass.getClassLoader().getParent() == parentClassLoader + && interfaces.equals(asSet(proxyClass.getInterfaces()))) { return proxyClass; // cache hit! } @@ -225,15 +245,17 @@ public final class ProxyBuilder<T> { TypeId<? extends T> generatedType = TypeId.get("L" + generatedName + ";"); TypeId<T> superType = TypeId.get(baseClass); generateConstructorsAndFields(dexMaker, generatedType, superType, baseClass); - Method[] methodsToProxy = getMethodsToProxy(baseClass); + Method[] methodsToProxy = getMethodsToProxyRecursive(); generateCodeForAllMethods(dexMaker, generatedType, methodsToProxy, superType); - dexMaker.declare(generatedType, generatedName + ".generated", PUBLIC, superType); + dexMaker.declare(generatedType, generatedName + ".generated", PUBLIC, superType, + getInterfacesAsTypeIds()); ClassLoader classLoader = dexMaker.generateAndLoad(parentClassLoader, dexCache); try { proxyClass = loadClass(classLoader, generatedName); } catch (IllegalAccessError e) { // Thrown when the base class is not accessible. - throw new UnsupportedOperationException("cannot proxy inaccessible classes", e); + throw new UnsupportedOperationException( + "cannot proxy inaccessible class " + baseClass, e); } catch (ClassNotFoundException e) { // Should not be thrown, we're sure to have generated this class. throw new AssertionError(e); @@ -310,6 +332,21 @@ public final class ProxyBuilder<T> { } } + // TODO: test coverage for isProxyClass + + /** + * 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); @@ -441,14 +478,14 @@ public final class ProxyBuilder<T> { /* * And to allow calling the original super method, the following is also generated: * - * public int super_doSomething(Bar param0, int param1) { + * public String super$doSomething$java_lang_String(Bar param0, int param1) { * int result = super.doSomething(param0, param1); * return result; * } */ - String superName = "super_" + name; + // TODO: don't include a super_ method if the target is abstract! MethodId<G, ?> callsSuperMethod = generatedType.getMethod( - resultType, superName, argTypes); + resultType, superMethodName(method), argTypes); Code superCode = dexMaker.declare(callsSuperMethod, PUBLIC); Local<G> superThis = superCode.getThis(generatedType); Local<?>[] superArgs = new Local<?>[argClasses.length]; @@ -481,12 +518,24 @@ public final class ProxyBuilder<T> { return temp; } - public static Object callSuper(Object proxy, Method method, Object... args) - throws SecurityException, IllegalAccessException, - InvocationTargetException, NoSuchMethodException { - return proxy.getClass() - .getMethod("super_" + method.getName(), method.getParameterTypes()) - .invoke(proxy, args); + public static Object callSuper(Object proxy, Method method, Object... args) throws Throwable { + try { + return proxy.getClass() + .getMethod(superMethodName(method), method.getParameterTypes()) + .invoke(proxy, args); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + + /** + * The super method must include the return type, otherwise its ambiguous + * for methods with covariant return types. + */ + private static String superMethodName(Method method) { + String returnType = method.getReturnType().getName(); + return "super$" + method.getName() + "$" + + returnType.replace('.', '_').replace('[', '_').replace(';', '_'); } private static void check(boolean condition, String message) { @@ -531,28 +580,28 @@ public final class ProxyBuilder<T> { return (Constructor<T>[]) clazz.getDeclaredConstructors(); } + private TypeId<?>[] getInterfacesAsTypeIds() { + TypeId<?>[] result = new TypeId<?>[interfaces.size()]; + int i = 0; + for (Class<?> implemented : interfaces) { + result[i++] = TypeId.get(implemented); + } + return result; + } + /** - * Gets all {@link Method} objects we can proxy in the hierarchy of the supplied class. + * Gets all {@link Method} objects we can proxy in the hierarchy of the + * supplied class. */ - private static <T> Method[] getMethodsToProxy(Class<T> clazz) { + private Method[] getMethodsToProxyRecursive() { Set<MethodSetEntry> methodsToProxy = new HashSet<MethodSetEntry>(); - for (Class<?> current = clazz; current != null; current = current.getSuperclass()) { - for (Method method : current.getDeclaredMethods()) { - if ((method.getModifiers() & Modifier.FINAL) != 0) { - // Skip final methods, we can't override them. - continue; - } - if ((method.getModifiers() & STATIC) != 0) { - // Skip static methods, overriding them has no effect. - continue; - } - if (method.getName().equals("finalize") && method.getParameterTypes().length == 0) { - // Skip finalize method, it's likely important that it execute as normal. - continue; - } - methodsToProxy.add(new MethodSetEntry(method)); - } + for (Class<?> c = baseClass; c != null; c = c.getSuperclass()) { + getMethodsToProxy(methodsToProxy, c); + } + for (Class<?> c : interfaces) { + getMethodsToProxy(methodsToProxy, c); } + Method[] results = new Method[methodsToProxy.size()]; int i = 0; for (MethodSetEntry entry : methodsToProxy) { @@ -561,6 +610,28 @@ public final class ProxyBuilder<T> { return results; } + private void getMethodsToProxy(Set<MethodSetEntry> sink, Class<?> c) { + for (Method method : c.getDeclaredMethods()) { + if ((method.getModifiers() & Modifier.FINAL) != 0) { + // Skip final methods, we can't override them. + continue; + } + if ((method.getModifiers() & STATIC) != 0) { + // Skip static methods, overriding them has no effect. + continue; + } + if (method.getName().equals("finalize") && method.getParameterTypes().length == 0) { + // Skip finalize method, it's likely important that it execute as normal. + continue; + } + sink.add(new MethodSetEntry(method)); + } + + for (Class<?> i : c.getInterfaces()) { + getMethodsToProxy(sink, i); + } + } + private static <T> String getMethodNameForProxyOf(Class<T> clazz) { return clazz.getSimpleName() + "_Proxy"; } @@ -596,6 +667,10 @@ public final class ProxyBuilder<T> { } } + private static <T> Set<T> asSet(T... array) { + return new CopyOnWriteArraySet<T>(Arrays.asList(array)); + } + private static MethodId<?, ?> getUnboxMethodForPrimitive(Class<?> methodReturnType) { return PRIMITIVE_TO_UNBOX_METHOD.get(methodReturnType); } 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; + } + }; + } +} diff --git a/src/test/java/com/google/dexmaker/stock/ProxyBuilderTest.java b/src/test/java/com/google/dexmaker/stock/ProxyBuilderTest.java index d613053..1b65ea8 100644 --- a/src/test/java/com/google/dexmaker/stock/ProxyBuilderTest.java +++ b/src/test/java/com/google/dexmaker/stock/ProxyBuilderTest.java @@ -22,7 +22,10 @@ import java.io.IOException; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.UndeclaredThrowableException; +import java.util.Arrays; import java.util.Random; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicInteger; import junit.framework.AssertionFailedError; import junit.framework.TestCase; @@ -164,7 +167,7 @@ public class ProxyBuilderTest extends TestCase { assertEquals(false, proxy.equals(proxy)); } - public static class AllPrimitiveMethods { + public static class AllReturnTypes { public boolean getBoolean() { return true; } public int getInt() { return 1; } public byte getByte() { return 2; } @@ -173,10 +176,12 @@ public class ProxyBuilderTest extends TestCase { public float getFloat() { return 5f; } public double getDouble() { return 6.0; } public char getChar() { return 'c'; } + public int[] getIntArray() { return new int[] { 8, 9 }; } + public String[] getStringArray() { return new String[] { "d", "e" }; } } - public void testAllPrimitiveReturnTypes() throws Throwable { - AllPrimitiveMethods proxy = proxyFor(AllPrimitiveMethods.class).build(); + public void testAllReturnTypes() throws Throwable { + AllReturnTypes proxy = proxyFor(AllReturnTypes.class).build(); fakeHandler.setFakeResult(false); assertEquals(false, proxy.getBoolean()); fakeHandler.setFakeResult(8); @@ -193,9 +198,13 @@ public class ProxyBuilderTest extends TestCase { assertEquals(13.0, proxy.getDouble()); fakeHandler.setFakeResult('z'); assertEquals('z', proxy.getChar()); + fakeHandler.setFakeResult(new int[] { -1, -2 }); + assertEquals("[-1, -2]", Arrays.toString(proxy.getIntArray())); + fakeHandler.setFakeResult(new String[] { "x", "y" }); + assertEquals("[x, y]", Arrays.toString(proxy.getStringArray())); } - public static class PassThroughAllPrimitives { + public static class PassThroughAllTypes { public boolean getBoolean(boolean input) { return input; } public int getInt(int input) { return input; } public byte getByte(byte input) { return input; } @@ -215,8 +224,8 @@ public class ProxyBuilderTest extends TestCase { } } - public void testPassThroughWorksForAllPrimitives() throws Exception { - PassThroughAllPrimitives proxy = proxyFor(PassThroughAllPrimitives.class) + public void testPassThroughWorksForAllTypes() throws Exception { + PassThroughAllTypes proxy = proxyFor(PassThroughAllTypes.class) .handler(new InvokeSuperHandler()) .build(); assertEquals(false, proxy.getBoolean(false)); @@ -244,12 +253,12 @@ public class ProxyBuilderTest extends TestCase { proxy.getNothing(); } - public static class ExtendsAllPrimitiveMethods extends AllPrimitiveMethods { + public static class ExtendsAllReturnTypes extends AllReturnTypes { public int example() { return 0; } } public void testProxyWorksForSuperclassMethodsAlso() throws Throwable { - ExtendsAllPrimitiveMethods proxy = proxyFor(ExtendsAllPrimitiveMethods.class).build(); + ExtendsAllReturnTypes proxy = proxyFor(ExtendsAllReturnTypes.class).build(); fakeHandler.setFakeResult(99); assertEquals(99, proxy.example()); assertEquals(99, proxy.getInt()); @@ -342,7 +351,7 @@ public class ProxyBuilderTest extends TestCase { public void testDefaultProxyHasSuperMethodToAccessOriginal() throws Exception { Object objectProxy = proxyFor(Object.class).build(); - assertNotNull(objectProxy.getClass().getMethod("super_hashCode")); + assertNotNull(objectProxy.getClass().getMethod("super$hashCode$int")); } public static class PrintsOddAndValue { @@ -371,6 +380,31 @@ public class ProxyBuilderTest extends TestCase { assertEquals("even 2", proxy.method(2)); assertEquals("odd 3", proxy.method(3)); } + + public void testCallSuperThrows() throws Exception { + InvocationHandler handler = new InvocationHandler() { + public Object invoke(Object o, Method method, Object[] objects) throws Throwable { + return ProxyBuilder.callSuper(o, method, objects); + } + }; + + FooThrows fooThrows = proxyFor(FooThrows.class) + .handler(handler) + .build(); + + try { + fooThrows.foo(); + fail(); + } catch (IllegalStateException expected) { + assertEquals("boom!", expected.getMessage()); + } + } + + public static class FooThrows { + public void foo() { + throw new IllegalStateException("boom!"); + } + } public static class DoubleReturn { public double getValue() { @@ -564,7 +598,166 @@ public class ProxyBuilderTest extends TestCase { assertTrue(a.getClass() != b.getClass()); } + + public void testAbstractClassWithUndeclaredInterfaceMethod() throws Throwable { + DeclaresInterface declaresInterface = proxyFor(DeclaresInterface.class) + .build(); + assertEquals("fake result", declaresInterface.call()); + try { + ProxyBuilder.callSuper(declaresInterface, Callable.class.getMethod("call")); + fail(); + } catch (AbstractMethodError expected) { + } + } + + public static abstract class DeclaresInterface implements Callable<String> { + } + + public void testImplementingInterfaces() throws Throwable { + SimpleClass simpleClass = proxyFor(SimpleClass.class) + .implementing(Callable.class) + .implementing(Comparable.class) + .build(); + assertEquals("fake result", simpleClass.simpleMethod()); + + Callable<?> asCallable = (Callable<?>) simpleClass; + assertEquals("fake result", asCallable.call()); + + Comparable<?> asComparable = (Comparable<?>) simpleClass; + fakeHandler.fakeResult = 3; + assertEquals(3, asComparable.compareTo(null)); + } + + public void testCallSuperWithInterfaceMethod() throws Throwable { + SimpleClass simpleClass = proxyFor(SimpleClass.class) + .implementing(Callable.class) + .build(); + try { + ProxyBuilder.callSuper(simpleClass, Callable.class.getMethod("call")); + fail(); + } catch (AbstractMethodError expected) { + } catch (NoSuchMethodError expected) { + } + } + public void testImplementInterfaceCallingThroughConcreteClass() throws Throwable { + InvocationHandler invocationHandler = new InvocationHandler() { + public Object invoke(Object o, Method method, Object[] objects) throws Throwable { + assertEquals("a", ProxyBuilder.callSuper(o, method, objects)); + return "b"; + } + }; + ImplementsCallable proxy = proxyFor(ImplementsCallable.class) + .implementing(Callable.class) + .handler(invocationHandler) + .build(); + assertEquals("b", proxy.call()); + assertEquals("a", ProxyBuilder.callSuper( + proxy, ImplementsCallable.class.getMethod("call"))); + } + + /** + * This test is a bit unintuitive because it exercises the synthetic methods + * that support covariant return types. Calling 'Object call()' on the + * interface bridges to 'String call()', and so the super method appears to + * also be proxied. + */ + public void testImplementInterfaceCallingThroughInterface() throws Throwable { + final AtomicInteger count = new AtomicInteger(); + + InvocationHandler invocationHandler = new InvocationHandler() { + public Object invoke(Object o, Method method, Object[] objects) throws Throwable { + count.incrementAndGet(); + return ProxyBuilder.callSuper(o, method, objects); + } + }; + + Callable<?> proxy = proxyFor(ImplementsCallable.class) + .implementing(Callable.class) + .handler(invocationHandler) + .build(); + + // the invocation handler is called twice! + assertEquals("a", proxy.call()); + assertEquals(2, count.get()); + + // the invocation handler is called, even though this is a callSuper() call! + assertEquals("a", ProxyBuilder.callSuper(proxy, Callable.class.getMethod("call"))); + assertEquals(3, count.get()); + } + + public static class ImplementsCallable implements Callable<String> { + public String call() throws Exception { + return "a"; + } + } + + /** + * This test shows that our generated proxies follow the bytecode convention + * where methods can have the same name but unrelated return types. This is + * different from javac's convention where return types must be assignable + * in one direction or the other. + */ + public void testInterfacesSameNamesDifferentReturnTypes() throws Throwable { + InvocationHandler handler = new InvocationHandler() { + public Object invoke(Object o, Method method, Object[] objects) throws Throwable { + if (method.getReturnType() == void.class) { + return null; + } else if (method.getReturnType() == String.class) { + return "X"; + } else if (method.getReturnType() == int.class) { + return 3; + } else { + throw new AssertionFailedError(); + } + } + }; + + Object o = proxyFor(Object.class) + .implementing(FooReturnsVoid.class, FooReturnsString.class, FooReturnsInt.class) + .handler(handler) + .build(); + + FooReturnsVoid a = (FooReturnsVoid) o; + a.foo(); + + FooReturnsString b = (FooReturnsString) o; + assertEquals("X", b.foo()); + + FooReturnsInt c = (FooReturnsInt) o; + assertEquals(3, c.foo()); + } + + public void testInterfacesSameNamesSameReturnType() throws Throwable { + Object o = proxyFor(Object.class) + .implementing(FooReturnsInt.class, FooReturnsInt2.class) + .build(); + + fakeHandler.setFakeResult(3); + + FooReturnsInt a = (FooReturnsInt) o; + assertEquals(3, a.foo()); + + FooReturnsInt2 b = (FooReturnsInt2) o; + assertEquals(3, b.foo()); + } + + public interface FooReturnsVoid { + void foo(); + } + + public interface FooReturnsString { + String foo(); + } + + public interface FooReturnsInt { + int foo(); + } + + public interface FooReturnsInt2 { + int foo(); + } + private ClassLoader newPathClassLoader() throws Exception { return (ClassLoader) Class.forName("dalvik.system.PathClassLoader") .getConstructor(String.class, ClassLoader.class) |