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