1 /*
  2  * Copyright (c) 2020, 2024, 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 testng/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 import org.testng.annotations.DataProvider;
 58 import org.testng.annotations.Test;
 59 
 60 import static java.lang.classfile.ClassFile.*;
 61 import static java.lang.constant.ConstantDescs.*;
 62 import static java.lang.invoke.MethodHandles.Lookup.*;
 63 import static org.testng.Assert.*;
 64 
 65 public class ClassDataTest {
 66     private static final Lookup LOOKUP = MethodHandles.lookup();
 67     private static final ClassDesc CD_ClassDataTest = ClassDataTest.class.describeConstable().orElseThrow();
 68 
 69     @Test
 70     public void testOriginalAccess() throws IllegalAccessException {
 71         Lookup lookup = hiddenClass(20);
 72         assertTrue(lookup.hasFullPrivilegeAccess());
 73 
 74         int value = MethodHandles.classData(lookup, "_", int.class);
 75         assertEquals(value, 20);
 76 
 77         Integer i = MethodHandles.classData(lookup, "_", Integer.class);
 78         assertEquals(i.intValue(), 20);
 79     }
 80 
 81     /*
 82      * A lookup class with no class data.
 83      */
 84     @Test
 85     public void noClassData() throws IllegalAccessException {
 86         assertNull(MethodHandles.classData(LOOKUP, "_", Object.class));
 87     }
 88 
 89     @DataProvider(name = "teleportedLookup")
 90     private 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     @Test(dataProvider = "teleportedLookup", expectedExceptions = { IllegalAccessException.class })
104     public void illegalAccess(Lookup lookup, int access) throws IllegalAccessException {
105         int lookupModes = lookup.lookupModes();
106         assertTrue((lookupModes & ORIGINAL) == 0);
107         assertEquals(lookupModes, access);
108         MethodHandles.classData(lookup, "_", int.class);
109     }
110 
111     @Test(expectedExceptions = { ClassCastException.class })
112     public void incorrectType() throws IllegalAccessException {
113         Lookup lookup = hiddenClass(20);
114         MethodHandles.classData(lookup, "_", Long.class);
115     }
116 
117     @Test(expectedExceptions = { IndexOutOfBoundsException.class })
118     public void invalidIndex() throws IllegalAccessException {
119         Lookup lookup = hiddenClass(List.of());
120         MethodHandles.classDataAt(lookup, "_", Object.class, 0);
121     }
122 
123     @Test(expectedExceptions = { NullPointerException.class })
124     public void unboxNull() throws IllegalAccessException {
125         List<Integer> list = new ArrayList<>();
126         list.add(null);
127         Lookup lookup = hiddenClass(list);
128         MethodHandles.classDataAt(lookup, "_", int.class, 0);
129     }
130 
131     @Test
132     public void nullElement() throws IllegalAccessException {
133         List<Object> list = new ArrayList<>();
134         list.add(null);
135         Lookup lookup = hiddenClass(list);
136         assertTrue(MethodHandles.classDataAt(lookup, "_", Object.class, 0) == null);
137     }
138 
139     @Test
140     public void intClassData() throws ReflectiveOperationException {
141         ClassByteBuilder builder = new ClassByteBuilder("T1-int");
142         byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, int.class).build();
143         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, 100, true);
144         int value = MethodHandles.classData(lookup, "_", int.class);
145         assertEquals(value, 100);
146         // call through condy
147         assertClassData(lookup, 100);
148     }
149 
150     @Test
151     public void floatClassData() throws ReflectiveOperationException {
152         ClassByteBuilder builder = new ClassByteBuilder("T1-float");
153         byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, float.class).build();
154         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, 0.1234f, true);
155         float value = MethodHandles.classData(lookup, "_", float.class);
156         assertEquals(value, 0.1234f);
157         // call through condy
158         assertClassData(lookup, 0.1234f);
159     }
160 
161     @Test
162     public void classClassData() throws ReflectiveOperationException {
163         Class<?> hc = hiddenClass(100).lookupClass();
164         ClassByteBuilder builder = new ClassByteBuilder("T2");
165         byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, Class.class).build();
166         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, hc, true);
167         Class<?> value = MethodHandles.classData(lookup, "_", Class.class);
168         assertEquals(value, hc);
169         // call through condy
170         assertClassData(lookup, hc);
171     }
172 
173     @Test
174     public void arrayClassData() throws ReflectiveOperationException {
175         ClassByteBuilder builder = new ClassByteBuilder("T3");
176         byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, String[].class).build();
177         String[] colors = new String[] { "red", "yellow", "blue"};
178         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, colors, true);
179         assertClassData(lookup, colors.clone());
180         // class data is modifiable and not a constant
181         colors[0] = "black";
182         // it will get back the modified class data
183         String[] value = MethodHandles.classData(lookup, "_", String[].class);
184         assertEquals(value, colors);
185         // even call through condy as it's not a constant
186         assertClassData(lookup, colors);
187     }
188 
189     @Test
190     public void listClassData() throws ReflectiveOperationException {
191         ClassByteBuilder builder = new ClassByteBuilder("T4");
192         byte[] bytes = builder.classDataAt(ACC_PUBLIC|ACC_STATIC, Integer.class, 2).build();
193         List<Integer> cd = List.of(100, 101, 102, 103);
194         int expected = 102;  // element at index=2
195         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, cd, true);
196         int value = MethodHandles.classDataAt(lookup, "_", int.class, 2);
197         assertEquals(value, expected);
198         // call through condy
199         assertClassData(lookup, expected);
200     }
201 
202     @Test
203     public void arrayListClassData() throws ReflectiveOperationException {
204         ClassByteBuilder builder = new ClassByteBuilder("T4");
205         byte[] bytes = builder.classDataAt(ACC_PUBLIC|ACC_STATIC, Integer.class, 1).build();
206         ArrayList<Integer> cd = new ArrayList<>();
207         Stream.of(100, 101, 102, 103).forEach(cd::add);
208         int expected = 101;  // element at index=1
209         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, cd, true);
210         int value = MethodHandles.classDataAt(lookup, "_", int.class, 1);
211         assertEquals(value, expected);
212         // call through condy
213         assertClassData(lookup, expected);
214     }
215 
216     private static Lookup hiddenClass(int value) {
217         ClassByteBuilder builder = new ClassByteBuilder("HC");
218         byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, int.class).build();
219         try {
220             return LOOKUP.defineHiddenClassWithClassData(bytes, value, true);
221         } catch (Throwable e) {
222             throw new RuntimeException(e);
223         }
224     }
225     private static Lookup hiddenClass(List<?> list) {
226         ClassByteBuilder builder = new ClassByteBuilder("HC");
227         byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, List.class).build();
228         try {
229             return LOOKUP.defineHiddenClassWithClassData(bytes, list, true);
230         } catch (Throwable e) {
231             throw new RuntimeException(e);
232         }
233     }
234 
235     @Test
236     public void condyInvokedFromVirtualMethod() throws ReflectiveOperationException {
237         ClassByteBuilder builder = new ClassByteBuilder("T5");
238         // generate classData instance method
239         byte[] bytes = builder.classData(ACC_PUBLIC, Class.class).build();
240         Lookup hcLookup = hiddenClass(100);
241         assertClassData(hcLookup, 100);
242         Class<?> hc = hcLookup.lookupClass();
243         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, hc, true);
244         Class<?> value = MethodHandles.classData(lookup, "_", Class.class);
245         assertEquals(value, hc);
246         // call through condy
247         Class<?> c = lookup.lookupClass();
248         assertClassData(lookup, c.newInstance(), hc);
249     }
250 
251     @Test
252     public void immutableListClassData() throws ReflectiveOperationException {
253         ClassByteBuilder builder = new ClassByteBuilder("T6");
254         // generate classDataAt instance method
255         byte[] bytes = builder.classDataAt(ACC_PUBLIC, Integer.class, 2).build();
256         List<Integer> cd = List.of(100, 101, 102, 103);
257         int expected = 102;  // element at index=2
258         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, cd, true);
259         int value = MethodHandles.classDataAt(lookup, "_", int.class, 2);
260         assertEquals(value, expected);
261         // call through condy
262         Class<?> c = lookup.lookupClass();
263         assertClassData(lookup, c.newInstance() ,expected);
264     }
265 
266     /*
267      * The return value of MethodHandles::classDataAt is the element
268      * contained in the list when the method is called.
269      * If MethodHandles::classDataAt is called via condy, the value
270      * will be captured as a constant.  If the class data is modified
271      * after the element at the given index is computed via condy,
272      * subsequent LDC of such ConstantDynamic entry will return the same
273      * value. However, direct invocation of MethodHandles::classDataAt
274      * will return the modified value.
275      */
276     @Test
277     public void mutableListClassData() throws ReflectiveOperationException {
278         ClassByteBuilder builder = new ClassByteBuilder("T7");
279         // generate classDataAt instance method
280         byte[] bytes = builder.classDataAt(ACC_PUBLIC, MethodType.class, 0).build();
281         MethodType mtype = MethodType.methodType(int.class, String.class);
282         List<MethodType> cd = new ArrayList<>(List.of(mtype));
283         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, cd, true);
284         // call through condy
285         Class<?> c = lookup.lookupClass();
286         assertClassData(lookup, c.newInstance(), mtype);
287         // modify the class data
288         assertTrue(cd.remove(0) == mtype);
289         cd.add(0,  MethodType.methodType(void.class));
290         MethodType newMType = cd.get(0);
291         // loading the element using condy returns the original value
292         assertClassData(lookup, c.newInstance(), mtype);
293         // direct invocation of MethodHandles.classDataAt returns the modified value
294         assertEquals(MethodHandles.classDataAt(lookup, "_", MethodType.class, 0), newMType);
295     }
296 
297     // helper method to extract from a class data map
298     public static <T> T getClassDataEntry(Lookup lookup, String key, Class<T> type) throws IllegalAccessException {
299         Map<String, T> cd = MethodHandles.classData(lookup, "_", Map.class);
300         return type.cast(cd.get(key));
301     }
302 
303     @Test
304     public void classDataMap() throws ReflectiveOperationException {
305         ClassByteBuilder builder = new ClassByteBuilder("map");
306         // generate classData static method
307         DirectMethodHandleDesc bsm = ConstantDescs.ofConstantBootstrap(CD_ClassDataTest, "getClassDataEntry", CD_Object);
308         // generate two accessor methods to get the entries from class data
309         byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, Map.class)
310                               .classData(ACC_PUBLIC|ACC_STATIC, "getClass",
311                                          Class.class, DynamicConstantDesc.ofNamed(bsm, "class", CD_Class))
312                               .classData(ACC_PUBLIC|ACC_STATIC, "getMethod",
313                                          MethodHandle.class, DynamicConstantDesc.ofNamed(bsm, "method", CD_MethodHandle))
314                               .build();
315 
316         // generate a hidden class
317         Lookup hcLookup = hiddenClass(100);
318         Class<?> hc = hcLookup.lookupClass();
319         assertClassData(hcLookup, 100);
320 
321         MethodHandle mh = hcLookup.findStatic(hc, "classData", MethodType.methodType(int.class));
322         Map<String, Object> cd = Map.of("class", hc, "method", mh);
323         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, cd, true);
324         assertClassData(lookup, cd);
325 
326         // validate the entries from the class data map
327         Class<?> c = lookup.lookupClass();
328         Method m = c.getMethod("getClass");
329         Class<?> v = (Class<?>)m.invoke(null);
330         assertEquals(hc, v);
331 
332         Method m1 = c.getMethod("getMethod");
333         MethodHandle v1 = (MethodHandle) m1.invoke(null);
334         assertEquals(mh, v1);
335     }
336 
337     @Test(expectedExceptions = { IllegalArgumentException.class })
338     public void nonDefaultName() throws ReflectiveOperationException {
339         ClassByteBuilder builder = new ClassByteBuilder("nonDefaultName");
340         byte[] bytes = builder.classData(ACC_PUBLIC|ACC_STATIC, Class.class)
341                               .build();
342         Lookup lookup = LOOKUP.defineHiddenClassWithClassData(bytes, ClassDataTest.class, true);
343         assertClassData(lookup, ClassDataTest.class);
344         // throw IAE
345         MethodHandles.classData(lookup, "non_default_name", Class.class);
346     }
347 
348     static class ClassByteBuilder {
349         private static final String OBJECT_CLS = "java/lang/Object";
350         private static final String MHS_CLS = "java/lang/invoke/MethodHandles";
351         private static final String CLASS_DATA_BSM_DESCR =
352                 "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;";
353         private Consumer<ClassBuilder> cw;
354         private final ClassDesc classname;
355 
356         /**
357          * A builder to generate a class file to access class data
358          * @param classname
359          */
360         ClassByteBuilder(String classname) {
361             this.classname = ClassDesc.ofInternalName(classname);
362             this.cw = clb -> {
363                 clb.withSuperclass(CD_Object);
364                 clb.withFlags(AccessFlag.FINAL);
365                 clb.withMethodBody(INIT_NAME, MTD_void, ACC_PUBLIC, cob -> {
366                     cob.aload(0);
367                     cob.invokespecial(CD_Object, INIT_NAME, MTD_void);
368                     cob.return_();
369                 });
370             };
371         }
372 
373         byte[] build() {
374             byte[] bytes = ClassFile.of().build(classname, cw);
375             Path p = Paths.get(classname + ".class");
376                 try (OutputStream os = Files.newOutputStream(p)) {
377                 os.write(bytes);
378             } catch (IOException e) {
379                 throw new UncheckedIOException(e);
380             }
381             return bytes;
382         }
383 
384         /*
385          * Generate classData method to load class data via condy
386          */
387         ClassByteBuilder classData(int accessFlags, Class<?> returnType) {
388             ClassDesc returnDesc = returnType.describeConstable().orElseThrow();
389             MethodTypeDesc mt = MethodTypeDesc.of(returnDesc);
390             cw = cw.andThen(clb -> {
391                 clb.withMethodBody("classData", mt, accessFlags, cob -> {
392                     cob.loadConstant(DynamicConstantDesc.ofNamed(BSM_CLASS_DATA, DEFAULT_NAME, returnDesc));
393                     cob.return_(TypeKind.from(returnType));
394                 });
395             });
396             return this;
397         }
398 
399         /*
400          * Generate classDataAt method to load an element from class data via condy
401          */
402         ClassByteBuilder classDataAt(int accessFlags, Class<?> returnType, int index) {
403             ClassDesc returnDesc = returnType.describeConstable().orElseThrow();
404             MethodTypeDesc mt = MethodTypeDesc.of(returnDesc);
405             cw = cw.andThen(clb -> {
406                 clb.withMethodBody("classData", mt, accessFlags, cob -> {
407                     cob.loadConstant(DynamicConstantDesc.ofNamed(BSM_CLASS_DATA_AT, DEFAULT_NAME, returnDesc, index));
408                     cob.return_(TypeKind.from(returnType));
409                 });
410             });
411             return this;
412         }
413 
414         ClassByteBuilder classData(int accessFlags, String name, Class<?> returnType, DynamicConstantDesc<?> dynamic) {
415             ClassDesc returnDesc = returnType.describeConstable().orElseThrow();
416             MethodTypeDesc mt = MethodTypeDesc.of(returnDesc);
417             cw = cw.andThen(clb -> {
418                 clb.withMethodBody(name, mt, accessFlags, cob -> {
419                     cob.loadConstant(dynamic);
420                     cob.return_(TypeKind.from(returnType));
421                 });
422             });
423             return this;
424         }
425     }
426 
427     /*
428      * Load an int constant from class data via condy and
429      * verify it matches the given value.
430      */
431     private void assertClassData(Lookup lookup, int value) throws ReflectiveOperationException {
432         Class<?> c = lookup.lookupClass();
433         Method m = c.getMethod("classData");
434         int v = (int) m.invoke(null);
435         assertEquals(value, v);
436     }
437 
438     /*
439      * Load an int constant from class data via condy and
440      * verify it matches the given value.
441      */
442     private void assertClassData(Lookup lookup, Object o, int value) throws ReflectiveOperationException {
443         Class<?> c = lookup.lookupClass();
444         Method m = c.getMethod("classData");
445         int v = (int) m.invoke(o);
446         assertEquals(value, v);
447     }
448 
449     /*
450      * Load a float constant from class data via condy and
451      * verify it matches the given value.
452      */
453     private void assertClassData(Lookup lookup, float value) throws ReflectiveOperationException {
454         Class<?> c = lookup.lookupClass();
455         Method m = c.getMethod("classData");
456         float v = (float) m.invoke(null);
457         assertEquals(value, v);
458     }
459 
460     /*
461      * Load a Class constant from class data via condy and
462      * verify it matches the given value.
463      */
464     private void assertClassData(Lookup lookup, Class<?> value) throws ReflectiveOperationException {
465         Class<?> c = lookup.lookupClass();
466         Method m = c.getMethod("classData");
467         Class<?> v = (Class<?>)m.invoke(null);
468         assertEquals(value, v);
469     }
470 
471     /*
472      * Load a Class from class data via condy and
473      * verify it matches the given value.
474      */
475     private void assertClassData(Lookup lookup, Object o, Class<?> value) throws ReflectiveOperationException {
476         Class<?> c = lookup.lookupClass();
477         Method m = c.getMethod("classData");
478         Object v = m.invoke(o);
479         assertEquals(value, v);
480     }
481 
482     /*
483      * Load an Object from class data via condy and
484      * verify it matches the given value.
485      */
486     private void assertClassData(Lookup lookup, Object value) throws ReflectiveOperationException {
487         Class<?> c = lookup.lookupClass();
488         Method m = c.getMethod("classData");
489         Object v = m.invoke(null);
490         assertEquals(value, v);
491     }
492 
493     /*
494      * Load an Object from class data via condy and
495      * verify it matches the given value.
496      */
497     private void assertClassData(Lookup lookup, Object o, Object value) throws ReflectiveOperationException {
498         Class<?> c = lookup.lookupClass();
499         Method m = c.getMethod("classData");
500         Object v = m.invoke(o);
501         assertEquals(value, v);
502     }
503 }
504 
505