1 /*
2  * Copyright 2021 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.server.appsearch.external.localstorage.util;
18 
19 import android.annotation.NonNull;
20 import android.app.appsearch.AppSearchResult;
21 import android.app.appsearch.exceptions.AppSearchException;
22 import android.util.Log;
23 
24 import com.android.internal.annotations.VisibleForTesting;
25 
26 import com.google.android.icing.proto.DocumentProto;
27 import com.google.android.icing.proto.PropertyConfigProto;
28 import com.google.android.icing.proto.PropertyProto;
29 import com.google.android.icing.proto.SchemaTypeConfigProto;
30 
31 /**
32  * Provides utility functions for working with package + database prefixes.
33  *
34  * @hide
35  */
36 public class PrefixUtil {
37     private static final String TAG = "AppSearchPrefixUtil";
38 
39     @VisibleForTesting public static final char DATABASE_DELIMITER = '/';
40 
41     @VisibleForTesting public static final char PACKAGE_DELIMITER = '$';
42 
PrefixUtil()43     private PrefixUtil() {}
44 
45     /** Creates prefix string for given package name and database name. */
46     @NonNull
createPrefix(@onNull String packageName, @NonNull String databaseName)47     public static String createPrefix(@NonNull String packageName, @NonNull String databaseName) {
48         return packageName + PACKAGE_DELIMITER + databaseName + DATABASE_DELIMITER;
49     }
50 
51     /** Creates prefix string for given package name. */
52     @NonNull
createPackagePrefix(@onNull String packageName)53     public static String createPackagePrefix(@NonNull String packageName) {
54         return packageName + PACKAGE_DELIMITER;
55     }
56 
57     /**
58      * Returns the package name that's contained within the {@code prefix}.
59      *
60      * @param prefix Prefix string that contains the package name inside of it. The package name
61      *     must be in the front of the string, and separated from the rest of the string by the
62      *     {@link #PACKAGE_DELIMITER}.
63      * @return Valid package name.
64      */
65     @NonNull
getPackageName(@onNull String prefix)66     public static String getPackageName(@NonNull String prefix) {
67         int delimiterIndex = prefix.indexOf(PACKAGE_DELIMITER);
68         if (delimiterIndex == -1) {
69             // This should never happen if we construct our prefixes properly
70             Log.e(TAG, "Malformed prefix doesn't contain package delimiter: " + prefix);
71             return "";
72         }
73         return prefix.substring(0, delimiterIndex);
74     }
75 
76     /**
77      * Returns the database name that's contained within the {@code prefix}.
78      *
79      * @param prefix Prefix string that contains the database name inside of it. The database name
80      *     must be between the {@link #PACKAGE_DELIMITER} and {@link #DATABASE_DELIMITER}
81      * @return Valid database name.
82      */
83     @NonNull
getDatabaseName(@onNull String prefix)84     public static String getDatabaseName(@NonNull String prefix) {
85         int packageDelimiterIndex = prefix.indexOf(PACKAGE_DELIMITER);
86         if (packageDelimiterIndex == -1) {
87             // This should never happen if we construct our prefixes properly
88             Log.e(TAG, "Malformed prefix doesn't contain package delimiter: " + prefix);
89             return "";
90         }
91         int databaseDelimiterIndex = prefix.indexOf(DATABASE_DELIMITER, packageDelimiterIndex + 1);
92         if (databaseDelimiterIndex == -1) {
93             // This should never happen if we construct our prefixes properly
94             Log.e(TAG, "Malformed prefix doesn't contain database delimiter: " + prefix);
95             return "";
96         }
97         return prefix.substring(packageDelimiterIndex + 1, databaseDelimiterIndex);
98     }
99 
100     /**
101      * Creates a string with the package and database prefix removed from the input string.
102      *
103      * @param prefixedString a string containing a package and database prefix.
104      * @return a string with the package and database prefix removed.
105      * @throws AppSearchException if the prefixed value does not contain a valid database name.
106      */
107     @NonNull
removePrefix(@onNull String prefixedString)108     public static String removePrefix(@NonNull String prefixedString) throws AppSearchException {
109         // The prefix is made up of the package, then the database. So we only need to find the
110         // database cutoff.
111         int delimiterIndex = prefixedString.indexOf(DATABASE_DELIMITER);
112         if (delimiterIndex == -1) {
113             throw new AppSearchException(
114                     AppSearchResult.RESULT_INTERNAL_ERROR,
115                     "The prefixed value \""
116                             + prefixedString
117                             + "\" doesn't contain a valid "
118                             + "database name");
119         }
120         // Add 1 to include the char size of the DATABASE_DELIMITER
121         return prefixedString.substring(delimiterIndex + 1);
122     }
123 
124     /**
125      * Creates a package and database prefix string from the input string.
126      *
127      * @param prefixedString a string containing a package and database prefix.
128      * @return a string with the package and database prefix
129      * @throws AppSearchException if the prefixed value does not contain a valid database name.
130      */
131     @NonNull
getPrefix(@onNull String prefixedString)132     public static String getPrefix(@NonNull String prefixedString) throws AppSearchException {
133         int delimiterIndex = prefixedString.indexOf(DATABASE_DELIMITER);
134         if (delimiterIndex == -1) {
135             throw new AppSearchException(
136                     AppSearchResult.RESULT_INTERNAL_ERROR,
137                     "The prefixed value \""
138                             + prefixedString
139                             + "\" doesn't contain a valid "
140                             + "database name");
141         }
142         // Add 1 to include the char size of the DATABASE_DELIMITER
143         return prefixedString.substring(0, delimiterIndex + 1);
144     }
145 
146     /**
147      * Prepends {@code prefix} to all types and namespaces mentioned anywhere in {@code
148      * documentBuilder}.
149      *
150      * @param documentBuilder The document to mutate
151      * @param prefix The prefix to add
152      */
addPrefixToDocument( @onNull DocumentProto.Builder documentBuilder, @NonNull String prefix)153     public static void addPrefixToDocument(
154             @NonNull DocumentProto.Builder documentBuilder, @NonNull String prefix) {
155         // Rewrite the type name to include/remove the prefix.
156         String newSchema = prefix + documentBuilder.getSchema();
157         documentBuilder.setSchema(newSchema);
158 
159         // Rewrite the namespace to include/remove the prefix.
160         documentBuilder.setNamespace(prefix + documentBuilder.getNamespace());
161 
162         // Recurse into derived documents
163         for (int propertyIdx = 0;
164                 propertyIdx < documentBuilder.getPropertiesCount();
165                 propertyIdx++) {
166             int documentCount = documentBuilder.getProperties(propertyIdx).getDocumentValuesCount();
167             if (documentCount > 0) {
168                 PropertyProto.Builder propertyBuilder =
169                         documentBuilder.getProperties(propertyIdx).toBuilder();
170                 for (int documentIdx = 0; documentIdx < documentCount; documentIdx++) {
171                     DocumentProto.Builder derivedDocumentBuilder =
172                             propertyBuilder.getDocumentValues(documentIdx).toBuilder();
173                     addPrefixToDocument(derivedDocumentBuilder, prefix);
174                     propertyBuilder.setDocumentValues(documentIdx, derivedDocumentBuilder);
175                 }
176                 documentBuilder.setProperties(propertyIdx, propertyBuilder);
177             }
178         }
179     }
180 
181     /**
182      * Removes any prefixes from types and namespaces mentioned anywhere in {@code documentBuilder}.
183      *
184      * @param documentBuilder The document to mutate
185      * @return Prefix name that was removed from the document.
186      * @throws AppSearchException if there are unexpected database prefixing errors.
187      */
188     @NonNull
removePrefixesFromDocument(@onNull DocumentProto.Builder documentBuilder)189     public static String removePrefixesFromDocument(@NonNull DocumentProto.Builder documentBuilder)
190             throws AppSearchException {
191         // Rewrite the type name and namespace to remove the prefix.
192         String schemaPrefix = getPrefix(documentBuilder.getSchema());
193         String namespacePrefix = getPrefix(documentBuilder.getNamespace());
194 
195         if (!schemaPrefix.equals(namespacePrefix)) {
196             throw new AppSearchException(
197                     AppSearchResult.RESULT_INTERNAL_ERROR,
198                     "Found unexpected"
199                             + " multiple prefix names in document: "
200                             + schemaPrefix
201                             + ", "
202                             + namespacePrefix);
203         }
204 
205         documentBuilder.setSchema(removePrefix(documentBuilder.getSchema()));
206         documentBuilder.setNamespace(removePrefix(documentBuilder.getNamespace()));
207 
208         // Recurse into derived documents
209         for (int propertyIdx = 0;
210                 propertyIdx < documentBuilder.getPropertiesCount();
211                 propertyIdx++) {
212             int documentCount = documentBuilder.getProperties(propertyIdx).getDocumentValuesCount();
213             if (documentCount > 0) {
214                 PropertyProto.Builder propertyBuilder =
215                         documentBuilder.getProperties(propertyIdx).toBuilder();
216                 for (int documentIdx = 0; documentIdx < documentCount; documentIdx++) {
217                     DocumentProto.Builder derivedDocumentBuilder =
218                             propertyBuilder.getDocumentValues(documentIdx).toBuilder();
219                     String nestedPrefix = removePrefixesFromDocument(derivedDocumentBuilder);
220                     if (!nestedPrefix.equals(schemaPrefix)) {
221                         throw new AppSearchException(
222                                 AppSearchResult.RESULT_INTERNAL_ERROR,
223                                 "Found unexpected multiple prefix names in document: "
224                                         + schemaPrefix
225                                         + ", "
226                                         + nestedPrefix);
227                     }
228                     propertyBuilder.setDocumentValues(documentIdx, derivedDocumentBuilder);
229                 }
230                 documentBuilder.setProperties(propertyIdx, propertyBuilder);
231             }
232         }
233 
234         return schemaPrefix;
235     }
236 
237     /**
238      * Removes any prefixes from types mentioned anywhere in {@code typeConfigBuilder}.
239      *
240      * @param typeConfigBuilder The schema type to mutate
241      * @return Prefix name that was removed from the schema type.
242      * @throws AppSearchException if there are unexpected database prefixing errors.
243      */
244     @NonNull
removePrefixesFromSchemaType( @onNull SchemaTypeConfigProto.Builder typeConfigBuilder)245     public static String removePrefixesFromSchemaType(
246             @NonNull SchemaTypeConfigProto.Builder typeConfigBuilder) throws AppSearchException {
247         String typePrefix = PrefixUtil.getPrefix(typeConfigBuilder.getSchemaType());
248         // Rewrite SchemaProto.types.schema_type
249         String newSchemaType = typeConfigBuilder.getSchemaType().substring(typePrefix.length());
250         typeConfigBuilder.setSchemaType(newSchemaType);
251 
252         // Rewrite SchemaProto.types.properties.schema_type
253         for (int propertyIdx = 0;
254                 propertyIdx < typeConfigBuilder.getPropertiesCount();
255                 propertyIdx++) {
256             if (!typeConfigBuilder.getProperties(propertyIdx).getSchemaType().isEmpty()) {
257                 PropertyConfigProto.Builder propertyConfigBuilder =
258                         typeConfigBuilder.getProperties(propertyIdx).toBuilder();
259                 String newPropertySchemaType =
260                         propertyConfigBuilder.getSchemaType().substring(typePrefix.length());
261                 propertyConfigBuilder.setSchemaType(newPropertySchemaType);
262                 typeConfigBuilder.setProperties(propertyIdx, propertyConfigBuilder);
263             }
264         }
265 
266         // Rewrite SchemaProto.types.parent_types
267         for (int parentTypeIdx = 0;
268                 parentTypeIdx < typeConfigBuilder.getParentTypesCount();
269                 parentTypeIdx++) {
270             String newParentType =
271                     typeConfigBuilder.getParentTypes(parentTypeIdx).substring(typePrefix.length());
272             typeConfigBuilder.setParentTypes(parentTypeIdx, newParentType);
273         }
274         return typePrefix;
275     }
276 }
277