1 /*
2  * Copyright (C) 2018 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 com.android.class2nonsdklist;
18 
19 import com.android.annotationvisitor.AnnotationConsumer;
20 import com.android.annotationvisitor.AnnotationHandler;
21 import com.android.annotationvisitor.AnnotationVisitor;
22 import com.android.annotationvisitor.JarReader;
23 import com.android.annotationvisitor.RepeatedAnnotationHandler;
24 import com.android.annotationvisitor.Status;
25 
26 import com.google.common.base.Splitter;
27 import com.google.common.collect.ImmutableMap;
28 import com.google.common.collect.ImmutableMap.Builder;
29 import com.google.common.io.Files;
30 
31 import org.apache.commons.cli.CommandLine;
32 import org.apache.commons.cli.CommandLineParser;
33 import org.apache.commons.cli.GnuParser;
34 import org.apache.commons.cli.HelpFormatter;
35 import org.apache.commons.cli.OptionBuilder;
36 import org.apache.commons.cli.Options;
37 import org.apache.commons.cli.ParseException;
38 
39 import java.io.File;
40 import java.io.IOException;
41 import java.nio.charset.StandardCharsets;
42 import java.util.Collections;
43 import java.util.HashMap;
44 import java.util.Map;
45 import java.util.Set;
46 import java.util.stream.Collectors;
47 
48 /**
49  * Build time tool for extracting a list of members from jar files that have the
50  * @UnsupportedAppUsage annotation, for building the non SDK API lists.
51  */
52 public class Class2NonSdkList {
53 
54     private static final String UNSUPPORTED_APP_USAGE_ANNOTATION =
55             "android.compat.annotation.UnsupportedAppUsage";
56 
57     private static final String FLAG_UNSUPPORTED = "unsupported";
58     private static final String FLAG_BLOCKED = "blocked";
59     private static final String FLAG_MAX_TARGET_O = "max-target-o";
60     private static final String FLAG_MAX_TARGET_P = "max-target-p";
61     private static final String FLAG_MAX_TARGET_Q = "max-target-q";
62     private static final String FLAG_MAX_TARGET_R = "max-target-r";
63 
64     private static final String FLAG_PUBLIC_API = "public-api";
65 
66     private static final Map<Integer, String> TARGET_SDK_TO_LIST_MAP;
67     static {
68         Map<Integer, String> map = new HashMap<>();
map.put(null, FLAG_UNSUPPORTED)69         map.put(null, FLAG_UNSUPPORTED);
70         map.put(0, FLAG_BLOCKED);
71         map.put(26, FLAG_MAX_TARGET_O);
72         map.put(28, FLAG_MAX_TARGET_P);
73         map.put(29, FLAG_MAX_TARGET_Q);
74         map.put(30, FLAG_MAX_TARGET_R);
75         map.put(10000, FLAG_UNSUPPORTED); // VMRuntime.SDK_VERSION_CUR_DEVELOPMENT
76         TARGET_SDK_TO_LIST_MAP = Collections.unmodifiableMap(map);
77     }
78 
79     private final Status mStatus;
80     private final String[] mJarFiles;
81     private final AnnotationConsumer mOutput;
82     private final Set<String> mPublicApis;
83 
main(String[] args)84     public static void main(String[] args) {
85         Options options = new Options();
86         options.addOption(OptionBuilder
87                 .withLongOpt("stub-api-flags")
88                 .hasArgs(1)
89                 .withDescription("CSV file with API flags generated from public API stubs. " +
90                         "Used to de-dupe bridge methods.")
91                 .create("s"));
92         options.addOption(OptionBuilder
93                 .withLongOpt("write-flags-csv")
94                 .hasArgs(1)
95                 .withDescription("Specify file to write hiddenapi flags to.")
96                 .create('w'));
97         options.addOption(OptionBuilder
98                 .withLongOpt("debug")
99                 .hasArgs(0)
100                 .withDescription("Enable debug")
101                 .create("d"));
102         options.addOption(OptionBuilder
103                 .withLongOpt("dump-all-members")
104                 .withDescription("Dump all members from jar files to stdout. Ignore annotations. " +
105                         "Do not use in conjunction with any other arguments.")
106                 .hasArgs(0)
107                 .create('m'));
108         options.addOption(OptionBuilder
109                 .withLongOpt("write-metadata-csv")
110                 .hasArgs(1)
111                 .withDescription("Specify a file to write API metaadata to. This is a CSV file " +
112                         "containing any annotation properties for all members. Do not use in " +
113                         "conjunction with --write-flags-csv.")
114                 .create('c'));
115         options.addOption(OptionBuilder
116                 .withLongOpt("help")
117                 .hasArgs(0)
118                 .withDescription("Show this help")
119                 .create('h'));
120 
121         CommandLineParser parser = new GnuParser();
122         CommandLine cmd;
123 
124         try {
125             cmd = parser.parse(options, args);
126         } catch (ParseException e) {
127             System.err.println(e.getMessage());
128             help(options);
129             return;
130         }
131         if (cmd.hasOption('h')) {
132             help(options);
133         }
134 
135 
136         String[] jarFiles = cmd.getArgs();
137         if (jarFiles.length == 0) {
138             System.err.println("Error: no jar files specified.");
139             help(options);
140         }
141 
142         Status status = new Status(cmd.hasOption('d'));
143 
144         if (cmd.hasOption('m')) {
145             dumpAllMembers(status, jarFiles);
146         } else {
147             try {
148                 Class2NonSdkList c2nsl = new Class2NonSdkList(
149                         status,
150                         cmd.getOptionValue('s', null),
151                         cmd.getOptionValue('w', null),
152                         cmd.getOptionValue('c', null),
153                         jarFiles);
154                 c2nsl.main();
155             } catch (IOException e) {
156                 status.error(e);
157             }
158         }
159 
160         if (status.ok()) {
161             System.exit(0);
162         } else {
163             System.exit(1);
164         }
165 
166     }
167 
Class2NonSdkList(Status status, String stubApiFlagsFile, String csvFlagsFile, String csvMetadataFile, String[] jarFiles)168     private Class2NonSdkList(Status status, String stubApiFlagsFile, String csvFlagsFile,
169             String csvMetadataFile, String[] jarFiles)
170             throws IOException {
171         mStatus = status;
172         mJarFiles = jarFiles;
173         if (csvMetadataFile != null) {
174             mOutput = new AnnotationPropertyWriter(csvMetadataFile);
175         } else {
176             mOutput = new HiddenapiFlagsWriter(csvFlagsFile);
177         }
178 
179         if (stubApiFlagsFile != null) {
180             mPublicApis =
181                     Files.readLines(new File(stubApiFlagsFile), StandardCharsets.UTF_8).stream()
182                         .map(s -> Splitter.on(",").splitToList(s))
183                         .filter(s -> s.contains(FLAG_PUBLIC_API))
184                         .map(s -> s.get(0))
185                         .collect(Collectors.toSet());
186         } else {
187             mPublicApis = Collections.emptySet();
188         }
189     }
190 
createAnnotationHandlers()191     private Map<String, AnnotationHandler> createAnnotationHandlers() {
192         Builder<String, AnnotationHandler> builder = ImmutableMap.builder();
193         UnsupportedAppUsageAnnotationHandler greylistAnnotationHandler =
194                 new UnsupportedAppUsageAnnotationHandler(
195                     mStatus, mOutput, mPublicApis, TARGET_SDK_TO_LIST_MAP);
196 
197         addRepeatedAnnotationHandlers(
198                 builder,
199                 classNameToSignature(UNSUPPORTED_APP_USAGE_ANNOTATION),
200                 classNameToSignature(UNSUPPORTED_APP_USAGE_ANNOTATION + "$Container"),
201                 greylistAnnotationHandler);
202 
203         CovariantReturnTypeHandler covariantReturnTypeHandler = new CovariantReturnTypeHandler(
204             mOutput, mPublicApis, FLAG_PUBLIC_API);
205 
206         return addRepeatedAnnotationHandlers(builder, CovariantReturnTypeHandler.ANNOTATION_NAME,
207             CovariantReturnTypeHandler.REPEATED_ANNOTATION_NAME, covariantReturnTypeHandler)
208             .build();
209     }
210 
classNameToSignature(String a)211     private String classNameToSignature(String a) {
212         return "L" + a.replace('.', '/') + ";";
213     }
214 
215     /**
216      * Add a handler for an annotation as well as an handler for the container annotation that is
217      * used when the annotation is repeated.
218      *
219      * @param builder the builder for the map to which the handlers will be added.
220      * @param annotationName the name of the annotation.
221      * @param containerAnnotationName the name of the annotation container.
222      * @param handler the handler for the annotation.
223      */
addRepeatedAnnotationHandlers( Builder<String, AnnotationHandler> builder, String annotationName, String containerAnnotationName, AnnotationHandler handler)224     private static Builder<String, AnnotationHandler> addRepeatedAnnotationHandlers(
225         Builder<String, AnnotationHandler> builder,
226         String annotationName, String containerAnnotationName,
227         AnnotationHandler handler) {
228         return builder
229             .put(annotationName, handler)
230             .put(containerAnnotationName, new RepeatedAnnotationHandler(annotationName, handler));
231     }
232 
main()233     private void main() {
234         Map<String, AnnotationHandler> handlers = createAnnotationHandlers();
235         for (String jarFile : mJarFiles) {
236             mStatus.debug("Processing jar file %s", jarFile);
237             try {
238                 JarReader reader = new JarReader(mStatus, jarFile);
239                 reader.stream().forEach(clazz -> new AnnotationVisitor(clazz, mStatus, handlers)
240                         .visit());
241                 reader.close();
242             } catch (IOException e) {
243                 mStatus.error(e);
244             }
245         }
246         mOutput.close();
247     }
248 
dumpAllMembers(Status status, String[] jarFiles)249     private static void dumpAllMembers(Status status, String[] jarFiles) {
250         for (String jarFile : jarFiles) {
251             status.debug("Processing jar file %s", jarFile);
252             try {
253                 JarReader reader = new JarReader(status, jarFile);
254                 reader.stream().forEach(clazz -> new MemberDumpingVisitor(clazz, status)
255                         .visit());
256                 reader.close();
257             } catch (IOException e) {
258                 status.error(e);
259             }
260         }
261     }
262 
help(Options options)263     private static void help(Options options) {
264         new HelpFormatter().printHelp(
265                 "class2nonsdklist path/to/classes.jar [classes2.jar ...]",
266                 "Extracts nonsdk entries from classes jar files given",
267                 options, null, true);
268         System.exit(1);
269     }
270 }
271