1 /* 2 * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package build.tools.deps; 27 28 import java.nio.file.DirectoryStream; 29 import java.nio.file.Files; 30 import java.nio.file.Path; 31 import java.nio.file.Paths; 32 import java.nio.charset.StandardCharsets; 33 import java.util.Set; 34 import java.util.HashSet; 35 import java.util.Map; 36 import java.util.HashMap; 37 import java.util.Enumeration; 38 import java.util.Properties; 39 import java.util.jar.JarEntry; 40 import java.util.jar.JarFile; 41 import java.io.InputStream; 42 import java.io.InputStreamReader; 43 import java.net.URL; 44 45 import com.sun.tools.classfile.ClassFile; 46 import com.sun.tools.classfile.Dependencies; 47 import com.sun.tools.classfile.Dependency; 48 49 /** 50 * A simple tool to check the JAR files in a JRE image to ensure that there 51 * aren't any references to types that do not exist. The tool is intended to 52 * be used in the JDK "profiles" build to help ensure that the profile 53 * definitions are kept up to date. 54 */ 55 56 public class CheckDeps { 57 58 // classfile API for finding dependencies 59 static final Dependency.Finder finder = Dependencies.getClassDependencyFinder(); 60 61 // "known types", found in rt.jar or other JAR files 62 static final Set<String> knownTypes = new HashSet<>(); 63 64 // References to unknown types. The map key is the unknown type, the 65 // map value is the set of classes that reference it. 66 static final Map<String,Set<String>> unknownRefs = new HashMap<>(); 67 68 // The property name is the name of an unknown type that is allowed to be 69 // references. The property value is a comma separated list of the types 70 // that are allowed to reference it. The list also includes the names of 71 // the profiles that the reference is allowed. 72 static final Properties allowedBadRefs = new Properties(); 73 74 /** 75 * Returns the class name for the given class file. In the case of inner 76 * classes then the enclosing class is returned in order to keep the 77 * rules simple. 78 */ 79 static String toClassName(String s) { 80 int i = s.indexOf('$'); 81 if (i > 0) 82 s = s.substring(0, i); 83 return s.replace("/", "."); 84 } 85 86 /** 87 * Analyze the dependencies of all classes in the given JAR file. The 88 * method updates knownTypes and unknownRefs as part of the analysis. 89 */ 90 static void analyzeDependencies(Path jarpath) throws Exception { 91 System.out.format("Analyzing %s%n", jarpath); 92 try (JarFile jf = new JarFile(jarpath.toFile())) { 93 Enumeration<JarEntry> entries = jf.entries(); 94 while (entries.hasMoreElements()) { 95 JarEntry e = entries.nextElement(); 96 String name = e.getName(); 97 if (name.endsWith(".class")) { 98 ClassFile cf = ClassFile.read(jf.getInputStream(e)); 99 for (Dependency d : finder.findDependencies(cf)) { 100 String origin = toClassName(d.getOrigin().getName()); 101 String target = toClassName(d.getTarget().getName()); 102 103 // origin is now known 104 unknownRefs.remove(origin); 105 knownTypes.add(origin); 106 107 // if the target is not known then record the reference 108 if (!knownTypes.contains(target)) { 109 Set<String> refs = unknownRefs.get(target); 110 if (refs == null) { 111 // first time seeing this unknown type 112 refs = new HashSet<>(); 113 unknownRefs.put(target, refs); 114 } 115 refs.add(origin); 116 } 117 } 118 } 119 } 120 } 121 } 122 123 /** 124 * We have closure (no references to types that do not exist) if 125 * unknownRefs is empty. When unknownRefs is not empty then it should 126 * only contain references that are allowed to be present (these are 127 * loaded from the refs.allowed properties file). 128 * 129 * @param the profile that is being tested, this determines the exceptions 130 * in {@code allowedBadRefs} that apply. 131 * 132 * @return {@code true} if there are no missing types or the only references 133 * to missing types are described by {@code allowedBadRefs}. 134 */ 135 static boolean checkClosure(String profile) { 136 // process the references to types that do not exist. 137 boolean fail = false; 138 for (Map.Entry<String,Set<String>> entry: unknownRefs.entrySet()) { 139 String target = entry.getKey(); 140 for (String origin: entry.getValue()) { 141 // check if origin -> target allowed 142 String value = allowedBadRefs.getProperty(target); 143 if (value == null) { 144 System.err.format("%s -> %s (unknown type)%n", origin, target); 145 fail = true; 146 } else { 147 // target is known, check if the origin is one that we 148 // expect and that the exception applies to the profile. 149 boolean found = false; 150 boolean applicable = false; 151 for (String s: value.split(",")) { 152 s = s.trim(); 153 if (s.equals(origin)) 154 found = true; 155 if (s.equals(profile)) 156 applicable = true; 157 } 158 if (!found || !applicable) { 159 if (!found) { 160 System.err.format("%s -> %s (not allowed)%n", origin, target); 161 } else { 162 System.err.format("%s -> %s (reference not applicable to %s)%n", 163 origin, target, profile); 164 } 165 fail = true; 166 } 167 } 168 169 } 170 } 171 172 return !fail; 173 } 174 175 static void fail(URL url) throws Exception { 176 System.err.println("One or more unexpected references encountered"); 177 if (url != null) 178 System.err.format("Check %s is up to date%n", Paths.get(url.toURI())); 179 System.exit(-1); 180 } 181 182 public static void main(String[] args) throws Exception { 183 // load properties file so that we know what missing types that are 184 // allowed to be referenced. 185 URL url = CheckDeps.class.getResource("refs.allowed"); 186 if (url != null) { 187 try (InputStream in = url.openStream()) { 188 allowedBadRefs.load(new InputStreamReader(in, StandardCharsets.UTF_8)); 189 } 190 } 191 192 if (args.length != 2) { 193 System.err.println("Usage: java CheckDeps <image> <profile>"); 194 System.exit(-1); 195 } 196 197 String image = args[0]; 198 String profile = args[1]; 199 200 // process JAR files on boot class path 201 Path lib = Paths.get(image, "lib"); 202 try (DirectoryStream<Path> stream = Files.newDirectoryStream(lib, "*.jar")) { 203 for (Path jarpath: stream) { 204 analyzeDependencies(jarpath); 205 } 206 } 207 208 // classes on boot class path should not reference other types 209 boolean okay = checkClosure(profile); 210 if (!okay) 211 fail(url); 212 213 // process JAR files in the extensions directory 214 try (DirectoryStream<Path> stream = Files.newDirectoryStream(lib.resolve("ext"), "*.jar")) { 215 for (Path jarpath: stream) { 216 analyzeDependencies(jarpath); 217 } 218 } 219 220 // re-check to ensure that the extensions doesn't reference types that 221 // do not exist. 222 okay = checkClosure(profile); 223 if (!okay) 224 fail(url); 225 } 226 }