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.base.Preconditions; 19 import com.google.common.collect.ImmutableSet; 20 import java.io.BufferedReader; 21 import java.io.IOException; 22 import java.io.InputStreamReader; 23 import java.util.HashSet; 24 import java.util.Set; 25 import java.util.jar.JarEntry; 26 import java.util.jar.JarFile; 27 import java.util.jar.JarInputStream; 28 import java.util.zip.ZipEntry; 29 import java.util.zip.ZipFile; 30 31 public class EntriesComparator { 32 private static String classesJarInAar = "classes.jar"; 33 34 /** 35 * Compares the entries in the given 2 jar files. However, this function does not compare the 36 * contents of the entries themselves. 37 * 38 * @param jarFile1 Path to the first jar file. 39 * @param jarFile2 Path to the second jar file. 40 * @return True iff the entries present in the two jar files are the same. 41 * @throws IOException if an error happens when reading jar files. 42 */ compareEntriesInJars(String jarFile1, String jarFile2)43 public static boolean compareEntriesInJars(String jarFile1, String jarFile2) throws IOException { 44 Preconditions.checkArgument(jarFile1.endsWith(".jar"), "invalid jar file: " + jarFile1); 45 Preconditions.checkArgument(jarFile2.endsWith(".jar"), "invalid jar file: " + jarFile2); 46 JarFile jar1 = new JarFile(jarFile1); 47 JarFile jar2 = new JarFile(jarFile2); 48 Set<String> jar1Entries = 49 jar1.stream().map(ZipEntry::getName).collect(ImmutableSet.toImmutableSet()); 50 Set<String> jar2Entries = 51 jar2.stream().map(ZipEntry::getName).collect(ImmutableSet.toImmutableSet()); 52 return jar1Entries.equals(jar2Entries); 53 } 54 55 /** 56 * Compares the entries in the given 2 aar files and the entries in "classes.jar" in them. 57 * However, this function does not compare the contents of the entries themselves. 58 * 59 * @param aarFile1 Path to the first aar file. 60 * @param aarFile2 Path to the second aar file. 61 * @return True iff the entries present in the two aar files are the same and entries in 62 * "classes.jar" in the two aar files are the same. 63 * @throws IOException if an error happens when reading aar files. 64 */ compareEntriesInAars(String aarFile1, String aarFile2)65 public static boolean compareEntriesInAars(String aarFile1, String aarFile2) throws IOException { 66 Preconditions.checkArgument(aarFile1.endsWith(".aar"), "invalid aar file: " + aarFile1); 67 Preconditions.checkArgument(aarFile2.endsWith(".aar"), "invalid aar file: " + aarFile2); 68 ZipFile zip1 = new ZipFile(aarFile1); 69 ZipFile zip2 = new ZipFile(aarFile2); 70 Set<String> zip1Entries = 71 zip1.stream().map(ZipEntry::getName).collect(ImmutableSet.toImmutableSet()); 72 Set<String> zip2Entries = 73 zip2.stream().map(ZipEntry::getName).collect(ImmutableSet.toImmutableSet()); 74 if (!zip1Entries.equals(zip2Entries)) { 75 return false; 76 } 77 78 // Check if all the entries in "classes.jar" in both aar files are the same. 79 // We expect to find a classes.jar entry in the aar files. 80 ZipEntry zip1Jar = zip1.getEntry(classesJarInAar); 81 ZipEntry zip2Jar = zip2.getEntry(classesJarInAar); 82 if (zip1Jar == null || zip2Jar == null) { 83 return false; 84 } 85 JarInputStream jarIS1 = new JarInputStream(zip1.getInputStream(zip1Jar)); 86 JarInputStream jarIS2 = new JarInputStream(zip2.getInputStream(zip2Jar)); 87 Set<String> jar1Entries = new HashSet<>(); 88 JarEntry jar1Entry = jarIS1.getNextJarEntry(); 89 while (jar1Entry != null) { 90 jar1Entries.add(jar1Entry.getName()); 91 jar1Entry = jarIS1.getNextJarEntry(); 92 } 93 Set<String> jar2Entries = new HashSet<>(); 94 JarEntry jar2Entry = jarIS2.getNextJarEntry(); 95 while (jar2Entry != null) { 96 jar2Entries.add(jar2Entry.getName()); 97 jar2Entry = jarIS2.getNextJarEntry(); 98 } 99 return jar1Entries.equals(jar2Entries); 100 } 101 readManifestFromJar(String jarfile)102 private static String readManifestFromJar(String jarfile) throws IOException { 103 JarFile jar = new JarFile(jarfile); 104 ZipEntry manifestEntry = jar.getEntry("META-INF/MANIFEST.MF"); 105 if (manifestEntry == null) { 106 throw new IllegalArgumentException("Jar does not contain a manifest at META-INF/MANIFEST.MF"); 107 } 108 StringBuilder stringBuilder = new StringBuilder(); 109 BufferedReader bufferedReader = 110 new BufferedReader(new InputStreamReader(jar.getInputStream(manifestEntry), "UTF-8")); 111 String currentLine; 112 while ((currentLine = bufferedReader.readLine()) != null) { 113 // Ignore empty new lines 114 if (currentLine.trim().length() > 0) { 115 stringBuilder.append(currentLine + "\n"); 116 } 117 } 118 return stringBuilder.toString(); 119 } 120 121 /** 122 * Compares the META-INF/MANIFEST.MF file in the given 2 jar files. We ignore empty newlines. 123 * 124 * @param jarFile1 Path to the first jar file. 125 * @param jarFile2 Path to the second jar file. 126 * @return True iff the MANIFEST.MF files in the two jar files exist and are the same. 127 * @throws IOException if an error happens when reading jar files. 128 * @throws IllegalArgumentException if either jar does not contain a manifest. 129 */ compareManifestContents(String jarFile1, String jarFile2)130 public static boolean compareManifestContents(String jarFile1, String jarFile2) 131 throws IOException { 132 Preconditions.checkArgument(jarFile1.endsWith(".jar"), "invalid jar file: " + jarFile1); 133 Preconditions.checkArgument(jarFile2.endsWith(".jar"), "invalid jar file: " + jarFile2); 134 String manifest1 = readManifestFromJar(jarFile1); 135 String manifest2 = readManifestFromJar(jarFile2); 136 return manifest1.equals(manifest2); 137 } 138 } 139