jdk-24/test/jdk/java/lang/invoke/defineHiddenClass/BasicTest.java
2024-04-18 05:50:24 +00:00

544 lines
22 KiB
Java

/*
* Copyright (c) 2019, 2024, 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
* @bug 8330467
* @modules jdk.compiler
* @library /test/lib
* @enablePreview
* @comment Change enablePreview with the flag in setup's compileSources
* @compile BadClassFile.jcod
* BadClassFile2.jcod
* BadClassFileVersion.jcod
* @build jdk.test.lib.Utils
* jdk.test.lib.compiler.CompilerUtils
* @run testng/othervm BasicTest
*/
import java.io.File;
import java.io.IOException;
import java.lang.classfile.ClassFile;
import java.lang.constant.ClassDesc;
import java.lang.invoke.MethodHandles.Lookup;
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 java.lang.classfile.ClassFile.*;
import static java.lang.constant.ConstantDescs.CD_Enum;
import static java.lang.constant.ConstantDescs.CD_Object;
import static java.lang.invoke.MethodHandles.lookup;
import static java.lang.invoke.MethodHandles.Lookup.ClassOption.*;
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, "--enable-preview",
"--release", Integer.toString(Runtime.version().feature()));
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<String> 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.isHidden());
assertFalse(c.isPrimitive());
assertTrue(intfs.length == 1);
assertTrue(intfs[0] == HiddenTest.class);
assertTrue(c.getCanonicalName() == null);
String hcName = "HiddenClass";
String hcSuffix = "0x[0-9a-f]+";
assertTrue(c.getName().matches(hcName + "/" + hcSuffix));
assertTrue(c.descriptorString().matches("L" + hcName + "." + hcSuffix + ";"), c.descriptorString());
// test array of hidden class
testHiddenArray(c);
// test setAccessible
checkSetAccessible(c, "realTest");
checkSetAccessible(c, "test");
}
// primitive class is not a hidden class
@Test
public void primitiveClass() {
assertFalse(int.class.isHidden());
assertFalse(String.class.isHidden());
}
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.isHidden());
String hcName = "HiddenClass";
String hcSuffix = "0x[0-9a-f]+";
assertTrue(arrayType.getName().matches("\\[" + "L" + hcName + "/" + hcSuffix + ";"));
assertTrue(arrayType.descriptorString().matches("\\[" + "L" + hcName + "." + hcSuffix + ";"));
assertTrue(arrayType.getComponentType().isHidden());
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;
}
}
}
// Define a hidden class that uses lambda and contains its implementation
// This verifies LambdaMetaFactory supports the caller which is a hidden class
@Test
public void testHiddenLambda() throws Throwable {
HiddenTest t = (HiddenTest)defineHiddenClass("HiddenLambda").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);
}
@DataProvider(name = "hiddenClasses")
private Object[][] hiddenClasses() {
return new Object[][] {
new Object[] { "HiddenInterface", false },
new Object[] { "AbstractClass", false },
// a hidden annotation is useless because it cannot be referenced by any class
new Object[] { "HiddenAnnotation", 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 that class file bytes that can be defined as a normal class
* can be successfully created as a hidden class even it might not
* make sense as a hidden class. For example, a hidden annotation
* is not useful as it cannot be referenced and an outer/inner class
* when defined as a hidden effectively becomes a final top-level class.
*/
@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);
}
@DataProvider(name = "emptyClasses")
private Object[][] emptyClasses() {
return new Object[][] {
new Object[] { "EmptyHiddenSynthetic", ACC_SYNTHETIC },
new Object[] { "EmptyHiddenEnum", ACC_ENUM },
new Object[] { "EmptyHiddenAbstractClass", ACC_ABSTRACT },
new Object[] { "EmptyHiddenInterface", ACC_ABSTRACT|ACC_INTERFACE },
new Object[] { "EmptyHiddenAnnotation", ACC_ANNOTATION|ACC_ABSTRACT|ACC_INTERFACE },
};
}
/*
* Test if an empty class with valid access flags can be created as a hidden class
* as long as it does not violate the restriction of a hidden class.
*
* A meaningful enum type defines constants of that enum type. So
* enum class containing constants of its type should not be a hidden
* class.
*/
@Test(dataProvider = "emptyClasses")
public void emptyHiddenClass(String name, int accessFlags) throws Exception {
byte[] bytes = (accessFlags == ACC_ENUM) ? classBytes(name, CD_Enum, accessFlags)
: classBytes(name, accessFlags);
Class<?> hc = lookup().defineHiddenClass(bytes, false).lookupClass();
switch (accessFlags) {
case ACC_SYNTHETIC:
assertTrue(hc.isSynthetic());
assertFalse(hc.isEnum());
assertFalse(hc.isAnnotation());
assertFalse(hc.isInterface());
break;
case ACC_ENUM:
assertFalse(hc.isSynthetic());
assertTrue(hc.isEnum());
assertFalse(hc.isAnnotation());
assertFalse(hc.isInterface());
break;
case ACC_ABSTRACT:
assertFalse(hc.isSynthetic());
assertFalse(hc.isEnum());
assertFalse(hc.isAnnotation());
assertFalse(hc.isInterface());
break;
case ACC_ABSTRACT|ACC_INTERFACE:
assertFalse(hc.isSynthetic());
assertFalse(hc.isEnum());
assertFalse(hc.isAnnotation());
assertTrue(hc.isInterface());
break;
case ACC_ANNOTATION|ACC_ABSTRACT|ACC_INTERFACE:
assertFalse(hc.isSynthetic());
assertFalse(hc.isEnum());
assertTrue(hc.isAnnotation());
assertTrue(hc.isInterface());
break;
default:
throw new IllegalArgumentException("unexpected access flag: " + accessFlags);
}
assertTrue(hc.isHidden());
assertTrue(hc.getModifiers() == (ACC_PUBLIC|accessFlags));
assertFalse(hc.isLocalClass());
assertFalse(hc.isMemberClass());
assertFalse(hc.isAnonymousClass());
assertFalse(hc.isArray());
}
// These class files can't be defined as hidden classes
@DataProvider(name = "cantBeHiddenClasses")
private Object[][] cantBeHiddenClasses() {
return new Object[][] {
// a hidden class can't be a field's declaring type
// enum class with static final HiddenEnum[] $VALUES:
new Object[] { "HiddenEnum" },
// supertype of this class is a hidden class
new Object[] { "HiddenSuper" },
// a record class whose equals(HiddenRecord, Object) method
// refers to a hidden class in the parameter type and fails
// verification. Perhaps this method signature should be reconsidered.
new Object[] { "HiddenRecord" },
};
}
/*
* These class files
*/
@Test(dataProvider = "cantBeHiddenClasses", expectedExceptions = NoClassDefFoundError.class)
public void failToDeriveAsHiddenClass(String name) throws Exception {
byte[] bytes = Files.readAllBytes(CLASSES_DIR.resolve(name + ".class"));
Class<?> hc = lookup().defineHiddenClass(bytes, false).lookupClass();
}
/*
* A hidden class can be successfully created but fails to be reflected
* if it refers to its own type in the descriptor.
* e.g. Class::getMethods resolves the declaring type of fields,
* parameter types and return type.
*/
@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;
}
}
}
@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);
}
@Test(expectedExceptions = { UnsupportedClassVersionError.class })
public void badClassFileVersion() throws Throwable {
Path dir = Paths.get(System.getProperty("test.classes", "."));
byte[] bytes = Files.readAllBytes(dir.resolve("BadClassFileVersion.class"));
lookup().defineHiddenClass(bytes, false);
}
// malformed class files
@DataProvider(name = "malformedClassFiles")
private Object[][] malformedClassFiles() throws IOException {
Path dir = Paths.get(System.getProperty("test.classes", "."));
return new Object[][] {
// `this_class` has invalid CP entry
new Object[] { Files.readAllBytes(dir.resolve("BadClassFile.class")) },
new Object[] { Files.readAllBytes(dir.resolve("BadClassFile2.class")) },
// truncated file
new Object[] { new byte[0] },
new Object[] { new byte[] {(byte) 0xCA, (byte) 0xBA, (byte) 0xBE, (byte) 0x00} },
};
}
@Test(dataProvider = "malformedClassFiles", expectedExceptions = ClassFormatError.class)
public void badClassFile(byte[] bytes) throws Throwable {
lookup().defineHiddenClass(bytes, 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.isHidden());
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.isHidden());
assertTrue(hc.getCanonicalName() == null);
assertTrue(hc.getName().contains("/"));
assertFalse(hc.isAnonymousClass());
assertFalse(hc.isLocalClass());
assertFalse(hc.isMemberClass());
assertFalse(hc.getSimpleName().isEmpty()); // sanity check
}
private static byte[] classBytes(String classname, int accessFlags) {
return classBytes(classname, CD_Object, accessFlags);
}
private static byte[] classBytes(String classname, ClassDesc superType, int accessFlags) {
return ClassFile.of().build(ClassDesc.ofInternalName(classname), clb -> clb
.withVersion(JAVA_14_VERSION, 0)
.withFlags(accessFlags | ACC_PUBLIC)
.withSuperclass(superType));
}
}