• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // ASM: a very small and fast Java bytecode manipulation framework
2 // Copyright (c) 2000-2011 INRIA, France Telecom
3 // All rights reserved.
4 //
5 // Redistribution and use in source and binary forms, with or without
6 // modification, are permitted provided that the following conditions
7 // are met:
8 // 1. Redistributions of source code must retain the above copyright
9 //    notice, this list of conditions and the following disclaimer.
10 // 2. Redistributions in binary form must reproduce the above copyright
11 //    notice, this list of conditions and the following disclaimer in the
12 //    documentation and/or other materials provided with the distribution.
13 // 3. Neither the name of the copyright holders nor the names of its
14 //    contributors may be used to endorse or promote products derived from
15 //    this software without specific prior written permission.
16 //
17 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21 // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22 // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23 // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25 // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
27 // THE POSSIBILITY OF SUCH DAMAGE.
28 package org.objectweb.asm.tools;
29 
30 import static java.lang.String.format;
31 import static java.util.stream.Collectors.toSet;
32 
33 import java.io.BufferedReader;
34 import java.io.File;
35 import java.io.IOException;
36 import java.io.InputStream;
37 import java.io.InputStreamReader;
38 import java.io.LineNumberReader;
39 import java.lang.module.ModuleDescriptor;
40 import java.nio.file.Files;
41 import java.nio.file.Path;
42 import java.util.ArrayList;
43 import java.util.HashMap;
44 import java.util.HashSet;
45 import java.util.List;
46 import java.util.Set;
47 import java.util.stream.Stream;
48 import java.util.zip.GZIPInputStream;
49 import org.objectweb.asm.ClassReader;
50 import org.objectweb.asm.ClassVisitor;
51 import org.objectweb.asm.ClassWriter;
52 import org.objectweb.asm.FieldVisitor;
53 import org.objectweb.asm.Handle;
54 import org.objectweb.asm.Label;
55 import org.objectweb.asm.MethodVisitor;
56 import org.objectweb.asm.ModuleVisitor;
57 import org.objectweb.asm.Opcodes;
58 import org.objectweb.asm.Type;
59 
60 /**
61  * A tool to transform classes in order to make them compatible with Java 1.5, and to check that
62  * they use only the JDK 1.5 API and JDK 1.5 class file features. The original classes can either be
63  * transformed "in place", or be copied first to destination directory and transformed here (leaving
64  * the original classes unchanged).
65  *
66  * @author Eric Bruneton
67  * @author Eugene Kuleshov
68  */
69 public class Retrofitter {
70 
71   /** The name of the module-info file. */
72   private static final String MODULE_INFO = "module-info.class";
73 
74   /** The name of the java.base module. */
75   private static final String JAVA_BASE_MODULE = "java.base";
76 
77   /**
78    * The fields and methods of the JDK 1.5 API. Each string has the form
79    * "<owner><name><descriptor>".
80    */
81   private final HashSet<String> jdkApi = new HashSet<>();
82 
83   /**
84    * The class hierarchy of the JDK 1.5 API. Maps each class name to the name of its super class.
85    */
86   private final HashMap<String, String> jdkHierarchy = new HashMap<>();
87 
88   /** The internal names of the packages exported by the retrofitted classes. */
89   private final HashSet<String> exports = new HashSet<>();
90 
91   /** The internal names of the packages imported by the retrofitted classes. */
92   private final HashSet<String> imports = new HashSet<>();
93 
94   /**
95    * Transforms the class files in the given directory, in place, in order to make them compatible
96    * with the JDK 1.5. Also generates a module-info class in this directory, with the given module
97    * version.
98    *
99    * @param args a directory containing compiled classes and the ASM release version.
100    * @throws IOException if a file can't be read or written.
101    */
main(final String[] args)102   public static void main(final String[] args) throws IOException {
103     if (args.length == 2) {
104       new Retrofitter().retrofit(new File(args[0]), args[1]);
105     } else {
106       System.err.println("Usage: Retrofitter <classes directory> <ASM release version>"); // NOPMD
107     }
108   }
109 
110   /**
111    * Transforms the class files in the given directory, in place, in order to make them compatible
112    * with the JDK 1.5. Also generates a module-info class in this directory, with the given module
113    * version.
114    *
115    * @param classesDir a directory containing compiled classes.
116    * @param version the module-info version.
117    * @throws IOException if a file can't be read or written.
118    */
retrofit(final File classesDir, final String version)119   public void retrofit(final File classesDir, final String version) throws IOException {
120     for (File classFile : getAllClasses(classesDir, new ArrayList<File>())) {
121       ClassReader classReader = new ClassReader(Files.newInputStream(classFile.toPath()));
122       ClassWriter classWriter = new ClassWriter(0);
123       classReader.accept(new ClassRetrofitter(classWriter), ClassReader.SKIP_FRAMES);
124       Files.write(classFile.toPath(), classWriter.toByteArray());
125     }
126     generateModuleInfoClass(classesDir, version);
127   }
128 
129   /**
130    * Verify that the class files in the given directory only use JDK 1.5 APIs, and that a
131    * module-info class is present with the expected content.
132    *
133    * @param classesDir a directory containing compiled classes.
134    * @param expectedVersion the expected module-info version.
135    * @param expectedExports the expected module-info exported packages.
136    * @param expectedRequires the expected module-info required modules.
137    * @throws IOException if a file can't be read.
138    * @throws IllegalArgumentException if the module-info class does not have the expected content.
139    */
verify( final File classesDir, final String expectedVersion, final List<String> expectedExports, final List<String> expectedRequires)140   public void verify(
141       final File classesDir,
142       final String expectedVersion,
143       final List<String> expectedExports,
144       final List<String> expectedRequires)
145       throws IOException {
146     if (jdkApi.isEmpty()) {
147       readJdkApi();
148     }
149     for (File classFile : getAllClasses(classesDir, new ArrayList<File>())) {
150       if (!classFile.getName().equals(MODULE_INFO)) {
151         new ClassReader(Files.newInputStream(classFile.toPath())).accept(new ClassVerifier(), 0);
152       }
153     }
154     verifyModuleInfoClass(
155         classesDir,
156         expectedVersion,
157         new HashSet<String>(expectedExports),
158         Stream.concat(expectedRequires.stream(), Stream.of(JAVA_BASE_MODULE)).collect(toSet()));
159   }
160 
getAllClasses(final File file, final List<File> allClasses)161   private List<File> getAllClasses(final File file, final List<File> allClasses)
162       throws IOException {
163     if (file.isDirectory()) {
164       File[] children = file.listFiles();
165       if (children == null) {
166         throw new IOException("Unable to read files of " + file);
167       }
168       for (File child : children) {
169         getAllClasses(child, allClasses);
170       }
171     } else if (file.getName().endsWith(".class")) {
172       allClasses.add(file);
173     }
174     return allClasses;
175   }
176 
generateModuleInfoClass(final File dstDir, final String version)177   private void generateModuleInfoClass(final File dstDir, final String version) throws IOException {
178     ClassWriter classWriter = new ClassWriter(0);
179     classWriter.visit(Opcodes.V9, Opcodes.ACC_MODULE, "module-info", null, null, null);
180     ArrayList<String> moduleNames = new ArrayList<>();
181     for (String exportName : exports) {
182       if (isAsmModule(exportName)) {
183         moduleNames.add(exportName);
184       }
185     }
186     if (moduleNames.size() != 1) {
187       throw new IllegalArgumentException("Module name can't be infered from classes");
188     }
189     ModuleVisitor moduleVisitor =
190         classWriter.visitModule(moduleNames.get(0).replace('/', '.'), Opcodes.ACC_OPEN, version);
191 
192     for (String importName : imports) {
193       if (isAsmModule(importName) && !exports.contains(importName)) {
194         moduleVisitor.visitRequire(importName.replace('/', '.'), Opcodes.ACC_TRANSITIVE, null);
195       }
196     }
197     moduleVisitor.visitRequire(JAVA_BASE_MODULE, Opcodes.ACC_MANDATED, null);
198 
199     for (String exportName : exports) {
200       moduleVisitor.visitExport(exportName, 0);
201     }
202     moduleVisitor.visitEnd();
203     classWriter.visitEnd();
204     Files.write(Path.of(dstDir.getAbsolutePath(), MODULE_INFO), classWriter.toByteArray());
205   }
206 
verifyModuleInfoClass( final File dstDir, final String expectedVersion, final Set<String> expectedExports, final Set<String> expectedRequires)207   private void verifyModuleInfoClass(
208       final File dstDir,
209       final String expectedVersion,
210       final Set<String> expectedExports,
211       final Set<String> expectedRequires)
212       throws IOException {
213     ModuleDescriptor module =
214         ModuleDescriptor.read(Files.newInputStream(Path.of(dstDir.getAbsolutePath(), MODULE_INFO)));
215     String version = module.version().map(ModuleDescriptor.Version::toString).orElse("");
216     if (!version.equals(expectedVersion)) {
217       throw new IllegalArgumentException(
218           format("Wrong module-info version '%s' (expected '%s')", version, expectedVersion));
219     }
220     Set<String> exports =
221         module.exports().stream().map(ModuleDescriptor.Exports::source).collect(toSet());
222     if (!exports.equals(expectedExports)) {
223       throw new IllegalArgumentException(
224           format("Wrong module-info exports %s (expected %s)", exports, expectedExports));
225     }
226     Set<String> requires =
227         module.requires().stream().map(ModuleDescriptor.Requires::name).collect(toSet());
228     if (!requires.equals(expectedRequires)) {
229       throw new IllegalArgumentException(
230           format("Wrong module-info requires %s (expected %s)", requires, expectedRequires));
231     }
232   }
233 
isAsmModule(final String packageName)234   private static boolean isAsmModule(final String packageName) {
235     return packageName.startsWith("org/objectweb/asm")
236         && !packageName.equals("org/objectweb/asm/signature");
237   }
238 
readJdkApi()239   private void readJdkApi() throws IOException {
240     try (InputStream inputStream =
241             new GZIPInputStream(
242                 Retrofitter.class.getClassLoader().getResourceAsStream("jdk1.5.0.12.txt.gz"));
243         BufferedReader reader = new LineNumberReader(new InputStreamReader(inputStream))) {
244       while (true) {
245         String line = reader.readLine();
246         if (line != null) {
247           if (line.startsWith("class")) {
248             String className = line.substring(6, line.lastIndexOf(' '));
249             String superClassName = line.substring(line.lastIndexOf(' ') + 1);
250             jdkHierarchy.put(className, superClassName);
251           } else {
252             jdkApi.add(line);
253           }
254         } else {
255           break;
256         }
257       }
258     } catch (IOException ioe) {
259       throw ioe;
260     }
261   }
262 
263   /** A ClassVisitor that retrofits classes to 1.5 version. */
264   class ClassRetrofitter extends ClassVisitor {
265 
ClassRetrofitter(final ClassVisitor classVisitor)266     public ClassRetrofitter(final ClassVisitor classVisitor) {
267       super(/* latest api =*/ Opcodes.ASM8, classVisitor);
268     }
269 
270     @Override
visit( final int version, final int access, final String name, final String signature, final String superName, final String[] interfaces)271     public void visit(
272         final int version,
273         final int access,
274         final String name,
275         final String signature,
276         final String superName,
277         final String[] interfaces) {
278       addPackageReferences(Type.getObjectType(name), /* export = */ true);
279       super.visit(Opcodes.V1_5, access, name, signature, superName, interfaces);
280     }
281 
282     @Override
visitField( final int access, final String name, final String descriptor, final String signature, final Object value)283     public FieldVisitor visitField(
284         final int access,
285         final String name,
286         final String descriptor,
287         final String signature,
288         final Object value) {
289       addPackageReferences(Type.getType(descriptor), /* export = */ false);
290       return super.visitField(access, name, descriptor, signature, value);
291     }
292 
293     @Override
visitMethod( final int access, final String name, final String descriptor, final String signature, final String[] exceptions)294     public MethodVisitor visitMethod(
295         final int access,
296         final String name,
297         final String descriptor,
298         final String signature,
299         final String[] exceptions) {
300       addPackageReferences(Type.getType(descriptor), /* export = */ false);
301       return new MethodVisitor(
302           api, super.visitMethod(access, name, descriptor, signature, exceptions)) {
303 
304         @Override
305         public void visitFieldInsn(
306             final int opcode, final String owner, final String name, final String descriptor) {
307           addPackageReferences(Type.getType(descriptor), /* export = */ false);
308           super.visitFieldInsn(opcode, owner, name, descriptor);
309         }
310 
311         @Override
312         public void visitMethodInsn(
313             final int opcode,
314             final String owner,
315             final String name,
316             final String descriptor,
317             final boolean isInterface) {
318           addPackageReferences(Type.getType(descriptor), /* export = */ false);
319           // Remove the addSuppressed() method calls generated for try-with-resources statements.
320           // This method is not defined in JDK1.5.
321           if (owner.equals("java/lang/Throwable")
322               && name.equals("addSuppressed")
323               && descriptor.equals("(Ljava/lang/Throwable;)V")) {
324             visitInsn(Opcodes.POP2);
325           } else {
326             super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
327           }
328         }
329 
330         @Override
331         public void visitTypeInsn(final int opcode, final String type) {
332           addPackageReferences(Type.getObjectType(type), /* export = */ false);
333           super.visitTypeInsn(opcode, type);
334         }
335 
336         @Override
337         public void visitMultiANewArrayInsn(final String descriptor, final int numDimensions) {
338           addPackageReferences(Type.getType(descriptor), /* export = */ false);
339           super.visitMultiANewArrayInsn(descriptor, numDimensions);
340         }
341 
342         @Override
343         public void visitTryCatchBlock(
344             final Label start, final Label end, final Label handler, final String type) {
345           if (type != null) {
346             addPackageReferences(Type.getObjectType(type), /* export = */ false);
347           }
348           super.visitTryCatchBlock(start, end, handler, type);
349         }
350       };
351     }
352 
addPackageReferences(final Type type, final boolean export)353     private void addPackageReferences(final Type type, final boolean export) {
354       switch (type.getSort()) {
355         case Type.ARRAY:
356           addPackageReferences(type.getElementType(), export);
357           break;
358         case Type.METHOD:
359           for (Type argumentType : type.getArgumentTypes()) {
360             addPackageReferences(argumentType, export);
361           }
362           addPackageReferences(type.getReturnType(), export);
363           break;
364         case Type.OBJECT:
365           String internalName = type.getInternalName();
366           int lastSlashIndex = internalName.lastIndexOf('/');
367           if (lastSlashIndex != -1) {
368             (export ? exports : imports).add(internalName.substring(0, lastSlashIndex));
369           }
370           break;
371         default:
372           break;
373       }
374     }
375   }
376 
377   /**
378    * A ClassVisitor checking that a class uses only JDK 1.5 class file features and the JDK 1.5 API.
379    */
380   class ClassVerifier extends ClassVisitor {
381 
382     /** The internal name of the visited class. */
383     String className;
384 
385     /** The name of the currently visited method. */
386     String currentMethodName;
387 
388     public ClassVerifier() {
389       // Make sure use we don't use Java 9 or higher classfile features.
390       // We also want to make sure we don't use Java 6, 7 or 8 classfile
391       // features (invokedynamic), but this can't be done in the same way.
392       // Instead, we use manual checks below.
393       super(Opcodes.ASM4, null);
394     }
395 
396     @Override
397     public void visit(
398         final int version,
399         final int access,
400         final String name,
401         final String signature,
402         final String superName,
403         final String[] interfaces) {
404       if ((version & 0xFFFF) > Opcodes.V1_5) {
405         throw new IllegalArgumentException(format("ERROR: %d version is newer than 1.5", version));
406       }
407       className = name;
408     }
409 
410     @Override
411     public MethodVisitor visitMethod(
412         final int access,
413         final String name,
414         final String descriptor,
415         final String signature,
416         final String[] exceptions) {
417       currentMethodName = name + descriptor;
418       MethodVisitor methodVisitor =
419           super.visitMethod(access, name, descriptor, signature, exceptions);
420       return new MethodVisitor(Opcodes.ASM4, methodVisitor) {
421         @Override
422         public void visitFieldInsn(
423             final int opcode, final String owner, final String name, final String descriptor) {
424           check(owner, name);
425         }
426 
427         @Override
428         public void visitMethodInsn(
429             final int opcode,
430             final String owner,
431             final String name,
432             final String descriptor,
433             final boolean isInterface) {
434           check(owner, name + descriptor);
435         }
436 
437         @Override
438         public void visitLdcInsn(final Object value) {
439           if (value instanceof Type) {
440             int sort = ((Type) value).getSort();
441             if (sort == Type.METHOD) {
442               throw new IllegalArgumentException(
443                   format(
444                       "ERROR: ldc with a MethodType called in %s %s is not available in JDK 1.5",
445                       className, currentMethodName));
446             }
447           } else if (value instanceof Handle) {
448             throw new IllegalArgumentException(
449                 format(
450                     "ERROR: ldc with a MethodHandle called in %s %s is not available in JDK 1.5",
451                     className, currentMethodName));
452           }
453         }
454 
455         @Override
456         public void visitInvokeDynamicInsn(
457             final String name,
458             final String descriptor,
459             final Handle bootstrapMethodHandle,
460             final Object... bootstrapMethodArguments) {
461           throw new IllegalArgumentException(
462               format(
463                   "ERROR: invokedynamic called in %s %s is not available in JDK 1.5",
464                   className, currentMethodName));
465         }
466       };
467     }
468 
469     /**
470      * Checks whether or not a field or method is defined in the JDK 1.5 API.
471      *
472      * @param owner A class name.
473      * @param member A field name or a method name and descriptor.
474      */
475     private void check(final String owner, final String member) {
476       if (owner.startsWith("java/")) {
477         String currentOwner = owner;
478         while (currentOwner != null) {
479           if (jdkApi.contains(currentOwner + ' ' + member)) {
480             return;
481           }
482           currentOwner = jdkHierarchy.get(currentOwner);
483         }
484         throw new IllegalArgumentException(
485             format(
486                 "ERROR: %s %s called in %s %s is not defined in the JDK 1.5 API",
487                 owner, member, className, currentMethodName));
488       }
489     }
490   }
491 }
492