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  
20  package org.apache.bcel.generic;
21  
22  import static org.junit.jupiter.api.Assertions.assertArrayEquals;
23  import static org.junit.jupiter.api.Assertions.assertNotNull;
24  import static org.junit.jupiter.api.Assertions.fail;
25  import static org.junit.jupiter.api.Assumptions.assumeTrue;
26  
27  import java.io.File;
28  import java.io.IOException;
29  import java.io.InputStream;
30  import java.nio.file.FileSystems;
31  import java.nio.file.FileVisitResult;
32  import java.nio.file.Files;
33  import java.nio.file.Path;
34  import java.nio.file.PathMatcher;
35  import java.nio.file.Paths;
36  import java.nio.file.SimpleFileVisitor;
37  import java.nio.file.attribute.BasicFileAttributes;
38  import java.util.Enumeration;
39  import java.util.jar.JarEntry;
40  import java.util.jar.JarFile;
41  import java.util.stream.Stream;
42  
43  import org.apache.bcel.classfile.ClassParser;
44  import org.apache.bcel.classfile.Code;
45  import org.apache.bcel.classfile.JavaClass;
46  import org.apache.bcel.classfile.Method;
47  import org.apache.bcel.util.ModularRuntimeImage;
48  import org.apache.commons.lang3.JavaVersion;
49  import org.apache.commons.lang3.SystemUtils;
50  import org.junit.jupiter.api.condition.DisabledOnJre;
51  import org.junit.jupiter.api.condition.JRE;
52  import org.junit.jupiter.params.ParameterizedTest;
53  import org.junit.jupiter.params.provider.MethodSource;
54  
55  /**
56   * Test that the generic dump() methods work on the JDK classes Reads each class into an instruction list and then dumps the instructions. The output bytes
57   * should be the same as the input.
58   * <p>
59   * Sets the property {@value JavaHome#EXTRA_JAVA_HOMES} to a {@link File#pathSeparator}-separated list of JRE/JDK paths for additional testing.
60   * </p>
61   * <p>
62   * For example:
63   * </p>
64   *
65   * <pre>
66   * mvn test -Dtest=JdkGenericDumpTest -DExtraJavaHomes="C:\Program Files\Java\openjdk\jdk-13;C:\Program Files\Java\openjdk\jdk-14"
67   * mvn test -Dtest=JdkGenericDumpTest -DExtraJavaRoot="C:\Program Files\Eclipse Adoptium"
68   * </pre>
69   * <p>
70   * Where "C:\Program Files\Eclipse Adoptium" contains JDK directories, for example:
71   * </p>
72   * <ul>
73   * <li>jdk-11.0.16.101-hotspot</li>
74   * <li>jdk-17.0.4.101-hotspot</li>
75   * <li>jdk-19.0.0.36-hotspot</li>
76   * <li>jdk-8.0.345.1-hotspot</li>
77   * </ul>
78   */
79  class JdkGenericDumpTest {
80  
81      private static class ClassParserFilesVisitor extends SimpleFileVisitor<Path> {
82  
83          private final PathMatcher matcher;
84  
85          @SuppressWarnings("resource") // FileSystems.getDefault() returns a singleton
86          ClassParserFilesVisitor(final String pattern) {
87              matcher = FileSystems.getDefault().getPathMatcher("glob:" + pattern);
88          }
89  
90          private void find(final Path path) throws IOException {
91              final Path name = path.getFileName();
92              if (name != null && matcher.matches(name)) {
93                  try (InputStream inputStream = Files.newInputStream(path)) {
94                      final ClassParser classParser = new ClassParser(inputStream, name.toAbsolutePath().toString());
95                      assertNotNull(classParser.parse());
96                  }
97              }
98          }
99  
100         @Override
101         public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) throws IOException {
102             find(dir);
103             return FileVisitResult.CONTINUE;
104         }
105 
106         @Override
107         public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
108             find(file);
109             return FileVisitResult.CONTINUE;
110         }
111 
112         @Override
113         public FileVisitResult visitFileFailed(final Path file, final IOException e) {
114             System.err.println(e);
115             return FileVisitResult.CONTINUE;
116         }
117     }
118 
119     private static final char[] hexArray = "0123456789ABCDEF".toCharArray();
120 
121     private static String bytesToHex(final byte[] bytes) {
122         final char[] hexChars = new char[bytes.length * 3];
123         int i = 0;
124         for (final byte b : bytes) {
125             final int v = b & 0xFF;
126             hexChars[i++] = hexArray[v >>> 4];
127             hexChars[i++] = hexArray[v & 0x0F];
128             hexChars[i++] = ' ';
129         }
130         return new String(hexChars);
131     }
132 
133     static Stream<Path> testJdkModules() {
134         // JUnit requires at least one parameter.
135         // Make sure we have at least one JMOD file, since Java 25 and up may not have them present by default, depending on your installation.
136         return Stream.concat(JavaHome.streamModulePath(), Stream.of(Paths.get("src/test/resources/jpms/empty/empty.jmod")));
137     }
138 
139     private void compare(final String name, final Method method) {
140         // System.out.println("Method: " + m);
141         final Code code = method.getCode();
142         if (code == null) {
143             return; // for example abstract method
144         }
145         final byte[] src = code.getCode();
146         final InstructionList instructionList = new InstructionList(src);
147         final byte[] out = instructionList.getByteCode();
148         if (src.length == out.length) {
149             assertArrayEquals(src, out, () -> name + ": " + method.toString());
150         } else {
151             System.out.println(name + ": " + method.toString() + " " + src.length + " " + out.length);
152             System.out.println(bytesToHex(src));
153             System.out.println(bytesToHex(out));
154             for (final InstructionHandle instructionHandle : instructionList) {
155                 System.out.println(instructionHandle.toString(false));
156             }
157             fail("Array comparison failure");
158         }
159     }
160 
161     private void testJar(final Path file) throws Exception {
162         System.out.println(file);
163         try (JarFile jar = new JarFile(file.toFile())) {
164             final Enumeration<JarEntry> en = jar.entries();
165             while (en.hasMoreElements()) {
166                 final JarEntry jarEntry = en.nextElement();
167                 final String name = jarEntry.getName();
168                 if (name.endsWith(JavaClass.EXTENSION)) {
169                     // System.out.println("- " + name);
170                     try (InputStream inputStream = jar.getInputStream(jarEntry)) {
171                         final ClassParser classParser = new ClassParser(inputStream, name);
172                         final JavaClass javaClass = classParser.parse();
173                         for (final Method method : javaClass.getMethods()) {
174                             compare(name, method);
175                         }
176                     }
177                 }
178             }
179         }
180     }
181 
182     @ParameterizedTest
183     @MethodSource("org.apache.bcel.generic.JavaHome#streamJarPath")
184     void testJdkJars(final Path jarPath) throws Exception {
185         testJar(jarPath);
186     }
187 
188     @ParameterizedTest
189     @DisabledOnJre(value = JRE.JAVA_8)
190     @MethodSource
191     void testJdkModules(final Path jmodPath) throws Exception {
192         testJar(jmodPath);
193     }
194 
195     @ParameterizedTest
196     @MethodSource("org.apache.bcel.generic.JavaHome#streamJavaHome")
197     void testJreModules(final JavaHome javaHome) throws Exception {
198         assumeTrue(SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_9));
199         try (ModularRuntimeImage mri = javaHome.getModularRuntimeImage()) {
200             for (final Path path : mri.modules()) {
201                 Files.walkFileTree(path, new ClassParserFilesVisitor("*.class"));
202             }
203         }
204     }
205 
206 }