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 }