1 /*
  2  * Copyright (c) 2020, 2026, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 
 24 /*
 25  * @test
 26  * @bug 8230501
 27  * @library /test/lib
 28  * @run junit/othervm ClassDataTest
 29  */
 30 
 31 import java.io.IOException;
 32 import java.io.OutputStream;
 33 import java.io.UncheckedIOException;
 34 import java.lang.classfile.ClassBuilder;
 35 import java.lang.classfile.ClassFile;
 36 import java.lang.classfile.TypeKind;
 37 import java.lang.constant.ClassDesc;
 38 import java.lang.constant.ConstantDescs;
 39 import java.lang.constant.DirectMethodHandleDesc;
 40 import java.lang.constant.DynamicConstantDesc;
 41 import java.lang.constant.MethodTypeDesc;
 42 import java.lang.invoke.MethodHandle;
 43 import java.lang.invoke.MethodHandles;
 44 import java.lang.invoke.MethodHandles.Lookup;
 45 import java.lang.invoke.MethodType;
 46 import java.lang.reflect.AccessFlag;
 47 import java.lang.reflect.Method;
 48 import java.nio.file.Files;
 49 import java.nio.file.Path;
 50 import java.nio.file.Paths;
 51 import java.util.ArrayList;
 52 import java.util.List;
 53 import java.util.Map;
 54 import java.util.function.Consumer;
 55 import java.util.stream.Stream;
 56 
 57 
 58 import static java.lang.classfile.ClassFile.*;
 59 import static java.lang.constant.ConstantDescs.*;
 60 import static java.lang.invoke.MethodHandles.Lookup.*;
 61 import static org.junit.jupiter.api.Assertions.*;
 62 import org.junit.jupiter.api.Test;
 63 import org.junit.jupiter.params.ParameterizedTest;
 64 import org.junit.jupiter.params.provider.MethodSource;
 65 
 66 public class ClassDataTest {
 67     private static final Lookup LOOKUP = MethodHandles.lookup();
 68     private static final ClassDesc CD_ClassDataTest = ClassDataTest.class.describeConstable().orElseThrow();
 69 
 70     @Test
 71     public void testOriginalAccess() throws IllegalAccessException {
 72         Lookup lookup = hiddenClass(20);
 73         assertTrue(lookup.hasFullPrivilegeAccess());
 74 
 75         int value = MethodHandles.classData(lookup, "_", int.class);
 76         assertEquals(20, value);
 77 
 78         Integer i = MethodHandles.classData(lookup, "_", Integer.class);
 79         assertEquals(20, i.intValue());
 80     }
 81 
 82     /*
 83      * A lookup class with no class data.
 84      */
 85     @Test
 86     public void noClassData() throws IllegalAccessException {
 87         assertNull(MethodHandles.classData(LOOKUP, "_", Object.class));
 88     }
 89 
 90     private static Object[][] teleportedLookup() throws ReflectiveOperationException {
 91         Lookup lookup = hiddenClass(30);
 92         Class<?> hc = lookup.lookupClass();
 93         assertClassData(lookup, 30);
 94 
 95         int fullAccess = PUBLIC|PROTECTED|PACKAGE|MODULE|PRIVATE;
 96         return new Object[][] {
 97                 new Object[] { MethodHandles.privateLookupIn(hc, LOOKUP), fullAccess},
 98                 new Object[] { LOOKUP.in(hc), fullAccess & ~(PROTECTED|PRIVATE) },
 99                 new Object[] { lookup.dropLookupMode(PRIVATE), fullAccess & ~(PROTECTED|PRIVATE) },
100         };
101     }
102 
103     @ParameterizedTest
104     @MethodSource("teleportedLookup")
105     public void illegalAccess(Lookup lookup, int access) throws IllegalAccessException {
106         int lookupModes = lookup.lookupModes();
107         assertEquals(0, lookupModes & ORIGINAL);
108         assertEquals(access, lookupModes);
109         assertThrows(IllegalAccessException.class, () -> MethodHandles.classData(lookup, "_", int.class));
110     }
111 
112     @Test
113     public void incorrectType() throws IllegalAccessException {
114         Lookup lookup = hiddenClass(20);
115         assertThrows(ClassCastException.class, () -> MethodHandles.classData(lookup, "_", Long.class));
116     }
117 
118     @Test
119     public void invalidIndex() throws IllegalAccessException {
120         Lookup lookup = hiddenClass(List.of());
121         assertThrows(IndexOutOfBoundsException.class, () -> MethodHandles.classDataAt(lookup, "_", Object.class, 0));
122     }
123 
124     @Test
125     public void unboxNull() throws IllegalAccessException {
126         List<Integer> list = new ArrayList<>();
127         list.add(null);
128         Lookup lookup = hiddenClass(list);
129         assertThrows(NullPointerException.class, () -> MethodHandles.classDataAt(lookup, "_", int.class, 0));
130     }
131 
132     @Test
133     public void nullElement() throws IllegalAccessException {
134         List<Object> list = new ArrayList<>();
135         list.add(null);
136         Lookup lookup = hiddenClass(list);
137         assertNull(MethodHandles.classDataAt(lookup, "_", Object.class, 0));
138     }
139 
140     @Test
141     public void intClassData() throws ReflectiveOperationException {
142         ClassByteBuilder builder = new ClassByteBuilder("T1-int");
143         byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, int.class).build();
144         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, 100, true);
145         int value = MethodHandles.classData(lookup, "_", int.class);
146         assertEquals(100, value);
147         // call through condy
148         assertClassData(lookup, 100);
149     }
150 
151     @Test
152     public void floatClassData() throws ReflectiveOperationException {
153         ClassByteBuilder builder = new ClassByteBuilder("T1-float");
154         byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, float.class).build();
155         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, 0.1234f, true);
156         float value = MethodHandles.classData(lookup, "_", float.class);
157         assertEquals(0.1234f, value);
158         // call through condy
159         assertClassData(lookup, 0.1234f);
160     }
161 
162     @Test
163     public void classClassData() throws ReflectiveOperationException {
164         Class<?> hc = hiddenClass(100).lookupClass();
165         ClassByteBuilder builder = new ClassByteBuilder("T2");
166         byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, Class.class).build();
167         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, hc, true);
168         Class<?> value = MethodHandles.classData(lookup, "_", Class.class);
169         assertEquals(hc, value);
170         // call through condy
171         assertClassData(lookup, hc);
172     }
173 
174     @Test
175     public void arrayClassData() throws ReflectiveOperationException {
176         ClassByteBuilder builder = new ClassByteBuilder("T3");
177         byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, String[].class).build();
178         String[] colors = new String[] { "red", "yellow", "blue"};
179         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, colors, true);
180         assertClassData(lookup, colors.clone());
181         // class data is modifiable and not a constant
182         colors[0] = "black";
183         // it will get back the modified class data
184         String[] value = MethodHandles.classData(lookup, "_", String[].class);
185         assertArrayEquals(colors, value);
186         // even call through condy as it's not a constant
187         assertClassData(lookup, colors);
188     }
189 
190     @Test
191     public void listClassData() throws ReflectiveOperationException {
192         ClassByteBuilder builder = new ClassByteBuilder("T4");
193         byte[] bytes = builder.classDataAt(ACC_PUBLIC|ACC_STATIC, Integer.class, 2).build();
194         List<Integer> cd = List.of(100, 101, 102, 103);
195         int expected = 102;  // element at index=2
196         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, cd, true);
197         int value = MethodHandles.classDataAt(lookup, "_", int.class, 2);
198         assertEquals(expected, value);
199         // call through condy
200         assertClassData(lookup, expected);
201     }
202 
203     @Test
204     public void arrayListClassData() throws ReflectiveOperationException {
205         ClassByteBuilder builder = new ClassByteBuilder("T4");
206         byte[] bytes = builder.classDataAt(ACC_PUBLIC|ACC_STATIC, Integer.class, 1).build();
207         ArrayList<Integer> cd = new ArrayList<>();
208         Stream.of(100, 101, 102, 103).forEach(cd::add);
209         int expected = 101;  // element at index=1
210         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, cd, true);
211         int value = MethodHandles.classDataAt(lookup, "_", int.class, 1);
212         assertEquals(expected, value);
213         // call through condy
214         assertClassData(lookup, expected);
215     }
216 
217     private static Lookup hiddenClass(int value) {
218         ClassByteBuilder builder = new ClassByteBuilder("HC");
219         byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, int.class).build();
220         return assertDoesNotThrow(() -> LOOKUP.defineHiddenClassWithClassData(bytes, value, true));
221     }
222     private static Lookup hiddenClass(List<?> list) {
223         ClassByteBuilder builder = new ClassByteBuilder("HC");
224         byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, List.class).build();
225         return assertDoesNotThrow(() -> LOOKUP.defineHiddenClassWithClassData(bytes, list, true));
226     }
227 
228     @Test
229     public void condyInvokedFromVirtualMethod() throws ReflectiveOperationException {
230         ClassByteBuilder builder = new ClassByteBuilder("T5");
231         // generate classData instance method
232         byte[] bytes = builder.classData(ACC_PUBLIC, Class.class).build();
233         Lookup hcLookup = hiddenClass(100);
234         assertClassData(hcLookup, 100);
235         Class<?> hc = hcLookup.lookupClass();
236         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, hc, true);
237         Class<?> value = MethodHandles.classData(lookup, "_", Class.class);
238         assertEquals(hc, value);
239         // call through condy
240         Class<?> c = lookup.lookupClass();
241         assertClassData(lookup, c.newInstance(), hc);
242     }
243 
244     @Test
245     public void immutableListClassData() throws ReflectiveOperationException {
246         ClassByteBuilder builder = new ClassByteBuilder("T6");
247         // generate classDataAt instance method
248         byte[] bytes = builder.classDataAt(ACC_PUBLIC, Integer.class, 2).build();
249         List<Integer> cd = List.of(100, 101, 102, 103);
250         int expected = 102;  // element at index=2
251         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, cd, true);
252         int value = MethodHandles.classDataAt(lookup, "_", int.class, 2);
253         assertEquals(expected, value);
254         // call through condy
255         Class<?> c = lookup.lookupClass();
256         assertClassData(lookup, c.newInstance() ,expected);
257     }
258 
259     /*
260      * The return value of MethodHandles::classDataAt is the element
261      * contained in the list when the method is called.
262      * If MethodHandles::classDataAt is called via condy, the value
263      * will be captured as a constant.  If the class data is modified
264      * after the element at the given index is computed via condy,
265      * subsequent LDC of such ConstantDynamic entry will return the same
266      * value. However, direct invocation of MethodHandles::classDataAt
267      * will return the modified value.
268      */
269     @Test
270     public void mutableListClassData() throws ReflectiveOperationException {
271         ClassByteBuilder builder = new ClassByteBuilder("T7");
272         // generate classDataAt instance method
273         byte[] bytes = builder.classDataAt(ACC_PUBLIC, MethodType.class, 0).build();
274         MethodType mtype = MethodType.methodType(int.class, String.class);
275         List<MethodType> cd = new ArrayList<>(List.of(mtype));
276         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, cd, true);
277         // call through condy
278         Class<?> c = lookup.lookupClass();
279         assertClassData(lookup, c.newInstance(), mtype);
280         // modify the class data
281         assertSame(mtype, cd.remove(0));
282         cd.add(0,  MethodType.methodType(void.class));
283         MethodType newMType = cd.get(0);
284         // loading the element using condy returns the original value
285         assertClassData(lookup, c.newInstance(), mtype);
286         // direct invocation of MethodHandles.classDataAt returns the modified value
287         assertEquals(newMType, MethodHandles.classDataAt(lookup, "_", MethodType.class, 0));
288     }
289 
290     // helper method to extract from a class data map
291     public static <T> T getClassDataEntry(Lookup lookup, String key, Class<T> type) throws IllegalAccessException {
292         Map<String, T> cd = MethodHandles.classData(lookup, "_", Map.class);
293         return type.cast(cd.get(key));
294     }
295 
296     @Test
297     public void classDataMap() throws ReflectiveOperationException {
298         ClassByteBuilder builder = new ClassByteBuilder("map");
299         // generate classData static method
300         DirectMethodHandleDesc bsm = ConstantDescs.ofConstantBootstrap(CD_ClassDataTest, "getClassDataEntry", CD_Object);
301         // generate two accessor methods to get the entries from class data
302         byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, Map.class)
303                               .classData(ACC_PUBLIC|ACC_STATIC, "getClass",
304                                          Class.class, DynamicConstantDesc.ofNamed(bsm, "class", CD_Class))
305                               .classData(ACC_PUBLIC|ACC_STATIC, "getMethod",
306                                          MethodHandle.class, DynamicConstantDesc.ofNamed(bsm, "method", CD_MethodHandle))
307                               .build();
308 
309         // generate a hidden class
310         Lookup hcLookup = hiddenClass(100);
311         Class<?> hc = hcLookup.lookupClass();
312         assertClassData(hcLookup, 100);
313 
314         MethodHandle mh = hcLookup.findStatic(hc, "classData", MethodType.methodType(int.class));
315         Map<String, Object> cd = Map.of("class", hc, "method", mh);
316         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, cd, true);
317         assertClassData(lookup, cd);
318 
319         // validate the entries from the class data map
320         Class<?> c = lookup.lookupClass();
321         Method m = c.getMethod("getClass");
322         Class<?> v = (Class<?>)m.invoke(null);
323         assertEquals(hc, v);
324 
325         Method m1 = c.getMethod("getMethod");
326         MethodHandle v1 = (MethodHandle) m1.invoke(null);
327         assertEquals(mh, v1);
328     }
329 
330     @Test
331     public void nonDefaultName() throws ReflectiveOperationException {
332         ClassByteBuilder builder = new ClassByteBuilder("nonDefaultName");
333         byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, Class.class)
334                               .build();
335         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, ClassDataTest.class, true);
336         assertClassData(lookup, ClassDataTest.class);
337         // throw IAE
338         assertThrows(IllegalArgumentException.class, () -> MethodHandles.classData(lookup, "non_default_name", Class.class));
339     }
340 
341     static class ClassByteBuilder {
342         private static final String OBJECT_CLS = "java/lang/Object";
343         private static final String MHS_CLS = "java/lang/invoke/MethodHandles";
344         private static final String CLASS_DATA_BSM_DESCR =
345                 "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;";
346         private Consumer<ClassBuilder> cw;
347         private final ClassDesc classname;
348 
349         /**
350          * A builder to generate a class file to access class data
351          * @param classname
352          */
353         ClassByteBuilder(String classname) {
354             this.classname = ClassDesc.ofInternalName(classname);
355             this.cw = clb -> {
356                 clb.withSuperclass(CD_Object);
357                 clb.withFlags(AccessFlag.FINAL);
358                 clb.withMethodBody(INIT_NAME, MTD_void, ACC_PUBLIC, cob -> {
359                     cob.aload(0);
360                     cob.invokespecial(CD_Object, INIT_NAME, MTD_void);
361                     cob.return_();
362                 });
363             };
364         }
365 
366         byte[] build() {
367             byte[] bytes = ClassFile.of().build(classname, cw);
368             Path p = Paths.get(classname + ".class");
369                 try (OutputStream os = Files.newOutputStream(p)) {
370                 os.write(bytes);
371             } catch (IOException e) {
372                 throw new UncheckedIOException(e);
373             }
374             return bytes;
375         }
376 
377         /*
378          * Generate classData method to load class data via condy
379          */
380         ClassByteBuilder classData(int accessFlags, Class<?> returnType) {
381             ClassDesc returnDesc = returnType.describeConstable().orElseThrow();
382             MethodTypeDesc mt = MethodTypeDesc.of(returnDesc);
383             cw = cw.andThen(clb -> {
384                 clb.withMethodBody("classData", mt, accessFlags, cob -> {
385                     cob.loadConstant(DynamicConstantDesc.ofNamed(BSM_CLASS_DATA, DEFAULT_NAME, returnDesc));
386                     cob.return_(TypeKind.from(returnType));
387                 });
388             });
389             return this;
390         }
391 
392         /*
393          * Generate classDataAt method to load an element from class data via condy
394          */
395         ClassByteBuilder classDataAt(int accessFlags, Class<?> returnType, int index) {
396             ClassDesc returnDesc = returnType.describeConstable().orElseThrow();
397             MethodTypeDesc mt = MethodTypeDesc.of(returnDesc);
398             cw = cw.andThen(clb -> {
399                 clb.withMethodBody("classData", mt, accessFlags, cob -> {
400                     cob.loadConstant(DynamicConstantDesc.ofNamed(BSM_CLASS_DATA_AT, DEFAULT_NAME, returnDesc, index));
401                     cob.return_(TypeKind.from(returnType));
402                 });
403             });
404             return this;
405         }
406 
407         ClassByteBuilder classData(int accessFlags, String name, Class<?> returnType, DynamicConstantDesc<?> dynamic) {
408             ClassDesc returnDesc = returnType.describeConstable().orElseThrow();
409             MethodTypeDesc mt = MethodTypeDesc.of(returnDesc);
410             cw = cw.andThen(clb -> {
411                 clb.withMethodBody(name, mt, accessFlags, cob -> {
412                     cob.loadConstant(dynamic);
413                     cob.return_(TypeKind.from(returnType));
414                 });
415             });
416             return this;
417         }
418     }
419 
420     /*
421      * Load an int constant from class data via condy and
422      * verify it matches the given value.
423      */
424     private static void assertClassData(Lookup lookup, int value) throws ReflectiveOperationException {
425         Class<?> c = lookup.lookupClass();
426         Method m = c.getMethod("classData");
427         int v = (int) m.invoke(null);
428         assertEquals(value, v);
429     }
430 
431     /*
432      * Load an int constant from class data via condy and
433      * verify it matches the given value.
434      */
435     private static void assertClassData(Lookup lookup, Object o, int value) throws ReflectiveOperationException {
436         Class<?> c = lookup.lookupClass();
437         Method m = c.getMethod("classData");
438         int v = (int) m.invoke(o);
439         assertEquals(value, v);
440     }
441 
442     /*
443      * Load a float constant from class data via condy and
444      * verify it matches the given value.
445      */
446     private static void assertClassData(Lookup lookup, float value) throws ReflectiveOperationException {
447         Class<?> c = lookup.lookupClass();
448         Method m = c.getMethod("classData");
449         float v = (float) m.invoke(null);
450         assertEquals(value, v);
451     }
452 
453     /*
454      * Load a Class constant from class data via condy and
455      * verify it matches the given value.
456      */
457     private static void assertClassData(Lookup lookup, Class<?> value) throws ReflectiveOperationException {
458         Class<?> c = lookup.lookupClass();
459         Method m = c.getMethod("classData");
460         Class<?> v = (Class<?>)m.invoke(null);
461         assertEquals(value, v);
462     }
463 
464     /*
465      * Load a Class from class data via condy and
466      * verify it matches the given value.
467      */
468     private static void assertClassData(Lookup lookup, Object o, Class<?> value) throws ReflectiveOperationException {
469         Class<?> c = lookup.lookupClass();
470         Method m = c.getMethod("classData");
471         Object v = m.invoke(o);
472         assertEquals(value, v);
473     }
474 
475     /*
476      * Load an Object from class data via condy and
477      * verify it matches the given value.
478      */
479     private static void assertClassData(Lookup lookup, Object value) throws ReflectiveOperationException {
480         Class<?> c = lookup.lookupClass();
481         Method m = c.getMethod("classData");
482         Object v = m.invoke(null);
483         assertEquals(value, v);
484     }
485 
486     /*
487      * Load an Object array from class data via condy and
488      * verify it matches the given value in content.
489      */
490     private static void assertClassData(Lookup lookup, Object[] value) throws ReflectiveOperationException {
491         Class<?> c = lookup.lookupClass();
492         Method m = c.getMethod("classData");
493         Object v = m.invoke(null);
494         assertArrayEquals(value, (Object[]) v);
495     }
496 
497     /*
498      * Load an Object from class data via condy and
499      * verify it matches the given value.
500      */
501     private static void assertClassData(Lookup lookup, Object o, Object value) throws ReflectiveOperationException {
502         Class<?> c = lookup.lookupClass();
503         Method m = c.getMethod("classData");
504         Object v = m.invoke(o);
505         assertEquals(value, v);
506     }
507 }
508 
509