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