diff --git a/src/net/sqlcipher/DatabaseErrorHandler.java b/src/net/sqlcipher/DatabaseErrorHandler.java new file mode 100644 index 00000000..58096f1d --- /dev/null +++ b/src/net/sqlcipher/DatabaseErrorHandler.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2010 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 net.sqlcipher; + +import net.sqlcipher.database.SQLiteDatabase; + +/** + * An interface to let the apps define the actions to take when the following errors are detected + * database corruption + */ +public interface DatabaseErrorHandler { + + /** + * defines the method to be invoked when database corruption is detected. + * @param dbObj the {@link SQLiteDatabase} object representing the database on which corruption + * is detected. + */ + void onCorruption(SQLiteDatabase dbObj); +} diff --git a/src/net/sqlcipher/DefaultDatabaseErrorHandler.java b/src/net/sqlcipher/DefaultDatabaseErrorHandler.java new file mode 100644 index 00000000..6be52c67 --- /dev/null +++ b/src/net/sqlcipher/DefaultDatabaseErrorHandler.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2010 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 net.sqlcipher; + +import java.io.File; +import java.util.List; + +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteException; + +import android.util.Log; +import android.util.Pair; + +/** + * Default class used to define the actions to take when the database corruption is reported + * by sqlite. + *
+ * If null is specified for DatabaeErrorHandler param in the above calls, then this class is used + * as the default {@link DatabaseErrorHandler}. + */ +public final class DefaultDatabaseErrorHandler implements DatabaseErrorHandler { + + private static final String TAG = "DefaultDatabaseErrorHandler"; + + /** + * defines the default method to be invoked when database corruption is detected. + * @param dbObj the {@link SQLiteDatabase} object representing the database on which corruption + * is detected. + */ + public void onCorruption(SQLiteDatabase dbObj) { + // NOTE: Unlike the AOSP, this version does NOT attempt to delete any attached databases. + // TBD: Are we really certain that the attached databases would really be corrupt? + Log.e(TAG, "Corruption reported by sqlite on database, deleting: " + dbObj.getPath()); + + if (dbObj.isOpen()) { + Log.e(TAG, "Database object for corrupted database is already open, closing"); + + try { + dbObj.close(); + } catch (Exception e) { + /* ignored */ + Log.e(TAG, "Exception closing Database object for corrupted database, ignored", e); + } + } + + deleteDatabaseFile(dbObj.getPath()); + } + + private void deleteDatabaseFile(String fileName) { + if (fileName.equalsIgnoreCase(":memory:") || fileName.trim().length() == 0) { + return; + } + Log.e(TAG, "deleting the database file: " + fileName); + try { + new File(fileName).delete(); + } catch (Exception e) { + /* print warning and ignore exception */ + Log.w(TAG, "delete failed: " + e.getMessage()); + } + } +} diff --git a/src/net/sqlcipher/database/SQLiteDatabase.java b/src/net/sqlcipher/database/SQLiteDatabase.java index 4767ed4a..f0eefaca 100644 --- a/src/net/sqlcipher/database/SQLiteDatabase.java +++ b/src/net/sqlcipher/database/SQLiteDatabase.java @@ -19,6 +19,8 @@ import net.sqlcipher.Cursor; import net.sqlcipher.CrossProcessCursorWrapper; import net.sqlcipher.DatabaseUtils; +import net.sqlcipher.DatabaseErrorHandler; +import net.sqlcipher.DefaultDatabaseErrorHandler; import net.sqlcipher.SQLException; import net.sqlcipher.database.SQLiteDebug.DbStats; import net.sqlcipher.database.SQLiteDatabaseHook; @@ -82,12 +84,42 @@ public int status(int operation, boolean reset){ return native_status(operation, reset); } + /** + * Change the password of the open database using sqlite3_rekey(). + * + * @param password new database password + * + * @throws SQLiteException if there is an issue changing the password internally + * OR if the database is not open + * + * FUTURE @todo throw IllegalStateException if the database is not open and + * update the test suite + */ public void changePassword(String password) throws SQLiteException { - native_rekey(password); + /* safeguard: */ + if (!isOpen()) { + throw new SQLiteException("database not open"); + } + native_rekey(password); } + /** + * Change the password of the open database using sqlite3_rekey(). + * + * @param password new database password (char array) + * + * @throws SQLiteException if there is an issue changing the password internally + * OR if the database is not open + * + * FUTURE @todo throw IllegalStateException if the database is not open and + * update the test suite + */ public void changePassword(char[] password) throws SQLiteException { - native_rekey(password); + /* safeguard: */ + if (!isOpen()) { + throw new SQLiteException("database not open"); + } + native_rekey(password); } private static void loadICUData(Context context, File workingDir) { @@ -356,6 +388,11 @@ public static synchronized void loadLibs (Context context, File workingDir) { private int mCacheFullWarnings; private static final int MAX_WARNINGS_ON_CACHESIZE_CONDITION = 1; + /** {@link DatabaseErrorHandler} to be used when SQLite returns any of the following errors + * Corruption + * */ + private final DatabaseErrorHandler mErrorHandler; + /** maintain stats about number of cache hits and misses */ private int mNumCacheHits; private int mNumCacheMisses; @@ -432,19 +469,10 @@ public void setLockingEnabled(boolean lockingEnabled) { private boolean mLockingEnabled = true; /* package */ void onCorruption() { - Log.e(TAG, "Removing corrupt database: " + mPath); - // EventLog.writeEvent(EVENT_DB_CORRUPT, mPath); - try { - // Close the database (if we can), which will cause subsequent operations to fail. - close(); - } finally { - // Delete the corrupt file. Don't re-create it now -- that would just confuse people - // -- but the next time someone tries to open it, they can set it up from scratch. - if (!mPath.equalsIgnoreCase(":memory")) { - // delete is only for non-memory database files - new File(mPath).delete(); - } - } + Log.e(TAG, "Calling error handler for corrupt database (detected) " + mPath); + + // NOTE: DefaultDatabaseErrorHandler deletes the corrupt file, EXCEPT for memory database + mErrorHandler.onCorruption(this); } /** @@ -985,47 +1013,109 @@ public static SQLiteDatabase openDatabase(String path, String password, CursorFa * @param factory an optional factory class that is called to instantiate a * cursor when query is called, or null for default * @param flags to control database access mode and other options + * @param hook to run on pre/post key events (may be null) + * @param errorHandler The {@link DatabaseErrorHandler} to be used when sqlite reports database + * corruption (or null for default). + * + * @return the newly opened database + * + * @throws SQLiteException if the database cannot be opened + * @throws IllegalArgumentException if the database path is null + */ + public static SQLiteDatabase openDatabase(String path, char[] password, CursorFactory factory, int flags, SQLiteDatabaseHook databaseHook) { + return openDatabase(path, password, factory, flags, databaseHook, new DefaultDatabaseErrorHandler()); + } + + /** + * Open the database according to the flags {@link #OPEN_READWRITE} + * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS} + * with optional hook to run on pre/post key events. + * + *
Sets the locale of the database to the the system's current locale. + * Call {@link #setLocale} if you would like something else.
+ * + * @param path to database file to open and/or create + * @param password to use to open and/or create database file + * @param factory an optional factory class that is called to instantiate a + * cursor when query is called, or null for default + * @param flags to control database access mode and other options * @param hook to run on pre/post key events + * @param errorHandler The {@link DatabaseErrorHandler} to be used when sqlite reports database + * corruption (or null for default). * * @return the newly opened database * * @throws SQLiteException if the database cannot be opened * @throws IllegalArgumentException if the database path is null */ - public static SQLiteDatabase openDatabase(String path, char[] password, CursorFactory factory, int flags, SQLiteDatabaseHook hook) { + public static SQLiteDatabase openDatabase(String path, String password, CursorFactory factory, int flags, + SQLiteDatabaseHook hook, DatabaseErrorHandler errorHandler) { + return openDatabase(path, password.toCharArray(), factory, flags, hook, errorHandler); + } + + /** + * Open the database according to the flags {@link #OPEN_READWRITE} + * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS} + * with optional hook to run on pre/post key events. + * + *Sets the locale of the database to the the system's current locale. + * Call {@link #setLocale} if you would like something else.
+ * + * @param path to database file to open and/or create + * @param password to use to open and/or create database file (char array) + * @param factory an optional factory class that is called to instantiate a + * cursor when query is called, or null for default + * @param flags to control database access mode and other options + * @param hook to run on pre/post key events (may be null) + * @param errorHandler The {@link DatabaseErrorHandler} to be used when sqlite reports database + * corruption (or null for default). + * + * @return the newly opened database + * + * @throws SQLiteException if the database cannot be opened + * @throws IllegalArgumentException if the database path is null + */ + public static SQLiteDatabase openDatabase(String path, char[] password, CursorFactory factory, int flags, + SQLiteDatabaseHook hook, DatabaseErrorHandler errorHandler) { SQLiteDatabase sqliteDatabase = null; try { // Open the database. - sqliteDatabase = new SQLiteDatabase(path, password, factory, flags, hook); - if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { - sqliteDatabase.enableSqlTracing(path); - } - if (SQLiteDebug.DEBUG_SQL_TIME) { - sqliteDatabase.enableSqlProfiling(path); - } + sqliteDatabase = new SQLiteDatabase(path, factory, flags, errorHandler); + sqliteDatabase.openDatabaseInternal(password, hook); } catch (SQLiteDatabaseCorruptException e) { - // Try to recover from this, if we can. - // TODO: should we do this for other open failures? - Log.e(TAG, "Deleting and re-creating corrupt database " + path, e); - // EventLog.writeEvent(EVENT_DB_CORRUPT, path); - if (!path.equalsIgnoreCase(":memory")) { - // delete is only for non-memory database files - new File(path).delete(); - } - sqliteDatabase = new SQLiteDatabase(path, password, factory, flags, hook); + // Try to recover from this, if possible. + // FUTURE TBD: should we consider this for other open failures? + Log.e(TAG, "Calling error handler for corrupt database " + path, e); + + // NOTE: if this errorHandler.onCorruption() throws the exception _should_ + // bubble back to the original caller. + // DefaultDatabaseErrorHandler deletes the corrupt file, EXCEPT for memory database + errorHandler.onCorruption(sqliteDatabase); + + // try *once* again: + sqliteDatabase = new SQLiteDatabase(path, factory, flags, errorHandler); + sqliteDatabase.openDatabaseInternal(password, hook); + } + + if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { + sqliteDatabase.enableSqlTracing(path); + } + if (SQLiteDebug.DEBUG_SQL_TIME) { + sqliteDatabase.enableSqlProfiling(path); } synchronized (sActiveDatabases) { sActiveDatabases.put(sqliteDatabase, null); } + return sqliteDatabase; } /** * Equivalent to openDatabase(file.getPath(), password, factory, CREATE_IF_NECESSARY, databaseHook). */ - public static SQLiteDatabase openOrCreateDatabase(File file, String password, CursorFactory factory, SQLiteDatabaseHook databaseHook){ + public static SQLiteDatabase openOrCreateDatabase(File file, String password, CursorFactory factory, SQLiteDatabaseHook databaseHook) { return openOrCreateDatabase(file.getPath(), password, factory, databaseHook); } @@ -1039,10 +1129,25 @@ public static SQLiteDatabase openOrCreateDatabase(String path, String password, /** * Equivalent to openDatabase(path, password, factory, CREATE_IF_NECESSARY, databaseHook). */ + public static SQLiteDatabase openOrCreateDatabase(File file, String password, CursorFactory factory, SQLiteDatabaseHook databaseHook, + DatabaseErrorHandler errorHandler) { + return openDatabase(file.getPath(), password.toCharArray(), factory, CREATE_IF_NECESSARY, databaseHook, errorHandler); + } + + public static SQLiteDatabase openOrCreateDatabase(String path, String password, CursorFactory factory, SQLiteDatabaseHook databaseHook, + DatabaseErrorHandler errorHandler) { + return openDatabase(path, password.toCharArray(), factory, CREATE_IF_NECESSARY, databaseHook, errorHandler); + } + public static SQLiteDatabase openOrCreateDatabase(String path, char[] password, CursorFactory factory, SQLiteDatabaseHook databaseHook) { return openDatabase(path, password, factory, CREATE_IF_NECESSARY, databaseHook); } + public static SQLiteDatabase openOrCreateDatabase(String path, char[] password, CursorFactory factory, SQLiteDatabaseHook databaseHook, + DatabaseErrorHandler errorHandler) { + return openDatabase(path, password, factory, CREATE_IF_NECESSARY, databaseHook, errorHandler); + } + /** * Equivalent to openDatabase(file.getPath(), password, factory, CREATE_IF_NECESSARY). */ @@ -1274,8 +1379,21 @@ public void setPageSize(long numBytes) { * @param table the table to mark as syncable * @param deletedTable The deleted table that corresponds to the * syncable table + * + * @throws SQLiteException if there is an issue executing the sql to mark the table as syncable + * OR if the database is not open + * + * FUTURE @todo throw IllegalStateException if the database is not open and + * update the test suite + * + * NOTE: This method was deprecated by the AOSP in Android API 11. */ public void markTableSyncable(String table, String deletedTable) { + /* safeguard: */ + if (!isOpen()) { + throw new SQLiteException("database not open"); + } + markTableSyncable(table, "_id", table, deletedTable); } @@ -1289,9 +1407,21 @@ public void markTableSyncable(String table, String deletedTable) { * @param foreignKey this is the column in table whose value is an _id in * updateTable * @param updateTable this is the table that will have its _sync_dirty + * + * @throws SQLiteException if there is an issue executing the sql to mark the table as syncable + * + * FUTURE @todo throw IllegalStateException if the database is not open and + * update the test suite + * + * NOTE: This method was deprecated by the AOSP in Android API 11. */ public void markTableSyncable(String table, String foreignKey, String updateTable) { + /* safeguard: */ + if (!isOpen()) { + throw new SQLiteException("database not open"); + } + markTableSyncable(table, foreignKey, updateTable, null); } @@ -1307,6 +1437,8 @@ public void markTableSyncable(String table, String foreignKey, * @param updateTable this is the table that will have its _sync_dirty * @param deletedTable The deleted table that corresponds to the * updateTable + * + * @throws SQLiteException if there is an issue executing the sql */ private void markTableSyncable(String table, String foreignKey, String updateTable, String deletedTable) { @@ -2132,7 +2264,8 @@ protected void finalize() { * @throws IllegalArgumentException if the database path is null */ public SQLiteDatabase(String path, char[] password, CursorFactory factory, int flags) { - this(path, password, factory, flags, null); + this(path, factory, flags, null); + this.openDatabaseInternal(password, null); } /** @@ -2152,19 +2285,41 @@ public SQLiteDatabase(String path, char[] password, CursorFactory factory, int f * @throws IllegalArgumentException if the database path is null */ public SQLiteDatabase(String path, char[] password, CursorFactory factory, int flags, SQLiteDatabaseHook databaseHook) { + this(path, factory, flags, null); + this.openDatabaseInternal(password, databaseHook); + } + /** + * Private constructor (without database password) which DOES NOT attempt to open the database. + * + * @param path The full path to the database + * @param factory The factory to use when creating cursors, may be NULL. + * @param flags to control database access mode and other options + * @param errorHandler The {@link DatabaseErrorHandler} to be used when sqlite reports database + * corruption (or null for default). + * + * @throws IllegalArgumentException if the database path is null + */ + private SQLiteDatabase(String path, CursorFactory factory, int flags, DatabaseErrorHandler errorHandler) { if (path == null) { throw new IllegalArgumentException("path should not be null"); } + mFlags = flags; mPath = path; + mSlowQueryThreshold = -1;//SystemProperties.getInt(LOG_SLOW_QUERIES_PROPERTY, -1); mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); mFactory = factory; mPrograms = new WeakHashMapAccepts input param: a concrete instance of {@link DatabaseErrorHandler} to be + * used to handle corruption when sqlite reports database corruption.
+ * * @param context to use to open or create the database * @param name of the database file, or null for an in-memory database * @param factory to use for creating cursor objects, or null for the default * @param version number of the database (starting at 1); if the database is older, * {@link #onUpgrade} will be used to upgrade the database * @param hook to run on pre/post key events + * @param errorHandler the {@link DatabaseErrorHandler} to be used when sqlite reports database + * corruption. */ public SQLiteOpenHelper(Context context, String name, CursorFactory factory, - int version, SQLiteDatabaseHook hook) { + int version, SQLiteDatabaseHook hook, DatabaseErrorHandler errorHandler) { if (version < 1) throw new IllegalArgumentException("Version must be >= 1, was " + version); + if (errorHandler == null) { + throw new IllegalArgumentException("DatabaseErrorHandler param value can't be null."); + } mContext = context; mName = name; mFactory = factory; mNewVersion = version; mHook = hook; + mErrorHandler = errorHandler; } /** @@ -128,8 +158,8 @@ public synchronized SQLiteDatabase getWritableDatabase(char[] password) { File dbPathFile = new File (path); if (!dbPathFile.exists()) dbPathFile.getParentFile().mkdirs(); - - db = SQLiteDatabase.openOrCreateDatabase(path, password, mFactory, mHook); + + db = SQLiteDatabase.openOrCreateDatabase(path, password, mFactory, mHook, mErrorHandler); }