Merge changes from topic "rezolve" into stage-aosp-master

* changes:
  Bind update() args as Object[] for performance.
  Extend SQLiteQueryBuilder for update and delete.
This commit is contained in:
Jeff Sharkey
2018-12-03 17:58:23 +00:00
committed by Android (Google) Code Review
2 changed files with 239 additions and 37 deletions

View File

@@ -12676,6 +12676,7 @@ package android.database.sqlite {
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);
@@ -12685,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

@@ -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,7 +44,8 @@ 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*)?");
@@ -94,13 +104,10 @@ public class SQLiteQueryBuilder {
*
* @param inWhere the chunk of text to append to the WHERE clause.
*/
public void appendWhere(@NonNull CharSequence inWhere) {
public void appendWhere(CharSequence inWhere) {
if (mWhereClause == null) {
mWhereClause = new StringBuilder(inWhere.length() + 16);
}
if (mWhereClause.length() == 0) {
mWhereClause.append('(');
}
mWhereClause.append(inWhere);
}
@@ -114,13 +121,10 @@ 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(@NonNull String inWhere) {
public void appendWhereEscapeString(String inWhere) {
if (mWhereClause == null) {
mWhereClause = new StringBuilder(inWhere.length() + 16);
}
if (mWhereClause.length() == 0) {
mWhereClause.append('(');
}
DatabaseUtils.appendEscapedSQLString(mWhereClause, inWhere);
}
@@ -400,6 +404,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.
@@ -408,24 +417,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
@@ -458,28 +607,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);
}
@@ -496,6 +627,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
@@ -670,4 +837,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 + ")";
}
}
}