• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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.asllib.util;
18 
19 
20 import org.w3c.dom.Document;
21 import org.w3c.dom.Element;
22 import org.w3c.dom.Node;
23 import org.w3c.dom.NodeList;
24 
25 import java.util.ArrayList;
26 import java.util.Arrays;
27 import java.util.List;
28 
29 public class XmlUtils {
30     public static final String DATA_TYPE_SEPARATOR = "_data_type_";
31 
32     public static final String HR_TAG_APP_METADATA_BUNDLES = "app-metadata-bundles";
33     public static final String HR_TAG_SYSTEM_APP_SAFETY_LABEL = "system-app-safety-label";
34     public static final String HR_TAG_SAFETY_LABELS = "safety-labels";
35     public static final String HR_TAG_TRANSPARENCY_INFO = "transparency-info";
36     public static final String HR_TAG_DEVELOPER_INFO = "developer-info";
37     public static final String HR_TAG_APP_INFO = "app-info";
38     public static final String HR_TAG_DATA_LABELS = "data-labels";
39     public static final String HR_TAG_SECURITY_LABELS = "security-labels";
40     public static final String HR_TAG_THIRD_PARTY_VERIFICATION = "third-party-verification";
41     public static final String HR_TAG_DATA_ACCESSED = "data-accessed";
42     public static final String HR_TAG_DATA_COLLECTED = "data-collected";
43     public static final String HR_TAG_DATA_COLLECTED_EPHEMERAL = "data-collected-ephemeral";
44     public static final String HR_TAG_DATA_SHARED = "data-shared";
45     public static final String HR_ATTR_NAME = "name";
46     public static final String HR_ATTR_EMAIL = "email";
47     public static final String HR_ATTR_ADDRESS = "address";
48     public static final String HR_ATTR_COUNTRY_REGION = "countryRegion";
49     public static final String HR_ATTR_DEVELOPER_RELATIONSHIP = "relationship";
50     public static final String HR_ATTR_WEBSITE = "website";
51     public static final String HR_ATTR_APP_DEVELOPER_REGISTRY_ID = "registryId";
52     public static final String HR_ATTR_DATA_CATEGORY = "dataCategory";
53     public static final String HR_ATTR_DATA_TYPE = "dataType";
54     public static final String HR_ATTR_IS_COLLECTION_OPTIONAL = "isCollectionOptional";
55     public static final String HR_ATTR_IS_SHARING_OPTIONAL = "isSharingOptional";
56     public static final String HR_ATTR_IS_DATA_DELETABLE = "isDataDeletable";
57     public static final String HR_ATTR_IS_DATA_ENCRYPTED = "isDataEncrypted";
58     // public static final String HR_ATTR_EPHEMERAL = "ephemeral";
59     public static final String HR_ATTR_PURPOSES = "purposes";
60     public static final String HR_ATTR_VERSION = "version";
61     public static final String HR_ATTR_URL = "url";
62     public static final String HR_ATTR_DECLARATION = "declaration";
63     public static final String HR_ATTR_TITLE = "title";
64     public static final String HR_ATTR_DESCRIPTION = "description";
65     public static final String HR_ATTR_CONTAINS_ADS = "containsAds";
66     public static final String HR_ATTR_OBEY_APS = "obeyAps";
67     public static final String HR_ATTR_ADS_FINGERPRINTING = "adsFingerprinting";
68     public static final String HR_ATTR_SECURITY_FINGERPRINTING = "securityFingerprinting";
69     public static final String HR_ATTR_PRIVACY_POLICY = "privacyPolicy";
70     public static final String HR_ATTR_SECURITY_ENDPOINTS = "securityEndpoints";
71     public static final String HR_ATTR_FIRST_PARTY_ENDPOINTS = "firstPartyEndpoints";
72     public static final String HR_ATTR_SERVICE_PROVIDER_ENDPOINTS = "serviceProviderEndpoints";
73     public static final String HR_ATTR_CATEGORY = "category";
74 
75     public static final String OD_TAG_BUNDLE = "bundle";
76     public static final String OD_TAG_PBUNDLE_AS_MAP = "pbundle_as_map";
77     public static final String OD_TAG_BOOLEAN = "boolean";
78     public static final String OD_TAG_LONG = "long";
79     public static final String OD_TAG_STRING = "string";
80     public static final String OD_TAG_INT_ARRAY = "int-array";
81     public static final String OD_TAG_STRING_ARRAY = "string-array";
82     public static final String OD_TAG_ITEM = "item";
83     public static final String OD_ATTR_NAME = "name";
84     public static final String OD_ATTR_VALUE = "value";
85     public static final String OD_ATTR_NUM = "num";
86     public static final String OD_NAME_SAFETY_LABELS = "safety_labels";
87     public static final String OD_NAME_TRANSPARENCY_INFO = "transparency_info";
88     public static final String OD_NAME_DEVELOPER_INFO = "developer_info";
89     public static final String OD_NAME_NAME = "name";
90     public static final String OD_NAME_EMAIL = "email";
91     public static final String OD_NAME_ADDRESS = "address";
92     public static final String OD_NAME_COUNTRY_REGION = "country_region";
93     public static final String OD_NAME_DEVELOPER_RELATIONSHIP = "relationship";
94     public static final String OD_NAME_WEBSITE = "website";
95     public static final String OD_NAME_APP_DEVELOPER_REGISTRY_ID = "app_developer_registry_id";
96     public static final String OD_NAME_APP_INFO = "app_info";
97     public static final String OD_NAME_TITLE = "title";
98     public static final String OD_NAME_DESCRIPTION = "description";
99     public static final String OD_NAME_CONTAINS_ADS = "contains_ads";
100     public static final String OD_NAME_OBEY_APS = "obey_aps";
101     public static final String OD_NAME_ADS_FINGERPRINTING = "ads_fingerprinting";
102     public static final String OD_NAME_SECURITY_FINGERPRINTING = "security_fingerprinting";
103     public static final String OD_NAME_PRIVACY_POLICY = "privacy_policy";
104     public static final String OD_NAME_SECURITY_ENDPOINT = "security_endpoint";
105     public static final String OD_NAME_FIRST_PARTY_ENDPOINT = "first_party_endpoint";
106     public static final String OD_NAME_SERVICE_PROVIDER_ENDPOINT = "service_provider_endpoint";
107     public static final String OD_NAME_CATEGORY = "category";
108     public static final String OD_NAME_VERSION = "version";
109     public static final String OD_NAME_URL = "url";
110     public static final String OD_NAME_DECLARATION = "declaration";
111     public static final String OD_NAME_SYSTEM_APP_SAFETY_LABEL = "system_app_safety_label";
112     public static final String OD_NAME_SECURITY_LABELS = "security_labels";
113     public static final String OD_NAME_THIRD_PARTY_VERIFICATION = "third_party_verification";
114     public static final String OD_NAME_DATA_LABELS = "data_labels";
115     public static final String OD_NAME_DATA_ACCESSED = "data_accessed";
116     public static final String OD_NAME_DATA_COLLECTED = "data_collected";
117     public static final String OD_NAME_DATA_SHARED = "data_shared";
118     public static final String OD_NAME_PURPOSES = "purposes";
119     public static final String OD_NAME_IS_COLLECTION_OPTIONAL = "is_collection_optional";
120     public static final String OD_NAME_IS_SHARING_OPTIONAL = "is_sharing_optional";
121     public static final String OD_NAME_IS_DATA_DELETABLE = "is_data_deletable";
122     public static final String OD_NAME_IS_DATA_ENCRYPTED = "is_data_encrypted";
123     public static final String OD_NAME_EPHEMERAL = "ephemeral";
124 
125     public static final String TRUE_STR = "true";
126     public static final String FALSE_STR = "false";
127 
128     /** Gets the top-level children with the tag name.. */
getChildrenByTagName(Node parentEle, String tagName)129     public static List<Element> getChildrenByTagName(Node parentEle, String tagName) {
130         var elements = XmlUtils.asElementList(parentEle.getChildNodes());
131         return elements.stream().filter(e -> e.getTagName().equals(tagName)).toList();
132     }
133 
134     /**
135      * Gets the single {@link Element} within {@param parentEle} and having the {@param tagName}.
136      */
getSingleChildElement(Node parentEle, String tagName, boolean required)137     public static Element getSingleChildElement(Node parentEle, String tagName, boolean required)
138             throws MalformedXmlException {
139         String parentTagNameForErrorMsg =
140                 (parentEle instanceof Element) ? ((Element) parentEle).getTagName() : "Node";
141         var elements = getChildrenByTagName(parentEle, tagName);
142 
143         if (elements.size() > 1) {
144             throw new MalformedXmlException(
145                     String.format(
146                             "Expected 1 %s in %s but got %s.",
147                             tagName, parentTagNameForErrorMsg, elements.size()));
148         } else if (elements.isEmpty()) {
149             if (required) {
150                 throw new MalformedXmlException(
151                         String.format(
152                                 "Expected 1 %s in %s but got 0.",
153                                 tagName, parentTagNameForErrorMsg));
154             } else {
155                 return null;
156             }
157         }
158         return elements.get(0);
159     }
160 
161     /** Gets the single {@link Element} within {@param elements}. */
getSingleElement(List<Element> elements)162     public static Element getSingleElement(List<Element> elements) {
163         if (elements.size() != 1) {
164             throw new IllegalStateException(
165                     String.format("Expected 1 element in list but got %s.", elements.size()));
166         }
167         return elements.get(0);
168     }
169 
170     /** Converts {@param nodeList} into List of {@link Element}. */
asElementList(NodeList nodeList)171     public static List<Element> asElementList(NodeList nodeList) {
172         List<Element> elementList = new ArrayList<Element>();
173         for (int i = 0; i < nodeList.getLength(); i++) {
174             var elementAsNode = nodeList.item(i);
175             if (elementAsNode instanceof Element) {
176                 elementList.add(((Element) elementAsNode));
177             }
178         }
179         return elementList;
180     }
181 
182     /** Appends {@param children} to the {@param ele}. */
appendChildren(Element ele, List<Element> children)183     public static void appendChildren(Element ele, List<Element> children) {
184         for (Element c : children) {
185             ele.appendChild(c);
186         }
187     }
188 
189     /** Gets the Boolean from the String value. */
fromString(String s)190     private static Boolean fromString(String s) {
191         if (s == null) {
192             return null;
193         }
194         if (s.equals(TRUE_STR)) {
195             return true;
196         } else if (s.equals(FALSE_STR)) {
197             return false;
198         }
199         return null;
200     }
201 
202     /** Creates an on-device PBundle DOM Element with the given attribute name. */
createPbundleEleWithName(Document doc, String name)203     public static Element createPbundleEleWithName(Document doc, String name) {
204         var ele = doc.createElement(XmlUtils.OD_TAG_PBUNDLE_AS_MAP);
205         ele.setAttribute(XmlUtils.OD_ATTR_NAME, name);
206         return ele;
207     }
208 
209     /** Create an on-device Boolean DOM Element with the given attribute name. */
createOdBooleanEle(Document doc, String name, boolean b)210     public static Element createOdBooleanEle(Document doc, String name, boolean b) {
211         var ele = doc.createElement(XmlUtils.OD_TAG_BOOLEAN);
212         ele.setAttribute(XmlUtils.OD_ATTR_NAME, name);
213         ele.setAttribute(XmlUtils.OD_ATTR_VALUE, String.valueOf(b));
214         return ele;
215     }
216 
217     /** Sets human-readable bool attribute if non-null. */
maybeSetHrBoolAttr(Element ele, String attrName, Boolean b)218     public static void maybeSetHrBoolAttr(Element ele, String attrName, Boolean b) {
219         if (b != null) {
220             ele.setAttribute(attrName, String.valueOf(b));
221         }
222     }
223 
224     /** Create an on-device Long DOM Element with the given attribute name. */
createOdLongEle(Document doc, String name, long l)225     public static Element createOdLongEle(Document doc, String name, long l) {
226         var ele = doc.createElement(XmlUtils.OD_TAG_LONG);
227         ele.setAttribute(XmlUtils.OD_ATTR_NAME, name);
228         ele.setAttribute(XmlUtils.OD_ATTR_VALUE, String.valueOf(l));
229         return ele;
230     }
231 
232     /** Create an on-device Long DOM Element with the given attribute name. */
createOdStringEle(Document doc, String name, String val)233     public static Element createOdStringEle(Document doc, String name, String val) {
234         var ele = doc.createElement(XmlUtils.OD_TAG_STRING);
235         ele.setAttribute(XmlUtils.OD_ATTR_NAME, name);
236         ele.setAttribute(XmlUtils.OD_ATTR_VALUE, val);
237         return ele;
238     }
239 
240     /** Create OD style array DOM Element, which can represent any time but is stored as Strings. */
createOdArray( Document doc, String arrayTag, String arrayName, List<String> arrayVals)241     public static Element createOdArray(
242             Document doc, String arrayTag, String arrayName, List<String> arrayVals) {
243         Element arrEle = doc.createElement(arrayTag);
244         arrEle.setAttribute(XmlUtils.OD_ATTR_NAME, arrayName);
245         arrEle.setAttribute(XmlUtils.OD_ATTR_NUM, String.valueOf(arrayVals.size()));
246         for (String s : arrayVals) {
247             Element itemEle = doc.createElement(XmlUtils.OD_TAG_ITEM);
248             itemEle.setAttribute(XmlUtils.OD_ATTR_VALUE, s);
249             arrEle.appendChild(itemEle);
250         }
251         return arrEle;
252     }
253 
254     /** Returns whether the String is null or empty. */
isNullOrEmpty(String s)255     public static boolean isNullOrEmpty(String s) {
256         return s == null || s.isEmpty();
257     }
258 
259     /** Tries getting required version attribute and throws exception if it doesn't exist */
tryGetVersion(Element ele)260     public static Long tryGetVersion(Element ele) throws MalformedXmlException {
261         long version;
262         try {
263             version = Long.parseLong(ele.getAttribute(XmlUtils.HR_ATTR_VERSION));
264         } catch (Exception e) {
265             throw new MalformedXmlException(
266                     String.format(
267                             "Malformed or missing required version in: %s", ele.getTagName()));
268         }
269         return version;
270     }
271 
272     /** Gets a pipeline-split attribute. */
getPipelineSplitAttr(Element ele, String attrName, boolean required)273     public static List<String> getPipelineSplitAttr(Element ele, String attrName, boolean required)
274             throws MalformedXmlException {
275         List<String> list = Arrays.stream(ele.getAttribute(attrName).split("\\|")).toList();
276         if ((list.isEmpty() || list.get(0).isEmpty()) && required) {
277             throw new MalformedXmlException(
278                     String.format(
279                             "Delimited string %s was required but missing, in %s.",
280                             attrName, ele.getTagName()));
281         }
282         return list;
283     }
284 
285     /** Gets a Boolean attribute. */
getBoolAttr(Element ele, String attrName, boolean required)286     public static Boolean getBoolAttr(Element ele, String attrName, boolean required)
287             throws MalformedXmlException {
288         Boolean b = XmlUtils.fromString(ele.getAttribute(attrName));
289         if (b == null && required) {
290             throw new MalformedXmlException(
291                     String.format(
292                             "Boolean %s was required but missing, in %s.",
293                             attrName, ele.getTagName()));
294         }
295         return b;
296     }
297 
298     /** Gets a Boolean attribute. */
getOdBoolEle(Element ele, String nameName, boolean required)299     public static Boolean getOdBoolEle(Element ele, String nameName, boolean required)
300             throws MalformedXmlException {
301         List<Element> boolEles =
302                 XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_BOOLEAN).stream()
303                         .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
304                         .toList();
305         if (boolEles.size() > 1) {
306             throw new MalformedXmlException(
307                     String.format(
308                             "Found more than one boolean %s in %s.", nameName, ele.getTagName()));
309         }
310         if (boolEles.isEmpty()) {
311             if (required) {
312                 throw new MalformedXmlException(
313                         String.format("Found no boolean %s in %s.", nameName, ele.getTagName()));
314             }
315             return null;
316         }
317         Element boolEle = boolEles.get(0);
318 
319         Boolean b = XmlUtils.fromString(boolEle.getAttribute(XmlUtils.OD_ATTR_VALUE));
320         if (b == null && required) {
321             throw new MalformedXmlException(
322                     String.format(
323                             "Boolean %s was required but missing, in %s.",
324                             nameName, ele.getTagName()));
325         }
326         return b;
327     }
328 
329     /** Gets an on-device Long attribute. */
getOdLongEle(Element ele, String nameName, boolean required)330     public static Long getOdLongEle(Element ele, String nameName, boolean required)
331             throws MalformedXmlException {
332         List<Element> longEles =
333                 XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_LONG).stream()
334                         .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
335                         .toList();
336         if (longEles.size() > 1) {
337             throw new MalformedXmlException(
338                     String.format(
339                             "Found more than one long %s in %s.", nameName, ele.getTagName()));
340         }
341         if (longEles.isEmpty()) {
342             if (required) {
343                 throw new MalformedXmlException(
344                         String.format("Found no long %s in %s.", nameName, ele.getTagName()));
345             }
346             return null;
347         }
348         Element longEle = longEles.get(0);
349         Long l = null;
350         try {
351             l = Long.parseLong(longEle.getAttribute(XmlUtils.OD_ATTR_VALUE));
352         } catch (NumberFormatException e) {
353             throw new MalformedXmlException(
354                     String.format(
355                             "%s in %s was not formatted as long", nameName, ele.getTagName()));
356         }
357         return l;
358     }
359 
360     /** Gets an on-device String attribute. */
getOdStringEle(Element ele, String nameName, boolean required)361     public static String getOdStringEle(Element ele, String nameName, boolean required)
362             throws MalformedXmlException {
363         List<Element> eles =
364                 XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_STRING).stream()
365                         .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
366                         .toList();
367         if (eles.size() > 1) {
368             throw new MalformedXmlException(
369                     String.format(
370                             "Found more than one string %s in %s.", nameName, ele.getTagName()));
371         }
372         if (eles.isEmpty()) {
373             if (required) {
374                 throw new MalformedXmlException(
375                         String.format("Found no string %s in %s.", nameName, ele.getTagName()));
376             }
377             return null;
378         }
379         String str = eles.get(0).getAttribute(XmlUtils.OD_ATTR_VALUE);
380         if (XmlUtils.isNullOrEmpty(str) && required) {
381             throw new MalformedXmlException(
382                     String.format(
383                             "%s in %s was empty or missing value", nameName, ele.getTagName()));
384         }
385         return str;
386     }
387 
388     /** Gets a OD Pbundle Element attribute with the specified name. */
getOdPbundleWithName(Element ele, String nameName, boolean required)389     public static Element getOdPbundleWithName(Element ele, String nameName, boolean required)
390             throws MalformedXmlException {
391         List<Element> eles =
392                 XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_PBUNDLE_AS_MAP).stream()
393                         .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
394                         .toList();
395         if (eles.size() > 1) {
396             throw new MalformedXmlException(
397                     String.format(
398                             "Found more than one pbundle %s in %s.", nameName, ele.getTagName()));
399         }
400         if (eles.isEmpty()) {
401             if (required) {
402                 throw new MalformedXmlException(
403                         String.format("Found no pbundle %s in %s.", nameName, ele.getTagName()));
404             }
405             return null;
406         }
407         return eles.get(0);
408     }
409 
410     /** Gets a required String attribute. */
getStringAttr(Element ele, String attrName)411     public static String getStringAttr(Element ele, String attrName) throws MalformedXmlException {
412         return getStringAttr(ele, attrName, true);
413     }
414 
415     /** Gets a String attribute; throws exception if required and non-existent. */
getStringAttr(Element ele, String attrName, boolean required)416     public static String getStringAttr(Element ele, String attrName, boolean required)
417             throws MalformedXmlException {
418         String s = ele.getAttribute(attrName);
419         if (isNullOrEmpty(s)) {
420             if (required) {
421                 throw new MalformedXmlException(
422                         String.format(
423                                 "Malformed or missing required %s in: %s",
424                                 attrName, ele.getTagName()));
425             } else {
426                 return null;
427             }
428         }
429         return s;
430     }
431 
432     /** Gets on-device style int array. */
getOdIntArray(Element ele, String nameName, boolean required)433     public static List<Integer> getOdIntArray(Element ele, String nameName, boolean required)
434             throws MalformedXmlException {
435         List<Element> intArrayEles =
436                 XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_INT_ARRAY).stream()
437                         .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
438                         .toList();
439         if (intArrayEles.size() > 1) {
440             throw new MalformedXmlException(
441                     String.format("Found more than one %s in %s.", nameName, ele.getTagName()));
442         }
443         if (intArrayEles.isEmpty()) {
444             if (required) {
445                 throw new MalformedXmlException(
446                         String.format("Found no %s in %s.", nameName, ele.getTagName()));
447             }
448             return null;
449         }
450         Element intArrayEle = intArrayEles.get(0);
451         List<Element> itemEles = XmlUtils.getChildrenByTagName(intArrayEle, XmlUtils.OD_TAG_ITEM);
452         List<Integer> ints = new ArrayList<Integer>();
453         for (Element itemEle : itemEles) {
454             ints.add(Integer.parseInt(XmlUtils.getStringAttr(itemEle, XmlUtils.OD_ATTR_VALUE)));
455         }
456         return ints;
457     }
458 
459     /** Gets on-device style String array. */
getOdStringArray(Element ele, String nameName, boolean required)460     public static List<String> getOdStringArray(Element ele, String nameName, boolean required)
461             throws MalformedXmlException {
462         List<Element> arrayEles =
463                 XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_STRING_ARRAY).stream()
464                         .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
465                         .toList();
466         if (arrayEles.size() > 1) {
467             throw new MalformedXmlException(
468                     String.format(
469                             "Found more than one string array %s in %s.",
470                             nameName, ele.getTagName()));
471         }
472         if (arrayEles.isEmpty()) {
473             if (required) {
474                 throw new MalformedXmlException(
475                         String.format(
476                                 "Found no string array %s in %s.", nameName, ele.getTagName()));
477             }
478             return null;
479         }
480         Element arrayEle = arrayEles.get(0);
481         List<Element> itemEles = XmlUtils.getChildrenByTagName(arrayEle, XmlUtils.OD_TAG_ITEM);
482         List<String> strs = new ArrayList<String>();
483         for (Element itemEle : itemEles) {
484             strs.add(XmlUtils.getStringAttr(itemEle, XmlUtils.OD_ATTR_VALUE, true));
485         }
486         return strs;
487     }
488 
489     /**
490      * Utility method for making a List from one element, to support easier refactoring if needed.
491      * For example, List.of() doesn't support null elements.
492      */
listOf(Element e)493     public static List<Element> listOf(Element e) {
494         return Arrays.asList(e);
495     }
496 }
497