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 package com.android.server.appsearch.appsindexer; 17 18 import android.annotation.NonNull; 19 import android.app.appsearch.AppSearchSchema; 20 import android.app.appsearch.AppSearchSchema.BooleanPropertyConfig; 21 import android.app.appsearch.AppSearchSchema.LongPropertyConfig; 22 import android.app.appsearch.AppSearchSchema.PropertyConfig; 23 import android.app.appsearch.AppSearchSchema.StringPropertyConfig; 24 import android.content.pm.PackageManager; 25 import android.content.res.AssetManager; 26 import android.util.ArrayMap; 27 import android.util.ArraySet; 28 import android.util.Log; 29 30 import com.android.server.appsearch.appsindexer.appsearchtypes.AppFunctionDocument; 31 import com.android.server.appsearch.appsindexer.appsearchtypes.AppFunctionStaticMetadata; 32 33 import org.xmlpull.v1.XmlPullParser; 34 import org.xmlpull.v1.XmlPullParserException; 35 import org.xmlpull.v1.XmlPullParserFactory; 36 37 import java.io.IOException; 38 import java.io.InputStream; 39 import java.util.Collections; 40 import java.util.Map; 41 import java.util.Objects; 42 import java.util.Set; 43 44 /** 45 * This class parses the XSD file from an app's assets and creates AppSearch schemas from document 46 * types. 47 * 48 * <p>The generated {@link AppSearchSchema} objects are used to set the schema under the {@link 49 * AppSearchHelper#APP_DATABASE} database. Within the database, each {@link AppSearchSchema} is 50 * named dynamically to be unique to the app package name. 51 * 52 * <p>Note: The XSD file should be generated by the App Functions SDK and always define 53 * AppFunctionStaticMetadata document type which will have {@link 54 * AppFunctionStaticMetadata#PARENT_TYPE_APPSEARCH_SCHEMA} as parent type. 55 */ 56 public class AppFunctionSchemaParser { 57 private static final String TAG = "AppSearchSchemaParser"; 58 private static final String XML_TAG_DOCUMENT_TYPE = "xs:documentType"; 59 private static final String XML_TAG_ELEMENT = "xs:element"; 60 private static final String XML_APPFN_NAMESPACE_PREFIX = "appfn:"; 61 private static final String XML_TAG_STRING_TYPE = "xs:string"; 62 private static final String XML_TAG_LONG_TYPE = "xs:long"; 63 private static final String XML_TAG_INT_TYPE = "xs:int"; 64 private static final String XML_TAG_BOOLEAN_TYPE = "xs:boolean"; 65 private static final String XML_ATTRIBUTE_INDEXING_TYPE = "indexingType"; 66 private static final String XML_ATTRIBUTE_TOKENIZER_TYPE = "tokenizerType"; 67 private static final String XML_ATTRIBUTE_JOINABLE_VALUE_TYPE = "joinableValueType"; 68 private static final String XML_ATTRIBUTE_CARDINALITY = "cardinality"; 69 private static final String XML_ATTRIBUTE_SHOULD_INDEX_NESTED_PROPERTIES = 70 "shouldIndexNestedProperties"; 71 private static final String XML_ATTRIBUTE_NAME = "name"; 72 private static final String XML_ATTRIBUTE_TYPE = "type"; 73 74 /** 75 * The maximum number of document types allowed in the XSD file. This is to prevent malicious 76 * apps from creating too many schema types in AppSearch by modifying the XSD file defined in 77 * App Functions SDK. 78 */ 79 private final int mMaxAllowedDocumentType; 80 81 /** 82 * @param maxAllowedDocumentType The maximum number of document types allowed in the XSD file. 83 * This is to prevent malicious apps from creating too many schema types in AppSearch by 84 * modifying the XSD file defined in App Functions SDK. 85 */ AppFunctionSchemaParser(int maxAllowedDocumentType)86 public AppFunctionSchemaParser(int maxAllowedDocumentType) { 87 mMaxAllowedDocumentType = maxAllowedDocumentType; 88 } 89 getAttributeBoolOrDefault( @onNull XmlPullParser parser, @NonNull String attributeName, boolean defaultValue)90 private static boolean getAttributeBoolOrDefault( 91 @NonNull XmlPullParser parser, @NonNull String attributeName, boolean defaultValue) { 92 Objects.requireNonNull(parser); 93 Objects.requireNonNull(attributeName); 94 95 String value = parser.getAttributeValue(/* namespace= */ null, attributeName); 96 return value == null ? defaultValue : Boolean.parseBoolean(value); 97 } 98 getAttributeIntOrDefault( @onNull XmlPullParser parser, @NonNull String attributeName, int defaultValue)99 private static int getAttributeIntOrDefault( 100 @NonNull XmlPullParser parser, @NonNull String attributeName, int defaultValue) { 101 Objects.requireNonNull(parser); 102 Objects.requireNonNull(attributeName); 103 104 String value = parser.getAttributeValue(/* namespace= */ null, attributeName); 105 return value == null ? defaultValue : Integer.parseInt(value); 106 } 107 108 /** 109 * Parses the XSD and create AppSearch schemas from document types. 110 * 111 * <p>The schema output isn't guaranteed to have valid dependencies, which can be caught during 112 * a {@link SyncAppSearchSession#setSchema} call. 113 * 114 * @param packageManager The PackageManager used to access app resources. 115 * @param packageName The package name of the app whose assets contain the XSD file. 116 * @param assetFilePath The path to the XSD file within the app's assets. 117 * @return A mapping of schema types to their corresponding {@link AppSearchSchema} objects, or 118 * an empty map if there's an error during parsing or if the AppFunctionStaticMetadata 119 * document type is not found. 120 */ 121 @NonNull parseAndCreateSchemas( @onNull PackageManager packageManager, @NonNull String packageName, @NonNull String assetFilePath)122 public Map<String, AppSearchSchema> parseAndCreateSchemas( 123 @NonNull PackageManager packageManager, 124 @NonNull String packageName, 125 @NonNull String assetFilePath) { 126 Objects.requireNonNull(packageManager); 127 Objects.requireNonNull(packageName); 128 Objects.requireNonNull(assetFilePath); 129 130 // Keep track of all nested schema types to validate root schema later. 131 Set<String> nestedSchemaTypes = new ArraySet<>(); 132 133 try { 134 AssetManager assetManager = 135 packageManager.getResourcesForApplication(packageName).getAssets(); 136 InputStream xsdInputStream = assetManager.open(assetFilePath); 137 Map<String, AppSearchSchema> schemas = 138 parseDocumentTypeAndCreateSchemas( 139 packageName, xsdInputStream, nestedSchemaTypes); 140 return getValidatedSchemas(schemas, packageName, nestedSchemaTypes); 141 } catch (Exception ex) { 142 // The code parses an XSD file from another app's assets, using a broad try-catch to 143 // handle potential errors since the XML structure might be unpredictable. 144 Log.e( 145 TAG, 146 String.format( 147 "Failed to parse XSD from package '%s', asset file '%s'", 148 packageName, assetFilePath), 149 ex); 150 } 151 return Collections.emptyMap(); 152 } 153 154 /** 155 * Parses the XSD and create AppSearch schemas from document types. 156 * 157 * @param packageName The package for which schemas are being parsed. 158 * @param xsdInputStream The input stream to the XSD file within the app's assets. 159 * @param nestedSchemaTypes A set to maintain all the nested schema types encountered when 160 * processing a root tag. This will be later used to validate that correct schema 161 * definitions exist for each nested type. 162 * @return A mapping of schema types to their corresponding {@link AppSearchSchema} objects, or 163 * an empty map if there's an error during parsing or if the AppFunctionStaticMetadata 164 * document type is not found. 165 */ parseDocumentTypeAndCreateSchemas( @onNull String packageName, @NonNull InputStream xsdInputStream, @NonNull Set<String> nestedSchemaTypes)166 private Map<String, AppSearchSchema> parseDocumentTypeAndCreateSchemas( 167 @NonNull String packageName, 168 @NonNull InputStream xsdInputStream, 169 @NonNull Set<String> nestedSchemaTypes) 170 throws XmlPullParserException, IOException, InvalidAppFunctionSchemaException { 171 Objects.requireNonNull(packageName); 172 Objects.requireNonNull(xsdInputStream); 173 Objects.requireNonNull(nestedSchemaTypes); 174 175 Map<String, AppSearchSchema> schemas = new ArrayMap<>(); 176 XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); 177 XmlPullParser parser = factory.newPullParser(); 178 parser.setInput(xsdInputStream, null); 179 180 AppSearchSchema.Builder schemaBuilder = null; 181 182 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { 183 switch (parser.getEventType()) { 184 case XmlPullParser.START_TAG: 185 if (XML_TAG_DOCUMENT_TYPE.equals(parser.getName())) { 186 if (schemas.size() >= mMaxAllowedDocumentType) { 187 throw new IllegalStateException( 188 "Exceeded max allowed document types: " 189 + mMaxAllowedDocumentType); 190 } 191 192 String documentTypeName = 193 parser.getAttributeValue(null, XML_ATTRIBUTE_NAME); 194 if (documentTypeName != null) { 195 schemaBuilder = 196 new AppSearchSchema.Builder( 197 AppFunctionDocument.getSchemaNameForPackage( 198 packageName, documentTypeName)); 199 200 // All AppFunctionStaticMetadata schemas defined in packages will 201 // inherit from AppFunctionStaticMetadata#PARENT_TYPE_APPSEARCH_SCHEMA. 202 if (documentTypeName.equals(AppFunctionStaticMetadata.SCHEMA_TYPE) 203 && AppFunctionStaticMetadata.shouldSetParentType()) { 204 schemaBuilder.addParentType(AppFunctionStaticMetadata.SCHEMA_TYPE); 205 } 206 } 207 } else if (XML_TAG_ELEMENT.equals(parser.getName()) && schemaBuilder != null) { 208 AppSearchSchema.PropertyConfig propertyConfig = 209 computePropertyConfigFromXsdType( 210 parser, packageName, nestedSchemaTypes); 211 if (propertyConfig != null) schemaBuilder.addProperty(propertyConfig); 212 } 213 break; 214 215 case XmlPullParser.END_TAG: 216 if (XML_TAG_DOCUMENT_TYPE.equals(parser.getName())) { 217 if (schemaBuilder != null) { 218 AppSearchSchema schema = schemaBuilder.build(); 219 schemas.put(schema.getSchemaType(), schema); 220 schemaBuilder = null; 221 } 222 } 223 break; 224 } 225 parser.next(); 226 } 227 228 return schemas; 229 } 230 computePropertyConfigFromXsdType( @onNull XmlPullParser parser, @NonNull String packageName, @NonNull Set<String> nestedSchemaTypes)231 private PropertyConfig computePropertyConfigFromXsdType( 232 @NonNull XmlPullParser parser, 233 @NonNull String packageName, 234 @NonNull Set<String> nestedSchemaTypes) 235 throws InvalidAppFunctionSchemaException { 236 Objects.requireNonNull(parser); 237 Objects.requireNonNull(packageName); 238 Objects.requireNonNull(nestedSchemaTypes); 239 240 String name = parser.getAttributeValue(null, XML_ATTRIBUTE_NAME); 241 validatePropertyName(name); 242 String type = parser.getAttributeValue(null, XML_ATTRIBUTE_TYPE); 243 244 if (name == null || type == null) return null; 245 246 int cardinality = 247 getAttributeIntOrDefault( 248 parser, XML_ATTRIBUTE_CARDINALITY, PropertyConfig.CARDINALITY_OPTIONAL); 249 250 switch (type) { 251 case XML_TAG_STRING_TYPE: 252 return new StringPropertyConfig.Builder(name) 253 .setCardinality(cardinality) 254 .setIndexingType( 255 getAttributeIntOrDefault( 256 parser, 257 XML_ATTRIBUTE_INDEXING_TYPE, 258 StringPropertyConfig.INDEXING_TYPE_NONE)) 259 .setTokenizerType( 260 getAttributeIntOrDefault( 261 parser, 262 XML_ATTRIBUTE_TOKENIZER_TYPE, 263 StringPropertyConfig.TOKENIZER_TYPE_NONE)) 264 .setJoinableValueType( 265 getAttributeIntOrDefault( 266 parser, 267 XML_ATTRIBUTE_JOINABLE_VALUE_TYPE, 268 StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)) 269 .build(); 270 case XML_TAG_LONG_TYPE: 271 case XML_TAG_INT_TYPE: 272 return new LongPropertyConfig.Builder(name) 273 .setCardinality(cardinality) 274 .setIndexingType( 275 getAttributeIntOrDefault( 276 parser, 277 XML_ATTRIBUTE_INDEXING_TYPE, 278 LongPropertyConfig.INDEXING_TYPE_NONE)) 279 .build(); 280 case XML_TAG_BOOLEAN_TYPE: 281 return new BooleanPropertyConfig.Builder(name).setCardinality(cardinality).build(); 282 default: 283 if (type.contains(XML_APPFN_NAMESPACE_PREFIX)) { 284 String localType = type.substring(type.indexOf(':') + 1); 285 String schemaType = 286 AppFunctionDocument.getSchemaNameForPackage(packageName, localType); 287 nestedSchemaTypes.add(schemaType); 288 return new AppSearchSchema.DocumentPropertyConfig.Builder(name, schemaType) 289 .setCardinality(cardinality) 290 .setShouldIndexNestedProperties( 291 getAttributeBoolOrDefault( 292 parser, 293 XML_ATTRIBUTE_SHOULD_INDEX_NESTED_PROPERTIES, 294 false)) 295 .build(); 296 } 297 throw new IllegalArgumentException("Unsupported type: " + type); 298 } 299 } 300 301 /** 302 * Validates the given property name to ensure it is not null, empty, and contains only 303 * alphanumeric characters (letters and digits). 304 * 305 * @param name the property name to validate 306 * @throws InvalidAppFunctionSchemaException if the name is empty, or contains non-alphanumeric 307 * characters 308 */ validatePropertyName(@onNull String name)309 private static void validatePropertyName(@NonNull String name) 310 throws InvalidAppFunctionSchemaException { 311 Objects.requireNonNull(name); 312 if (name.isEmpty()) { 313 throw new InvalidAppFunctionSchemaException( 314 "Property name in the schema cannot be null or empty."); 315 } 316 for (int i = 0; i < name.length(); i++) { 317 char c = name.charAt(i); 318 if (!Character.isLetterOrDigit(c)) { 319 throw new InvalidAppFunctionSchemaException( 320 "Property name must contain only alphanumeric characters: " + name); 321 } 322 } 323 } 324 325 /** 326 * Validates and returns the provided schema map for a given package. 327 * 328 * <p>This method ensures that the required schemas and properties are present in the schema 329 * map. Specifically: 330 * 331 * <ul> 332 * <li>It verifies that the schema for {@link AppFunctionStaticMetadata} exists for the given 333 * package and checks for the presence of required properties from {@link 334 * AppFunctionStaticMetadata#PARENT_TYPE_APPSEARCH_SCHEMA}. 335 * <li>It checks for any missing schema definitions for nested schema types. 336 * </ul> 337 * 338 * @param schemaMap A map where the key is the schema type name, and the value is the 339 * corresponding {@link AppSearchSchema}. 340 * @param packageName The name of the package for which the schemas are being validated. 341 * @param nestedSchemaTypes A set of all the nested schema types used to validate that correct 342 * schema definitions exist for each nested type. 343 * @return The validated schema map. 344 * @throws InvalidAppFunctionSchemaException If any required schema or properties are missing. 345 */ getValidatedSchemas( @onNull Map<String, AppSearchSchema> schemaMap, @NonNull String packageName, @NonNull Set<String> nestedSchemaTypes)346 private Map<String, AppSearchSchema> getValidatedSchemas( 347 @NonNull Map<String, AppSearchSchema> schemaMap, 348 @NonNull String packageName, 349 @NonNull Set<String> nestedSchemaTypes) 350 throws InvalidAppFunctionSchemaException { 351 Objects.requireNonNull(schemaMap); 352 Objects.requireNonNull(packageName); 353 Objects.requireNonNull(nestedSchemaTypes); 354 355 String appFunctionStaticMetadataSchemaType = 356 AppFunctionDocument.getSchemaNameForPackage( 357 packageName, AppFunctionStaticMetadata.SCHEMA_TYPE); 358 if (!schemaMap.containsKey(appFunctionStaticMetadataSchemaType)) { 359 throw new InvalidAppFunctionSchemaException( 360 "Missing schema definition for AppFunctionStaticMetadata in package: " 361 + packageName); 362 } 363 364 Set<String> undefinedSchemas = new ArraySet<>(nestedSchemaTypes); 365 undefinedSchemas.removeAll(schemaMap.keySet()); 366 if (!undefinedSchemas.isEmpty()) { 367 throw new InvalidAppFunctionSchemaException( 368 "In package: " 369 + packageName 370 + ", missing schema definitions for following nested schema types: " 371 + undefinedSchemas); 372 } 373 374 return schemaMap; 375 } 376 377 /** Exception thrown when the parsed schema fails any of the validations. */ 378 private static class InvalidAppFunctionSchemaException extends Exception { InvalidAppFunctionSchemaException(String message)379 InvalidAppFunctionSchemaException(String message) { 380 super(message); 381 } 382 } 383 } 384