blob: 80cfe89ffc1c6e0458dcb89904f7cf8e96c58f68 [file] [log] [blame]
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.appsearch.external.localstorage.util;
import android.annotation.NonNull;
import android.app.appsearch.AppSearchResult;
import android.app.appsearch.exceptions.AppSearchException;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.google.android.icing.proto.DocumentProto;
import com.google.android.icing.proto.PropertyProto;
/**
* Provides utility functions for working with package + database prefixes.
*
* @hide
*/
public class PrefixUtil {
private static final String TAG = "AppSearchPrefixUtil";
@VisibleForTesting public static final char DATABASE_DELIMITER = '/';
@VisibleForTesting public static final char PACKAGE_DELIMITER = '$';
private PrefixUtil() {}
/** Creates prefix string for given package name and database name. */
@NonNull
public static String createPrefix(@NonNull String packageName, @NonNull String databaseName) {
return packageName + PACKAGE_DELIMITER + databaseName + DATABASE_DELIMITER;
}
/** Creates prefix string for given package name. */
@NonNull
public static String createPackagePrefix(@NonNull String packageName) {
return packageName + PACKAGE_DELIMITER;
}
/**
* Returns the package name that's contained within the {@code prefix}.
*
* @param prefix Prefix string that contains the package name inside of it. The package name
* must be in the front of the string, and separated from the rest of the string by the
* {@link #PACKAGE_DELIMITER}.
* @return Valid package name.
*/
@NonNull
public static String getPackageName(@NonNull String prefix) {
int delimiterIndex = prefix.indexOf(PACKAGE_DELIMITER);
if (delimiterIndex == -1) {
// This should never happen if we construct our prefixes properly
Log.wtf(TAG, "Malformed prefix doesn't contain package delimiter: " + prefix);
return "";
}
return prefix.substring(0, delimiterIndex);
}
/**
* Returns the database name that's contained within the {@code prefix}.
*
* @param prefix Prefix string that contains the database name inside of it. The database name
* must be between the {@link #PACKAGE_DELIMITER} and {@link #DATABASE_DELIMITER}
* @return Valid database name.
*/
@NonNull
public static String getDatabaseName(@NonNull String prefix) {
// TODO (b/184050178) Start database delimiter index search from after package delimiter
int packageDelimiterIndex = prefix.indexOf(PACKAGE_DELIMITER);
int databaseDelimiterIndex = prefix.indexOf(DATABASE_DELIMITER);
if (packageDelimiterIndex == -1) {
// This should never happen if we construct our prefixes properly
Log.wtf(TAG, "Malformed prefix doesn't contain package delimiter: " + prefix);
return "";
}
if (databaseDelimiterIndex == -1) {
// This should never happen if we construct our prefixes properly
Log.wtf(TAG, "Malformed prefix doesn't contain database delimiter: " + prefix);
return "";
}
return prefix.substring(packageDelimiterIndex + 1, databaseDelimiterIndex);
}
/**
* Creates a string with the package and database prefix removed from the input string.
*
* @param prefixedString a string containing a package and database prefix.
* @return a string with the package and database prefix removed.
* @throws AppSearchException if the prefixed value does not contain a valid database name.
*/
@NonNull
public static String removePrefix(@NonNull String prefixedString) throws AppSearchException {
// The prefix is made up of the package, then the database. So we only need to find the
// database cutoff.
int delimiterIndex;
if ((delimiterIndex = prefixedString.indexOf(DATABASE_DELIMITER)) != -1) {
// Add 1 to include the char size of the DATABASE_DELIMITER
return prefixedString.substring(delimiterIndex + 1);
}
throw new AppSearchException(
AppSearchResult.RESULT_INTERNAL_ERROR,
"The prefixed value \""
+ prefixedString
+ "\" doesn't contain a valid "
+ "database name");
}
/**
* Creates a package and database prefix string from the input string.
*
* @param prefixedString a string containing a package and database prefix.
* @return a string with the package and database prefix
* @throws AppSearchException if the prefixed value does not contain a valid database name.
*/
@NonNull
public static String getPrefix(@NonNull String prefixedString) throws AppSearchException {
int databaseDelimiterIndex = prefixedString.indexOf(DATABASE_DELIMITER);
if (databaseDelimiterIndex == -1) {
throw new AppSearchException(
AppSearchResult.RESULT_INTERNAL_ERROR,
"The prefixed value \""
+ prefixedString
+ "\" doesn't contain a valid "
+ "database name");
}
// Add 1 to include the char size of the DATABASE_DELIMITER
return prefixedString.substring(0, databaseDelimiterIndex + 1);
}
/**
* Prepends {@code prefix} to all types and namespaces mentioned anywhere in {@code
* documentBuilder}.
*
* @param documentBuilder The document to mutate
* @param prefix The prefix to add
*/
public static void addPrefixToDocument(
@NonNull DocumentProto.Builder documentBuilder, @NonNull String prefix) {
// Rewrite the type name to include/remove the prefix.
String newSchema = prefix + documentBuilder.getSchema();
documentBuilder.setSchema(newSchema);
// Rewrite the namespace to include/remove the prefix.
documentBuilder.setNamespace(prefix + documentBuilder.getNamespace());
// Recurse into derived documents
for (int propertyIdx = 0;
propertyIdx < documentBuilder.getPropertiesCount();
propertyIdx++) {
int documentCount = documentBuilder.getProperties(propertyIdx).getDocumentValuesCount();
if (documentCount > 0) {
PropertyProto.Builder propertyBuilder =
documentBuilder.getProperties(propertyIdx).toBuilder();
for (int documentIdx = 0; documentIdx < documentCount; documentIdx++) {
DocumentProto.Builder derivedDocumentBuilder =
propertyBuilder.getDocumentValues(documentIdx).toBuilder();
addPrefixToDocument(derivedDocumentBuilder, prefix);
propertyBuilder.setDocumentValues(documentIdx, derivedDocumentBuilder);
}
documentBuilder.setProperties(propertyIdx, propertyBuilder);
}
}
}
/**
* Removes any prefixes from types and namespaces mentioned anywhere in {@code documentBuilder}.
*
* @param documentBuilder The document to mutate
* @return Prefix name that was removed from the document.
* @throws AppSearchException if there are unexpected database prefixing errors.
*/
@NonNull
public static String removePrefixesFromDocument(@NonNull DocumentProto.Builder documentBuilder)
throws AppSearchException {
// Rewrite the type name and namespace to remove the prefix.
String schemaPrefix = getPrefix(documentBuilder.getSchema());
String namespacePrefix = getPrefix(documentBuilder.getNamespace());
if (!schemaPrefix.equals(namespacePrefix)) {
throw new AppSearchException(
AppSearchResult.RESULT_INTERNAL_ERROR,
"Found unexpected"
+ " multiple prefix names in document: "
+ schemaPrefix
+ ", "
+ namespacePrefix);
}
documentBuilder.setSchema(removePrefix(documentBuilder.getSchema()));
documentBuilder.setNamespace(removePrefix(documentBuilder.getNamespace()));
// Recurse into derived documents
for (int propertyIdx = 0;
propertyIdx < documentBuilder.getPropertiesCount();
propertyIdx++) {
int documentCount = documentBuilder.getProperties(propertyIdx).getDocumentValuesCount();
if (documentCount > 0) {
PropertyProto.Builder propertyBuilder =
documentBuilder.getProperties(propertyIdx).toBuilder();
for (int documentIdx = 0; documentIdx < documentCount; documentIdx++) {
DocumentProto.Builder derivedDocumentBuilder =
propertyBuilder.getDocumentValues(documentIdx).toBuilder();
String nestedPrefix = removePrefixesFromDocument(derivedDocumentBuilder);
if (!nestedPrefix.equals(schemaPrefix)) {
throw new AppSearchException(
AppSearchResult.RESULT_INTERNAL_ERROR,
"Found unexpected multiple prefix names in document: "
+ schemaPrefix
+ ", "
+ nestedPrefix);
}
propertyBuilder.setDocumentValues(documentIdx, derivedDocumentBuilder);
}
documentBuilder.setProperties(propertyIdx, propertyBuilder);
}
}
return schemaPrefix;
}
}