• 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 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