• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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         // TODO (b/184050178) Start database delimiter index search from after package delimiter
86         int packageDelimiterIndex = prefix.indexOf(PACKAGE_DELIMITER);
87         int databaseDelimiterIndex = prefix.indexOf(DATABASE_DELIMITER);
88         if (packageDelimiterIndex == -1) {
89             // This should never happen if we construct our prefixes properly
90             Log.e(TAG, "Malformed prefix doesn't contain package delimiter: " + prefix);
91             return "";
92         }
93         if (databaseDelimiterIndex == -1) {
94             // This should never happen if we construct our prefixes properly
95             Log.e(TAG, "Malformed prefix doesn't contain database delimiter: " + prefix);
96             return "";
97         }
98         return prefix.substring(packageDelimiterIndex + 1, databaseDelimiterIndex);
99     }
100 
101     /**
102      * Creates a string with the package and database prefix removed from the input string.
103      *
104      * @param prefixedString a string containing a package and database prefix.
105      * @return a string with the package and database prefix removed.
106      * @throws AppSearchException if the prefixed value does not contain a valid database name.
107      */
108     @NonNull
removePrefix(@onNull String prefixedString)109     public static String removePrefix(@NonNull String prefixedString) throws AppSearchException {
110         // The prefix is made up of the package, then the database. So we only need to find the
111         // database cutoff.
112         int delimiterIndex;
113         if ((delimiterIndex = prefixedString.indexOf(DATABASE_DELIMITER)) != -1) {
114             // Add 1 to include the char size of the DATABASE_DELIMITER
115             return prefixedString.substring(delimiterIndex + 1);
116         }
117         throw new AppSearchException(
118                 AppSearchResult.RESULT_INTERNAL_ERROR,
119                 "The prefixed value \""
120                         + prefixedString
121                         + "\" doesn't contain a valid "
122                         + "database name");
123     }
124 
125     /**
126      * Creates a package and database prefix string from the input string.
127      *
128      * @param prefixedString a string containing a package and database prefix.
129      * @return a string with the package and database prefix
130      * @throws AppSearchException if the prefixed value does not contain a valid database name.
131      */
132     @NonNull
getPrefix(@onNull String prefixedString)133     public static String getPrefix(@NonNull String prefixedString) throws AppSearchException {
134         int databaseDelimiterIndex = prefixedString.indexOf(DATABASE_DELIMITER);
135         if (databaseDelimiterIndex == -1) {
136             throw new AppSearchException(
137                     AppSearchResult.RESULT_INTERNAL_ERROR,
138                     "The prefixed value \""
139                             + prefixedString
140                             + "\" doesn't contain a valid "
141                             + "database name");
142         }
143 
144         // Add 1 to include the char size of the DATABASE_DELIMITER
145         return prefixedString.substring(0, databaseDelimiterIndex + 1);
146     }
147 
148     /**
149      * Prepends {@code prefix} to all types and namespaces mentioned anywhere in {@code
150      * documentBuilder}.
151      *
152      * @param documentBuilder The document to mutate
153      * @param prefix The prefix to add
154      */
addPrefixToDocument( @onNull DocumentProto.Builder documentBuilder, @NonNull String prefix)155     public static void addPrefixToDocument(
156             @NonNull DocumentProto.Builder documentBuilder, @NonNull String prefix) {
157         // Rewrite the type name to include/remove the prefix.
158         String newSchema = prefix + documentBuilder.getSchema();
159         documentBuilder.setSchema(newSchema);
160 
161         // Rewrite the namespace to include/remove the prefix.
162         documentBuilder.setNamespace(prefix + documentBuilder.getNamespace());
163 
164         // Recurse into derived documents
165         for (int propertyIdx = 0;
166                 propertyIdx < documentBuilder.getPropertiesCount();
167                 propertyIdx++) {
168             int documentCount = documentBuilder.getProperties(propertyIdx).getDocumentValuesCount();
169             if (documentCount > 0) {
170                 PropertyProto.Builder propertyBuilder =
171                         documentBuilder.getProperties(propertyIdx).toBuilder();
172                 for (int documentIdx = 0; documentIdx < documentCount; documentIdx++) {
173                     DocumentProto.Builder derivedDocumentBuilder =
174                             propertyBuilder.getDocumentValues(documentIdx).toBuilder();
175                     addPrefixToDocument(derivedDocumentBuilder, prefix);
176                     propertyBuilder.setDocumentValues(documentIdx, derivedDocumentBuilder);
177                 }
178                 documentBuilder.setProperties(propertyIdx, propertyBuilder);
179             }
180         }
181     }
182 
183     /**
184      * Removes any prefixes from types and namespaces mentioned anywhere in {@code documentBuilder}.
185      *
186      * @param documentBuilder The document to mutate
187      * @return Prefix name that was removed from the document.
188      * @throws AppSearchException if there are unexpected database prefixing errors.
189      */
190     @NonNull
removePrefixesFromDocument(@onNull DocumentProto.Builder documentBuilder)191     public static String removePrefixesFromDocument(@NonNull DocumentProto.Builder documentBuilder)
192             throws AppSearchException {
193         // Rewrite the type name and namespace to remove the prefix.
194         String schemaPrefix = getPrefix(documentBuilder.getSchema());
195         String namespacePrefix = getPrefix(documentBuilder.getNamespace());
196 
197         if (!schemaPrefix.equals(namespacePrefix)) {
198             throw new AppSearchException(
199                     AppSearchResult.RESULT_INTERNAL_ERROR,
200                     "Found unexpected"
201                             + " multiple prefix names in document: "
202                             + schemaPrefix
203                             + ", "
204                             + namespacePrefix);
205         }
206 
207         documentBuilder.setSchema(removePrefix(documentBuilder.getSchema()));
208         documentBuilder.setNamespace(removePrefix(documentBuilder.getNamespace()));
209 
210         // Recurse into derived documents
211         for (int propertyIdx = 0;
212                 propertyIdx < documentBuilder.getPropertiesCount();
213                 propertyIdx++) {
214             int documentCount = documentBuilder.getProperties(propertyIdx).getDocumentValuesCount();
215             if (documentCount > 0) {
216                 PropertyProto.Builder propertyBuilder =
217                         documentBuilder.getProperties(propertyIdx).toBuilder();
218                 for (int documentIdx = 0; documentIdx < documentCount; documentIdx++) {
219                     DocumentProto.Builder derivedDocumentBuilder =
220                             propertyBuilder.getDocumentValues(documentIdx).toBuilder();
221                     String nestedPrefix = removePrefixesFromDocument(derivedDocumentBuilder);
222                     if (!nestedPrefix.equals(schemaPrefix)) {
223                         throw new AppSearchException(
224                                 AppSearchResult.RESULT_INTERNAL_ERROR,
225                                 "Found unexpected multiple prefix names in document: "
226                                         + schemaPrefix
227                                         + ", "
228                                         + nestedPrefix);
229                     }
230                     propertyBuilder.setDocumentValues(documentIdx, derivedDocumentBuilder);
231                 }
232                 documentBuilder.setProperties(propertyIdx, propertyBuilder);
233             }
234         }
235 
236         return schemaPrefix;
237     }
238 
239     /**
240      * Removes any prefixes from types mentioned anywhere in {@code typeConfigBuilder}.
241      *
242      * @param typeConfigBuilder The schema type to mutate
243      * @return Prefix name that was removed from the schema type.
244      * @throws AppSearchException if there are unexpected database prefixing errors.
245      */
246     @NonNull
removePrefixesFromSchemaType( @onNull SchemaTypeConfigProto.Builder typeConfigBuilder)247     public static String removePrefixesFromSchemaType(
248             @NonNull SchemaTypeConfigProto.Builder typeConfigBuilder) throws AppSearchException {
249         String typePrefix = PrefixUtil.getPrefix(typeConfigBuilder.getSchemaType());
250         // Rewrite SchemaProto.types.schema_type
251         String newSchemaType = typeConfigBuilder.getSchemaType().substring(typePrefix.length());
252         typeConfigBuilder.setSchemaType(newSchemaType);
253 
254         // Rewrite SchemaProto.types.properties.schema_type
255         for (int propertyIdx = 0;
256                 propertyIdx < typeConfigBuilder.getPropertiesCount();
257                 propertyIdx++) {
258             if (!typeConfigBuilder.getProperties(propertyIdx).getSchemaType().isEmpty()) {
259                 PropertyConfigProto.Builder propertyConfigBuilder =
260                         typeConfigBuilder.getProperties(propertyIdx).toBuilder();
261                 String newPropertySchemaType =
262                         propertyConfigBuilder.getSchemaType().substring(typePrefix.length());
263                 propertyConfigBuilder.setSchemaType(newPropertySchemaType);
264                 typeConfigBuilder.setProperties(propertyIdx, propertyConfigBuilder);
265             }
266         }
267         return typePrefix;
268     }
269 }
270