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