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