| /* |
| * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| /* @test |
| * @modules java.base/java.lang:open |
| * java.base/jdk.internal.org.objectweb.asm |
| * @run testng/othervm test.DefineClassTest |
| * @summary Basic test for java.lang.invoke.MethodHandles.Lookup.defineClass |
| */ |
| |
| package test; |
| |
| import java.lang.invoke.MethodHandles.Lookup; |
| import static java.lang.invoke.MethodHandles.*; |
| import static java.lang.invoke.MethodHandles.Lookup.*; |
| import java.net.URL; |
| import java.net.URLClassLoader; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| |
| import jdk.internal.org.objectweb.asm.ClassWriter; |
| import jdk.internal.org.objectweb.asm.MethodVisitor; |
| import static jdk.internal.org.objectweb.asm.Opcodes.*; |
| |
| import org.testng.annotations.Test; |
| import static org.testng.Assert.*; |
| |
| public class DefineClassTest { |
| private static final String THIS_PACKAGE = DefineClassTest.class.getPackageName(); |
| |
| /** |
| * Test that a class has the same class loader, and is in the same package and |
| * protection domain, as a lookup class. |
| */ |
| void testSameAbode(Class<?> clazz, Class<?> lc) { |
| assertTrue(clazz.getClassLoader() == lc.getClassLoader()); |
| assertEquals(clazz.getPackageName(), lc.getPackageName()); |
| assertTrue(clazz.getProtectionDomain() == lc.getProtectionDomain()); |
| } |
| |
| /** |
| * Tests that a class is discoverable by name using Class.forName and |
| * lookup.findClass |
| */ |
| void testDiscoverable(Class<?> clazz, Lookup lookup) throws Exception { |
| String cn = clazz.getName(); |
| ClassLoader loader = clazz.getClassLoader(); |
| assertTrue(Class.forName(cn, false, loader) == clazz); |
| assertTrue(lookup.findClass(cn) == clazz); |
| } |
| |
| /** |
| * Basic test of defineClass to define a class in the same package as test. |
| */ |
| @Test |
| public void testDefineClass() throws Exception { |
| final String CLASS_NAME = THIS_PACKAGE + ".Foo"; |
| Lookup lookup = lookup(); |
| Class<?> clazz = lookup.defineClass(generateClass(CLASS_NAME)); |
| |
| // test name |
| assertEquals(clazz.getName(), CLASS_NAME); |
| |
| // test loader/package/protection-domain |
| testSameAbode(clazz, lookup.lookupClass()); |
| |
| // test discoverable |
| testDiscoverable(clazz, lookup); |
| |
| // attempt defineClass again |
| try { |
| lookup.defineClass(generateClass(CLASS_NAME)); |
| assertTrue(false); |
| } catch (LinkageError expected) { } |
| } |
| |
| /** |
| * Test public/package/protected/private access from class defined with defineClass. |
| */ |
| @Test |
| public void testAccess() throws Exception { |
| final String THIS_CLASS = this.getClass().getName(); |
| final String CLASS_NAME = THIS_PACKAGE + ".Runner"; |
| Lookup lookup = lookup(); |
| |
| // public |
| byte[] classBytes = generateRunner(CLASS_NAME + nextNumber(), THIS_CLASS, "method1"); |
| testInvoke(lookup.defineClass(classBytes)); |
| |
| // package |
| classBytes = generateRunner(CLASS_NAME + nextNumber(), THIS_CLASS, "method2"); |
| testInvoke(lookup.defineClass(classBytes)); |
| |
| // protected (same package) |
| classBytes = generateRunner(CLASS_NAME + nextNumber(), THIS_CLASS, "method3"); |
| testInvoke(lookup.defineClass(classBytes)); |
| |
| // private |
| classBytes = generateRunner(CLASS_NAME + nextNumber(), THIS_CLASS, "method4"); |
| Class<?> clazz = lookup.defineClass(classBytes); |
| Runnable r = (Runnable) clazz.newInstance(); |
| try { |
| r.run(); |
| assertTrue(false); |
| } catch (IllegalAccessError expected) { } |
| } |
| |
| public static void method1() { } |
| static void method2() { } |
| protected static void method3() { } |
| private static void method4() { } |
| |
| void testInvoke(Class<?> clazz) throws Exception { |
| Object obj = clazz.newInstance(); |
| ((Runnable) obj).run(); |
| } |
| |
| /** |
| * Test that defineClass does not run the class initializer |
| */ |
| @Test |
| public void testInitializerNotRun() throws Exception { |
| final String THIS_CLASS = this.getClass().getName(); |
| final String CLASS_NAME = THIS_PACKAGE + ".ClassWithClinit"; |
| |
| byte[] classBytes = generateClassWithInitializer(CLASS_NAME, THIS_CLASS, "fail"); |
| Class<?> clazz = lookup().defineClass(classBytes); |
| |
| // trigger initializer to run |
| try { |
| clazz.newInstance(); |
| assertTrue(false); |
| } catch (ExceptionInInitializerError e) { |
| assertTrue(e.getCause() instanceof IllegalCallerException); |
| } |
| } |
| |
| static void fail() { throw new IllegalCallerException(); } |
| |
| |
| /** |
| * Test defineClass to define classes in a package containing classes with |
| * different protection domains. |
| */ |
| @Test |
| public void testTwoProtectionDomains() throws Exception { |
| // p.C1 in one exploded directory |
| Path dir1 = Files.createTempDirectory("classes"); |
| Path p = Files.createDirectory(dir1.resolve("p")); |
| Files.write(p.resolve("C1.class"), generateClass("p.C1")); |
| URL url1 = dir1.toUri().toURL(); |
| |
| // p.C2 in another exploded directory |
| Path dir2 = Files.createTempDirectory("classes"); |
| p = Files.createDirectory(dir2.resolve("p")); |
| Files.write(p.resolve("C2.class"), generateClass("p.C2")); |
| URL url2 = dir2.toUri().toURL(); |
| |
| // load p.C1 and p.C2 |
| ClassLoader loader = new URLClassLoader(new URL[] { url1, url2 }); |
| Class<?> target1 = Class.forName("p.C1", false, loader); |
| Class<?> target2 = Class.forName("p.C2", false, loader); |
| assertTrue(target1.getClassLoader() == loader); |
| assertTrue(target1.getClassLoader() == loader); |
| assertNotEquals(target1.getProtectionDomain(), target2.getProtectionDomain()); |
| |
| // protection domain 1 |
| Lookup lookup1 = privateLookupIn(target1, lookup()); |
| |
| Class<?> clazz = lookup1.defineClass(generateClass("p.Foo")); |
| testSameAbode(clazz, lookup1.lookupClass()); |
| testDiscoverable(clazz, lookup1); |
| |
| // protection domain 2 |
| Lookup lookup2 = privateLookupIn(target2, lookup()); |
| |
| clazz = lookup2.defineClass(generateClass("p.Bar")); |
| testSameAbode(clazz, lookup2.lookupClass()); |
| testDiscoverable(clazz, lookup2); |
| } |
| |
| /** |
| * Test defineClass defining a class to the boot loader |
| */ |
| @Test |
| public void testBootLoader() throws Exception { |
| Lookup lookup = privateLookupIn(Thread.class, lookup()); |
| assertTrue(lookup.getClass().getClassLoader() == null); |
| |
| Class<?> clazz = lookup.defineClass(generateClass("java.lang.Foo")); |
| assertEquals(clazz.getName(), "java.lang.Foo"); |
| testSameAbode(clazz, Thread.class); |
| testDiscoverable(clazz, lookup); |
| } |
| |
| @Test(expectedExceptions = { IllegalArgumentException.class }) |
| public void testWrongPackage() throws Exception { |
| lookup().defineClass(generateClass("other.C")); |
| } |
| |
| @Test(expectedExceptions = { IllegalAccessException.class }) |
| public void testNoPackageAccess() throws Exception { |
| Lookup lookup = lookup().dropLookupMode(PACKAGE); |
| lookup.defineClass(generateClass(THIS_PACKAGE + ".C")); |
| } |
| |
| @Test(expectedExceptions = { ClassFormatError.class }) |
| public void testTruncatedClassFile() throws Exception { |
| lookup().defineClass(new byte[0]); |
| } |
| |
| @Test(expectedExceptions = { NullPointerException.class }) |
| public void testNull() throws Exception { |
| lookup().defineClass(null); |
| } |
| |
| /** |
| * Generates a class file with the given class name |
| */ |
| byte[] generateClass(String className) { |
| ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS |
| + ClassWriter.COMPUTE_FRAMES); |
| cw.visit(V1_9, |
| ACC_PUBLIC + ACC_SUPER, |
| className.replace(".", "/"), |
| null, |
| "java/lang/Object", |
| null); |
| |
| // <init> |
| MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); |
| mv.visitVarInsn(ALOAD, 0); |
| mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); |
| mv.visitInsn(RETURN); |
| mv.visitMaxs(0, 0); |
| mv.visitEnd(); |
| |
| cw.visitEnd(); |
| return cw.toByteArray(); |
| } |
| |
| /** |
| * Generate a class file with the given class name. The class implements Runnable |
| * with a run method to invokestatic the given targetClass/targetMethod. |
| */ |
| byte[] generateRunner(String className, |
| String targetClass, |
| String targetMethod) throws Exception { |
| |
| ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS |
| + ClassWriter.COMPUTE_FRAMES); |
| cw.visit(V1_9, |
| ACC_PUBLIC + ACC_SUPER, |
| className.replace(".", "/"), |
| null, |
| "java/lang/Object", |
| new String[] { "java/lang/Runnable" }); |
| |
| // <init> |
| MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); |
| mv.visitVarInsn(ALOAD, 0); |
| mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); |
| mv.visitInsn(RETURN); |
| mv.visitMaxs(0, 0); |
| mv.visitEnd(); |
| |
| // run() |
| String tc = targetClass.replace(".", "/"); |
| mv = cw.visitMethod(ACC_PUBLIC, "run", "()V", null, null); |
| mv.visitMethodInsn(INVOKESTATIC, tc, targetMethod, "()V", false); |
| mv.visitInsn(RETURN); |
| mv.visitMaxs(0, 0); |
| mv.visitEnd(); |
| |
| cw.visitEnd(); |
| return cw.toByteArray(); |
| } |
| |
| /** |
| * Generate a class file with the given class name. The class will initializer |
| * to invokestatic the given targetClass/targetMethod. |
| */ |
| byte[] generateClassWithInitializer(String className, |
| String targetClass, |
| String targetMethod) throws Exception { |
| |
| ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS |
| + ClassWriter.COMPUTE_FRAMES); |
| cw.visit(V1_9, |
| ACC_PUBLIC + ACC_SUPER, |
| className.replace(".", "/"), |
| null, |
| "java/lang/Object", |
| null); |
| |
| // <init> |
| MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); |
| mv.visitVarInsn(ALOAD, 0); |
| mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); |
| mv.visitInsn(RETURN); |
| mv.visitMaxs(0, 0); |
| mv.visitEnd(); |
| |
| // <clinit> |
| String tc = targetClass.replace(".", "/"); |
| mv = cw.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null); |
| mv.visitMethodInsn(INVOKESTATIC, tc, targetMethod, "()V", false); |
| mv.visitInsn(RETURN); |
| mv.visitMaxs(0, 0); |
| mv.visitEnd(); |
| |
| cw.visitEnd(); |
| return cw.toByteArray(); |
| } |
| |
| private int nextNumber() { |
| return ++nextNumber; |
| } |
| |
| private int nextNumber; |
| } |