• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019. Uber Technologies
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *    http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.uber.nullaway.jarinfer;
17 
18 import com.google.common.collect.ImmutableSet;
19 import com.google.common.collect.Sets;
20 import java.io.BufferedReader;
21 import java.io.ByteArrayOutputStream;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.InputStreamReader;
25 import java.io.OutputStream;
26 import java.util.Iterator;
27 import java.util.List;
28 import java.util.Set;
29 import java.util.jar.JarEntry;
30 import java.util.jar.JarFile;
31 import java.util.jar.JarInputStream;
32 import java.util.jar.JarOutputStream;
33 import java.util.zip.ZipEntry;
34 import java.util.zip.ZipFile;
35 import java.util.zip.ZipOutputStream;
36 import org.apache.commons.io.IOUtils;
37 import org.objectweb.asm.ClassReader;
38 import org.objectweb.asm.ClassWriter;
39 import org.objectweb.asm.Opcodes;
40 import org.objectweb.asm.tree.AnnotationNode;
41 import org.objectweb.asm.tree.ClassNode;
42 import org.objectweb.asm.tree.MethodNode;
43 
44 /** Annotates the given methods and method parameters with the specified annotations using ASM. */
45 public final class BytecodeAnnotator {
46   private static boolean debug = false;
47 
LOG(boolean cond, String tag, String msg)48   private static void LOG(boolean cond, String tag, String msg) {
49     if (cond) {
50       System.out.println("[" + tag + "] " + msg);
51     }
52   }
53 
54   public static final String javaxNullableDesc = "Ljavax/annotation/Nullable;";
55   public static final String javaxNonnullDesc = "Ljavax/annotation/Nonnull;";
56   // Consider android.support.annotation.* as a configuration option for older code?
57   public static final String androidNullableDesc = "Landroidx/annotation/Nullable;";
58   public static final String androidNonnullDesc = "Landroidx/annotation/NonNull;";
59 
60   public static final ImmutableSet<String> NULLABLE_ANNOTATIONS =
61       ImmutableSet.of(
62           javaxNullableDesc,
63           androidNullableDesc,
64           // We don't support adding the annotations below, but they would still be redundant,
65           // specially when converted by tools which rewrite these sort of annotation (often
66           // to their androidx.* variant)
67           "Landroid/support/annotation/Nullable;",
68           "Lorg/jetbrains/annotations/Nullable;");
69 
70   public static final ImmutableSet<String> NONNULL_ANNOTATIONS =
71       ImmutableSet.of(
72           javaxNonnullDesc,
73           androidNonnullDesc,
74           // See above
75           "Landroid/support/annotation/NonNull;",
76           "Lorg/jetbrains/annotations/NotNull;");
77 
78   public static final Sets.SetView<String> NULLABILITY_ANNOTATIONS =
79       Sets.union(NULLABLE_ANNOTATIONS, NONNULL_ANNOTATIONS);
80 
81   // Constants used for signed jar processing
82   private static final String SIGNED_JAR_ERROR_MESSAGE =
83       "JarInfer will not process signed jars by default. "
84           + "Please take one of the following actions:\n"
85           + "\t1) Remove the signature from the original jar before passing it to jarinfer,\n"
86           + "\t2) Pass the --strip-jar-signatures flag to JarInfer and the tool will remove signature "
87           + "metadata for you, or\n"
88           + "\t3) Exclude this jar from those being processed by JarInfer.";
89   private static final String BASE64_PATTERN =
90       "(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?";
91   private static final String DIGEST_ENTRY_PATTERN =
92       "Name: [A-Za-z0-9/\\$\\n\\s\\-\\.]+[A-Za-z0-9]\\nSHA-256-Digest: " + BASE64_PATTERN;
93 
annotationsShouldBeVisible(String nullableDesc)94   private static boolean annotationsShouldBeVisible(String nullableDesc) {
95     if (nullableDesc.equals(javaxNullableDesc)) {
96       return true;
97     } else if (nullableDesc.equals(androidNullableDesc)) {
98       return false;
99     } else {
100       throw new Error("Unknown nullness annotation visibility");
101     }
102   }
103 
listHasNullnessAnnotations(List<AnnotationNode> annotationList)104   private static boolean listHasNullnessAnnotations(List<AnnotationNode> annotationList) {
105     if (annotationList != null) {
106       for (AnnotationNode node : annotationList) {
107         if (NULLABILITY_ANNOTATIONS.contains(node.desc)) {
108           return true;
109         }
110       }
111     }
112     return false;
113   }
114 
115   /**
116    * Returns true if any part of this method already has @Nullable/@NonNull annotations, in which
117    * case we skip it, assuming that the developer already captured the desired spec.
118    *
119    * @param method The method node.
120    * @return true iff either the return or any parameter formal has a nullness annotation.
121    */
hasNullnessAnnotations(MethodNode method)122   private static boolean hasNullnessAnnotations(MethodNode method) {
123     if (listHasNullnessAnnotations(method.visibleAnnotations)
124         || listHasNullnessAnnotations(method.invisibleAnnotations)) {
125       return true;
126     }
127     if (method.visibleParameterAnnotations != null) {
128       for (List<AnnotationNode> annotationList : method.visibleParameterAnnotations) {
129         if (listHasNullnessAnnotations(annotationList)) {
130           return true;
131         }
132       }
133     }
134     if (method.invisibleParameterAnnotations != null) {
135       for (List<AnnotationNode> annotationList : method.invisibleParameterAnnotations) {
136         if (listHasNullnessAnnotations(annotationList)) {
137           return true;
138         }
139       }
140     }
141     return false;
142   }
143 
annotateBytecode( InputStream is, OutputStream os, MethodParamAnnotations nonnullParams, MethodReturnAnnotations nullableReturns, String nullableDesc, String nonnullDesc)144   private static void annotateBytecode(
145       InputStream is,
146       OutputStream os,
147       MethodParamAnnotations nonnullParams,
148       MethodReturnAnnotations nullableReturns,
149       String nullableDesc,
150       String nonnullDesc)
151       throws IOException {
152     ClassReader cr = new ClassReader(is);
153     ClassWriter cw = new ClassWriter(0);
154     ClassNode cn = new ClassNode(Opcodes.ASM9);
155     cr.accept(cn, 0);
156 
157     String className = cn.name.replace('/', '.');
158     List<MethodNode> methods = cn.methods;
159     for (MethodNode method : methods) {
160       // Skip methods that already have nullability annotations anywhere in their signature
161       if (hasNullnessAnnotations(method)) {
162         continue;
163       }
164       boolean visible = annotationsShouldBeVisible(nullableDesc);
165       String methodSignature = className + "." + method.name + method.desc;
166       if (nullableReturns.contains(methodSignature)) {
167         // Add a @Nullable annotation on this method to indicate that the method can return null.
168         method.visitAnnotation(nullableDesc, visible);
169         LOG(debug, "DEBUG", "Added nullable return annotation for " + methodSignature);
170       }
171       Set<Integer> params = nonnullParams.get(methodSignature);
172       if (params != null) {
173         boolean isStatic = (method.access & Opcodes.ACC_STATIC) != 0;
174         for (Integer param : params) {
175           int paramNum = isStatic ? param : param - 1;
176           // Add a @Nonnull annotation on this parameter.
177           method.visitParameterAnnotation(paramNum, nonnullDesc, visible);
178           LOG(
179               debug,
180               "DEBUG",
181               "Added nonnull parameter annotation for #" + param + " in " + methodSignature);
182         }
183       }
184     }
185 
186     cn.accept(cw);
187     os.write(cw.toByteArray());
188   }
189 
190   /**
191    * Annotates the methods and method parameters in the given class with the specified annotations.
192    *
193    * @param is InputStream for the input class.
194    * @param os OutputStream for the output class.
195    * @param nonnullParams Map from methods to their nonnull params.
196    * @param nullableReturns List of methods that return nullable.
197    * @param debug flag to output debug logs.
198    * @throws IOException if an error happens when reading or writing to class streams.
199    */
annotateBytecodeInClass( InputStream is, OutputStream os, MethodParamAnnotations nonnullParams, MethodReturnAnnotations nullableReturns, boolean debug)200   public static void annotateBytecodeInClass(
201       InputStream is,
202       OutputStream os,
203       MethodParamAnnotations nonnullParams,
204       MethodReturnAnnotations nullableReturns,
205       boolean debug)
206       throws IOException {
207     BytecodeAnnotator.debug = debug;
208     LOG(debug, "DEBUG", "nullableReturns: " + nullableReturns);
209     LOG(debug, "DEBUG", "nonnullParams: " + nonnullParams);
210     annotateBytecode(is, os, nonnullParams, nullableReturns, javaxNullableDesc, javaxNonnullDesc);
211   }
212 
copyAndAnnotateJarEntry( JarEntry jarEntry, InputStream is, JarOutputStream jarOS, MethodParamAnnotations nonnullParams, MethodReturnAnnotations nullableReturns, String nullableDesc, String nonnullDesc, boolean stripJarSignatures)213   private static void copyAndAnnotateJarEntry(
214       JarEntry jarEntry,
215       InputStream is,
216       JarOutputStream jarOS,
217       MethodParamAnnotations nonnullParams,
218       MethodReturnAnnotations nullableReturns,
219       String nullableDesc,
220       String nonnullDesc,
221       boolean stripJarSignatures)
222       throws IOException {
223     String entryName = jarEntry.getName();
224     if (entryName.endsWith(".class")) {
225       jarOS.putNextEntry(new ZipEntry(jarEntry.getName()));
226       annotateBytecode(is, jarOS, nonnullParams, nullableReturns, nullableDesc, nonnullDesc);
227     } else if (entryName.equals("META-INF/MANIFEST.MF")) {
228       // Read full file
229       StringBuilder stringBuilder = new StringBuilder();
230       BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
231       String currentLine;
232       while ((currentLine = br.readLine()) != null) {
233         stringBuilder.append(currentLine + "\n");
234       }
235       String manifestText = stringBuilder.toString();
236       // Check for evidence of jar signing, note that lines can be split if too long so regex
237       // matching line by line will have false negatives.
238       String manifestMinusDigests = manifestText.replaceAll(DIGEST_ENTRY_PATTERN, "");
239       if (!manifestText.equals(manifestMinusDigests) && !stripJarSignatures) {
240         throw new SignedJarException(SIGNED_JAR_ERROR_MESSAGE);
241       }
242       jarOS.putNextEntry(new ZipEntry(jarEntry.getName()));
243       jarOS.write(manifestMinusDigests.getBytes("UTF-8"));
244     } else if (entryName.startsWith("META-INF/")
245         && (entryName.endsWith(".DSA")
246             || entryName.endsWith(".RSA")
247             || entryName.endsWith(".SF"))) {
248       if (!stripJarSignatures) {
249         throw new SignedJarException(SIGNED_JAR_ERROR_MESSAGE);
250       } // the case where stripJarSignatures==true is handled by default by skipping these files
251     } else {
252       jarOS.putNextEntry(new ZipEntry(jarEntry.getName()));
253       jarOS.write(IOUtils.toByteArray(is));
254     }
255     jarOS.closeEntry();
256   }
257 
258   /**
259    * Annotates the methods and method parameters in the classes in the given jar with the specified
260    * annotations.
261    *
262    * @param inputJar JarFile to annotate.
263    * @param jarOS OutputStream of the output jar file.
264    * @param nonnullParams Map from methods to their nonnull params.
265    * @param nullableReturns List of methods that return nullable.
266    * @param debug flag to output debug logs.
267    * @throws IOException if an error happens when reading or writing to jar or class streams.
268    */
annotateBytecodeInJar( JarFile inputJar, JarOutputStream jarOS, MethodParamAnnotations nonnullParams, MethodReturnAnnotations nullableReturns, boolean stripJarSignatures, boolean debug)269   public static void annotateBytecodeInJar(
270       JarFile inputJar,
271       JarOutputStream jarOS,
272       MethodParamAnnotations nonnullParams,
273       MethodReturnAnnotations nullableReturns,
274       boolean stripJarSignatures,
275       boolean debug)
276       throws IOException {
277     BytecodeAnnotator.debug = debug;
278     LOG(debug, "DEBUG", "nullableReturns: " + nullableReturns);
279     LOG(debug, "DEBUG", "nonnullParams: " + nonnullParams);
280     // Do not use JarInputStream in place of JarFile/JarEntry. JarInputStream misses MANIFEST.MF
281     // while iterating over the entries in the stream.
282     // Reference: https://bugs.openjdk.java.net/browse/JDK-8215788
283     // Note: we can't just put the code below inside stream().forach(), because it can throw
284     // IOException.
285     for (JarEntry jarEntry : (Iterable<JarEntry>) inputJar.stream()::iterator) {
286       InputStream is = inputJar.getInputStream(jarEntry);
287       copyAndAnnotateJarEntry(
288           jarEntry,
289           is,
290           jarOS,
291           nonnullParams,
292           nullableReturns,
293           javaxNullableDesc,
294           javaxNonnullDesc,
295           stripJarSignatures);
296     }
297   }
298 
299   /**
300    * Annotates the methods and method parameters in the classes in "classes.jar" in the given aar
301    * file with the specified annotations.
302    *
303    * @param inputZip AarFile to annotate.
304    * @param zipOS OutputStream of the output aar file.
305    * @param nonnullParams Map from methods to their nonnull params.
306    * @param nullableReturns List of methods that return nullable.
307    * @param debug flag to output debug logs.
308    * @throws IOException if an error happens when reading or writing to AAR/JAR/class streams.
309    */
annotateBytecodeInAar( ZipFile inputZip, ZipOutputStream zipOS, MethodParamAnnotations nonnullParams, MethodReturnAnnotations nullableReturns, boolean stripJarSignatures, boolean debug)310   public static void annotateBytecodeInAar(
311       ZipFile inputZip,
312       ZipOutputStream zipOS,
313       MethodParamAnnotations nonnullParams,
314       MethodReturnAnnotations nullableReturns,
315       boolean stripJarSignatures,
316       boolean debug)
317       throws IOException {
318     BytecodeAnnotator.debug = debug;
319     LOG(debug, "DEBUG", "nullableReturns: " + nullableReturns);
320     LOG(debug, "DEBUG", "nonnullParams: " + nonnullParams);
321     // Error Prone doesn't like usages of the old Java Enumerator APIs. ZipFile does not implement
322     // Iterable, and likely never will (see  https://bugs.openjdk.java.net/browse/JDK-6581715).
323     // Additionally, inputZip.stream() returns a Stream<? extends ZipEntry>, and a for-each loop
324     // has trouble handling the corresponding ::iterator  method reference. So this seems like the
325     // best remaining way:
326     Iterator<? extends ZipEntry> zipIterator = inputZip.stream().iterator();
327     while (zipIterator.hasNext()) {
328       ZipEntry zipEntry = zipIterator.next();
329       InputStream is = inputZip.getInputStream(zipEntry);
330       zipOS.putNextEntry(new ZipEntry(zipEntry.getName()));
331       if (zipEntry.getName().equals("classes.jar")) {
332         JarInputStream jarIS = new JarInputStream(is);
333         JarEntry inputJarEntry = jarIS.getNextJarEntry();
334 
335         ByteArrayOutputStream byteArrayOS = new ByteArrayOutputStream();
336         JarOutputStream jarOS = new JarOutputStream(byteArrayOS);
337         while (inputJarEntry != null) {
338           copyAndAnnotateJarEntry(
339               inputJarEntry,
340               jarIS,
341               jarOS,
342               nonnullParams,
343               nullableReturns,
344               androidNullableDesc,
345               androidNonnullDesc,
346               stripJarSignatures);
347           inputJarEntry = jarIS.getNextJarEntry();
348         }
349         jarOS.flush();
350         jarOS.close();
351         zipOS.write(byteArrayOS.toByteArray());
352       } else {
353         zipOS.write(IOUtils.toByteArray(is));
354       }
355       zipOS.closeEntry();
356     }
357   }
358 }
359