1 /*
2  * Copyright (C) 2017 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.settingslib.license;
18 
19 import android.text.TextUtils;
20 import android.util.Log;
21 import android.util.Xml;
22 
23 import androidx.annotation.VisibleForTesting;
24 
25 import org.xmlpull.v1.XmlPullParser;
26 import org.xmlpull.v1.XmlPullParserException;
27 
28 import java.io.File;
29 import java.io.FileInputStream;
30 import java.io.FileReader;
31 import java.io.IOException;
32 import java.io.InputStreamReader;
33 import java.io.PrintWriter;
34 import java.util.ArrayList;
35 import java.util.Collections;
36 import java.util.HashMap;
37 import java.util.HashSet;
38 import java.util.List;
39 import java.util.Map;
40 import java.util.Set;
41 import java.util.SortedMap;
42 import java.util.TreeMap;
43 import java.util.zip.GZIPInputStream;
44 
45 /**
46  * The utility class that generate a license html file from xml files.
47  * All the HTML snippets and logic are copied from build/make/tools/generate-notice-files.py.
48  *
49  * TODO: Remove duplicate codes once backward support ends.
50  */
51 class LicenseHtmlGeneratorFromXml {
52     private static final String TAG = "LicenseGeneratorFromXml";
53 
54     private static final String TAG_ROOT = "licenses";
55     private static final String TAG_FILE_NAME = "file-name";
56     private static final String TAG_FILE_CONTENT = "file-content";
57     private static final String ATTR_CONTENT_ID = "contentId";
58     private static final String ATTR_LIBRARY_NAME = "lib";
59 
60     private static final String HTML_HEAD_STRING =
61             "<html><head>\n"
62             + "<style type=\"text/css\">\n"
63             + "body { padding: 0; font-family: sans-serif; }\n"
64             + ".same-license { background-color: #eeeeee;\n"
65             + "                border-top: 20px solid white;\n"
66             + "                padding: 10px; }\n"
67             + ".label { font-weight: bold; }\n"
68             + ".file-list { margin-left: 1em; color: blue; }\n"
69             + "</style>\n"
70             + "</head>"
71             + "<body topmargin=\"0\" leftmargin=\"0\" rightmargin=\"0\" bottommargin=\"0\">\n"
72             + "<div class=\"toc\">";
73     private static final String LIBRARY_HEAD_STRING =
74             "<strong>Libraries</strong>\n<ul class=\"libraries\">";
75     private static final String LIBRARY_TAIL_STRING = "</ul>\n<strong>Files</strong>";
76 
77     private static final String FILES_HEAD_STRING = "<ul class=\"files\">";
78     private static final String FILES_TAIL_STRING = "</ul>\n</div><!-- table of contents -->";
79 
80     private static final String CONTENT_HEAD_STRING =
81             "<table cellpadding=\"0\" cellspacing=\"0\" border=\"0\">";
82     private static final String CONTENT_TAIL_STRING = "</table>";
83 
84     private static final String IMAGES_HEAD_STRING =
85             "<div class=\"images-list\"><strong>Images</strong>\n<ul class=\"images\">";
86     private static final String IMAGES_TAIL_STRING = "</ul></div>\n";
87 
88     private static final String PATH_COUNTS_HEAD_STRING =
89             "<div class=\"path-counts\"><table>\n  <tr><th>Path prefix</th><th>Count</th></tr>\n";
90     private static final String PATH_COUNTS_TAIL_STRING = "</table></div>\n";
91 
92     private static final String HTML_TAIL_STRING =
93             "</body></html>";
94 
95     private final List<File> mXmlFiles;
96 
97     /*
98      * A map from a file name to a library name (may be empty) to a content id (MD5 sum of file
99      * content) for its license.
100      * For example, "/system/priv-app/TeleService/TeleService.apk" maps to "service/Telephony" to
101      * "9645f39e9db895a4aa6e02cb57294595". Here "9645f39e9db895a4aa6e02cb57294595" is a MD5 sum
102      * of the content of packages/services/Telephony/MODULE_LICENSE_APACHE2.
103      */
104     private final Map<String, Map<String, Set<String>>> mFileNameToLibraryToContentIdMap =
105             new HashMap();
106 
107     /*
108      * A map from a content id (MD5 sum of file content) to a license file content.
109      * For example, "9645f39e9db895a4aa6e02cb57294595" maps to the content string of
110      * packages/services/Telephony/MODULE_LICENSE_APACHE2. Here "9645f39e9db895a4aa6e02cb57294595"
111      * is a MD5 sum of the file content.
112      */
113     private final Map<String, String> mContentIdToFileContentMap = new HashMap();
114 
115     static class ContentIdAndFileNames {
116         final String mContentId;
117         final Map<String, List<String>> mLibraryToFileNameMap = new TreeMap();
118 
ContentIdAndFileNames(String contentId)119         ContentIdAndFileNames(String contentId) {
120             mContentId = contentId;
121         }
122     }
123 
LicenseHtmlGeneratorFromXml(List<File> xmlFiles)124     private LicenseHtmlGeneratorFromXml(List<File> xmlFiles) {
125         mXmlFiles = xmlFiles;
126     }
127 
generateHtml(List<File> xmlFiles, File outputFile, String noticeHeader)128     public static boolean generateHtml(List<File> xmlFiles, File outputFile,
129             String noticeHeader) {
130         LicenseHtmlGeneratorFromXml genertor = new LicenseHtmlGeneratorFromXml(xmlFiles);
131         return genertor.generateHtml(outputFile, noticeHeader);
132     }
133 
generateHtml(File outputFile, String noticeHeader)134     private boolean generateHtml(File outputFile, String noticeHeader) {
135         for (File xmlFile : mXmlFiles) {
136             parse(xmlFile);
137         }
138 
139         if (mFileNameToLibraryToContentIdMap.isEmpty() || mContentIdToFileContentMap.isEmpty()) {
140             return false;
141         }
142 
143         PrintWriter writer = null;
144         try {
145             writer = new PrintWriter(outputFile);
146 
147             generateHtml(mXmlFiles, mFileNameToLibraryToContentIdMap, mContentIdToFileContentMap,
148                     writer, noticeHeader);
149 
150             writer.flush();
151             writer.close();
152             return true;
153         } catch (IOException | SecurityException e) {
154             Log.e(TAG, "Failed to generate " + outputFile, e);
155 
156             if (writer != null) {
157                 writer.close();
158             }
159             return false;
160         }
161     }
162 
parse(File xmlFile)163     private void parse(File xmlFile) {
164         if (xmlFile == null || !xmlFile.exists() || xmlFile.length() == 0) {
165             return;
166         }
167 
168         InputStreamReader in = null;
169         try {
170             if (xmlFile.getName().endsWith(".gz")) {
171                 in = new InputStreamReader(new GZIPInputStream(new FileInputStream(xmlFile)));
172             } else {
173                 in = new FileReader(xmlFile);
174             }
175 
176             parse(in, mFileNameToLibraryToContentIdMap, mContentIdToFileContentMap);
177 
178             in.close();
179         } catch (XmlPullParserException | IOException e) {
180             Log.e(TAG, "Failed to parse " + xmlFile, e);
181             if (in != null) {
182                 try {
183                     in.close();
184                 } catch (IOException ie) {
185                     Log.w(TAG, "Failed to close " + xmlFile);
186                 }
187             }
188         }
189     }
190 
191     /*
192      * Parses an input stream and fills a map from a file name to a content id for its license
193      * and a map from a content id to a license file content.
194      *
195      * Following xml format is expected from the input stream.
196      *
197      *     <licenses>
198      *     <file-name contentId="content_id_of_license1">file1</file-name>
199      *     <file-name contentId="content_id_of_license2" lib="name of library">file2</file-name>
200      *     <file-name contentId="content_id_of_license2" lib="another library">file2</file-name>
201      *     ...
202      *     <file-content contentId="content_id_of_license1">license1 file contents</file-content>
203      *     <file-content contentId="content_id_of_license2">license2 file contents</file-content>
204      *     ...
205      *     </licenses>
206      */
207     @VisibleForTesting
parse(InputStreamReader in, Map<String, Map<String, Set<String>>> outFileNameToLibraryToContentIdMap, Map<String, String> outContentIdToFileContentMap)208     static void parse(InputStreamReader in,
209             Map<String, Map<String, Set<String>>> outFileNameToLibraryToContentIdMap,
210             Map<String, String> outContentIdToFileContentMap)
211                     throws XmlPullParserException, IOException {
212         Map<String, Map<String, Set<String>>> fileNameToLibraryToContentIdMap =
213                 new HashMap<String, Map<String, Set<String>>>();
214         Map<String, String> contentIdToFileContentMap = new HashMap<String, String>();
215 
216         XmlPullParser parser = Xml.newPullParser();
217         parser.setInput(in);
218         parser.nextTag();
219 
220         parser.require(XmlPullParser.START_TAG, "", TAG_ROOT);
221 
222         int state = parser.getEventType();
223         while (state != XmlPullParser.END_DOCUMENT) {
224             if (state == XmlPullParser.START_TAG) {
225                 if (TAG_FILE_NAME.equals(parser.getName())) {
226                     String contentId = parser.getAttributeValue("", ATTR_CONTENT_ID);
227                     String libraryName = parser.getAttributeValue("", ATTR_LIBRARY_NAME);
228                     if (!TextUtils.isEmpty(contentId)) {
229                         String fileName = readText(parser).trim();
230                         if (!TextUtils.isEmpty(fileName)) {
231                             Map<String, Set<String>> libs =
232                                     fileNameToLibraryToContentIdMap.computeIfAbsent(
233                                             fileName, k -> new HashMap<>());
234                             Set<String> contentIds = libs.computeIfAbsent(
235                                             libraryName, k -> new HashSet<>());
236                             contentIds.add(contentId);
237                         }
238                     }
239                 } else if (TAG_FILE_CONTENT.equals(parser.getName())) {
240                     String contentId = parser.getAttributeValue("", ATTR_CONTENT_ID);
241                     if (!TextUtils.isEmpty(contentId)
242                             && !outContentIdToFileContentMap.containsKey(contentId)
243                             && !contentIdToFileContentMap.containsKey(contentId)) {
244                         String fileContent = readText(parser);
245                         if (!TextUtils.isEmpty(fileContent)) {
246                             contentIdToFileContentMap.put(contentId, fileContent);
247                         }
248                     }
249                 }
250             }
251 
252             state = parser.next();
253         }
254         for (Map.Entry<String, Map<String, Set<String>>> mapEntry :
255                 fileNameToLibraryToContentIdMap.entrySet()) {
256             outFileNameToLibraryToContentIdMap.merge(
257                     mapEntry.getKey(), mapEntry.getValue(), (m1, m2) -> {
258                         for (Map.Entry<String, Set<String>> entry : m2.entrySet()) {
259                             m1.merge(entry.getKey(), entry.getValue(), (s1, s2) -> {
260                                 s1.addAll(s2);
261                                 return s1;
262                             });
263                         }
264                         return m1;
265                     });
266         }
267         outContentIdToFileContentMap.putAll(contentIdToFileContentMap);
268     }
269 
readText(XmlPullParser parser)270     private static String readText(XmlPullParser parser)
271             throws IOException, XmlPullParserException {
272         StringBuffer result = new StringBuffer();
273         int state = parser.next();
274         while (state == XmlPullParser.TEXT) {
275             result.append(parser.getText());
276             state = parser.next();
277         }
278         return result.toString();
279     }
280 
pathPrefix(String path)281     private static String pathPrefix(String path) {
282         String prefix = path;
283         while (prefix.length() > 0 && prefix.substring(0, 1).equals("/")) {
284             prefix = prefix.substring(1);
285         }
286         int idx = prefix.indexOf("/");
287         if (idx > 0) {
288             prefix = prefix.substring(0, idx);
289         }
290         return prefix;
291     }
292 
293     @VisibleForTesting
generateHtml(List<File> xmlFiles, Map<String, Map<String, Set<String>>> fileNameToLibraryToContentIdMap, Map<String, String> contentIdToFileContentMap, PrintWriter writer, String noticeHeader)294     static void generateHtml(List<File> xmlFiles,
295             Map<String, Map<String, Set<String>>> fileNameToLibraryToContentIdMap,
296             Map<String, String> contentIdToFileContentMap, PrintWriter writer,
297             String noticeHeader) throws IOException {
298         List<String> fileNameList = new ArrayList();
299         fileNameList.addAll(fileNameToLibraryToContentIdMap.keySet());
300         Collections.sort(fileNameList);
301 
302         SortedMap<String, Integer> prefixToCount = new TreeMap();
303         for (String f : fileNameList) {
304             String prefix = pathPrefix(f);
305             prefixToCount.merge(prefix, 1, Integer::sum);
306         }
307 
308         SortedMap<String, Set<String>> libraryToContentIdMap = new TreeMap();
309         for (Map<String, Set<String>> libraryToContentValue :
310                 fileNameToLibraryToContentIdMap.values()) {
311             for (Map.Entry<String, Set<String>> entry : libraryToContentValue.entrySet()) {
312                 if (TextUtils.isEmpty(entry.getKey())) {
313                     continue;
314                 }
315                 libraryToContentIdMap.merge(
316                         entry.getKey(), entry.getValue(), (s1, s2) -> {
317                             s1.addAll(s2);
318                             return s1;
319                         });
320             }
321         }
322 
323         writer.println(HTML_HEAD_STRING);
324 
325         if (!TextUtils.isEmpty(noticeHeader)) {
326             writer.println(noticeHeader);
327         }
328 
329         int count = 0;
330         Map<String, Integer> contentIdToOrderMap = new HashMap();
331         List<ContentIdAndFileNames> contentIdAndFileNamesList = new ArrayList();
332 
333         if (!libraryToContentIdMap.isEmpty()) {
334             writer.println(LIBRARY_HEAD_STRING);
335             for (Map.Entry<String, Set<String>> entry: libraryToContentIdMap.entrySet()) {
336                 String libraryName = entry.getKey();
337                 for (String contentId : entry.getValue()) {
338                     // Assigns an id to a newly referred license file content.
339                     if (!contentIdToOrderMap.containsKey(contentId)) {
340                         contentIdToOrderMap.put(contentId, count);
341 
342                         // An index in contentIdAndFileNamesList is the order of each element.
343                         contentIdAndFileNamesList.add(new ContentIdAndFileNames(contentId));
344                         count++;
345                     }
346                     int id = contentIdToOrderMap.get(contentId);
347                     writer.format("<li><a href=\"#id%d\">%s</a></li>\n", id, libraryName);
348                 }
349             }
350             writer.println(LIBRARY_TAIL_STRING);
351         }
352 
353         if (!fileNameList.isEmpty()) {
354             writer.println(FILES_HEAD_STRING);
355             // Prints all the file list with a link to its license file content.
356             for (String fileName : fileNameList) {
357                 for (Map.Entry<String, Set<String>> libToContentId :
358                         fileNameToLibraryToContentIdMap.get(fileName).entrySet()) {
359                     String libraryName = libToContentId.getKey();
360                     if (libraryName == null) {
361                         libraryName = "";
362                     }
363                     for (String contentId : libToContentId.getValue()) {
364                         // Assigns an id to a newly referred license file content.
365                         if (!contentIdToOrderMap.containsKey(contentId)) {
366                             contentIdToOrderMap.put(contentId, count);
367 
368                             // An index in contentIdAndFileNamesList is the order of each element.
369                             contentIdAndFileNamesList.add(new ContentIdAndFileNames(contentId));
370                             count++;
371                         }
372 
373                         int id = contentIdToOrderMap.get(contentId);
374                         ContentIdAndFileNames elem = contentIdAndFileNamesList.get(id);
375                         List<String> files = elem.mLibraryToFileNameMap.computeIfAbsent(
376                                 libraryName, k -> new ArrayList());
377                         files.add(fileName);
378                         if (TextUtils.isEmpty(libraryName)) {
379                             writer.format("<li><a href=\"#id%d\">%s</a></li>\n", id, fileName);
380                         } else {
381                             writer.format("<li><a href=\"#id%d\">%s - %s</a></li>\n",
382                                     id, fileName, libraryName);
383                         }
384                     }
385                 }
386             }
387             writer.println(FILES_TAIL_STRING);
388         }
389 
390         if (!contentIdAndFileNamesList.isEmpty()) {
391             writer.println(CONTENT_HEAD_STRING);
392             // Prints all contents of the license files in order of id.
393             for (ContentIdAndFileNames contentIdAndFileNames : contentIdAndFileNamesList) {
394                 // Assigns an id to a newly referred license file content (should never happen here)
395                 if (!contentIdToOrderMap.containsKey(contentIdAndFileNames.mContentId)) {
396                     contentIdToOrderMap.put(contentIdAndFileNames.mContentId, count);
397                     count++;
398                 }
399                 int id = contentIdToOrderMap.get(contentIdAndFileNames.mContentId);
400                 writer.format("<tr id=\"id%d\"><td class=\"same-license\">\n", id);
401                 for (Map.Entry<String, List<String>> libraryFiles :
402                         contentIdAndFileNames.mLibraryToFileNameMap.entrySet()) {
403                     String libraryName = libraryFiles.getKey();
404                     if (TextUtils.isEmpty(libraryName)) {
405                         writer.println("<div class=\"label\">Notices for file(s):</div>");
406                     } else {
407                         writer.format("<div class=\"label\"><strong>%s</strong> used by:</div>\n",
408                                 libraryName);
409                     }
410                     writer.println("<div class=\"file-list\">");
411                     for (String fileName : libraryFiles.getValue()) {
412                         writer.format("%s <br/>\n", fileName);
413                     }
414                     writer.println("</div><!-- file-list -->");
415                 }
416                 writer.println("<pre class=\"license-text\">");
417                 writer.println(contentIdToFileContentMap.get(
418                         contentIdAndFileNames.mContentId));
419                 writer.println("</pre><!-- license-text -->");
420                 writer.println("</td></tr><!-- same-license -->");
421             }
422             writer.println(CONTENT_TAIL_STRING);
423         }
424 
425         if (!xmlFiles.isEmpty()) {
426             writer.println(IMAGES_HEAD_STRING);
427             for (File file : xmlFiles) {
428                 writer.format("  <li>%s</li>\n", pathPrefix(file.getCanonicalPath()));
429             }
430             writer.println(IMAGES_TAIL_STRING);
431         }
432 
433         if (!prefixToCount.isEmpty()) {
434             writer.println(PATH_COUNTS_HEAD_STRING);
435             for (Map.Entry<String, Integer> entry : prefixToCount.entrySet()) {
436                 writer.format("  <tr><td>%s</td><td>%d</td></tr>\n",
437                         entry.getKey(), entry.getValue());
438             }
439             writer.println(PATH_COUNTS_TAIL_STRING);
440         }
441 
442         writer.println(HTML_TAIL_STRING);
443     }
444 }
445