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  * @bug 8325485
 27  * @summary Testing ClassFile on small Corpus.
 28  * @build helpers.* testdata.*
 29  * @run junit/othervm/timeout=480 -Djunit.jupiter.execution.parallel.enabled=true CorpusTest
 30  */
 31 import helpers.ClassRecord;
 32 import helpers.ClassRecord.CompatibilityFilter;
 33 import helpers.Transforms;
 34 import jdk.internal.classfile.impl.BufWriterImpl;
 35 import jdk.internal.classfile.impl.Util;
 36 import org.junit.jupiter.params.ParameterizedTest;
 37 import org.junit.jupiter.params.provider.MethodSource;
 38 import org.junit.jupiter.api.parallel.Execution;
 39 import org.junit.jupiter.api.parallel.ExecutionMode;
 40 
 41 import java.io.ByteArrayInputStream;
 42 import java.lang.classfile.attribute.CodeAttribute;
 43 import java.util.*;
 44 
 45 import static helpers.ClassRecord.assertEqualsDeep;
 46 import static java.util.stream.Collectors.joining;
 47 import static org.junit.jupiter.api.Assertions.*;
 48 import static helpers.TestUtil.assertEmpty;
 49 
 50 import java.io.IOException;
 51 import java.net.URI;
 52 import java.net.URISyntaxException;
 53 import java.nio.file.FileSystem;
 54 import java.nio.file.FileSystems;
 55 import java.nio.file.Files;
 56 import java.nio.file.Path;
 57 import java.nio.file.Paths;
 58 import java.util.stream.Stream;
 59 import java.lang.classfile.Attributes;
 60 import java.lang.classfile.BufWriter;
 61 import java.lang.classfile.ClassFile;
 62 import java.lang.classfile.ClassTransform;
 63 import java.lang.classfile.CodeTransform;
 64 import java.lang.classfile.constantpool.ConstantPool;
 65 import java.lang.classfile.constantpool.PoolEntry;
 66 import java.lang.classfile.constantpool.Utf8Entry;
 67 import jdk.internal.classfile.impl.DirectCodeBuilder;
 68 import jdk.internal.classfile.impl.UnboundAttribute;
 69 import java.lang.classfile.instruction.LineNumber;
 70 import java.lang.classfile.instruction.LocalVariable;
 71 import java.lang.classfile.instruction.LocalVariableType;
 72 
 73 /**
 74  * CorpusTest
 75  */
 76 @Execution(ExecutionMode.CONCURRENT)
 77 class CorpusTest {
 78 
 79     protected static final FileSystem JRT = FileSystems.getFileSystem(URI.create("jrt:/"));
 80     protected static final String testFilter = null; //"modules/java.base/java/util/function/Supplier.class";
 81 
 82     static void splitTableAttributes(String sourceClassFile, String targetClassFile) throws IOException, URISyntaxException {
 83         var root = Paths.get(URI.create(CorpusTest.class.getResource("CorpusTest.class").toString())).getParent();
 84         var cc = ClassFile.of();
 85         Files.write(root.resolve(targetClassFile), cc.transformClass(cc.parse(root.resolve(sourceClassFile)), ClassTransform.transformingMethodBodies((cob, coe) -> {
 86             var dcob = (DirectCodeBuilder)cob;
 87             var curPc = dcob.curPc();
 88             switch (coe) {
 89                 case LineNumber ln -> dcob.writeAttribute(new UnboundAttribute.AdHocAttribute<>(Attributes.lineNumberTable()) {
 90                     @Override
 91                     public void writeBody(BufWriterImpl b) {
 92                         b.writeU2(1);
 93                         b.writeU2(curPc);
 94                         b.writeU2(ln.line());
 95                     }
 96 
 97                     @Override
 98                     public Utf8Entry attributeName() {
 99                         return cob.constantPool().utf8Entry(Attributes.NAME_LINE_NUMBER_TABLE);
100                     }
101                 });
102                 case LocalVariable lv -> dcob.writeAttribute(new UnboundAttribute.AdHocAttribute<>(Attributes.localVariableTable()) {
103                     @Override
104                     public void writeBody(BufWriterImpl b) {
105                         b.writeU2(1);
106                         Util.writeLocalVariable(b, lv);
107                     }
108 
109                     @Override
110                     public Utf8Entry attributeName() {
111                         return cob.constantPool().utf8Entry(Attributes.NAME_LOCAL_VARIABLE_TABLE);
112                     }
113                 });
114                 case LocalVariableType lvt -> dcob.writeAttribute(new UnboundAttribute.AdHocAttribute<>(Attributes.localVariableTypeTable()) {
115                     @Override
116                     public void writeBody(BufWriterImpl b) {
117                         b.writeU2(1);
118                         Util.writeLocalVariable(b, lvt);
119                     }
120 
121                     @Override
122                     public Utf8Entry attributeName() {
123                         return cob.constantPool().utf8Entry(Attributes.NAME_LOCAL_VARIABLE_TYPE_TABLE);
124                     }
125                 });
126                 default -> cob.with(coe);
127             }
128         })));
129 //        ClassRecord.assertEqualsDeep(
130 //                ClassRecord.ofClassModel(ClassModel.of(Files.readAllBytes(root.resolve(targetClassFile)))),
131 //                ClassRecord.ofClassModel(ClassModel.of(Files.readAllBytes(root.resolve(sourceClassFile)))));
132 //        ClassPrinter.toYaml(ClassModel.of(Files.readAllBytes(root.resolve(targetClassFile))), ClassPrinter.Verbosity.TRACE_ALL, System.out::print);
133     }
134 
135     static Path[] corpus() throws IOException, URISyntaxException {
136         splitTableAttributes("testdata/Pattern2.class", "testdata/Pattern2-split.class");
137         return Stream.of(
138                 Files.walk(JRT.getPath("modules/java.base/java/util")),
139                 Files.walk(JRT.getPath("modules"), 2).filter(p -> p.endsWith("module-info.class")),
140                 Files.walk(Paths.get(URI.create(CorpusTest.class.getResource("CorpusTest.class").toString())).getParent()))
141                 .flatMap(p -> p)
142                 .filter(p -> Files.isRegularFile(p) && p.toString().endsWith(".class") && !p.endsWith("DeadCodePattern.class"))
143                 .filter(p -> testFilter == null || p.toString().equals(testFilter))
144                 .toArray(Path[]::new);
145     }
146 
147 
148     @ParameterizedTest
149     @MethodSource("corpus")
150     void testNullAdaptations(Path path) throws Exception {
151         byte[] bytes = Files.readAllBytes(path);
152 
153         Optional<ClassRecord> oldRecord;
154         Optional<ClassRecord> newRecord;
155         Map<Transforms.NoOpTransform, Exception> errors = new HashMap<>();
156         Map<Integer, Integer> baseDups = findDups(bytes);
157 
158         for (Transforms.NoOpTransform m : Transforms.NoOpTransform.values()) {
159             if (m == Transforms.NoOpTransform.ARRAYCOPY
160                 || m == Transforms.NoOpTransform.SHARED_3_NO_STACKMAP
161                 || m == Transforms.NoOpTransform.CLASS_REMAPPER
162                 || m.name().startsWith("ASM"))
163                 continue;
164 
165             try {
166                 byte[] transformed = m.shared && m.classTransform != null
167                                      ? ClassFile.of(ClassFile.StackMapsOption.DROP_STACK_MAPS)
168                                                 .transformClass(ClassFile.of().parse(bytes), m.classTransform)
169                                      : m.transform.apply(bytes);
170                 Map<Integer, Integer> newDups = findDups(transformed);
171                 oldRecord = m.classRecord(bytes);
172                 newRecord = m.classRecord(transformed);
173                 if (oldRecord.isPresent() && newRecord.isPresent())
174                     assertEqualsDeep(newRecord.get(), oldRecord.get(),
175                             "Class[%s] with %s".formatted(path, m.name()));
176                 switch (m) {
177                     case SHARED_1, SHARED_2, SHARED_3, SHARED_3L, SHARED_3P:
178                         if (newDups.size() > baseDups.size()) {
179                             System.out.println(String.format("Incremental dups in file %s (%s): %s / %s", path, m, baseDups, newDups));
180                         }
181                         compareCp(bytes, transformed);
182                         break;
183                     case UNSHARED_1, UNSHARED_2, UNSHARED_3:
184                         if (!newDups.isEmpty()) {
185                             System.out.println(String.format("Dups in file %s (%s): %s", path, m, newDups));
186                         }
187                         break;
188                 }
189             }
190             catch (Exception ex) {
191                 System.err.printf("Error processing %s with %s: %s.%s%n", path, m.name(),
192                                   ex.getClass(), ex.getMessage());
193                 ex.printStackTrace(System.err);
194                 errors.put(m, ex);
195             }
196         }
197 
198         if (!errors.isEmpty()) {
199             String msg = String.format("Failures for %s:%n", path)
200                          + errors.entrySet().stream()
201                                  .map(e -> {
202                                      Exception exception = e.getValue();
203                                      StackTraceElement[] trace = exception.getStackTrace();
204                                      return String.format("    Mode %s: %s (%s:%d)",
205                                                    e.getKey(), exception.toString(),
206                                                    trace.length > 0 ? trace[0].getClassName() : "unknown",
207                                                    trace.length > 0 ? trace[0].getLineNumber() : 0);
208                                  })
209                                  .collect(joining("\n"));
210             fail(String.format("Errors in testNullAdapt: %s", msg));
211         }
212 
213         // test read and transform
214         var cc = ClassFile.of();
215         var classModel = cc.parse(bytes);
216         assertEqualsDeep(ClassRecord.ofClassModel(classModel), ClassRecord.ofStreamingElements(classModel),
217                          "ClassModel (actual) vs StreamingElements (expected)");
218 
219         byte[] newBytes = cc.build(
220                 classModel.thisClass().asSymbol(),
221                 classModel::forEach);
222         var newModel = cc.parse(newBytes);
223         assertEqualsDeep(ClassRecord.ofClassModel(newModel, CompatibilityFilter.By_ClassBuilder),
224                 ClassRecord.ofClassModel(classModel, CompatibilityFilter.By_ClassBuilder),
225                 "ClassModel[%s] transformed by ClassBuilder (actual) vs ClassModel before transformation (expected)".formatted(path));
226 
227         assertEmpty(cc.verify(newModel));
228 
229         //testing maxStack and maxLocals are calculated identically by StackMapGenerator and StackCounter
230         byte[] noStackMaps = ClassFile.of(ClassFile.StackMapsOption.DROP_STACK_MAPS)
231                                       .transformClass(newModel,
232                                                          ClassTransform.transformingMethodBodies(CodeTransform.ACCEPT_ALL));
233         var noStackModel = cc.parse(noStackMaps);
234         var itStack = newModel.methods().iterator();
235         var itNoStack = noStackModel.methods().iterator();
236         while (itStack.hasNext()) {
237             assertTrue(itNoStack.hasNext());
238             var m1 = itStack.next();
239             var m2 = itNoStack.next();
240             var text1 = m1.methodName().stringValue() + m1.methodType().stringValue() + ": "
241                       + m1.code().map(CodeAttribute.class::cast)
242                                  .map(c -> c.maxLocals() + " / " + c.maxStack()).orElse("-");
243             var text2 = m2.methodName().stringValue() + m2.methodType().stringValue() + ": "
244                       + m2.code().map(CodeAttribute.class::cast)
245                                  .map(c -> c.maxLocals() + " / " + c.maxStack()).orElse("-");
246             assertEquals(text1, text2);
247         }
248         assertFalse(itNoStack.hasNext());
249     }
250 
251 //    @Test(enabled = false)
252 //    public void checkDups() {
253         // Checks input files for dups -- and there are.  Not clear this test has value.
254         // Tests above
255 //        Map<Integer, Integer> dups = findDups(bytes);
256 //        if (!dups.isEmpty()) {
257 //            String dupsString = dups.entrySet().stream()
258 //                                    .map(e -> String.format("%d -> %d", e.getKey(), e.getValue()))
259 //                                    .collect(joining(", "));
260 //            System.out.println(String.format("Duplicate entries in input file %s: %s", path, dupsString));
261 //        }
262 //    }
263 
264     private void compareCp(byte[] orig, byte[] transformed) {
265         var cc = ClassFile.of();
266         var cp1 = cc.parse(orig).constantPool();
267         var cp2 = cc.parse(transformed).constantPool();
268 
269         for (int i = 1; i < cp1.size(); i += cp1.entryByIndex(i).width()) {
270             assertEquals(cpiToString(cp1.entryByIndex(i)), cpiToString(cp2.entryByIndex(i)));
271         }
272 
273         if (cp1.size() != cp2.size()) {
274             StringBuilder failMsg = new StringBuilder("Extra entries in constant pool (" + (cp2.size() - cp1.size()) + "): ");
275             for (int i = cp1.size(); i < cp2.size(); i += cp2.entryByIndex(i).width())
276                 failMsg.append("\n").append(cp2.entryByIndex(i));
277             fail(failMsg.toString());
278         }
279     }
280 
281     private static String cpiToString(PoolEntry e) {
282         String s = e.toString();
283         if (e instanceof Utf8Entry ue)
284             s = "CONSTANT_Utf8_info[value: \"%s\"]".formatted(ue.stringValue());
285         return s;
286     }
287 
288     private static Map<Integer, Integer> findDups(byte[] bytes) {
289         Map<Integer, Integer> dups = new HashMap<>();
290         var cf = ClassFile.of().parse(bytes);
291         var pool = cf.constantPool();
292         Set<String> entryStrings = new HashSet<>();
293         for (int i = 1; i < pool.size(); i += pool.entryByIndex(i).width()) {
294             String s = cpiToString(pool.entryByIndex(i));
295             if (entryStrings.contains(s)) {
296                 for (int j=1; j<i; j += pool.entryByIndex(j).width()) {
297                     var e2 = pool.entryByIndex(j);
298                     if (s.equals(cpiToString(e2)))
299                         dups.put(i, j);
300                 }
301             }
302             entryStrings.add(s);
303         }
304         return dups;
305     }
306 }