Merge changes from topic "sqlitez"

* changes:
  Support for appending "standalone" WHERE chunks.
  Bind update() args as Object[] for performance.
  Extend SQLiteQueryBuilder for update and delete.
  Execute "strict" queries with extra parentheses.
  Revert SQLiteQueryBuilder for now.
  Execute "strict" queries with extra parentheses.
  Add support for appending standalone phrases.
  GROUP BY and HAVING aren't ready to be strict.
  Extend SQLiteQueryBuilder for update and delete.
This commit is contained in:
Treehugger Robot
2018-12-02 01:30:04 +00:00
committed by Gerrit Code Review
9 changed files with 553 additions and 113 deletions

View File

@@ -12669,12 +12669,14 @@ package android.database.sqlite {
method public static void appendColumns(java.lang.StringBuilder, java.lang.String[]);
method public void appendWhere(java.lang.CharSequence);
method public void appendWhereEscapeString(java.lang.String);
method public void appendWhereStandalone(java.lang.CharSequence);
method public java.lang.String buildQuery(java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String);
method public deprecated java.lang.String buildQuery(java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String);
method public static java.lang.String buildQueryString(boolean, java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String);
method public java.lang.String buildUnionQuery(java.lang.String[], java.lang.String, java.lang.String);
method public java.lang.String buildUnionSubQuery(java.lang.String, java.lang.String[], java.util.Set<java.lang.String>, int, java.lang.String, java.lang.String, java.lang.String, java.lang.String);
method public deprecated java.lang.String buildUnionSubQuery(java.lang.String, java.lang.String[], java.util.Set<java.lang.String>, int, java.lang.String, java.lang.String, java.lang.String[], java.lang.String, java.lang.String);
method public int delete(android.database.sqlite.SQLiteDatabase, java.lang.String, java.lang.String[]);
method public java.lang.String getTables();
method public android.database.Cursor query(android.database.sqlite.SQLiteDatabase, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String);
method public android.database.Cursor query(android.database.sqlite.SQLiteDatabase, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String);
@@ -12684,6 +12686,7 @@ package android.database.sqlite {
method public void setProjectionMap(java.util.Map<java.lang.String, java.lang.String>);
method public void setStrict(boolean);
method public void setTables(java.lang.String);
method public int update(android.database.sqlite.SQLiteDatabase, android.content.ContentValues, java.lang.String, java.lang.String[]);
}
public class SQLiteReadOnlyDatabaseException extends android.database.sqlite.SQLiteException {

View File

@@ -261,6 +261,13 @@ public abstract class ContentResolver {
*/
public static final String QUERY_ARG_SQL_SORT_ORDER = "android:query-arg-sql-sort-order";
/** {@hide} */
public static final String QUERY_ARG_SQL_GROUP_BY = "android:query-arg-sql-group-by";
/** {@hide} */
public static final String QUERY_ARG_SQL_HAVING = "android:query-arg-sql-having";
/** {@hide} */
public static final String QUERY_ARG_SQL_LIMIT = "android:query-arg-sql-limit";
/**
* Specifies the list of columns against which to sort results. When first column values
* are identical, records are then sorted based on second column values, and so on.

View File

@@ -19,6 +19,7 @@ package android.content;
import android.annotation.UnsupportedAppUsage;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.ArrayMap;
import android.util.Log;
import java.util.ArrayList;
@@ -33,17 +34,21 @@ import java.util.Set;
public final class ContentValues implements Parcelable {
public static final String TAG = "ContentValues";
/** Holds the actual values */
/**
* @hide
* @deprecated kept around for lame people doing reflection
*/
@Deprecated
@UnsupportedAppUsage
private HashMap<String, Object> mValues;
private final ArrayMap<String, Object> mMap;
/**
* Creates an empty set of values using the default initial size
*/
public ContentValues() {
// Choosing a default size of 8 based on analysis of typical
// consumption by applications.
mValues = new HashMap<String, Object>(8);
mMap = new ArrayMap<>();
}
/**
@@ -52,7 +57,7 @@ public final class ContentValues implements Parcelable {
* @param size the initial size of the set of values
*/
public ContentValues(int size) {
mValues = new HashMap<String, Object>(size, 1.0f);
mMap = new ArrayMap<>(size);
}
/**
@@ -61,19 +66,24 @@ public final class ContentValues implements Parcelable {
* @param from the values to copy
*/
public ContentValues(ContentValues from) {
mValues = new HashMap<String, Object>(from.mValues);
mMap = new ArrayMap<>(from.mMap);
}
/**
* Creates a set of values copied from the given HashMap. This is used
* by the Parcel unmarshalling code.
*
* @param values the values to start with
* {@hide}
* @hide
* @deprecated kept around for lame people doing reflection
*/
@Deprecated
@UnsupportedAppUsage
private ContentValues(HashMap<String, Object> values) {
mValues = values;
private ContentValues(HashMap<String, Object> from) {
mMap = new ArrayMap<>();
mMap.putAll(from);
}
/** {@hide} */
private ContentValues(Parcel in) {
mMap = new ArrayMap<>(in.readInt());
in.readArrayMap(mMap, null);
}
@Override
@@ -81,12 +91,17 @@ public final class ContentValues implements Parcelable {
if (!(object instanceof ContentValues)) {
return false;
}
return mValues.equals(((ContentValues) object).mValues);
return mMap.equals(((ContentValues) object).mMap);
}
/** {@hide} */
public ArrayMap<String, Object> getValues() {
return mMap;
}
@Override
public int hashCode() {
return mValues.hashCode();
return mMap.hashCode();
}
/**
@@ -96,7 +111,7 @@ public final class ContentValues implements Parcelable {
* @param value the data for the value to put
*/
public void put(String key, String value) {
mValues.put(key, value);
mMap.put(key, value);
}
/**
@@ -105,7 +120,7 @@ public final class ContentValues implements Parcelable {
* @param other the ContentValues from which to copy
*/
public void putAll(ContentValues other) {
mValues.putAll(other.mValues);
mMap.putAll(other.mMap);
}
/**
@@ -115,7 +130,7 @@ public final class ContentValues implements Parcelable {
* @param value the data for the value to put
*/
public void put(String key, Byte value) {
mValues.put(key, value);
mMap.put(key, value);
}
/**
@@ -125,7 +140,7 @@ public final class ContentValues implements Parcelable {
* @param value the data for the value to put
*/
public void put(String key, Short value) {
mValues.put(key, value);
mMap.put(key, value);
}
/**
@@ -135,7 +150,7 @@ public final class ContentValues implements Parcelable {
* @param value the data for the value to put
*/
public void put(String key, Integer value) {
mValues.put(key, value);
mMap.put(key, value);
}
/**
@@ -145,7 +160,7 @@ public final class ContentValues implements Parcelable {
* @param value the data for the value to put
*/
public void put(String key, Long value) {
mValues.put(key, value);
mMap.put(key, value);
}
/**
@@ -155,7 +170,7 @@ public final class ContentValues implements Parcelable {
* @param value the data for the value to put
*/
public void put(String key, Float value) {
mValues.put(key, value);
mMap.put(key, value);
}
/**
@@ -165,7 +180,7 @@ public final class ContentValues implements Parcelable {
* @param value the data for the value to put
*/
public void put(String key, Double value) {
mValues.put(key, value);
mMap.put(key, value);
}
/**
@@ -175,7 +190,7 @@ public final class ContentValues implements Parcelable {
* @param value the data for the value to put
*/
public void put(String key, Boolean value) {
mValues.put(key, value);
mMap.put(key, value);
}
/**
@@ -185,7 +200,7 @@ public final class ContentValues implements Parcelable {
* @param value the data for the value to put
*/
public void put(String key, byte[] value) {
mValues.put(key, value);
mMap.put(key, value);
}
/**
@@ -194,7 +209,7 @@ public final class ContentValues implements Parcelable {
* @param key the name of the value to make null
*/
public void putNull(String key) {
mValues.put(key, null);
mMap.put(key, null);
}
/**
@@ -203,7 +218,7 @@ public final class ContentValues implements Parcelable {
* @return the number of values
*/
public int size() {
return mValues.size();
return mMap.size();
}
/**
@@ -214,7 +229,7 @@ public final class ContentValues implements Parcelable {
* TODO: consider exposing this new method publicly
*/
public boolean isEmpty() {
return mValues.isEmpty();
return mMap.isEmpty();
}
/**
@@ -223,14 +238,14 @@ public final class ContentValues implements Parcelable {
* @param key the name of the value to remove
*/
public void remove(String key) {
mValues.remove(key);
mMap.remove(key);
}
/**
* Removes all values.
*/
public void clear() {
mValues.clear();
mMap.clear();
}
/**
@@ -240,7 +255,7 @@ public final class ContentValues implements Parcelable {
* @return {@code true} if the value is present, {@code false} otherwise
*/
public boolean containsKey(String key) {
return mValues.containsKey(key);
return mMap.containsKey(key);
}
/**
@@ -252,7 +267,7 @@ public final class ContentValues implements Parcelable {
* was previously added with the given {@code key}
*/
public Object get(String key) {
return mValues.get(key);
return mMap.get(key);
}
/**
@@ -262,7 +277,7 @@ public final class ContentValues implements Parcelable {
* @return the String for the value
*/
public String getAsString(String key) {
Object value = mValues.get(key);
Object value = mMap.get(key);
return value != null ? value.toString() : null;
}
@@ -273,7 +288,7 @@ public final class ContentValues implements Parcelable {
* @return the Long value, or {@code null} if the value is missing or cannot be converted
*/
public Long getAsLong(String key) {
Object value = mValues.get(key);
Object value = mMap.get(key);
try {
return value != null ? ((Number) value).longValue() : null;
} catch (ClassCastException e) {
@@ -298,7 +313,7 @@ public final class ContentValues implements Parcelable {
* @return the Integer value, or {@code null} if the value is missing or cannot be converted
*/
public Integer getAsInteger(String key) {
Object value = mValues.get(key);
Object value = mMap.get(key);
try {
return value != null ? ((Number) value).intValue() : null;
} catch (ClassCastException e) {
@@ -323,7 +338,7 @@ public final class ContentValues implements Parcelable {
* @return the Short value, or {@code null} if the value is missing or cannot be converted
*/
public Short getAsShort(String key) {
Object value = mValues.get(key);
Object value = mMap.get(key);
try {
return value != null ? ((Number) value).shortValue() : null;
} catch (ClassCastException e) {
@@ -348,7 +363,7 @@ public final class ContentValues implements Parcelable {
* @return the Byte value, or {@code null} if the value is missing or cannot be converted
*/
public Byte getAsByte(String key) {
Object value = mValues.get(key);
Object value = mMap.get(key);
try {
return value != null ? ((Number) value).byteValue() : null;
} catch (ClassCastException e) {
@@ -373,7 +388,7 @@ public final class ContentValues implements Parcelable {
* @return the Double value, or {@code null} if the value is missing or cannot be converted
*/
public Double getAsDouble(String key) {
Object value = mValues.get(key);
Object value = mMap.get(key);
try {
return value != null ? ((Number) value).doubleValue() : null;
} catch (ClassCastException e) {
@@ -398,7 +413,7 @@ public final class ContentValues implements Parcelable {
* @return the Float value, or {@code null} if the value is missing or cannot be converted
*/
public Float getAsFloat(String key) {
Object value = mValues.get(key);
Object value = mMap.get(key);
try {
return value != null ? ((Number) value).floatValue() : null;
} catch (ClassCastException e) {
@@ -423,7 +438,7 @@ public final class ContentValues implements Parcelable {
* @return the Boolean value, or {@code null} if the value is missing or cannot be converted
*/
public Boolean getAsBoolean(String key) {
Object value = mValues.get(key);
Object value = mMap.get(key);
try {
return (Boolean) value;
} catch (ClassCastException e) {
@@ -451,7 +466,7 @@ public final class ContentValues implements Parcelable {
* {@code byte[]}
*/
public byte[] getAsByteArray(String key) {
Object value = mValues.get(key);
Object value = mMap.get(key);
if (value instanceof byte[]) {
return (byte[]) value;
} else {
@@ -465,7 +480,7 @@ public final class ContentValues implements Parcelable {
* @return a set of all of the keys and values
*/
public Set<Map.Entry<String, Object>> valueSet() {
return mValues.entrySet();
return mMap.entrySet();
}
/**
@@ -474,30 +489,31 @@ public final class ContentValues implements Parcelable {
* @return a set of all of the keys
*/
public Set<String> keySet() {
return mValues.keySet();
return mMap.keySet();
}
public static final Parcelable.Creator<ContentValues> CREATOR =
new Parcelable.Creator<ContentValues>() {
@SuppressWarnings({"deprecation", "unchecked"})
@Override
public ContentValues createFromParcel(Parcel in) {
// TODO - what ClassLoader should be passed to readHashMap?
HashMap<String, Object> values = in.readHashMap(null);
return new ContentValues(values);
return new ContentValues(in);
}
@Override
public ContentValues[] newArray(int size) {
return new ContentValues[size];
}
};
@Override
public int describeContents() {
return 0;
}
@SuppressWarnings("deprecation")
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeMap(mValues);
parcel.writeInt(mMap.size());
parcel.writeArrayMap(mMap);
}
/**
@@ -507,7 +523,7 @@ public final class ContentValues implements Parcelable {
@Deprecated
@UnsupportedAppUsage
public void putStringArrayList(String key, ArrayList<String> value) {
mValues.put(key, value);
mMap.put(key, value);
}
/**
@@ -518,7 +534,7 @@ public final class ContentValues implements Parcelable {
@Deprecated
@UnsupportedAppUsage
public ArrayList<String> getStringArrayList(String key) {
return (ArrayList<String>) mValues.get(key);
return (ArrayList<String>) mMap.get(key);
}
/**
@@ -528,7 +544,7 @@ public final class ContentValues implements Parcelable {
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (String name : mValues.keySet()) {
for (String name : mMap.keySet()) {
String value = getAsString(name);
if (sb.length() > 0) sb.append(" ");
sb.append(name + "=" + value);

View File

@@ -17,6 +17,7 @@
package android.database;
import android.annotation.UnsupportedAppUsage;
import android.annotation.Nullable;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
@@ -35,6 +36,8 @@ import android.os.ParcelFileDescriptor;
import android.text.TextUtils;
import android.util.Log;
import com.android.internal.util.ArrayUtils;
import java.io.FileNotFoundException;
import java.io.PrintStream;
import java.text.Collator;
@@ -216,6 +219,92 @@ public class DatabaseUtils {
}
}
/**
* Bind the given selection with the given selection arguments.
* <p>
* Internally assumes that '?' is only ever used for arguments, and doesn't
* appear as a literal or escaped value.
* <p>
* This method is typically useful for trusted code that needs to cook up a
* fully-bound selection.
*
* @hide
*/
public static @Nullable String bindSelection(@Nullable String selection,
@Nullable Object... selectionArgs) {
if (selection == null) return null;
// If no arguments provided, so we can't bind anything
if (ArrayUtils.isEmpty(selectionArgs)) return selection;
// If no bindings requested, so we can shortcut
if (selection.indexOf('?') == -1) return selection;
// Track the chars immediately before and after each bind request, to
// decide if it needs additional whitespace added
char before = ' ';
char after = ' ';
int argIndex = 0;
final int len = selection.length();
final StringBuilder res = new StringBuilder(len);
for (int i = 0; i < len; ) {
char c = selection.charAt(i++);
if (c == '?') {
// Assume this bind request is guarded until we find a specific
// trailing character below
after = ' ';
// Sniff forward to see if the selection is requesting a
// specific argument index
int start = i;
for (; i < len; i++) {
c = selection.charAt(i);
if (c < '0' || c > '9') {
after = c;
break;
}
}
if (start != i) {
argIndex = Integer.parseInt(selection.substring(start, i)) - 1;
}
// Manually bind the argument into the selection, adding
// whitespace when needed for clarity
final Object arg = selectionArgs[argIndex++];
if (before != ' ' && before != '=') res.append(' ');
switch (DatabaseUtils.getTypeOfObject(arg)) {
case Cursor.FIELD_TYPE_NULL:
res.append("NULL");
break;
case Cursor.FIELD_TYPE_INTEGER:
res.append(((Number) arg).longValue());
break;
case Cursor.FIELD_TYPE_FLOAT:
res.append(((Number) arg).doubleValue());
break;
case Cursor.FIELD_TYPE_BLOB:
throw new IllegalArgumentException("Blobs not supported");
case Cursor.FIELD_TYPE_STRING:
default:
if (arg instanceof Boolean) {
// Provide compatibility with legacy applications which may pass
// Boolean values in bind args.
res.append(((Boolean) arg).booleanValue() ? 1 : 0);
} else {
res.append('\'');
res.append(arg.toString());
res.append('\'');
}
break;
}
if (after != ' ') res.append(' ');
} else {
res.append(c);
before = c;
}
}
return res.toString();
}
/**
* Returns data type of the given object's value.
*<p>

View File

@@ -193,8 +193,9 @@ public final class SQLiteDatabase extends SQLiteClosable {
*/
public static final int CONFLICT_NONE = 0;
/** {@hide} */
@UnsupportedAppUsage
private static final String[] CONFLICT_VALUES = new String[]
public static final String[] CONFLICT_VALUES = new String[]
{"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "};
/**

View File

@@ -17,17 +17,26 @@
package android.database.sqlite;
import android.annotation.UnsupportedAppUsage;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.os.Build;
import android.os.CancellationSignal;
import android.os.OperationCanceledException;
import android.provider.BaseColumns;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import libcore.util.EmptyArray;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
@@ -35,8 +44,7 @@ import java.util.regex.Pattern;
* This is a convenience class that helps build SQL queries to be sent to
* {@link SQLiteDatabase} objects.
*/
public class SQLiteQueryBuilder
{
public class SQLiteQueryBuilder {
private static final String TAG = "SQLiteQueryBuilder";
private static final Pattern sLimitPattern =
Pattern.compile("\\s*\\d+\\s*(,\\s*\\d+\\s*)?");
@@ -95,13 +103,10 @@ public class SQLiteQueryBuilder
*
* @param inWhere the chunk of text to append to the WHERE clause.
*/
public void appendWhere(CharSequence inWhere) {
public void appendWhere(@NonNull CharSequence inWhere) {
if (mWhereClause == null) {
mWhereClause = new StringBuilder(inWhere.length() + 16);
}
if (mWhereClause.length() == 0) {
mWhereClause.append('(');
}
mWhereClause.append(inWhere);
}
@@ -115,16 +120,34 @@ public class SQLiteQueryBuilder
* @param inWhere the chunk of text to append to the WHERE clause. it will be escaped
* to avoid SQL injection attacks
*/
public void appendWhereEscapeString(String inWhere) {
public void appendWhereEscapeString(@NonNull String inWhere) {
if (mWhereClause == null) {
mWhereClause = new StringBuilder(inWhere.length() + 16);
}
if (mWhereClause.length() == 0) {
mWhereClause.append('(');
}
DatabaseUtils.appendEscapedSQLString(mWhereClause, inWhere);
}
/**
* Add a standalone chunk to the {@code WHERE} clause of this query.
* <p>
* This method differs from {@link #appendWhere(CharSequence)} in that it
* automatically appends {@code AND} to any existing {@code WHERE} clause
* already under construction before appending the given standalone
* expression wrapped in parentheses.
*
* @param inWhere the standalone expression to append to the {@code WHERE}
* clause. It will be wrapped in parentheses when it's appended.
*/
public void appendWhereStandalone(@NonNull CharSequence inWhere) {
if (mWhereClause == null) {
mWhereClause = new StringBuilder(inWhere.length() + 16);
}
if (mWhereClause.length() > 0) {
mWhereClause.append(" AND ");
}
mWhereClause.append('(').append(inWhere).append(')');
}
/**
* Sets the projection map for the query. The projection map maps
* from column names that the caller passes into query to database
@@ -380,6 +403,11 @@ public class SQLiteQueryBuilder
return null;
}
final String sql;
final String unwrappedSql = buildQuery(
projectionIn, selection, groupBy, having,
sortOrder, limit);
if (mStrict && selection != null && selection.length() > 0) {
// Validate the user-supplied selection to detect syntactic anomalies
// in the selection string that could indicate a SQL injection attempt.
@@ -388,24 +416,164 @@ public class SQLiteQueryBuilder
// originally specified. An attacker cannot create an expression that
// would escape the SQL expression while maintaining balanced parentheses
// in both the wrapped and original forms.
String sqlForValidation = buildQuery(projectionIn, "(" + selection + ")", groupBy,
// NOTE: The ordering of the below operations is important; we must
// execute the wrapped query to ensure the untrusted clause has been
// fully isolated.
// Validate the unwrapped query
db.validateSql(unwrappedSql, cancellationSignal); // will throw if query is invalid
// Execute wrapped query for extra protection
final String wrappedSql = buildQuery(projectionIn, wrap(selection), groupBy,
having, sortOrder, limit);
db.validateSql(sqlForValidation, cancellationSignal); // will throw if query is invalid
sql = wrappedSql;
} else {
// Execute unwrapped query
sql = unwrappedSql;
}
String sql = buildQuery(
projectionIn, selection, groupBy, having,
sortOrder, limit);
final String[] sqlArgs = selectionArgs;
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Performing query: " + sql);
if (Build.IS_DEBUGGABLE) {
Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
} else {
Log.d(TAG, sql);
}
}
return db.rawQueryWithFactory(
mFactory, sql, selectionArgs,
mFactory, sql, sqlArgs,
SQLiteDatabase.findEditTable(mTables),
cancellationSignal); // will throw if query is invalid
}
/**
* Perform an update by combining all current settings and the
* information passed into this method.
*
* @param db the database to update on
* @param selection A filter declaring which rows to return,
* formatted as an SQL WHERE clause (excluding the WHERE
* itself). Passing null will return all rows for the given URL.
* @param selectionArgs You may include ?s in selection, which
* will be replaced by the values from selectionArgs, in order
* that they appear in the selection. The values will be bound
* as Strings.
* @return the number of rows updated
*/
public int update(@NonNull SQLiteDatabase db, @NonNull ContentValues values,
@Nullable String selection, @Nullable String[] selectionArgs) {
Objects.requireNonNull(mTables, "No tables defined");
Objects.requireNonNull(db, "No database defined");
Objects.requireNonNull(values, "No values defined");
final String sql;
final String unwrappedSql = buildUpdate(values, selection);
if (mStrict) {
// Validate the user-supplied selection to detect syntactic anomalies
// in the selection string that could indicate a SQL injection attempt.
// The idea is to ensure that the selection clause is a valid SQL expression
// by compiling it twice: once wrapped in parentheses and once as
// originally specified. An attacker cannot create an expression that
// would escape the SQL expression while maintaining balanced parentheses
// in both the wrapped and original forms.
// NOTE: The ordering of the below operations is important; we must
// execute the wrapped query to ensure the untrusted clause has been
// fully isolated.
// Validate the unwrapped query
db.validateSql(unwrappedSql, null); // will throw if query is invalid
// Execute wrapped query for extra protection
final String wrappedSql = buildUpdate(values, wrap(selection));
sql = wrappedSql;
} else {
// Execute unwrapped query
sql = unwrappedSql;
}
if (selectionArgs == null) {
selectionArgs = EmptyArray.STRING;
}
final ArrayMap<String, Object> rawValues = values.getValues();
final int valuesLength = rawValues.size();
final Object[] sqlArgs = new Object[valuesLength + selectionArgs.length];
for (int i = 0; i < sqlArgs.length; i++) {
if (i < valuesLength) {
sqlArgs[i] = rawValues.valueAt(i);
} else {
sqlArgs[i] = selectionArgs[i - valuesLength];
}
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
if (Build.IS_DEBUGGABLE) {
Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
} else {
Log.d(TAG, sql);
}
}
return db.executeSql(sql, sqlArgs);
}
/**
* Perform a delete by combining all current settings and the
* information passed into this method.
*
* @param db the database to delete on
* @param selection A filter declaring which rows to return,
* formatted as an SQL WHERE clause (excluding the WHERE
* itself). Passing null will return all rows for the given URL.
* @param selectionArgs You may include ?s in selection, which
* will be replaced by the values from selectionArgs, in order
* that they appear in the selection. The values will be bound
* as Strings.
* @return the number of rows deleted
*/
public int delete(@NonNull SQLiteDatabase db, @Nullable String selection,
@Nullable String[] selectionArgs) {
Objects.requireNonNull(mTables, "No tables defined");
Objects.requireNonNull(db, "No database defined");
final String sql;
final String unwrappedSql = buildDelete(selection);
if (mStrict) {
// Validate the user-supplied selection to detect syntactic anomalies
// in the selection string that could indicate a SQL injection attempt.
// The idea is to ensure that the selection clause is a valid SQL expression
// by compiling it twice: once wrapped in parentheses and once as
// originally specified. An attacker cannot create an expression that
// would escape the SQL expression while maintaining balanced parentheses
// in both the wrapped and original forms.
// NOTE: The ordering of the below operations is important; we must
// execute the wrapped query to ensure the untrusted clause has been
// fully isolated.
// Validate the unwrapped query
db.validateSql(unwrappedSql, null); // will throw if query is invalid
// Execute wrapped query for extra protection
final String wrappedSql = buildDelete(wrap(selection));
sql = wrappedSql;
} else {
// Execute unwrapped query
sql = unwrappedSql;
}
final String[] sqlArgs = selectionArgs;
if (Log.isLoggable(TAG, Log.DEBUG)) {
if (Build.IS_DEBUGGABLE) {
Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
} else {
Log.d(TAG, sql);
}
}
return db.executeSql(sql, sqlArgs);
}
/**
* Construct a SELECT statement suitable for use in a group of
* SELECT statements that will be joined through UNION operators
@@ -438,28 +606,10 @@ public class SQLiteQueryBuilder
String[] projectionIn, String selection, String groupBy,
String having, String sortOrder, String limit) {
String[] projection = computeProjection(projectionIn);
StringBuilder where = new StringBuilder();
boolean hasBaseWhereClause = mWhereClause != null && mWhereClause.length() > 0;
if (hasBaseWhereClause) {
where.append(mWhereClause.toString());
where.append(')');
}
// Tack on the user's selection, if present.
if (selection != null && selection.length() > 0) {
if (hasBaseWhereClause) {
where.append(" AND ");
}
where.append('(');
where.append(selection);
where.append(')');
}
String where = computeWhere(selection);
return buildQueryString(
mDistinct, mTables, projection, where.toString(),
mDistinct, mTables, projection, where,
groupBy, having, sortOrder, limit);
}
@@ -476,6 +626,42 @@ public class SQLiteQueryBuilder
return buildQuery(projectionIn, selection, groupBy, having, sortOrder, limit);
}
/** {@hide} */
public String buildUpdate(ContentValues values, String selection) {
if (values == null || values.isEmpty()) {
throw new IllegalArgumentException("Empty values");
}
StringBuilder sql = new StringBuilder(120);
sql.append("UPDATE ");
sql.append(mTables);
sql.append(" SET ");
final ArrayMap<String, Object> rawValues = values.getValues();
for (int i = 0; i < rawValues.size(); i++) {
if (i > 0) {
sql.append(',');
}
sql.append(rawValues.keyAt(i));
sql.append("=?");
}
final String where = computeWhere(selection);
appendClause(sql, " WHERE ", where);
return sql.toString();
}
/** {@hide} */
public String buildDelete(String selection) {
StringBuilder sql = new StringBuilder(120);
sql.append("DELETE FROM ");
sql.append(mTables);
final String where = computeWhere(selection);
appendClause(sql, " WHERE ", where);
return sql.toString();
}
/**
* Construct a SELECT statement suitable for use in a group of
* SELECT statements that will be joined through UNION operators
@@ -650,4 +836,37 @@ public class SQLiteQueryBuilder
}
return null;
}
private @Nullable String computeWhere(@Nullable String selection) {
final boolean hasInternal = !TextUtils.isEmpty(mWhereClause);
final boolean hasExternal = !TextUtils.isEmpty(selection);
if (hasInternal || hasExternal) {
final StringBuilder where = new StringBuilder();
if (hasInternal) {
where.append('(').append(mWhereClause).append(')');
}
if (hasInternal && hasExternal) {
where.append(" AND ");
}
if (hasExternal) {
where.append('(').append(selection).append(')');
}
return where.toString();
} else {
return null;
}
}
/**
* Wrap given argument in parenthesis, unless it's {@code null} or
* {@code ()}, in which case return it verbatim.
*/
private @Nullable String wrap(@Nullable String arg) {
if (TextUtils.isEmpty(arg)) {
return arg;
} else {
return "(" + arg + ")";
}
}
}

View File

@@ -308,6 +308,23 @@ public class ArrayUtils {
return array;
}
@SuppressWarnings("unchecked")
public static @NonNull <T> T[] concatElements(Class<T> kind, @Nullable T[] a, @Nullable T[] b) {
final int an = (a != null) ? a.length : 0;
final int bn = (b != null) ? b.length : 0;
if (an == 0 && bn == 0) {
if (kind == String.class) {
return (T[]) EmptyArray.STRING;
} else if (kind == Object.class) {
return (T[]) EmptyArray.OBJECT;
}
}
final T[] res = (T[]) Array.newInstance(kind, an + bn);
if (an > 0) System.arraycopy(a, 0, res, 0, an);
if (bn > 0) System.arraycopy(b, 0, res, an, bn);
return res;
}
/**
* Adds value to given array if not already present, providing set-like
* behavior.

View File

@@ -0,0 +1,66 @@
/*
* Copyright (C) 2018 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 android.database;
import static android.database.DatabaseUtils.bindSelection;
import static org.junit.Assert.assertEquals;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class DatabaseUtilsTest {
private static final Object[] ARGS = { "baz", 4, null };
@Test
public void testBindSelection_none() throws Exception {
assertEquals(null,
bindSelection(null, ARGS));
assertEquals("",
bindSelection("", ARGS));
assertEquals("foo=bar",
bindSelection("foo=bar", ARGS));
}
@Test
public void testBindSelection_normal() throws Exception {
assertEquals("foo='baz'",
bindSelection("foo=?", ARGS));
assertEquals("foo='baz' AND bar=4",
bindSelection("foo=? AND bar=?", ARGS));
assertEquals("foo='baz' AND bar=4 AND meow=NULL",
bindSelection("foo=? AND bar=? AND meow=?", ARGS));
}
@Test
public void testBindSelection_whitespace() throws Exception {
assertEquals("BETWEEN 5 AND 10",
bindSelection("BETWEEN? AND ?", 5, 10));
assertEquals("IN 'foo'",
bindSelection("IN?", "foo"));
}
@Test
public void testBindSelection_indexed() throws Exception {
assertEquals("foo=10 AND bar=11 AND meow=1",
bindSelection("foo=?10 AND bar=? AND meow=?1",
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12));
}
}

View File

@@ -16,9 +16,10 @@
package com.android.internal.util;
import android.test.MoreAsserts;
import static com.android.internal.util.ArrayUtils.concatElements;
import static org.junit.Assert.assertArrayEquals;
import java.util.Arrays;
import junit.framework.TestCase;
/**
@@ -92,29 +93,29 @@ public class ArrayUtilsTest extends TestCase {
}
public void testAppendInt() throws Exception {
MoreAsserts.assertEquals(new int[] { 1 },
assertArrayEquals(new int[] { 1 },
ArrayUtils.appendInt(null, 1));
MoreAsserts.assertEquals(new int[] { 1 },
assertArrayEquals(new int[] { 1 },
ArrayUtils.appendInt(new int[] { }, 1));
MoreAsserts.assertEquals(new int[] { 1, 2 },
assertArrayEquals(new int[] { 1, 2 },
ArrayUtils.appendInt(new int[] { 1 }, 2));
MoreAsserts.assertEquals(new int[] { 1, 2 },
assertArrayEquals(new int[] { 1, 2 },
ArrayUtils.appendInt(new int[] { 1, 2 }, 1));
}
public void testRemoveInt() throws Exception {
assertNull(ArrayUtils.removeInt(null, 1));
MoreAsserts.assertEquals(new int[] { },
assertArrayEquals(new int[] { },
ArrayUtils.removeInt(new int[] { }, 1));
MoreAsserts.assertEquals(new int[] { 1, 2, 3, },
assertArrayEquals(new int[] { 1, 2, 3, },
ArrayUtils.removeInt(new int[] { 1, 2, 3}, 4));
MoreAsserts.assertEquals(new int[] { 2, 3, },
assertArrayEquals(new int[] { 2, 3, },
ArrayUtils.removeInt(new int[] { 1, 2, 3}, 1));
MoreAsserts.assertEquals(new int[] { 1, 3, },
assertArrayEquals(new int[] { 1, 3, },
ArrayUtils.removeInt(new int[] { 1, 2, 3}, 2));
MoreAsserts.assertEquals(new int[] { 1, 2, },
assertArrayEquals(new int[] { 1, 2, },
ArrayUtils.removeInt(new int[] { 1, 2, 3}, 3));
MoreAsserts.assertEquals(new int[] { 2, 3, 1 },
assertArrayEquals(new int[] { 2, 3, 1 },
ArrayUtils.removeInt(new int[] { 1, 2, 3, 1 }, 1));
}
@@ -129,30 +130,51 @@ public class ArrayUtilsTest extends TestCase {
}
public void testAppendLong() throws Exception {
MoreAsserts.assertEquals(new long[] { 1 },
assertArrayEquals(new long[] { 1 },
ArrayUtils.appendLong(null, 1));
MoreAsserts.assertEquals(new long[] { 1 },
assertArrayEquals(new long[] { 1 },
ArrayUtils.appendLong(new long[] { }, 1));
MoreAsserts.assertEquals(new long[] { 1, 2 },
assertArrayEquals(new long[] { 1, 2 },
ArrayUtils.appendLong(new long[] { 1 }, 2));
MoreAsserts.assertEquals(new long[] { 1, 2 },
assertArrayEquals(new long[] { 1, 2 },
ArrayUtils.appendLong(new long[] { 1, 2 }, 1));
}
public void testRemoveLong() throws Exception {
assertNull(ArrayUtils.removeLong(null, 1));
MoreAsserts.assertEquals(new long[] { },
assertArrayEquals(new long[] { },
ArrayUtils.removeLong(new long[] { }, 1));
MoreAsserts.assertEquals(new long[] { 1, 2, 3, },
assertArrayEquals(new long[] { 1, 2, 3, },
ArrayUtils.removeLong(new long[] { 1, 2, 3}, 4));
MoreAsserts.assertEquals(new long[] { 2, 3, },
assertArrayEquals(new long[] { 2, 3, },
ArrayUtils.removeLong(new long[] { 1, 2, 3}, 1));
MoreAsserts.assertEquals(new long[] { 1, 3, },
assertArrayEquals(new long[] { 1, 3, },
ArrayUtils.removeLong(new long[] { 1, 2, 3}, 2));
MoreAsserts.assertEquals(new long[] { 1, 2, },
assertArrayEquals(new long[] { 1, 2, },
ArrayUtils.removeLong(new long[] { 1, 2, 3}, 3));
MoreAsserts.assertEquals(new long[] { 2, 3, 1 },
assertArrayEquals(new long[] { 2, 3, 1 },
ArrayUtils.removeLong(new long[] { 1, 2, 3, 1 }, 1));
}
public void testConcatEmpty() throws Exception {
assertArrayEquals(new Long[] {},
concatElements(Long.class, null, null));
assertArrayEquals(new Long[] {},
concatElements(Long.class, new Long[] {}, null));
assertArrayEquals(new Long[] {},
concatElements(Long.class, null, new Long[] {}));
assertArrayEquals(new Long[] {},
concatElements(Long.class, new Long[] {}, new Long[] {}));
}
public void testconcatElements() throws Exception {
assertArrayEquals(new Long[] { 1L },
concatElements(Long.class, new Long[] { 1L }, new Long[] {}));
assertArrayEquals(new Long[] { 1L },
concatElements(Long.class, new Long[] {}, new Long[] { 1L }));
assertArrayEquals(new Long[] { 1L, 2L },
concatElements(Long.class, new Long[] { 1L }, new Long[] { 2L }));
assertArrayEquals(new Long[] { 1L, 2L, 3L, 4L },
concatElements(Long.class, new Long[] { 1L, 2L }, new Long[] { 3L, 4L }));
}
}