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 }