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