/* * Copyright (c) 2020, 2023, 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 8230501 * @library /test/lib * @enablePreview * @run testng/othervm ClassDataTest */ import java.io.IOException; import java.io.OutputStream; import java.io.UncheckedIOException; import java.lang.classfile.ClassBuilder; import java.lang.classfile.ClassFile; import java.lang.classfile.TypeKind; import java.lang.constant.ClassDesc; import java.lang.constant.ConstantDescs; import java.lang.constant.DirectMethodHandleDesc; import java.lang.constant.DynamicConstantDesc; import java.lang.constant.MethodTypeDesc; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodHandles.Lookup; import java.lang.invoke.MethodType; import java.lang.reflect.AccessFlag; import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.Consumer; import java.util.stream.Stream; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import static java.lang.classfile.ClassFile.*; import static java.lang.constant.ConstantDescs.*; import static java.lang.invoke.MethodHandles.Lookup.*; import static org.testng.Assert.*; public class ClassDataTest { private static final Lookup LOOKUP = MethodHandles.lookup(); private static final ClassDesc CD_ClassDataTest = ClassDataTest.class.describeConstable().orElseThrow(); @Test public void testOriginalAccess() throws IllegalAccessException { Lookup lookup = hiddenClass(20); assertTrue(lookup.hasFullPrivilegeAccess()); int value = MethodHandles.classData(lookup, "_", int.class); assertEquals(value, 20); Integer i = MethodHandles.classData(lookup, "_", Integer.class); assertEquals(i.intValue(), 20); } /* * A lookup class with no class data. */ @Test public void noClassData() throws IllegalAccessException { assertNull(MethodHandles.classData(LOOKUP, "_", Object.class)); } @DataProvider(name = "teleportedLookup") private Object[][] teleportedLookup() throws ReflectiveOperationException { Lookup lookup = hiddenClass(30); Class hc = lookup.lookupClass(); assertClassData(lookup, 30); int fullAccess = PUBLIC|PROTECTED|PACKAGE|MODULE|PRIVATE; return new Object[][] { new Object[] { MethodHandles.privateLookupIn(hc, LOOKUP), fullAccess}, new Object[] { LOOKUP.in(hc), fullAccess & ~(PROTECTED|PRIVATE) }, new Object[] { lookup.dropLookupMode(PRIVATE), fullAccess & ~(PROTECTED|PRIVATE) }, }; } @Test(dataProvider = "teleportedLookup", expectedExceptions = { IllegalAccessException.class }) public void illegalAccess(Lookup lookup, int access) throws IllegalAccessException { int lookupModes = lookup.lookupModes(); assertTrue((lookupModes & ORIGINAL) == 0); assertEquals(lookupModes, access); MethodHandles.classData(lookup, "_", int.class); } @Test(expectedExceptions = { ClassCastException.class }) public void incorrectType() throws IllegalAccessException { Lookup lookup = hiddenClass(20); MethodHandles.classData(lookup, "_", Long.class); } @Test(expectedExceptions = { IndexOutOfBoundsException.class }) public void invalidIndex() throws IllegalAccessException { Lookup lookup = hiddenClass(List.of()); MethodHandles.classDataAt(lookup, "_", Object.class, 0); } @Test(expectedExceptions = { NullPointerException.class }) public void unboxNull() throws IllegalAccessException { List list = new ArrayList<>(); list.add(null); Lookup lookup = hiddenClass(list); MethodHandles.classDataAt(lookup, "_", int.class, 0); } @Test public void nullElement() throws IllegalAccessException { List list = new ArrayList<>(); list.add(null); Lookup lookup = hiddenClass(list); assertTrue(MethodHandles.classDataAt(lookup, "_", Object.class, 0) == null); } @Test public void intClassData() throws ReflectiveOperationException { ClassByteBuilder builder = new ClassByteBuilder("T1-int"); byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, int.class).build(); Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, 100, true); int value = MethodHandles.classData(lookup, "_", int.class); assertEquals(value, 100); // call through condy assertClassData(lookup, 100); } @Test public void floatClassData() throws ReflectiveOperationException { ClassByteBuilder builder = new ClassByteBuilder("T1-float"); byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, float.class).build(); Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, 0.1234f, true); float value = MethodHandles.classData(lookup, "_", float.class); assertEquals(value, 0.1234f); // call through condy assertClassData(lookup, 0.1234f); } @Test public void classClassData() throws ReflectiveOperationException { Class hc = hiddenClass(100).lookupClass(); ClassByteBuilder builder = new ClassByteBuilder("T2"); byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, Class.class).build(); Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, hc, true); Class value = MethodHandles.classData(lookup, "_", Class.class); assertEquals(value, hc); // call through condy assertClassData(lookup, hc); } @Test public void arrayClassData() throws ReflectiveOperationException { ClassByteBuilder builder = new ClassByteBuilder("T3"); byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, String[].class).build(); String[] colors = new String[] { "red", "yellow", "blue"}; Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, colors, true); assertClassData(lookup, colors.clone()); // class data is modifiable and not a constant colors[0] = "black"; // it will get back the modified class data String[] value = MethodHandles.classData(lookup, "_", String[].class); assertEquals(value, colors); // even call through condy as it's not a constant assertClassData(lookup, colors); } @Test public void listClassData() throws ReflectiveOperationException { ClassByteBuilder builder = new ClassByteBuilder("T4"); byte[] bytes = builder.classDataAt(ACC_PUBLIC|ACC_STATIC, Integer.class, 2).build(); List cd = List.of(100, 101, 102, 103); int expected = 102; // element at index=2 Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, cd, true); int value = MethodHandles.classDataAt(lookup, "_", int.class, 2); assertEquals(value, expected); // call through condy assertClassData(lookup, expected); } @Test public void arrayListClassData() throws ReflectiveOperationException { ClassByteBuilder builder = new ClassByteBuilder("T4"); byte[] bytes = builder.classDataAt(ACC_PUBLIC|ACC_STATIC, Integer.class, 1).build(); ArrayList cd = new ArrayList<>(); Stream.of(100, 101, 102, 103).forEach(cd::add); int expected = 101; // element at index=1 Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, cd, true); int value = MethodHandles.classDataAt(lookup, "_", int.class, 1); assertEquals(value, expected); // call through condy assertClassData(lookup, expected); } private static Lookup hiddenClass(int value) { ClassByteBuilder builder = new ClassByteBuilder("HC"); byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, int.class).build(); try { return LOOKUP.defineHiddenClassWithClassData(bytes, value, true); } catch (Throwable e) { throw new RuntimeException(e); } } private static Lookup hiddenClass(List list) { ClassByteBuilder builder = new ClassByteBuilder("HC"); byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, List.class).build(); try { return LOOKUP.defineHiddenClassWithClassData(bytes, list, true); } catch (Throwable e) { throw new RuntimeException(e); } } @Test public void condyInvokedFromVirtualMethod() throws ReflectiveOperationException { ClassByteBuilder builder = new ClassByteBuilder("T5"); // generate classData instance method byte[] bytes = builder.classData(ACC_PUBLIC, Class.class).build(); Lookup hcLookup = hiddenClass(100); assertClassData(hcLookup, 100); Class hc = hcLookup.lookupClass(); Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, hc, true); Class value = MethodHandles.classData(lookup, "_", Class.class); assertEquals(value, hc); // call through condy Class c = lookup.lookupClass(); assertClassData(lookup, c.newInstance(), hc); } @Test public void immutableListClassData() throws ReflectiveOperationException { ClassByteBuilder builder = new ClassByteBuilder("T6"); // generate classDataAt instance method byte[] bytes = builder.classDataAt(ACC_PUBLIC, Integer.class, 2).build(); List cd = List.of(100, 101, 102, 103); int expected = 102; // element at index=2 Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, cd, true); int value = MethodHandles.classDataAt(lookup, "_", int.class, 2); assertEquals(value, expected); // call through condy Class c = lookup.lookupClass(); assertClassData(lookup, c.newInstance() ,expected); } /* * The return value of MethodHandles::classDataAt is the element * contained in the list when the method is called. * If MethodHandles::classDataAt is called via condy, the value * will be captured as a constant. If the class data is modified * after the element at the given index is computed via condy, * subsequent LDC of such ConstantDynamic entry will return the same * value. However, direct invocation of MethodHandles::classDataAt * will return the modified value. */ @Test public void mutableListClassData() throws ReflectiveOperationException { ClassByteBuilder builder = new ClassByteBuilder("T7"); // generate classDataAt instance method byte[] bytes = builder.classDataAt(ACC_PUBLIC, MethodType.class, 0).build(); MethodType mtype = MethodType.methodType(int.class, String.class); List cd = new ArrayList<>(List.of(mtype)); Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, cd, true); // call through condy Class c = lookup.lookupClass(); assertClassData(lookup, c.newInstance(), mtype); // modify the class data assertTrue(cd.remove(0) == mtype); cd.add(0, MethodType.methodType(void.class)); MethodType newMType = cd.get(0); // loading the element using condy returns the original value assertClassData(lookup, c.newInstance(), mtype); // direct invocation of MethodHandles.classDataAt returns the modified value assertEquals(MethodHandles.classDataAt(lookup, "_", MethodType.class, 0), newMType); } // helper method to extract from a class data map public static T getClassDataEntry(Lookup lookup, String key, Class type) throws IllegalAccessException { Map cd = MethodHandles.classData(lookup, "_", Map.class); return type.cast(cd.get(key)); } @Test public void classDataMap() throws ReflectiveOperationException { ClassByteBuilder builder = new ClassByteBuilder("map"); // generate classData static method DirectMethodHandleDesc bsm = ConstantDescs.ofConstantBootstrap(CD_ClassDataTest, "getClassDataEntry", CD_Object); // generate two accessor methods to get the entries from class data byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, Map.class) .classData(ACC_PUBLIC|ACC_STATIC, "getClass", Class.class, DynamicConstantDesc.ofNamed(bsm, "class", CD_Class)) .classData(ACC_PUBLIC|ACC_STATIC, "getMethod", MethodHandle.class, DynamicConstantDesc.ofNamed(bsm, "method", CD_MethodHandle)) .build(); // generate a hidden class Lookup hcLookup = hiddenClass(100); Class hc = hcLookup.lookupClass(); assertClassData(hcLookup, 100); MethodHandle mh = hcLookup.findStatic(hc, "classData", MethodType.methodType(int.class)); Map cd = Map.of("class", hc, "method", mh); Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, cd, true); assertClassData(lookup, cd); // validate the entries from the class data map Class c = lookup.lookupClass(); Method m = c.getMethod("getClass"); Class v = (Class)m.invoke(null); assertEquals(hc, v); Method m1 = c.getMethod("getMethod"); MethodHandle v1 = (MethodHandle) m1.invoke(null); assertEquals(mh, v1); } @Test(expectedExceptions = { IllegalArgumentException.class }) public void nonDefaultName() throws ReflectiveOperationException { ClassByteBuilder builder = new ClassByteBuilder("nonDefaultName"); byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, Class.class) .build(); Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, ClassDataTest.class, true); assertClassData(lookup, ClassDataTest.class); // throw IAE MethodHandles.classData(lookup, "non_default_name", Class.class); } static class ClassByteBuilder { private static final String OBJECT_CLS = "java/lang/Object"; private static final String MHS_CLS = "java/lang/invoke/MethodHandles"; private static final String CLASS_DATA_BSM_DESCR = "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;"; private Consumer cw; private final ClassDesc classname; /** * A builder to generate a class file to access class data * @param classname */ ClassByteBuilder(String classname) { this.classname = ClassDesc.ofInternalName(classname); this.cw = clb -> { clb.withSuperclass(CD_Object); clb.withFlags(AccessFlag.FINAL); clb.withMethodBody(INIT_NAME, MTD_void, ACC_PUBLIC, cob -> { cob.aload(0); cob.invokespecial(CD_Object, INIT_NAME, MTD_void); cob.return_(); }); }; } byte[] build() { byte[] bytes = ClassFile.of().build(classname, cw); Path p = Paths.get(classname + ".class"); try (OutputStream os = Files.newOutputStream(p)) { os.write(bytes); } catch (IOException e) { throw new UncheckedIOException(e); } return bytes; } /* * Generate classData method to load class data via condy */ ClassByteBuilder classData(int accessFlags, Class returnType) { ClassDesc returnDesc = returnType.describeConstable().orElseThrow(); MethodTypeDesc mt = MethodTypeDesc.of(returnDesc); cw = cw.andThen(clb -> { clb.withMethodBody("classData", mt, accessFlags, cob -> { cob.loadConstant(DynamicConstantDesc.ofNamed(BSM_CLASS_DATA, DEFAULT_NAME, returnDesc)); cob.return_(TypeKind.from(returnType)); }); }); return this; } /* * Generate classDataAt method to load an element from class data via condy */ ClassByteBuilder classDataAt(int accessFlags, Class returnType, int index) { ClassDesc returnDesc = returnType.describeConstable().orElseThrow(); MethodTypeDesc mt = MethodTypeDesc.of(returnDesc); cw = cw.andThen(clb -> { clb.withMethodBody("classData", mt, accessFlags, cob -> { cob.loadConstant(DynamicConstantDesc.ofNamed(BSM_CLASS_DATA_AT, DEFAULT_NAME, returnDesc, index)); cob.return_(TypeKind.from(returnType)); }); }); return this; } ClassByteBuilder classData(int accessFlags, String name, Class returnType, DynamicConstantDesc dynamic) { ClassDesc returnDesc = returnType.describeConstable().orElseThrow(); MethodTypeDesc mt = MethodTypeDesc.of(returnDesc); cw = cw.andThen(clb -> { clb.withMethodBody(name, mt, accessFlags, cob -> { cob.loadConstant(dynamic); cob.return_(TypeKind.from(returnType)); }); }); return this; } } /* * Load an int constant from class data via condy and * verify it matches the given value. */ private void assertClassData(Lookup lookup, int value) throws ReflectiveOperationException { Class c = lookup.lookupClass(); Method m = c.getMethod("classData"); int v = (int) m.invoke(null); assertEquals(value, v); } /* * Load an int constant from class data via condy and * verify it matches the given value. */ private void assertClassData(Lookup lookup, Object o, int value) throws ReflectiveOperationException { Class c = lookup.lookupClass(); Method m = c.getMethod("classData"); int v = (int) m.invoke(o); assertEquals(value, v); } /* * Load a float constant from class data via condy and * verify it matches the given value. */ private void assertClassData(Lookup lookup, float value) throws ReflectiveOperationException { Class c = lookup.lookupClass(); Method m = c.getMethod("classData"); float v = (float) m.invoke(null); assertEquals(value, v); } /* * Load a Class constant from class data via condy and * verify it matches the given value. */ private void assertClassData(Lookup lookup, Class value) throws ReflectiveOperationException { Class c = lookup.lookupClass(); Method m = c.getMethod("classData"); Class v = (Class)m.invoke(null); assertEquals(value, v); } /* * Load a Class from class data via condy and * verify it matches the given value. */ private void assertClassData(Lookup lookup, Object o, Class value) throws ReflectiveOperationException { Class c = lookup.lookupClass(); Method m = c.getMethod("classData"); Object v = m.invoke(o); assertEquals(value, v); } /* * Load an Object from class data via condy and * verify it matches the given value. */ private void assertClassData(Lookup lookup, Object value) throws ReflectiveOperationException { Class c = lookup.lookupClass(); Method m = c.getMethod("classData"); Object v = m.invoke(null); assertEquals(value, v); } /* * Load an Object from class data via condy and * verify it matches the given value. */ private void assertClassData(Lookup lookup, Object o, Object value) throws ReflectiveOperationException { Class c = lookup.lookupClass(); Method m = c.getMethod("classData"); Object v = m.invoke(o); assertEquals(value, v); } }