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