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