1 /* 2 * Copyright (C) 2022 The Android Open Source Project 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 17 package libcore.tools.analyzer.openjdk; 18 19 import static java.util.stream.Collectors.toList; 20 import static java.util.stream.Collectors.toMap; 21 22 import libcore.tools.analyzer.openjdk.DependencyAnalyzer.Result.MethodDependency; 23 import libcore.tools.analyzer.openjdk.SignaturesCollector.SignaturesCollection; 24 25 import com.beust.jcommander.JCommander; 26 import com.beust.jcommander.Parameter; 27 import com.beust.jcommander.Parameters; 28 29 import org.objectweb.asm.ClassReader; 30 import org.objectweb.asm.Opcodes; 31 import org.objectweb.asm.Type; 32 import org.objectweb.asm.tree.AnnotationNode; 33 import org.objectweb.asm.tree.ClassNode; 34 import org.objectweb.asm.tree.FieldNode; 35 import org.objectweb.asm.tree.MethodNode; 36 import org.objectweb.asm.util.TraceClassVisitor; 37 import java.io.IOException; 38 import java.io.InputStream; 39 import java.io.OutputStream; 40 import java.io.PrintWriter; 41 import java.io.UncheckedIOException; 42 import java.nio.file.Path; 43 import java.util.ArrayList; 44 import java.util.List; 45 import java.util.Map; 46 import java.util.Objects; 47 import java.util.Optional; 48 import java.util.function.Predicate; 49 import java.util.regex.Pattern; 50 import java.util.stream.Collectors; 51 import java.util.stream.Stream; 52 import java.util.zip.ZipEntry; 53 import java.util.zip.ZipFile; 54 55 public class Main { 56 57 private static class MainArgs { 58 59 @Parameter(names = "-h", help = true, description = "Shows this help message") 60 public boolean help = false; 61 } 62 63 @Parameters(commandNames = CommandDump.NAME, commandDescription = "Dump a class from the " 64 + "classpath. This tool is similar to javap tool provided in the OpenJDK.") 65 private static class CommandDump { 66 public static final String NAME = "dump"; 67 68 @Parameter(names = {"-cp", "--classpath"}, description = "file path to a .jmod or .jar file" 69 + " or one of the following java version: oj, 8, 9, 11, 17, 21") 70 public String classpathFile = "21"; 71 72 @Parameter(required = true, arity = 1, 73 description = "<class>. The fully qualified name of the class in the classpath. " 74 + "Note that inner class uses $ separator and $ has to be escaped in shell," 75 + " e.g. java.lang.Character\\$Subset") 76 public List<String> classNames; 77 78 @Parameter(names = "-h", help = true, description = "Shows this help message") 79 public boolean help = false; 80 run()81 private void run() throws UncheckedIOException { 82 Path jmod = AndroidHostEnvUtil.parseInputClasspath(classpathFile); 83 String className = classNames.get(0); 84 try (ZipFile zipFile = new ZipFile(jmod.toFile())) { 85 ZipEntry e = ClassFileUtil.getEntryFromClassNameOrThrow(zipFile, className); 86 try (InputStream in = zipFile.getInputStream(e)) { 87 ClassReader classReader = new ClassReader(in); 88 PrintWriter printer = new PrintWriter(System.out); 89 TraceClassVisitor traceClassVisitor = new TraceClassVisitor(printer); 90 classReader.accept(traceClassVisitor, 0); 91 } 92 } catch (IOException e) { 93 throw new UncheckedIOException(e); 94 } 95 } 96 } 97 98 @Parameters(commandNames = CommandApiDiff.NAME, commandDescription = "List the new and " 99 + "removed APIs by comparing the older and newer implementation in 2 jar/jmod files") 100 private static class CommandApiDiff { 101 public static final String NAME = "api-diff"; 102 103 @Parameter(names = {"-b", "--base"}, description = "file path to a .jmod or .jar file or " 104 + "one of the following java version: oj, 8, 9, 11, 17, 21") 105 public String baseClasspath = "11"; 106 107 @Parameter(names = {"-n", "--new"}, description = "file path to a .jmod or .jar file or " 108 + "one of the following java version: oj, 8, 9, 11, 17, 21") 109 public String newClasspath = "21"; 110 111 @Parameter(required = true, arity = 1, 112 description = "<class>. The fully qualified name of the class in the classpath. " 113 + "Note that inner class uses $ separator and $ has to be escaped in shell," 114 + " e.g. java.lang.Character\\$Subset") 115 public List<String> classNames; 116 117 @Parameter(names = "-h", help = true, description = "Shows this help message") 118 public boolean help = false; 119 run()120 private void run() throws UncheckedIOException { 121 Path basePath = AndroidHostEnvUtil.parseInputClasspath(baseClasspath); 122 Path newPath = AndroidHostEnvUtil.parseInputClasspath(newClasspath); 123 String className = classNames.get(0); 124 DiffAnalyzer analyzer; 125 try (ZipFile baseZip = new ZipFile(basePath.toFile()); 126 ZipFile newZip = new ZipFile(newPath.toFile())) { 127 ZipEntry baseEntry = ClassFileUtil.getEntryFromClassNameOrThrow(baseZip, className); 128 ZipEntry newEntry = ClassFileUtil.getEntryFromClassNameOrThrow(newZip, className); 129 try (InputStream baseIn = baseZip.getInputStream(baseEntry); 130 InputStream newIn = newZip.getInputStream(newEntry)) { 131 analyzer = DiffAnalyzer.analyze(baseIn, newIn); 132 } 133 } catch (IOException e) { 134 throw new UncheckedIOException(e); 135 } 136 137 System.out.println("Class:" + className); 138 analyzer.print(System.out); 139 } 140 } 141 142 private static class DiffAnalyzer { 143 List<MethodNode> newMethods = new ArrayList<>(); 144 145 List<MethodNode> removedMethods = new ArrayList<>(); 146 147 List<MethodNode> newlyDeprecatedMethods = new ArrayList<>(); 148 List<FieldNode> newFields = new ArrayList<>(); 149 150 List<FieldNode> removedFields = new ArrayList<>(); 151 152 List<FieldNode> newlyDeprecatedFields = new ArrayList<>(); 153 analyze(InputStream baseIn, InputStream newIn)154 private static DiffAnalyzer analyze(InputStream baseIn, InputStream newIn) 155 throws IOException { 156 ClassNode baseClass = ClassFileUtil.parseClass(baseIn); 157 ClassNode newClass = ClassFileUtil.parseClass(newIn); 158 return analyze(baseClass, newClass); 159 } 160 analyze(ClassNode baseClass, ClassNode newClass)161 private static DiffAnalyzer analyze(ClassNode baseClass, ClassNode newClass) { 162 Map<String, MethodNode> baseMethods = getExposedMethods(baseClass) 163 .collect(toMap(DiffAnalyzer::toApiSignature, node -> node)); 164 Map<String, MethodNode> newMethods = getExposedMethods(newClass) 165 .collect(toMap(DiffAnalyzer::toApiSignature, node -> node)); 166 167 DiffAnalyzer result = new DiffAnalyzer(); 168 result.newMethods = getExposedMethods(newClass) 169 .filter(node -> !baseMethods.containsKey(toApiSignature(node))) 170 .collect(toList()); 171 result.removedMethods = getExposedMethods(baseClass) 172 .filter(node -> !newMethods.containsKey(toApiSignature(node))) 173 .collect(toList()); 174 result.newlyDeprecatedMethods = getExposedMethods(newClass) 175 .filter(DiffAnalyzer::isDeprecated) 176 .filter(node -> !baseMethods.containsKey(toApiSignature(node)) 177 || !isDeprecated(baseMethods.get(toApiSignature(node))) ) 178 .collect(toList()); 179 180 181 Map<String, FieldNode> baseFields = getExposedFields(baseClass) 182 .collect(toMap(node -> node.name, node -> node)); 183 Map<String, FieldNode> newFields = getExposedFields(newClass) 184 .collect(toMap(node -> node.name, node -> node)); 185 186 result.newFields = getExposedFields(newClass) 187 .filter(node -> !baseFields.containsKey(node.name)) 188 .collect(toList()); 189 result.removedFields = getExposedFields(baseClass) 190 .filter(node -> !newFields.containsKey(node.name)) 191 .collect(toList()); 192 result.newlyDeprecatedFields = getExposedFields(newClass) 193 .filter(DiffAnalyzer::isDeprecated) 194 .filter(node -> !baseFields.containsKey(node.name) 195 || !isDeprecated(baseFields.get(node.name)) ) 196 .collect(toList()); 197 198 return result; 199 } 200 201 /** 202 * Known issue: this signature doesn't differentiate static and virtual methods. 203 */ toApiSignature(MethodNode node)204 private static String toApiSignature(MethodNode node) { 205 return node.name + node.desc; 206 } 207 getExposedMethods(ClassNode classNode)208 private static Stream<MethodNode> getExposedMethods(ClassNode classNode) { 209 if (!isExposed(classNode.access)) { 210 return Stream.empty(); 211 } 212 213 return classNode.methods.stream() 214 .filter(m -> isExposed(m.access)) 215 .flatMap(DiffAnalyzer::getImpliedMethods); 216 } 217 218 /** 219 * Get all implied methods that are not generated by javac. 220 * 221 * For example, {@link dalvik.annotation.codegen.CovariantReturnType} is used by dexers 222 * to generate synthetic methods with a different return type. 223 */ getImpliedMethods(MethodNode node)224 private static Stream<MethodNode> getImpliedMethods(MethodNode node) { 225 if (node.invisibleAnnotations == null 226 // Synthetic methods generated by javac can be annotated with 227 // @CovariantReturnType, but can be safely ignored to avoid double counting. 228 || (node.access & Opcodes.ACC_SYNTHETIC) != 0) { 229 return Stream.of(node); 230 } 231 232 Stream<MethodNode> syntheticMethods = node.invisibleAnnotations.stream() 233 .flatMap(a -> { // flatten CovariantReturnTypes.value 234 if (!"Ldalvik/annotation/codegen/CovariantReturnType$CovariantReturnTypes;" 235 .equals(a.desc)) { 236 return Stream.of(a); 237 } 238 for (int i = 0; i < a.values.size() - 1; i++) { 239 if ("value".equals(a.values.get(i))) { 240 Object type = a.values.get(i + 1); 241 if (type instanceof List nodes) { 242 return Stream.concat(Stream.of(a), 243 (Stream<AnnotationNode>) nodes.stream()); 244 } 245 } 246 } 247 return Stream.of(a); 248 249 }) 250 .filter(a -> "Ldalvik/annotation/codegen/CovariantReturnType;".equals(a.desc)) 251 .map(a -> { 252 for (int i = 0; i < a.values.size() - 1; i++) { 253 if ("returnType".equals(a.values.get(i))) { 254 Object type = a.values.get(i + 1); 255 if (type instanceof Type) { 256 return (Type) type; 257 } 258 } 259 } 260 return null; 261 }) 262 .filter(Predicate.not(Objects::isNull)) 263 .map(returnType -> { 264 String desc = Type.getMethodDescriptor(returnType, 265 Type.getArgumentTypes(node.desc)); 266 // It doesn't copy everything, e.g. annotations, but we only need 267 // access, name and desc for matching purpose. 268 return new MethodNode(node.access, node.name, desc, null, 269 node.exceptions.toArray(new String[0])); 270 271 }); 272 273 return Stream.concat(Stream.of(node), syntheticMethods); 274 } 275 276 getExposedFields(ClassNode classNode)277 private static Stream<FieldNode> getExposedFields(ClassNode classNode) { 278 if (!isExposed(classNode.access)) { 279 return Stream.empty(); 280 } 281 282 return classNode.fields.stream() 283 .filter(m -> isExposed(m.access)); 284 } 285 isExposed(int flags)286 private static boolean isExposed(int flags) { 287 return (flags & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) != 0; 288 } 289 isDeprecated(MethodNode node)290 private static boolean isDeprecated(MethodNode node) { 291 return node.visibleAnnotations != null && 292 node.visibleAnnotations.stream() 293 .anyMatch(anno ->"Ljava/lang/Deprecated;".equals(anno.desc)); 294 } 295 isDeprecated(FieldNode node)296 private static boolean isDeprecated(FieldNode node) { 297 return node.visibleAnnotations != null && 298 node.visibleAnnotations.stream() 299 .anyMatch(anno ->"Ljava/lang/Deprecated;".equals(anno.desc)); 300 } 301 print(OutputStream out)302 void print(OutputStream out) { 303 PrintWriter writer = new PrintWriter(out, /*autoFlush=*/true); 304 305 printMethods(writer, "New methods", newMethods); 306 printMethods(writer, "Removed methods", removedMethods); 307 printMethods(writer, "Newly deprecated methods", newlyDeprecatedMethods); 308 309 printFields(writer, "New fields", newFields); 310 printFields(writer, "Removed fields", removedFields); 311 printFields(writer, "Newly deprecated fields", newlyDeprecatedFields); 312 } 313 printMethods(PrintWriter w, String header, List<MethodNode> nodes)314 private static void printMethods(PrintWriter w, String header, List<MethodNode> nodes) { 315 if (nodes.isEmpty()) { 316 return; 317 } 318 319 w.println(" " + header + ":"); 320 for (MethodNode node : nodes) { 321 w.println(" " + toApiSignature(node)); 322 } 323 } 324 printFields(PrintWriter w, String header, List<FieldNode> nodes)325 private static void printFields(PrintWriter w, String header, List<FieldNode> nodes) { 326 if (nodes.isEmpty()) { 327 return; 328 } 329 330 w.println(" " + header + ":"); 331 for (FieldNode node : nodes) { 332 w.println(" " + node.name + ": " + node.desc); 333 } 334 } 335 } 336 337 @Parameters(commandNames = CommandShowDeps.NAME, commandDescription = "Show the dependencies of" 338 + " classes or packages.") 339 private static class CommandShowDeps { 340 341 public static final String NAME = "show-deps"; 342 343 @Parameter(names = {"-cp"}, description = "file path to a .jmod or .jar file or " 344 + "one of the following java version: oj, 8, 9, 11, 17, 21") 345 public String classpath = "21"; 346 347 /** 348 * @see DependencyAnalyzer.ExcludeClasspathFilter 349 */ 350 @Parameter(names = {"-x", "--exclude-deps-in-classpath"}, description = "a file path to a " 351 + ".jmod or .jar file or one of the following java version: oj, 8, 9, 11, 17, 21. " 352 + "The classes, methods and fields that exist in this classpath are " 353 + "excluded from the output list of dependencies.") 354 public String excludeClasspath = "oj"; 355 356 @Parameter(required = true, 357 description = "(<classes>|<packages>). The class or package names in the classpath. " 358 + "Note that inner class uses $ separator and $ has to be escaped in shell," 359 + " e.g. java.lang.Character\\$Subset") 360 public List<String> classesOrPackages; 361 362 @Parameter(names = "-i", description = "Show the dependencies within the provided " 363 + "<classes> / <packages>") 364 public boolean includeInternal = false; 365 366 /** 367 * @see DependencyAnalyzer.ExpectedUpstreamFilter 368 */ 369 @Parameter(names = "-e", description = "Exclude the existing dependencies in the OpenJDK " 370 + "version of the file specified in the libcore/EXPECTED_UPSTREAM file. Such " 371 + "dependencies are likely to be eliminated / replaced in the libcore version " 372 + "already even though the new OpenJDK version still depends on them.") 373 public boolean usesExpectedUpstreamAsBaseDeps = false; 374 375 @Parameter(names = "-c", description = "Only show class-level dependencies, not " 376 + "field-level or method-level dependency details.") 377 public boolean classOnly = false; 378 379 @Parameter(names = "-h", help = true, description = "Shows this help message") 380 public boolean help = false; 381 382 private final PrintWriter mWriter = new PrintWriter(System.out, /*autoFlush=*/true); 383 run()384 private void run() { 385 Path cp = AndroidHostEnvUtil.parseInputClasspath(classpath); 386 Path ecp = excludeClasspath == null || excludeClasspath.isEmpty() ? null 387 : AndroidHostEnvUtil.parseInputClasspath(excludeClasspath); 388 DependencyAnalyzer analyzer = new DependencyAnalyzer(cp, ecp, 389 includeInternal, usesExpectedUpstreamAsBaseDeps); 390 391 DependencyAnalyzer.Result result = analyzer.analyze(classesOrPackages); 392 var details = result.getDetails(); 393 boolean isSingleClass = isInputSingleClass(); 394 if (isSingleClass) { 395 mWriter.println("Input Class: " + classesOrPackages.get(0)); 396 } 397 398 for (var classEntry : details.entrySet()) { 399 printMethodDependencies(isSingleClass, classEntry.getKey(), classEntry.getValue()); 400 } 401 mWriter.println(" Summary:"); 402 var collection = result.getAggregated(); 403 printInsn(null, collection); 404 } 405 printMethodDependencies(boolean isSingleClass, String className, List<MethodDependency> deps)406 private void printMethodDependencies(boolean isSingleClass, String className, 407 List<MethodDependency> deps) { 408 boolean isClassNamePrinted = false; 409 for (var dep : deps) { 410 // Try not to flood and print tons of methods which have no dependency. 411 // A native method has no method and field dependency in java code, and thus 412 // print it when the user intends to understand the dependency of a single 413 // class. 414 if (!dep.mDependency.isEmpty() 415 || (isSingleClass && (dep.mNode.access & Opcodes.ACC_NATIVE) != 0)) { 416 if (!isClassNamePrinted) { 417 mWriter.println("Class: " + className); 418 isClassNamePrinted = true; 419 } 420 printInsn(dep.mNode, dep.mDependency); 421 } 422 } 423 424 } 425 isInputSingleClass()426 private boolean isInputSingleClass() { 427 if (classesOrPackages.size() != 1) { 428 return false; 429 } 430 431 String[] split = classesOrPackages.get(0).split("\\."); 432 String last = split[split.length - 1]; 433 return last.length() >= 1 && Character.isUpperCase(last.charAt(0)); 434 } 435 436 printInsn(MethodNode method, SignaturesCollection collection)437 private void printInsn(MethodNode method, SignaturesCollection collection) { 438 PrintWriter writer = mWriter; 439 440 if (method != null) { 441 String printedMethod = ""; 442 printedMethod += (method.access & Opcodes.ACC_PUBLIC) != 0 ? "public " : ""; 443 printedMethod += (method.access & Opcodes.ACC_PROTECTED) != 0 ? "protected " : ""; 444 printedMethod += (method.access & Opcodes.ACC_PRIVATE) != 0 ? "private " : ""; 445 printedMethod += (method.access & Opcodes.ACC_STATIC) != 0 ? "static " : ""; 446 printedMethod += (method.access & Opcodes.ACC_ABSTRACT) != 0 ? "abstract " : ""; 447 printedMethod += (method.access & Opcodes.ACC_NATIVE) != 0 ? "native " : ""; 448 printedMethod += (method.access & Opcodes.ACC_SYNTHETIC) != 0 ? "synthetic " : ""; 449 printedMethod += method.name + method.desc; 450 writer.println(" Method: " + printedMethod); 451 } 452 453 collection.getClassStream() 454 .findAny() 455 .ifPresent(s -> writer.println(" Type dependencies:")); 456 collection.getClassStream() 457 .forEach(s -> writer.println(" " + s)); 458 459 var methodStrs = collection.getMethodStream() 460 .map(m -> classOnly ? m.getOwner() : m.toString()) 461 .sorted() 462 .distinct() 463 .collect(Collectors.toUnmodifiableList()); 464 if (!methodStrs.isEmpty()) { 465 writer.println(" Method invokes:"); 466 for (String s : methodStrs) { 467 writer.println(" " + s); 468 } 469 } 470 var fieldStrs = collection.getFieldStream() 471 .map(f -> classOnly ? f.getOwner() : f.toString()) 472 .sorted() 473 .distinct() 474 .collect(Collectors.toUnmodifiableList()); 475 if (!fieldStrs.isEmpty()) { 476 writer.println(" Field accesses:"); 477 for (String s : fieldStrs) { 478 writer.println(" " + s); 479 } 480 } 481 writer.println(); 482 } 483 } 484 485 @Parameters(commandNames = CommandListNoDeps.NAME, 486 commandDescription = "List classes without any dependency in the target version. " 487 + "These classes in libcore/ojluni/ can be upgraded to the target version " 488 + "in a standalone way without upgrading the other classes.") 489 private static class CommandListNoDeps { 490 491 public static final String NAME = "list-no-deps"; 492 493 @Parameter(names = {"-t", "--target"}, description = "file path to a .jmod or .jar file or " 494 + "one of the following OpenJDK version: 9, 11, 17, 21") 495 public String classpath = "21"; 496 497 @Parameter(names = "-h", help = true, description = "Shows this help message") 498 public boolean help = false; 499 run()500 private void run() throws UncheckedIOException { 501 if (!List.of("9", "11", "17", "21").contains(classpath)) { 502 throw new IllegalArgumentException("Only 9, 11, 17, 21 java version is supported. " 503 + "This java version isn't supported: " + classpath); 504 } 505 int targetVersion = Integer.parseInt(classpath); 506 507 Path cp = AndroidHostEnvUtil.parseInputClasspath(classpath); 508 Path excludeClasspath = AndroidHostEnvUtil.parseInputClasspath("oj"); 509 DependencyAnalyzer analyzer = new DependencyAnalyzer(cp, excludeClasspath, 510 false, true); 511 512 List<ExpectedUpstreamFile.ExpectedUpstreamEntry> entries; 513 try { 514 entries = new ExpectedUpstreamFile().readAllEntries(); 515 } catch (IOException e) { 516 throw new UncheckedIOException(e); 517 } 518 519 List<String> noDepClasses = new ArrayList<>(); 520 PrintWriter writer = new PrintWriter(System.out, /*autoFlush=*/true); 521 for (ExpectedUpstreamFile.ExpectedUpstreamEntry entry : entries) { 522 String path = entry.dstPath; 523 if (!path.startsWith("ojluni/src/main/java/") || !path.endsWith(".java")) { 524 continue; 525 } 526 String jdkStr = entry.gitRef.split("/")[0]; 527 if (jdkStr.length() < 5) { 528 // ignore unparsable entry 529 continue; 530 } 531 532 int jdkVersion; 533 try { 534 jdkVersion = Integer.parseInt(jdkStr.substring( 535 "jdk".length(), jdkStr.length() - 1)); 536 } catch (NumberFormatException e) { 537 // ignore unparsable entry 538 continue; 539 } 540 541 if (jdkVersion >= targetVersion) { 542 continue; 543 } 544 545 String className = path.substring("ojluni/src/main/java/".length(), 546 path.length() - ".java".length()).replace('/', '.'); 547 try { 548 DependencyAnalyzer.Result classResult = analyzer.analyze(List.of(className)); 549 if (classResult.getAggregated().isEmpty()) { 550 noDepClasses.add(className); 551 } 552 } catch (IllegalArgumentException e) { 553 // Print the classes not found. The classes are either not in java.base 554 // or removed in the target OpenJDK version. 555 writer.println("Class not found: " + className + " in " + cp.toString()); 556 } 557 } 558 559 writer.println("Classes with no deps: "); 560 for (String name : noDepClasses) { 561 writer.println(" " + name); 562 } 563 } 564 } 565 @Parameters(commandNames = CommandListNewApis.NAME, 566 commandDescription = "List the new classes / methods / fields in java.base version.") 567 private static class CommandListNewApis { 568 569 public static final String NAME = "list-new-apis"; 570 571 @Parameter(names = {"-b", "--base"}, 572 description = "file path to a .jmod or .jar file or" 573 + "one of the following OpenJDK version: 8, 9, 11, 17, 21") 574 public String base = "oj"; 575 576 @Parameter(names = {"-t", "--target"}, 577 description = "file path to a .jmod or .jar file or" 578 + "one of the following OpenJDK version: 8, 9, 11, 17, 21") 579 public String classpath = "21"; 580 581 @Parameter(names = {"-d"}, 582 description = "Disable the API filters read from " + UnsupportedNewApis.FILE_NAME) 583 public boolean disableFilter = false; 584 585 @Parameter(names = "-h", help = true, description = "Shows this help message") 586 public boolean help = false; 587 run()588 private void run() throws UncheckedIOException { 589 Path newClassPath = AndroidHostEnvUtil.parseInputClasspath(classpath); 590 Path baseClasspath = AndroidHostEnvUtil.parseInputClasspath(base); 591 592 SignaturesCollector collector = new SignaturesCollector(); 593 // Set up filter if it's enabled. 594 Predicate<String> classNamePredicate; 595 if (disableFilter) { 596 classNamePredicate = (s) -> true; 597 } else { 598 UnsupportedNewApis unsupportedApis = UnsupportedNewApis.getInstance(); 599 classNamePredicate = Predicate.not(unsupportedApis::contains); 600 collector.setFieldFilter(Predicate.not(unsupportedApis::contains)); 601 collector.setMethodFilter(Predicate.not(unsupportedApis::contains)); 602 } 603 604 try (ZipFile baseZip = new ZipFile(baseClasspath.toFile()); 605 ZipFile newZip = new ZipFile(newClassPath.toFile())) { 606 607 Predicate<String> nameTest = Pattern.compile( 608 "^(classes/)?java(x)?/.*\\.class$").asMatchPredicate(); 609 var zipEntries = newZip.entries(); 610 while (zipEntries.hasMoreElements()) { 611 ZipEntry zipEntry = zipEntries.nextElement(); 612 if (!nameTest.test(zipEntry.getName())) { 613 continue; 614 } 615 616 ClassNode newNode = readClassNode(newZip, zipEntry); 617 if (!isClassExposed(newZip, newNode, classNamePredicate)) { 618 continue; 619 } 620 621 ZipEntry baseEntry = ClassFileUtil.getEntryFromClassName(baseZip, 622 newNode.name, true); 623 String internalClassName = newNode.name; 624 // Add the class name if the entire class is missing. 625 if (baseEntry == null) { 626 collector.addClass(internalClassName); 627 continue; 628 } 629 630 ClassNode baseNode = readClassNode(baseZip, baseEntry); 631 DiffAnalyzer analyzer = DiffAnalyzer.analyze(baseNode, newNode); 632 for (FieldNode fieldNode : analyzer.newFields) { 633 collector.add(internalClassName, fieldNode); 634 } 635 for (MethodNode methodNode : analyzer.newMethods) { 636 collector.add(internalClassName, methodNode); 637 } 638 } 639 } catch (IOException e) { 640 throw new UncheckedIOException(e); 641 } 642 643 PrintWriter writer = new PrintWriter(System.out, true); 644 SignaturesCollection collection = collector.getCollection(); 645 collection.getClassStream() 646 .forEach(writer::println); 647 Stream.concat( 648 collection.getMethodStream().map(SignaturesCollector.Method::toString), 649 collection.getFieldStream().map( 650 f -> f.getOwner() + "#" + f.getName() + ":" + f.getDesc()) 651 ).sorted().forEach(writer::println); 652 } 653 readClassNode(ZipFile zipFile, ZipEntry entry)654 private static ClassNode readClassNode(ZipFile zipFile, ZipEntry entry) throws IOException { 655 try (InputStream in = zipFile.getInputStream(entry)) { 656 return ClassFileUtil.parseClass(in); 657 } 658 } 659 660 /** 661 * Return true if the class is public / protected. However, if it's inner class, it returns 662 * true only if the outer classes are all public / protected as well. 663 */ isClassExposed(ZipFile zipFile, ClassNode node, Predicate<String> classNamePredicate)664 private static boolean isClassExposed(ZipFile zipFile, ClassNode node, 665 Predicate<String> classNamePredicate) throws IOException { 666 if (!DiffAnalyzer.isExposed(node.access) || !classNamePredicate.test(node.name)) { 667 return false; 668 } 669 670 Optional<String> outerClass = node.innerClasses.stream() 671 .filter(inner -> node.name.equals(inner.name) && inner.outerName != null) 672 .map(innerClassNode -> innerClassNode.outerName) 673 .findFirst(); 674 675 if (outerClass.isEmpty()) { 676 return true; 677 } 678 679 ZipEntry zipEntry = ClassFileUtil.getEntryFromClassName(zipFile, outerClass.get(), 680 true); 681 if (zipEntry == null) { 682 return true; 683 } 684 685 // TODO: Lookup in a cache before parsing the .class file. 686 try (InputStream in = zipFile.getInputStream(zipEntry)) { 687 ClassNode outerNode = ClassFileUtil.parseClass(in); 688 return isClassExposed(zipFile, outerNode, classNamePredicate); 689 } 690 } 691 } 692 main(String[] argv)693 public static void main(String[] argv) { 694 MainArgs mainArgs = new MainArgs(); 695 CommandDump commandDump = new CommandDump(); 696 CommandApiDiff commandApiDiff = new CommandApiDiff(); 697 CommandShowDeps commandShowDeps = new CommandShowDeps(); 698 CommandListNoDeps commandListNoDeps = new CommandListNoDeps(); 699 CommandListNewApis commandListNewApis = new CommandListNewApis(); 700 JCommander jCommander = JCommander.newBuilder() 701 .addObject(mainArgs) 702 .addCommand(commandDump) 703 .addCommand(commandApiDiff) 704 .addCommand(commandShowDeps) 705 .addCommand(commandListNoDeps) 706 .addCommand(commandListNewApis) 707 .build(); 708 jCommander.parse(argv); 709 710 if (mainArgs.help || jCommander.getParsedCommand() == null) { 711 jCommander.usage(); 712 return; 713 } 714 715 switch (jCommander.getParsedCommand()) { 716 case CommandDump.NAME: 717 if (commandDump.help) { 718 jCommander.usage(CommandDump.NAME); 719 } else { 720 commandDump.run(); 721 } 722 break; 723 case CommandApiDiff.NAME: 724 if (commandApiDiff.help) { 725 jCommander.usage(CommandApiDiff.NAME); 726 } else { 727 commandApiDiff.run(); 728 } 729 break; 730 case CommandShowDeps.NAME: 731 if (commandShowDeps.help) { 732 jCommander.usage(CommandShowDeps.NAME); 733 } else { 734 commandShowDeps.run(); 735 } 736 break; 737 case CommandListNoDeps.NAME: 738 if (commandShowDeps.help) { 739 jCommander.usage(CommandShowDeps.NAME); 740 } else { 741 commandListNoDeps.run(); 742 } 743 break; 744 case CommandListNewApis.NAME: 745 if (commandListNewApis.help) { 746 jCommander.usage(CommandListNewApis.NAME); 747 } else { 748 commandListNewApis.run(); 749 } 750 break; 751 default: 752 throw new IllegalArgumentException("Unknown sub-command: " + 753 jCommander.getParsedCommand()); 754 } 755 } 756 } 757