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