1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.bcel.util;
20
21 import static org.junit.jupiter.api.Assertions.assertEquals;
22 import static org.junit.jupiter.api.Assertions.assertFalse;
23 import static org.junit.jupiter.api.Assertions.assertNotNull;
24 import static org.junit.jupiter.api.Assertions.assertThrows;
25 import static org.junit.jupiter.api.Assertions.assertTrue;
26
27 import java.io.BufferedInputStream;
28 import java.io.ByteArrayOutputStream;
29 import java.io.File;
30 import java.io.OutputStream;
31 import java.io.PrintStream;
32 import java.nio.charset.StandardCharsets;
33 import java.nio.file.Files;
34 import java.nio.file.Path;
35 import java.util.regex.Matcher;
36 import java.util.regex.Pattern;
37
38 import org.apache.bcel.AbstractTest;
39 import org.apache.bcel.Const;
40 import org.apache.bcel.HelloWorldCreator;
41 import org.apache.bcel.classfile.JavaClass;
42 import org.apache.bcel.classfile.Utility;
43 import org.apache.bcel.generic.BinaryOpCreator;
44 import org.apache.bcel.generic.ClassGen;
45 import org.apache.bcel.generic.ConstantPoolGen;
46 import org.apache.bcel.generic.InstructionConst;
47 import org.apache.bcel.generic.InstructionFactory;
48 import org.apache.bcel.generic.InstructionList;
49 import org.apache.bcel.generic.MethodGen;
50 import org.apache.bcel.generic.Type;
51 import org.apache.commons.lang3.SystemProperties;
52 import org.apache.commons.lang3.SystemUtils;
53 import org.junit.jupiter.api.Test;
54 import org.junit.jupiter.api.condition.DisabledForJreRange;
55 import org.junit.jupiter.api.condition.JRE;
56 import org.junit.jupiter.params.ParameterizedTest;
57 import org.junit.jupiter.params.provider.ValueSource;
58
59 class BCELifierTest extends AbstractTest {
60
61 private static Pattern CANON1 = Pattern.compile("#\\d+");
62 private static Pattern CANON2 = Pattern.compile(" +");
63 private static Pattern CANON3 = Pattern.compile("//.+");
64
65 private static final String EOL = System.lineSeparator();
66 public static final String CLASSPATH = "." + File.pathSeparator + SystemProperties.getJavaClassPath();
67
68
69 private String canonHashRef(String input) {
70 input = CANON1.matcher(input).replaceAll("#n");
71 input = CANON2.matcher(input).replaceAll(" ");
72 return CANON3.matcher(input).replaceAll("");
73 }
74
75 private String exec(final File workDir, final String... args) throws Exception {
76
77
78
79 final ProcessBuilder pb = new ProcessBuilder(args);
80 pb.directory(workDir);
81 pb.redirectErrorStream(true);
82 final Process proc = pb.start();
83 try (BufferedInputStream is = new BufferedInputStream(proc.getInputStream())) {
84 final byte[] buff = new byte[2048];
85 int len;
86
87 final StringBuilder sb = new StringBuilder();
88 while ((len = is.read(buff)) != -1) {
89 sb.append(new String(buff, 0, len));
90 }
91 final String output = sb.toString();
92 assertEquals(0, proc.waitFor(), output);
93 return output;
94 }
95 }
96
97 private String getAppJava() {
98 return getJavaHomeApp("java");
99 }
100
101 private String getAppJavaC() {
102 return getJavaHomeApp("javac");
103 }
104
105 private String getAppJavaP() {
106 return getJavaHomeApp("javap");
107 }
108
109 private String getJavaHomeApp(final String app) {
110 final Path path = getJavaHomeBinPath().resolve(app);
111 if (Files.exists(path)) {
112 return path.toAbsolutePath().toString();
113 }
114 if (SystemUtils.IS_OS_WINDOWS && Files.exists(getJavaHomeBinPath().resolve(app + ".exe"))) {
115 return path.toAbsolutePath().toString();
116 }
117 return app;
118 }
119
120 private Path getJavaHomeBinPath() {
121 return SystemUtils.getJavaHomePath().resolve("bin");
122 }
123
124 @ParameterizedTest
125 @ValueSource(strings = {
126
127 "iadd 3 2 = 5",
128 "isub 3 2 = 1",
129 "imul 3 2 = 6",
130 "idiv 3 2 = 1",
131 "irem 3 2 = 1",
132 "iand 3 2 = 2",
133 "ior 3 2 = 3",
134 "ixor 3 2 = 1",
135 "ishl 4 1 = 8",
136 "ishr 4 1 = 2",
137 "iushr 4 1 = 2",
138 "ladd 3 2 = 5",
139 "lsub 3 2 = 1",
140 "lmul 3 2 = 6",
141 "ldiv 3 2 = 1",
142 "lrem 3 2 = 1",
143 "land 3 2 = 2",
144 "lor 3 2 = 3",
145 "lxor 3 2 = 1",
146 "lshl 4 1 = 8",
147 "lshr 4 1 = 2",
148 "lushr 4 1 = 2",
149 "fadd 3 2 = 5.0",
150 "fsub 3 2 = 1.0",
151 "fmul 3 2 = 6.0",
152 "fdiv 3 2 = 1.5",
153 "frem 3 2 = 1.0",
154 "dadd 3 2 = 5.0",
155 "dsub 3 2 = 1.0",
156 "dmul 3 2 = 6.0",
157 "ddiv 3 2 = 1.5",
158 "drem 3 2 = 1.0"
159
160 })
161 void testBinaryOp(final String exp) throws Exception {
162 BinaryOpCreator.main(new String[] {});
163 final File workDir = new File("target");
164 final Pattern pattern = Pattern.compile("([a-z]{3,5}) ([-+]?\\d*\\.?\\d+) ([-+]?\\d*\\.?\\d+) = ([-+]?\\d*\\.?\\d+)");
165 final Matcher matcher = pattern.matcher(exp);
166 assertTrue(matcher.matches());
167 final String op = matcher.group(1);
168 final String a = matcher.group(2);
169 final String b = matcher.group(3);
170 final String expected = matcher.group(4);
171 final String javaAgent = getJavaAgent();
172 if (javaAgent == null) {
173 assertEquals(expected + EOL, exec(workDir, getAppJava(), "-cp", CLASSPATH, "org.apache.bcel.generic.BinaryOp", op, a, b));
174 } else {
175 final String runtimeExecJavaAgent = javaAgent.replace("jacoco.exec", "jacoco_org.apache.bcel.generic.BinaryOp.exec");
176 assertEquals(expected + EOL, exec(workDir, getAppJava(), runtimeExecJavaAgent, "-cp", CLASSPATH, "org.apache.bcel.generic.BinaryOp", op, a, b));
177 }
178 }
179
180 @Test
181 void testClassNameRejectedWhenNotJavaIdentifier() {
182
183
184
185 final ClassGen cg = new ClassGen("Evil {}\nclass Injected {}//", "java.lang.Object", "Evil.java", Const.ACC_PUBLIC | Const.ACC_SUPER,
186 new String[] {});
187 final BCELifier bcelifier = new BCELifier(cg.getJavaClass(), new ByteArrayOutputStream());
188 assertThrows(IllegalArgumentException.class, bcelifier::start);
189 }
190
191 @Test
192 void testClassNamesEscapedInOutput() throws Exception {
193
194 final String toEscapeSuper = "java.lang.Object\"); System.exit(1); _cg = new ClassGen(\"x";
195 final String toEscapeSource = "Example.java\"); System.exit(2); String s = (\"";
196 final ClassGen cg = new ClassGen("Example", toEscapeSuper, toEscapeSource, Const.ACC_PUBLIC | Const.ACC_SUPER, new String[] {});
197
198 final ByteArrayOutputStream os = new ByteArrayOutputStream();
199 new BCELifier(cg.getJavaClass(), os).start();
200 final String source = new String(os.toByteArray(), StandardCharsets.UTF_8);
201
202 assertTrue(source.contains(Utility.convertString(toEscapeSuper)), source);
203 assertTrue(source.contains(Utility.convertString(toEscapeSource)), source);
204 assertFalse(source.contains('"' + toEscapeSuper + '"'), source);
205 assertFalse(source.contains('"' + toEscapeSource + '"'), source);
206 }
207
208 private void testClassOnPath(final String javaClassFileName) throws Exception {
209 final File workDir = new File("target", getClass().getSimpleName());
210 Files.createDirectories(workDir.getParentFile().toPath());
211 final File inFile = new File(javaClassFileName);
212 final JavaClass javaClass = BCELifier.getJavaClass(inFile.getName().replace(JavaClass.EXTENSION, ""));
213 assertNotNull(javaClass);
214
215
216 final String javaClassName = javaClass.getClassName();
217 final String javapOutInital = exec(null, getAppJavaP(), "-cp", CLASSPATH, "-p", "-c", javaClassName);
218 final String outCreatorFileName = javaClass.getSourceFilePath().replace(".java", "Creator.java");
219 final File outCreatorFile = new File(workDir, outCreatorFileName);
220 Files.createDirectories(outCreatorFile.getParentFile().toPath());
221 final String javaAgent = getJavaAgent();
222 final String bcelifierClassName = BCELifier.class.getName();
223 final String creatorJavaSource;
224
225 if (javaAgent == null) {
226 creatorJavaSource = exec(workDir, getAppJava(), "-cp", CLASSPATH, bcelifierClassName, javaClassName);
227 } else {
228 final String runtimeExecJavaAgent = javaAgent.replace("jacoco.exec", "jacoco_" + inFile.getName() + ".exec");
229 creatorJavaSource = exec(workDir, getAppJava(), runtimeExecJavaAgent, "-cp", CLASSPATH, bcelifierClassName, javaClassName);
230 }
231
232 Files.write(outCreatorFile.toPath(), creatorJavaSource.getBytes(StandardCharsets.UTF_8));
233
234 assertEquals("", exec(workDir, getAppJavaC(), "-cp", CLASSPATH, outCreatorFileName.toString()));
235 final String creatorClassName = javaClassName + "Creator";
236
237 if (javaAgent == null) {
238 assertEquals("", exec(workDir, getAppJava(), "-cp", CLASSPATH, creatorClassName));
239 } else {
240 final String runtimeExecJavaAgent = javaAgent.replace("jacoco.exec", "jacoco_" + Utility.pathToPackage(outCreatorFileName) + ".exec");
241 assertEquals("", exec(workDir, getAppJava(), runtimeExecJavaAgent, "-cp", CLASSPATH, creatorClassName));
242 }
243
244 final String javapOutput = exec(workDir, getAppJavaP(), "-p", "-c", inFile.getName());
245
246 assertEquals(canonHashRef(javapOutInital), canonHashRef(javapOutput));
247 }
248
249 @Test
250 void testCreateInvokeEscapesConstantPoolName() throws Exception {
251
252 final String toEscapeName = "evil\"); System.exit(1); il.append(\"";
253 final ClassGen cg = new ClassGen("Example", "java.lang.Object", "Example.java", Const.ACC_PUBLIC | Const.ACC_SUPER, new String[] {});
254 final ConstantPoolGen cp = cg.getConstantPool();
255 final InstructionFactory factory = new InstructionFactory(cg, cp);
256 final InstructionList il = new InstructionList();
257 il.append(InstructionConst.ALOAD_0);
258 il.append(factory.createInvoke("java.lang.Object", toEscapeName, Type.VOID, Type.NO_ARGS, Const.INVOKEVIRTUAL));
259 il.append(InstructionConst.RETURN);
260 final MethodGen mg = new MethodGen(Const.ACC_PUBLIC, Type.VOID, Type.NO_ARGS, new String[] {}, "m", "Example", il, cp);
261 mg.setMaxStack();
262 mg.setMaxLocals();
263 cg.addMethod(mg.getMethod());
264
265 final ByteArrayOutputStream os = new ByteArrayOutputStream();
266 new BCELifier(cg.getJavaClass(), os).start();
267 final String source = new String(os.toByteArray(), StandardCharsets.UTF_8);
268
269 assertTrue(source.contains("_factory.createInvoke(\"java.lang.Object\", \"" + Utility.convertString(toEscapeName) + "\", "), source);
270 assertFalse(source.contains('"' + toEscapeName + '"'), source);
271 }
272
273 @Test
274 void testHelloWorld() throws Exception {
275 HelloWorldCreator.main(new String[] {});
276 final File workDir = new File("target");
277 final String javaAgent = getJavaAgent();
278 if (javaAgent == null) {
279 assertEquals("Hello World!" + EOL, exec(workDir, getAppJava(), "-cp", CLASSPATH, "org.apache.bcel.HelloWorld"));
280 } else {
281 final String runtimeExecJavaAgent = javaAgent.replace("jacoco.exec", "jacoco_org.apache.bcel.HelloWorld.exec");
282 assertEquals("Hello World!" + EOL, exec(workDir, getAppJava(), runtimeExecJavaAgent, "-cp", CLASSPATH, "org.apache.bcel.HelloWorld"));
283 }
284 }
285
286
287
288
289
290
291 @ParameterizedTest
292 @ValueSource(strings = {
293
294 "org.apache.commons.lang.math.Fraction.class",
295 "org.apache.commons.lang.exception.NestableDelegate.class",
296 "org.apache.commons.lang.builder.CompareToBuilder.class",
297 "org.apache.commons.lang.builder.ToStringBuilder.class",
298 "org.apache.commons.lang.SerializationUtils.class",
299 "org.apache.commons.lang.ArrayUtils.class",
300 "target/test-classes/Java4Example.class"
301
302 })
303 void testJavapCompare(final String pathToClass) throws Exception {
304 testClassOnPath(pathToClass);
305 }
306
307
308
309
310 @ParameterizedTest
311 @ValueSource(strings = {
312
313 "target/test-classes/Java8Example.class",
314 "target/test-classes/Java8Example2.class",
315
316 })
317 @DisabledForJreRange(min = JRE.JAVA_25)
318 void testJavapCompareJava25KnownBroken(final String pathToClass) throws Exception {
319 testClassOnPath(pathToClass);
320 }
321
322 @Test
323 void testMainNoArg() throws Exception {
324 final PrintStream sysout = System.out;
325 try {
326 final ByteArrayOutputStream out = new ByteArrayOutputStream();
327 System.setOut(new PrintStream(out));
328 BCELifier.main(new String[0]);
329 final String outputNoArgs = new String(out.toByteArray());
330 assertEquals("Usage: BCELifier className" + EOL + "\tThe class must exist on the classpath" + EOL, outputNoArgs);
331 } finally {
332 System.setOut(sysout);
333 }
334 }
335
336 @Test
337 void testMethodNameEscapedInOutput() throws Exception {
338
339 final String evilName = "evil\"); System.exit(1); String x = (\"";
340 final ClassGen cg = new ClassGen("Example", "java.lang.Object", "Example.java", Const.ACC_PUBLIC | Const.ACC_SUPER, new String[] {});
341 final ConstantPoolGen cp = cg.getConstantPool();
342 final InstructionList il = new InstructionList();
343 il.append(InstructionConst.RETURN);
344 final MethodGen mg = new MethodGen(Const.ACC_PUBLIC, Type.VOID, Type.NO_ARGS, new String[] {}, evilName, "Example", il, cp);
345 mg.setMaxStack();
346 mg.setMaxLocals();
347 cg.addMethod(mg.getMethod());
348
349 final ByteArrayOutputStream os = new ByteArrayOutputStream();
350 new BCELifier(cg.getJavaClass(), os).start();
351 final String source = new String(os.toByteArray(), StandardCharsets.UTF_8);
352
353 assertTrue(source.contains("\"" + Utility.convertString(evilName) + "\", \"Example\", il, _cp);"), source);
354 assertFalse(source.contains('"' + evilName + '"'), source);
355 }
356
357 @ParameterizedTest
358 @ValueSource(strings = { "StackMapExample", "StackMapExample2" })
359 void testStackMap(final String className) throws Exception {
360 testJavapCompare(className);
361 final File workDir = new File("target");
362 assertEquals("Hello World" + EOL, exec(workDir, getAppJava(), "-cp", CLASSPATH, className, "Hello"));
363 }
364
365 @Test
366 void testStart() throws Exception {
367 final OutputStream os = new ByteArrayOutputStream();
368 final JavaClass javaClass = BCELifier.getJavaClass("Java8Example");
369 assertNotNull(javaClass);
370 final BCELifier bcelifier = new BCELifier(javaClass, os);
371 bcelifier.start();
372 }
373 }