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