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