1 /*
  2  * Copyright (c) 2022, 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  * @summary Testing ClassFile local variable table.
 27  * @compile -g testdata/Lvt.java
 28  * @run junit LvtTest
 29  */
 30 import helpers.ClassRecord;
 31 import helpers.Transforms;
 32 import java.lang.classfile.*;
 33 
 34 import java.io.*;
 35 import java.lang.constant.ClassDesc;
 36 import java.net.URI;
 37 import java.nio.file.Files;
 38 import java.nio.file.Paths;
 39 import java.util.ArrayList;
 40 import java.util.List;
 41 
 42 import java.lang.classfile.Attributes;
 43 import java.lang.classfile.attribute.SourceFileAttribute;
 44 import java.lang.classfile.constantpool.ConstantPoolBuilder;
 45 import java.lang.classfile.constantpool.Utf8Entry;
 46 import java.lang.classfile.instruction.LocalVariable;
 47 import java.lang.classfile.instruction.LocalVariableType;
 48 import java.lang.reflect.AccessFlag;
 49 import org.junit.jupiter.api.Test;
 50 
 51 import static helpers.TestConstants.CD_ArrayList;
 52 import static helpers.TestConstants.CD_PrintStream;
 53 import static helpers.TestConstants.CD_System;
 54 import static helpers.TestConstants.MTD_INT_VOID;
 55 import static helpers.TestConstants.MTD_VOID;
 56 import static helpers.TestUtil.ExpectedLvRecord;
 57 import static helpers.TestUtil.ExpectedLvtRecord;
 58 import static java.lang.classfile.ClassFile.ACC_PUBLIC;
 59 import static java.lang.classfile.ClassFile.ACC_STATIC;
 60 import static java.lang.constant.ConstantDescs.*;
 61 import java.lang.constant.MethodTypeDesc;
 62 import static org.junit.jupiter.api.Assertions.*;
 63 
 64 class LvtTest {
 65     static byte[] fileBytes;
 66 
 67     static {
 68         try {
 69             fileBytes = Files.readAllBytes(Paths.get(URI.create(testdata.Lvt.class.getResource("Lvt.class").toString())));
 70         }
 71         catch (IOException e) {
 72             throw new ExceptionInInitializerError(e);
 73         }
 74     }
 75 
 76     @Test
 77     void getLVTEntries() {
 78         ClassModel c = ClassFile.of().parse(fileBytes);
 79         CodeModel co = c.methods().stream()
 80                         .filter(mm -> mm.methodName().stringValue().equals("m"))
 81                         .map(MethodModel::code)
 82                         .findFirst()
 83                         .get()
 84                         .orElseThrow();
 85 
 86         List<LocalVariable> lvs = new ArrayList<>();
 87         co.forEach(e -> {
 88             if (e instanceof LocalVariable l) lvs.add(l);
 89         });
 90 
 91         List<ExpectedLvRecord> expected = List.of(
 92                 ExpectedLvRecord.of(5, "j", "I", 9, 21),
 93                 ExpectedLvRecord.of(0, "this", "Ltestdata/Lvt;", 0, 31),
 94                 ExpectedLvRecord.of(1, "a", "Ljava/lang/String;", 0, 31),
 95                 ExpectedLvRecord.of(4, "d", "[C", 6, 25));
 96 
 97         // Exploits non-symmetric "equals" in ExpectedLvRecord
 98         assertTrue(expected.equals(lvs));
 99     }
100 
101     @Test
102     void buildLVTEntries() throws Exception {
103         var cc = ClassFile.of();
104         ClassModel c = cc.parse(fileBytes);
105 
106         // Compare transformed model and original with CodeBuilder filter
107         byte[] newClass = cc.transformClass(c, Transforms.threeLevelNoop);
108         ClassRecord orig = ClassRecord.ofClassModel(cc.parse(fileBytes), ClassRecord.CompatibilityFilter.By_ClassBuilder);
109         ClassRecord transformed = ClassRecord.ofClassModel(cc.parse(newClass), ClassRecord.CompatibilityFilter.By_ClassBuilder);
110         ClassRecord.assertEqualsDeep(transformed, orig);
111     }
112 
113     @Test
114     void testCreateLoadLVT() throws Exception {
115         var cc = ClassFile.of();
116         byte[] bytes = cc.build(ClassDesc.of("MyClass"), cb -> {
117             cb.withFlags(AccessFlag.PUBLIC);
118             cb.withVersion(52, 0);
119             cb.with(SourceFileAttribute.of(cb.constantPool().utf8Entry(("MyClass.java"))))
120               .withMethod("<init>", MethodTypeDesc.of(CD_void), 0, mb -> mb
121                       .withCode(codeb -> codeb.aload(0)
122                                               .invokespecial(CD_Object, "<init>", MTD_VOID, false)
123                                               .return_()
124                       )
125               )
126               .withMethod("main", MethodTypeDesc.of(CD_void, CD_String.arrayType()),
127                           ACC_PUBLIC | ACC_STATIC,
128                           mb -> mb
129                               .withCode(c0 -> {
130                                   ConstantPoolBuilder cpb = cb.constantPool();
131                                   Utf8Entry slotName = cpb.utf8Entry("this");
132                                   Utf8Entry desc = cpb.utf8Entry("LMyClass;");
133                                   Utf8Entry i1n = cpb.utf8Entry("res");
134                                   Utf8Entry i2 = cpb.utf8Entry("i");
135                                   Utf8Entry intSig = cpb.utf8Entry("I");
136                                   Label start = c0.newLabel();
137                                   Label end = c0.newLabel();
138                                   Label i1 = c0.newLabel();
139                                   Label preEnd = c0.newLabel();
140                                   Label loopTop = c0.newLabel();
141                                   Label loopEnd = c0.newLabel();
142                                   c0.localVariable(1, i1n, intSig, i1, preEnd) // LV Entries can be added before the labels
143                                     .localVariable(2, i2, intSig, loopTop, preEnd)
144                                     .labelBinding(start)
145                                     .iconst_1()         // 0
146                                     .istore(1)          // 1
147                                     .labelBinding(i1)
148                                     .iconst_1()         // 2
149                                     .istore(2)          // 3
150                                     .labelBinding(loopTop)
151                                     .iload(2)           // 4
152                                     .bipush(10)         // 5
153                                     .if_icmpge(loopEnd) // 6
154                                     .iload(1)           // 7
155                                     .iload(2)           // 8
156                                     .imul()             // 9
157                                     .istore(1)          // 10
158                                     .iinc(2, 1)    // 11
159                                     .goto_(loopTop)     // 12
160                                     .labelBinding(loopEnd)
161                                     .getstatic(CD_System, "out", CD_PrintStream)   // 13
162                                     .iload(1)
163                                     .invokevirtual(CD_PrintStream, "println", MTD_INT_VOID)  // 15
164                                     .labelBinding(preEnd)
165                                     .return_()
166                                     .labelBinding(end)
167                                     .localVariable(0, slotName, desc, start, end); // and lv entries can be added after the labels
168                               }));
169         });
170 
171         var c = cc.parse(bytes);
172         var main = c.methods().get(1);
173         var lvt = main.code().get().findAttribute(Attributes.localVariableTable()).get();
174         var lvs = lvt.localVariables();
175 
176         assertEquals(lvs.size(), 3);
177         List<ExpectedLvRecord> expected = List.of(
178                 ExpectedLvRecord.of(1, "res", "I", 2, 25),
179                 ExpectedLvRecord.of(2, "i", "I", 4, 23),
180                 ExpectedLvRecord.of(0, "this", "LMyClass;", 0, 28));
181 
182         // Exploits non-symmetric "equals" in ExpectedLvRecord
183         assertTrue(expected.equals(lvs));
184     }
185 
186     @Test
187     void getLVTTEntries() {
188         ClassModel c = ClassFile.of().parse(fileBytes);
189         CodeModel co = c.methods().stream()
190                         .filter(mm -> mm.methodName().stringValue().equals("n"))
191                         .map(MethodModel::code)
192                         .findFirst()
193                         .get()
194                         .orElseThrow();
195 
196         List<LocalVariableType> lvts = new ArrayList<>();
197         co.forEach(e -> {
198             if (e instanceof LocalVariableType l) lvts.add(l);
199         });
200 
201         /* From javap:
202 
203         LocalVariableTypeTable:
204         Start  Length  Slot  Name   Signature
205         51       8     6     f   Ljava/util/List<*>;
206         0      64     1     u   TU;
207         0      64     2     z   Ljava/lang/Class<+Ljava/util/List<*>;>;
208         8      56     3     v   Ljava/util/ArrayList<Ljava/lang/Integer;>;
209         17      47     4     s   Ljava/util/Set<-Ljava/util/Set;>;
210         */
211 
212         List<ExpectedLvtRecord> expected = List.of(
213                 ExpectedLvtRecord.of(6, "f", "Ljava/util/List<*>;", 51, 8),
214                 ExpectedLvtRecord.of(1, "u", "TU;", 0, 64),
215                 ExpectedLvtRecord.of(2, "z", "Ljava/lang/Class<+Ljava/util/List<*>;>;", 0, 64),
216                 ExpectedLvtRecord.of(3, "v", "Ljava/util/ArrayList<Ljava/lang/Integer;>;", 8, 56),
217                 ExpectedLvtRecord.of(4, "s", "Ljava/util/Set<-Ljava/util/Set<*>;>;", 17, 47)
218         );
219 
220         // Exploits non-symmetric "equals" in ExpectedLvRecord
221         for (int i = 0; i < lvts.size(); i++) {
222             assertTrue(expected.get(i).equals(lvts.get(i)));
223         }
224     }
225 
226     @Test
227     void testCreateLoadLVTT() throws Exception {
228         var cc = ClassFile.of();
229         byte[] bytes = cc.build(ClassDesc.of("MyClass"), cb -> {
230             cb.withFlags(AccessFlag.PUBLIC);
231             cb.withVersion(52, 0);
232             cb.with(SourceFileAttribute.of(cb.constantPool().utf8Entry(("MyClass.java"))))
233 
234               .withMethod("<init>", MethodTypeDesc.of(CD_void), 0, mb -> mb
235                       .withCode(codeb -> codeb.aload(0)
236                                               .invokespecial(CD_Object, "<init>", MTD_VOID, false)
237                                               .return_()
238                       )
239               )
240 
241               .withMethod("m", MethodTypeDesc.of(CD_Object, CD_Object.arrayType()),
242                           ACC_PUBLIC,
243                           mb -> mb.withFlags(AccessFlag.PUBLIC)
244                                   .withCode(c0 -> {
245                                       ConstantPoolBuilder cpb = cb.constantPool();
246                                       Utf8Entry slotName = cpb.utf8Entry("this");
247                                       Utf8Entry desc = cpb.utf8Entry("LMyClass;");
248                                       Utf8Entry juList = cpb.utf8Entry("Ljava/util/List;");
249                                       Utf8Entry TU = cpb.utf8Entry("TU;");
250                                       Utf8Entry sig = cpb.utf8Entry("Ljava/util/List<+Ljava/lang/Object;>;");
251                                       Utf8Entry l = cpb.utf8Entry("l");
252                                       Utf8Entry jlObject = cpb.utf8Entry("Ljava/lang/Object;");
253                                       Utf8Entry u = cpb.utf8Entry("u");
254 
255                                       Label start = c0.newLabel();
256                                       Label end = c0.newLabel();
257                                       Label beforeRet = c0.newLabel();
258 
259                                       c0.localVariable(2, l, juList, beforeRet, end)
260                                         .localVariableType(1, u, TU, start, end)
261                                         .labelBinding(start)
262                                         .new_(ClassDesc.of("java.util.ArrayList"))
263                                         .dup()
264                                         .invokespecial(CD_ArrayList, "<init>", MTD_VOID, false)
265                                         .astore(2)
266                                         .labelBinding(beforeRet)
267                                         .localVariableType(2, l, sig, beforeRet, end)
268                                         .aload(1)
269                                         .areturn()
270                                         .labelBinding(end)
271                                         .localVariable(0, slotName, desc, start, end)
272                                         .localVariable(1, u, jlObject, start, end);
273                                   }));
274         });
275         var c = cc.parse(bytes);
276         var main = c.methods().get(1);
277         var lvtt = main.code().get().findAttribute(Attributes.localVariableTypeTable()).get();
278         var lvts = lvtt.localVariableTypes();
279 
280         /* From javap:
281 
282         LocalVariableTypeTable:
283         Start  Length  Slot  Name   Signature
284             0      10     1     u   TU;
285             8       2     2     l   Ljava/util/List<+Ljava/lang/Object;>;
286          */
287 
288         List<ExpectedLvtRecord> expected = List.of(
289                 ExpectedLvtRecord.of(1, "u", "TU;", 0, 10),
290                 ExpectedLvtRecord.of(2, "l", "Ljava/util/List<+Ljava/lang/Object;>;", 8, 2)
291         );
292 
293         // Exploits non-symmetric "equals" in ExpectedLvRecord
294         for (int i = 0; i < lvts.size(); i++) {
295             assertTrue(expected.get(i).equals(lvts.get(i)));
296         }
297     }
298 
299     @Test
300     void skipDebugSkipsLVT() {
301         ClassModel c = ClassFile.of(ClassFile.DebugElementsOption.DROP_DEBUG).parse(fileBytes);
302 
303         c.forEach(e -> {
304             if (e instanceof MethodModel m) {
305                 m.forEach(el -> {
306                     if (el instanceof CodeModel cm) {
307                         cm.forEach(elem -> {
308                             assertFalse(elem instanceof LocalVariable);
309                             assertFalse(elem instanceof LocalVariableType);
310                         });
311                     }
312                 });
313             }
314         });
315     }
316 }