--- /dev/null 2020-03-26 16:03:09.000000000 -0700 +++ new/test/jdk/java/lang/invoke/defineHiddenClass/BasicTest.java 2020-03-26 16:03:08.000000000 -0700 @@ -0,0 +1,374 @@ +/* + * Copyright (c) 2019, 2020, 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 + * @library /test/lib + * @build jdk.test.lib.Utils + * jdk.test.lib.compiler.CompilerUtils + * BasicTest + * @run testng/othervm BasicTest + * @run testng/othervm -Xcomp BasicTest + */ + +import java.io.File; +import java.io.IOException; +import java.lang.invoke.MethodHandles.Lookup; + +import static java.lang.invoke.MethodHandles.lookup; +import static java.lang.invoke.MethodHandles.Lookup.ClassOption.*; + +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import jdk.test.lib.compiler.CompilerUtils; +import jdk.test.lib.Utils; + +import org.testng.annotations.BeforeTest; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import static org.testng.Assert.*; + +interface HiddenTest { + void test(); +} + +public class BasicTest { + + private static final Path SRC_DIR = Paths.get(Utils.TEST_SRC, "src"); + private static final Path CLASSES_DIR = Paths.get("classes"); + private static final Path CLASSES_10_DIR = Paths.get("classes_10"); + + private static byte[] hiddenClassBytes; + + @BeforeTest + static void setup() throws IOException { + compileSources(SRC_DIR, CLASSES_DIR); + + hiddenClassBytes = Files.readAllBytes(CLASSES_DIR.resolve("HiddenClass.class")); + + // compile with --release 10 with no NestHost and NestMembers attribute + compileSources(SRC_DIR.resolve("Outer.java"), CLASSES_10_DIR, "--release", "10"); + compileSources(SRC_DIR.resolve("EnclosingClass.java"), CLASSES_10_DIR, "--release", "10"); + } + + static void compileSources(Path sourceFile, Path dest, String... options) throws IOException { + Stream ops = Stream.of("-cp", Utils.TEST_CLASSES + File.pathSeparator + CLASSES_DIR); + if (options != null && options.length > 0) { + ops = Stream.concat(ops, Arrays.stream(options)); + } + if (!CompilerUtils.compile(sourceFile, dest, ops.toArray(String[]::new))) { + throw new RuntimeException("Compilation of the test failed: " + sourceFile); + } + } + + static Class defineHiddenClass(String name) throws Exception { + byte[] bytes = Files.readAllBytes(CLASSES_DIR.resolve(name + ".class")); + Class hc = lookup().defineHiddenClass(bytes, false).lookupClass(); + assertHiddenClass(hc); + singletonNest(hc); + return hc; + } + + // basic test on a hidden class + @Test + public void hiddenClass() throws Throwable { + HiddenTest t = (HiddenTest)defineHiddenClass("HiddenClass").newInstance(); + t.test(); + + // sanity check + Class c = t.getClass(); + Class[] intfs = c.getInterfaces(); + assertTrue(c.isHiddenClass()); + assertFalse(c.isPrimitive()); + assertTrue(intfs.length == 1); + assertTrue(intfs[0] == HiddenTest.class); + assertTrue(c.getCanonicalName() == null); + assertTrue(c.getName().startsWith("HiddenClass/")); + + // test array of hidden class + testHiddenArray(c); + + // test setAccessible + checkSetAccessible(c, "realTest"); + checkSetAccessible(c, "test"); + } + + @Test + public void primitiveClass() { + assertFalse(int.class.isHiddenClass()); + assertFalse(String.class.isHiddenClass()); + } + + private void testHiddenArray(Class type) throws Exception { + // array of hidden class + Object array = Array.newInstance(type, 2); + Class arrayType = array.getClass(); + assertTrue(arrayType.isArray()); + assertTrue(Array.getLength(array) == 2); + assertFalse(arrayType.isHiddenClass()); + assertTrue(arrayType.getName().startsWith("[LHiddenClass/"), "unexpected name: " + arrayType.getName()); + + assertTrue(arrayType.getComponentType().isHiddenClass()); + assertTrue(arrayType.getComponentType() == type); + Object t = type.newInstance(); + Array.set(array, 0, t); + Object o = Array.get(array, 0); + assertTrue(o == t); + } + + private void checkSetAccessible(Class c, String name, Class... ptypes) throws Exception { + Method m = c.getDeclaredMethod(name, ptypes); + assertTrue(m.trySetAccessible()); + m.setAccessible(true); + } + + // Define a hidden class that uses lambda + // This verifies LambdaMetaFactory supports the caller which is a hidden class + @Test + public void testLambda() throws Throwable { + HiddenTest t = (HiddenTest)defineHiddenClass("Lambda").newInstance(); + try { + t.test(); + } catch (Error e) { + if (!e.getMessage().equals("thrown by " + t.getClass().getName())) { + throw e; + } + } + } + + // Verify the nest host and nest members of a hidden class and hidden nestmate class + @Test + public void testHiddenNestHost() throws Throwable { + byte[] hc1 = hiddenClassBytes; + Lookup lookup1 = lookup().defineHiddenClass(hc1, false); + Class host = lookup1.lookupClass(); + + byte[] hc2 = Files.readAllBytes(CLASSES_DIR.resolve("Lambda.class")); + Lookup lookup2 = lookup1.defineHiddenClass(hc2, false, NESTMATE); + Class member = lookup2.lookupClass(); + + // test nest membership and reflection API + assertTrue(host.isNestmateOf(member)); + assertTrue(host.getNestHost() == host); + // getNestHost and getNestMembers return the same value when calling + // on a nest member and the nest host + assertTrue(member.getNestHost() == host.getNestHost()); + assertTrue(Arrays.equals(member.getNestMembers(), host.getNestMembers())); + // getNestMembers includes the nest host that can be a hidden class but + // only includes static nest members + assertTrue(host.getNestMembers().length == 1); + assertTrue(host.getNestMembers()[0] == host); + } + + @Test + public void hiddenCantReflect() throws Throwable { + HiddenTest t = (HiddenTest)defineHiddenClass("HiddenCantReflect").newInstance(); + t.test(); + + Class c = t.getClass(); + Class[] intfs = c.getInterfaces(); + assertTrue(intfs.length == 1); + assertTrue(intfs[0] == HiddenTest.class); + + try { + // this would cause loading of class HiddenCantReflect and NCDFE due + // to error during verification + c.getDeclaredMethods(); + } catch (NoClassDefFoundError e) { + Throwable x = e.getCause(); + if (x == null || !(x instanceof ClassNotFoundException && x.getMessage().contains("HiddenCantReflect"))) { + throw e; + } + } + } + + @DataProvider(name = "hiddenClasses") + private Object[][] hiddenClasses() { + return new Object[][] { + new Object[] { "HiddenInterface", false }, + new Object[] { "AbstractClass", false }, + // class file with bad NestHost, NestMembers and InnerClasses or EnclosingMethod attribute + // define them as nestmate to verify Class::getNestHost and getNestMembers + new Object[] { "Outer", true }, + new Object[] { "Outer$Inner", true }, + new Object[] { "EnclosingClass", true }, + new Object[] { "EnclosingClass$1", true }, + }; + } + + @Test(dataProvider = "hiddenClasses") + public void defineHiddenClass(String name, boolean nestmate) throws Exception { + byte[] bytes = Files.readAllBytes(CLASSES_DIR.resolve(name + ".class")); + Class hc; + Class host; + if (nestmate) { + hc = lookup().defineHiddenClass(bytes, false, NESTMATE).lookupClass(); + host = lookup().lookupClass().getNestHost(); + } else { + hc = lookup().defineHiddenClass(bytes, false).lookupClass(); + host = hc; + } + assertTrue(hc.getNestHost() == host); + assertTrue(hc.getNestMembers().length == 1); + assertTrue(hc.getNestMembers()[0] == host); + } + + @Test(expectedExceptions = NoClassDefFoundError.class) + public void hiddenSuperClass() throws Exception { + byte[] bytes = Files.readAllBytes(CLASSES_DIR.resolve("HiddenSuper.class")); + Class hc = lookup().defineHiddenClass(bytes, false).lookupClass(); + } + + @Test(expectedExceptions = {IllegalArgumentException.class}) + public void cantDefineModule() throws Throwable { + Path src = Paths.get("module-info.java"); + Path dir = CLASSES_DIR.resolve("m"); + Files.write(src, List.of("module m {}"), StandardCharsets.UTF_8); + compileSources(src, dir); + + byte[] bytes = Files.readAllBytes(dir.resolve("module-info.class")); + lookup().defineHiddenClass(bytes, false); + } + + @Test(expectedExceptions = {IllegalArgumentException.class}) + public void cantDefineClassInAnotherPackage() throws Throwable { + Path src = Paths.get("ClassInAnotherPackage.java"); + Files.write(src, List.of("package p;", "public class ClassInAnotherPackage {}"), StandardCharsets.UTF_8); + compileSources(src, CLASSES_DIR); + + byte[] bytes = Files.readAllBytes(CLASSES_DIR.resolve("p").resolve("ClassInAnotherPackage.class")); + lookup().defineHiddenClass(bytes, false); + } + + @Test(expectedExceptions = {IllegalAccessException.class}) + public void lessPrivilegedLookup() throws Throwable { + Lookup lookup = lookup().dropLookupMode(Lookup.PRIVATE); + lookup.defineHiddenClass(hiddenClassBytes, false); + } + + @DataProvider(name = "nestedTypesOrAnonymousClass") + private Object[][] nestedTypesOrAnonymousClass() { + return new Object[][] { + // class file with bad InnerClasses or EnclosingMethod attribute + new Object[] { "Outer", null }, + new Object[] { "Outer$Inner", "Outer" }, + new Object[] { "EnclosingClass", null }, + new Object[] { "EnclosingClass$1", "EnclosingClass" }, + }; + } + + @Test(dataProvider = "nestedTypesOrAnonymousClass") + public void hasInnerClassesOrEnclosingMethodAttribute(String className, String badDeclaringClassName) throws Throwable { + byte[] bytes = Files.readAllBytes(CLASSES_10_DIR.resolve(className + ".class")); + Class hc = lookup().defineHiddenClass(bytes, false).lookupClass(); + hiddenClassWithBadAttribute(hc, badDeclaringClassName); + } + + // define a hidden class with static nest membership + @Test + public void hasStaticNestHost() throws Exception { + byte[] bytes = Files.readAllBytes(CLASSES_DIR.resolve("Outer$Inner.class")); + Class hc = lookup().defineHiddenClass(bytes, false).lookupClass(); + hiddenClassWithBadAttribute(hc, "Outer"); + } + + @Test + public void hasStaticNestMembers() throws Throwable { + byte[] bytes = Files.readAllBytes(CLASSES_DIR.resolve("Outer.class")); + Class hc = lookup().defineHiddenClass(bytes, false).lookupClass(); + assertHiddenClass(hc); + assertTrue(hc.getNestHost() == hc); + Class[] members = hc.getNestMembers(); + assertTrue(members.length == 1 && members[0] == hc); + } + + // a hidden class with bad InnerClasses or EnclosingMethod attribute + private void hiddenClassWithBadAttribute(Class hc, String badDeclaringClassName) { + assertTrue(hc.isHiddenClass()); + assertTrue(hc.getCanonicalName() == null); + assertTrue(hc.getName().contains("/")); + + if (badDeclaringClassName == null) { + // the following reflection API assumes a good name in InnerClasses + // or EnclosingMethod attribute can successfully be resolved. + assertTrue(hc.getSimpleName().length() > 0); + assertFalse(hc.isAnonymousClass()); + assertFalse(hc.isLocalClass()); + assertFalse(hc.isMemberClass()); + } else { + declaringClassNotFound(hc, badDeclaringClassName); + } + + // validation of nest membership + assertTrue(hc.getNestHost() == hc); + // validate the static nest membership + Class[] members = hc.getNestMembers(); + assertTrue(members.length == 1 && members[0] == hc); + } + + // Class::getSimpleName, Class::isMemberClass + private void declaringClassNotFound(Class c, String cn) { + try { + // fail to find declaring/enclosing class + c.isMemberClass(); + assertTrue(false); + } catch (NoClassDefFoundError e) { + if (!e.getMessage().equals(cn)) { + throw e; + } + } + try { + // fail to find declaring/enclosing class + c.getSimpleName(); + assertTrue(false); + } catch (NoClassDefFoundError e) { + if (!e.getMessage().equals(cn)) { + throw e; + } + } + } + + private static void singletonNest(Class hc) { + assertTrue(hc.getNestHost() == hc); + assertTrue(hc.getNestMembers().length == 1); + assertTrue(hc.getNestMembers()[0] == hc); + } + + private static void assertHiddenClass(Class hc) { + assertTrue(hc.isHiddenClass()); + assertTrue(hc.getCanonicalName() == null); + assertTrue(hc.getName().contains("/")); + assertFalse(hc.isAnonymousClass()); + assertFalse(hc.isLocalClass()); + assertFalse(hc.isMemberClass()); + assertFalse(hc.getSimpleName().isEmpty()); // sanity check + } +}