blob: 85c5a3b65df8e5435174b7fdde2a06e2bf61f96a [file] [log] [blame]
/*
* Copyright (C) 2013 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.android.tools.idea.rendering;
import com.google.common.collect.Lists;
import junit.framework.TestCase;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.org.objectweb.asm.ClassReader;
import org.jetbrains.org.objectweb.asm.ClassVisitor;
import org.jetbrains.org.objectweb.asm.ClassWriter;
import org.jetbrains.org.objectweb.asm.FieldVisitor;
import org.jetbrains.org.objectweb.asm.MethodVisitor;
import org.jetbrains.org.objectweb.asm.Opcodes;
import org.jetbrains.org.objectweb.asm.tree.ClassNode;
import java.net.URL;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.jetbrains.org.objectweb.asm.Opcodes.*;
import static com.android.tools.idea.rendering.ClassConverter.*;
public class ClassConverterTest extends TestCase {
public void testClassVersionToJdk() {
assertEquals("1.5", classVersionToJdk(49));
assertEquals("1.6", classVersionToJdk(50));
assertEquals("1.7", classVersionToJdk(51));
assertEquals("1.8", classVersionToJdk(52));
assertEquals("1.4", classVersionToJdk(48));
assertEquals("1.3", classVersionToJdk(47));
assertEquals("1.2", classVersionToJdk(46));
assertEquals("1.1", classVersionToJdk(45));
}
public void testJdkToClassVersion() {
assertEquals(-1, jdkToClassVersion("?"));
assertEquals(49, jdkToClassVersion("1.5"));
assertEquals(49, jdkToClassVersion("1.5 "));
assertEquals(49, jdkToClassVersion("1.5.0"));
assertEquals(49, jdkToClassVersion("1.5.0_50"));
assertEquals(50, jdkToClassVersion("1.6"));
assertEquals(50, jdkToClassVersion("1.6.1"));
assertEquals(51, jdkToClassVersion("1.7.0"));
assertEquals(45, jdkToClassVersion("1.1"));
}
public void testFindHighestMajorVersion() {
InconvertibleClassError v1 = new InconvertibleClassError(null, "foo1", 49, 0);
InconvertibleClassError v2 = new InconvertibleClassError(null, "foo2", 51, 0);
InconvertibleClassError v3 = new InconvertibleClassError(null, "foo3", 49, 0);
assertEquals(51, findHighestMajorVersion(Lists.<Throwable>newArrayList(v1, v2, v3)));
}
public void testGetCurrentClassVersion() {
assertTrue(ClassConverter.getCurrentClassVersion() >= 50);
}
public void testGetCurrentJdkVersion() {
assertTrue(ClassConverter.getCurrentJdkVersion().startsWith("1."));
}
public void testMangling() throws Exception {
// Compile
// public class Test { public static int test() { return 42; } }
// then compile with javac Test.java and take the binary contents of Test.class
byte[] data = new byte[] {
(byte)202, (byte)254, (byte)186, (byte)190, (byte)0, (byte)0, (byte)0, (byte)50, (byte)0, (byte)15,
(byte)10, (byte)0, (byte)3, (byte)0, (byte)12, (byte)7, (byte)0, (byte)13, (byte)7, (byte)0,
(byte)14, (byte)1, (byte)0, (byte)6, (byte)60, (byte)105, (byte)110, (byte)105, (byte)116, (byte)62,
(byte)1, (byte)0, (byte)3, (byte)40, (byte)41, (byte)86, (byte)1, (byte)0, (byte)4, (byte)67,
(byte)111, (byte)100, (byte)101, (byte)1, (byte)0, (byte)15, (byte)76, (byte)105, (byte)110, (byte)101,
(byte)78, (byte)117, (byte)109, (byte)98, (byte)101, (byte)114, (byte)84, (byte)97, (byte)98, (byte)108,
(byte)101, (byte)1, (byte)0, (byte)4, (byte)116, (byte)101, (byte)115, (byte)116, (byte)1, (byte)0,
(byte)3, (byte)40, (byte)41, (byte)73, (byte)1, (byte)0, (byte)10, (byte)83, (byte)111, (byte)117,
(byte)114, (byte)99, (byte)101, (byte)70, (byte)105, (byte)108, (byte)101, (byte)1, (byte)0, (byte)9,
(byte)84, (byte)101, (byte)115, (byte)116, (byte)46, (byte)106, (byte)97, (byte)118, (byte)97, (byte)12,
(byte)0, (byte)4, (byte)0, (byte)5, (byte)1, (byte)0, (byte)4, (byte)84, (byte)101, (byte)115,
(byte)116, (byte)1, (byte)0, (byte)16, (byte)106, (byte)97, (byte)118, (byte)97, (byte)47, (byte)108,
(byte)97, (byte)110, (byte)103, (byte)47, (byte)79, (byte)98, (byte)106, (byte)101, (byte)99, (byte)116,
(byte)0, (byte)33, (byte)0, (byte)2, (byte)0, (byte)3, (byte)0, (byte)0, (byte)0, (byte)0,
(byte)0, (byte)2, (byte)0, (byte)1, (byte)0, (byte)4, (byte)0, (byte)5, (byte)0, (byte)1,
(byte)0, (byte)6, (byte)0, (byte)0, (byte)0, (byte)29, (byte)0, (byte)1, (byte)0, (byte)1,
(byte)0, (byte)0, (byte)0, (byte)5, (byte)42, (byte)183, (byte)0, (byte)1, (byte)177, (byte)0,
(byte)0, (byte)0, (byte)1, (byte)0, (byte)7, (byte)0, (byte)0, (byte)0, (byte)6, (byte)0,
(byte)1, (byte)0, (byte)0, (byte)0, (byte)1, (byte)0, (byte)9, (byte)0, (byte)8, (byte)0,
(byte)9, (byte)0, (byte)1, (byte)0, (byte)6, (byte)0, (byte)0, (byte)0, (byte)27, (byte)0,
(byte)1, (byte)0, (byte)0, (byte)0, (byte)0, (byte)0, (byte)3, (byte)16, (byte)42, (byte)172,
(byte)0, (byte)0, (byte)0, (byte)1, (byte)0, (byte)7, (byte)0, (byte)0, (byte)0, (byte)6,
(byte)0, (byte)1, (byte)0, (byte)0, (byte)0, (byte)1, (byte)0, (byte)1, (byte)0, (byte)10,
(byte)0, (byte)0, (byte)0, (byte)2, (byte)0, (byte)11
};
assertTrue(ClassConverter.isValidClassFile(data));
assertFalse(ClassConverter.isValidClassFile(new byte[100]));
assertEquals(50, ClassConverter.getMajorVersion(data));
assertEquals(0, ClassConverter.getMinorVersion(data));
assertEquals(0xCAFEBABE, ClassConverter.getMagic(data));
ClassLoader classLoader = new TestClassLoader(data);
Class<?> clz = classLoader.loadClass("Test");
assertNotNull(clz);
Object result = clz.getMethod("test").invoke(null);
assertEquals(Integer.valueOf(42), result);
Class<?> oldClz = clz;
data = ClassConverter.rewriteClass(data, 48, Integer.MIN_VALUE);
assertEquals(48, ClassConverter.getMajorVersion(data));
classLoader = new TestClassLoader(data);
clz = classLoader.loadClass("Test");
assertNotNull(clz);
assertNotSame(clz, oldClz);
result = clz.getMethod("test").invoke(null);
assertEquals(Integer.valueOf(42), result);
data = ClassConverter.rewriteClass(data, Integer.MAX_VALUE, 52); // latest known
assertEquals(52, ClassConverter.getMajorVersion(data));
classLoader = new TestClassLoader(data);
clz = classLoader.loadClass("Test");
assertNotNull(clz);
result = clz.getMethod("test").invoke(null);
assertEquals(Integer.valueOf(42), result);
// Make sure that that class cannot actually be loaded in the regular way
try {
final byte[] finalData = data;
ClassLoader cl = new ClassLoader() {
@Override
protected Class<?> findClass(String s) throws ClassNotFoundException {
assertEquals("Test", s);
return defineClass(null, finalData, 0, finalData.length);
}
};
cl.loadClass("Test");
fail("Expected class loading error");
} catch (UnsupportedClassVersionError e) {
// pass
} catch (Throwable t) {
fail("Expected class loading error");
}
}
// Method that generates the binary dump of the view below:
//
//class TestView extends View {
// public TestView(Context context) {
// super(context);
// }
//
// @Override
// protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// }
//}
//
// The binary dump was created by using ASMifier running:
// java -classpath asm-debug-all-5.0.2.jar:. org.objectweb.asm.util.ASMifier TestView.class
private static byte[] dumpTestViewClass () throws Exception {
ClassWriter cw = new ClassWriter(0);
MethodVisitor mv;
cw.visit(V1_7, ACC_SUPER, "TestView", null, "android/view/View", null);
{
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "(Landroid/content/Context;)V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 1);
mv.visitMethodInsn(INVOKESPECIAL, "android/view/View", "<init>", "(Landroid/content/Context;)V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PROTECTED, "onMeasure", "(II)V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ILOAD, 1);
mv.visitVarInsn(ILOAD, 2);
mv.visitMethodInsn(INVOKESPECIAL, "android/view/View", "onMeasure", "(II)V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(3, 3);
mv.visitEnd();
}
cw.visitEnd();
return cw.toByteArray();
}
public void testMethodWrapping() throws Exception {
byte[] data = ClassConverterTest.dumpTestViewClass();
assertTrue(ClassConverter.isValidClassFile(data));
byte[] modified = ClassConverter.rewriteClass(data);
assertTrue(ClassConverter.isValidClassFile(data));
// Parse both classes and compare
ClassNode classNode = new ClassNode();
ClassReader classReader = new ClassReader(modified);
classReader.accept(classNode, 0);
assertEquals(3, classNode.methods.size());
final Set<String> methods = new HashSet<String>();
classNode.accept(new ClassVisitor(Opcodes.ASM5) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
methods.add(name + desc);
return super.visitMethod(access, name, desc, signature, exceptions);
}
});
assertTrue(methods.contains("onMeasure(II)V"));
assertTrue(methods.contains("onMeasure_Original(II)V"));
}
public void testMethodWrapping2() throws Exception {
final byte[] firstData = getFirstOnMeasureClass();
final byte[] secondData = getSecondOnMeasureClass();
assertTrue(ClassConverter.isValidClassFile(firstData));
assertTrue(ClassConverter.isValidClassFile(secondData));
ClassLoader loader = new ClassLoader() {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
if (name.equals("FirstClass")) {
return defineClass(name, firstData, 0, firstData.length);
}
if (name.equals("SecondClass")) {
return defineClass(name, secondData, 0, secondData.length);
}
return super.findClass(name);
}
};
Class<?> clazz = loader.loadClass("FirstClass");
Object o = clazz.newInstance();
int outValue = (Integer) clazz.getMethod("test").invoke(o);
assertEquals(1, outValue);
clazz = loader.loadClass("SecondClass");
o = clazz.newInstance();
outValue = (Integer)clazz.getMethod("test").invoke(o);
assertEquals(2, outValue);
// Modify the classes and repeat.
final byte[] modifiedFirstData = ClassConverter.rewriteClass(firstData, 50, 0);
final byte[] modifiedSecondData = ClassConverter.rewriteClass(secondData, 50, 0);
loader = new ClassLoader() {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
if (name.equals("FirstClass")) {
return defineClass(name, modifiedFirstData, 0, modifiedFirstData.length);
}
if (name.equals("SecondClass")) {
return defineClass(name, modifiedSecondData, 0, modifiedSecondData.length);
}
return super.findClass(name);
}
};
clazz = loader.loadClass("FirstClass");
o = clazz.newInstance();
outValue = (Integer) clazz.getMethod("test").invoke(o);
assertEquals(1, outValue);
clazz = loader.loadClass("SecondClass");
o = clazz.newInstance();
outValue = (Integer)clazz.getMethod("test").invoke(o);
assertEquals(2, outValue);
}
private static byte[] getFirstOnMeasureClass() {
// Use asmifier to convert the compiled version of following class to generation code:
// public class FirstClass {
// int a;
// protected void onMeasure(int x, int y) {a = 1;}
// public int test() { onMeasure(0,0); return a;}}
ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
cw.visit(V1_6, ACC_PUBLIC + ACC_SUPER, "FirstClass", null, "java/lang/Object", null);
{
fv = cw.visitField(0, "a", "I", null, null);
fv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PROTECTED, "onMeasure", "(II)V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitInsn(ICONST_1);
mv.visitFieldInsn(PUTFIELD, "FirstClass", "a", "I");
mv.visitInsn(RETURN);
mv.visitMaxs(2, 3);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC, "test", "()I", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitInsn(ICONST_0);
mv.visitInsn(ICONST_0);
mv.visitMethodInsn(INVOKEVIRTUAL, "FirstClass", "onMeasure", "(II)V", false);
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "FirstClass", "a", "I");
mv.visitInsn(IRETURN);
mv.visitMaxs(3, 1);
mv.visitEnd();
}
cw.visitEnd();
return cw.toByteArray();
}
private static byte[] getSecondOnMeasureClass() {
// Use asmifier to convert the compiled version of following class to generation code:
// class SecondClass extends FirstClass {@Override protected void onMeasure(int x, int y) {super.onMeasure(x, y); a = 2;}}
ClassWriter cw = new ClassWriter(0);
MethodVisitor mv;
cw.visit(V1_6, ACC_PUBLIC + ACC_SUPER, "SecondClass", null, "FirstClass", null);
{
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "FirstClass", "<init>", "()V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PROTECTED, "onMeasure", "(II)V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ILOAD, 1);
mv.visitVarInsn(ILOAD, 2);
mv.visitMethodInsn(INVOKESPECIAL, "FirstClass", "onMeasure", "(II)V", false);
mv.visitVarInsn(ALOAD, 0);
mv.visitInsn(ICONST_2);
mv.visitFieldInsn(PUTFIELD, "SecondClass", "a", "I");
mv.visitInsn(RETURN);
mv.visitMaxs(3, 3);
mv.visitEnd();
}
cw.visitEnd();
return cw.toByteArray();
}
private static class TestClassLoader extends RenderClassLoader {
final byte[] myData;
public TestClassLoader(byte[] data) {
super(null);
myData = data;
}
@Override
protected List<URL> getExternalJars() {
return Collections.emptyList();
}
@NotNull
@Override
protected Class<?> load(String name) throws ClassNotFoundException {
assertEquals("Test", name);
Class<?> clz = loadClass(name, myData);
assertNotNull(clz);
return clz;
}
}
}