View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   https://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
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+"); // numbers may vary in length
62      private static Pattern CANON2 = Pattern.compile(" +"); // collapse spaces
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      // Canonicalise the javap output so it compares better
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          // System.out.print(workDir + ": ");
77          // Stream.of(args).forEach(e -> System.out.print(e + " "));
78          // System.out.println();
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     // @formatter:off
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     // @formatter:on
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         // Class file names may contain characters the Java language forbids (JVMS 4.2.2 bans only . ; [ /).
183         // BCELifier must refuse to emit such a name in identifier position rather than let a crafted
184         // this_class inject statements into the generated source.
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         // Superclass and source file names are constant-pool derived and can hold any UTF-8.
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         // Gets javap output of the input class
215         // System.out.println(exec(null, getJavaP(), "-version"));
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         // Creates the BCEL Java app source in creatorJavaSource
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         // Write the BCEL Java app source to a file
232         Files.write(outCreatorFile.toPath(), creatorJavaSource.getBytes(StandardCharsets.UTF_8));
233         // Compiles the BCEL Java app
234         assertEquals("", exec(workDir, getAppJavaC(), "-cp", CLASSPATH, outCreatorFileName.toString()));
235         final String creatorClassName = javaClassName + "Creator";
236         // Runs the BCEL Java app to create a class file
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         // Runs javap on the BCEL generated class file
244         final String javapOutput = exec(workDir, getAppJavaP(), "-p", "-c", inFile.getName());
245         // Finally compares the output of the roundtrip
246         assertEquals(canonHashRef(javapOutInital), canonHashRef(javapOutput));
247     }
248 
249     @Test
250     void testCreateInvokeEscapesConstantPoolName() throws Exception {
251         // A hostile constant pool can hold any UTF-8 as a referenced method name.
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      * Dumps a class using "javap" and compare with the same class recreated using BCELifier, "javac", "java" and dumped with "javap".
288      *
289      * TODO: detect if JDK present and skip test if not
290      */
291     @ParameterizedTest
292     @ValueSource(strings = {
293     // @formatter:off
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     // @formatter:on
302     })
303     void testJavapCompare(final String pathToClass) throws Exception {
304         testClassOnPath(pathToClass);
305     }
306 
307     /*
308      * See https://issues.apache.org/jira/browse/BCEL-378
309      */
310     @ParameterizedTest
311     @ValueSource(strings = {
312     // @formatter:off
313         "target/test-classes/Java8Example.class",
314         "target/test-classes/Java8Example2.class",
315     // @formatter:on
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         // A hostile constant pool can hold any UTF-8 as a method name.
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 }