diff --git a/build.gradle b/build.gradle index 6975f17..01dd35f 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { google() } dependencies { - def gradleToolsVersion = "8.9.0" + def gradleToolsVersion = "9.3.0" classpath "com.android.tools.build:gradle:${gradleToolsVersion}" } } @@ -15,6 +15,7 @@ plugins { id('maven-publish') id('signing') id("io.github.gradle-nexus.publish-plugin") version "1.1.0" + id('com.google.devtools.ksp') version '2.3.10' } allprojects { @@ -33,10 +34,10 @@ project.ext { if(project.hasProperty('sqlcipherAndroidVersion') && "${sqlcipherAndroidVersion}") { libraryVersion = "${sqlcipherAndroidVersion}" } else { - libraryVersion = "4.17.0" + libraryVersion = "4.18.0" } minSdkVersion = 23 - androidXSQLiteVersion = "2.6.2" + androidXSQLiteVersion = "2.7.0" roomVersion = "2.8.4" androidNdkVersion = "25.2.9519653" mavenLocalRepositoryPrefix = "file://" diff --git a/gradle.properties b/gradle.properties index 223c6a0..e3164b5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,5 +17,4 @@ # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -android.enableJetifier=true android.useAndroidX=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ff702a8..b8144b8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ -#Thu Feb 19 08:45:35 CST 2026 +#Wed Jul 29 08:02:53 CDT 2026 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=f397b287023acdba1e9f6fc5ea72d22dd63669d59ed4a289a29b1a76eee151c6 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionSha256Sum=553c78f50dafcd54d65b9a444649057857469edf836431389695608536d6b746 +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/sqlcipher/build.gradle b/sqlcipher/build.gradle index 872bcb6..1ee6345 100644 --- a/sqlcipher/build.gradle +++ b/sqlcipher/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'com.android.library' apply plugin: "maven-publish" apply plugin: "signing" +apply plugin: "com.google.devtools.ksp" import org.gradle.internal.logging.text.StyledTextOutputFactory import static org.gradle.internal.logging.text.StyledTextOutput.Style @@ -11,14 +12,14 @@ import static java.nio.file.StandardCopyOption.REPLACE_EXISTING def log = services.get(StyledTextOutputFactory).create("sqlcipher") android { - compileSdkVersion 34 + compileSdkVersion 37 namespace "net.zetetic.database" defaultConfig { minSdkVersion "${rootProject.ext.minSdkVersion}" - targetSdkVersion 34 + targetSdkVersion 37 versionCode 1 versionName "${rootProject.ext.libraryVersion}" - project.archivesBaseName = "sqlcipher-android-${versionName}" + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' // The following argument makes the Android Test Orchestrator run its // "pm clear" command after each test invocation. This command ensures @@ -26,15 +27,19 @@ android { testInstrumentationRunnerArguments clearPackageData: 'true' consumerProguardFiles 'consumer-rules.pro' } + base { + archivesName = "sqlcipher-android-${rootProject.ext.libraryVersion}" + } + testOptions { + execution 'ANDROIDX_TEST_ORCHESTRATOR' + } buildTypes { release { minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } releaseNoX86 { minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' ndk { abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86_64' } @@ -59,9 +64,6 @@ android { ndkVersion "${rootProject.ext.androidNdkVersion}" - useLibrary 'android.test.base' // for android.test.AndroidTestCase - useLibrary 'android.test.runner' // for android.test.MoreAsserts - java { toolchain { languageVersion = JavaLanguageVersion.of(17) @@ -153,10 +155,15 @@ dependencies { // Needed for the Support API interfaces implementation "androidx.sqlite:sqlite:${rootProject.ext.androidXSQLiteVersion}" - // Needed for the Support API in tests - androidTestImplementation "androidx.room:room-common:${rootProject.ext.roomVersion}" - androidTestImplementation "androidx.room:room-runtime:${rootProject.ext.roomVersion}" - androidTestAnnotationProcessor "androidx.room:room-compiler:${rootProject.ext.roomVersion}" + // Needed for the Room library in tests + def room_version = "3.0.1" + androidTestImplementation "androidx.room3:room3-runtime:$room_version" + kspAndroidTest "androidx.room3:room3-compiler:$room_version" + // Needed for roomDatabase.getSupportWrapper() tests + androidTestImplementation "androidx.room3:room3-sqlite-wrapper:$room_version" + + // Needed for runTest + androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0" // Needed for supporting tests androidTestImplementation 'androidx.test.ext:junit:1.3.0' @@ -164,6 +171,7 @@ dependencies { androidTestImplementation 'androidx.test:core:1.7.0' androidTestImplementation 'org.hamcrest:hamcrest-library:3.0' testImplementation 'junit:junit:4.13.2' + androidTestUtil 'androidx.test:orchestrator:1.6.1' } allprojects { diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/AbstractCursorTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/AbstractCursorTest.java similarity index 97% rename from sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/AbstractCursorTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/AbstractCursorTest.java index 2db7b1e..d90e44f 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/AbstractCursorTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/AbstractCursorTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.zetetic.database.database_cts; +package net.zetetic.database; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -23,14 +23,12 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import android.annotation.SuppressLint; import android.content.Context; import android.database.CharArrayBuffer; import android.database.ContentObserver; import android.database.CursorIndexOutOfBoundsException; import android.database.DataSetObserver; -import net.zetetic.database.AbstractCursor; -import net.zetetic.database.CursorWindow; + import net.zetetic.database.sqlcipher.SQLiteDatabase; import android.net.Uri; @@ -42,7 +40,6 @@ import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -193,8 +190,6 @@ public void testOnChange() throws InterruptedException { assertTrue(mock.hadCalledOnChange()); } - @SuppressLint("IgnoreWithoutReason") - @Ignore @Test public void testOnMove() { assertFalse(mTestAbstractCursor.getOnMoveRet()); @@ -209,8 +204,6 @@ public void testOnMove() { assertEquals(5, mTestAbstractCursor.getNewPos()); } - @SuppressLint("IgnoreWithoutReason") - @Ignore @Test public void testOnMove_samePosition() { mTestAbstractCursor.moveToFirst(); @@ -367,8 +360,6 @@ public void testDeactivate() { assertTrue(mock.hadCalledOnInvalid()); } - @SuppressLint("IgnoreWithoutReason") - @Ignore @Test public void testCopyStringToBuffer() { CharArrayBuffer ca = new CharArrayBuffer(1000); @@ -384,8 +375,6 @@ public void testCopyStringToBuffer() { assertEquals(sb.toString(), new String(ca.data, 0, ca.sizeCopied)); } - @SuppressLint("IgnoreWithoutReason") - @Ignore @Test public void testCheckPosition() { // Test with position = -1. @@ -527,6 +516,7 @@ public boolean onMove(int oldPosition, int newPosition) { mOldPosition = oldPosition; mNewPosition = newPosition; mRowsMovedSum += Math.abs(newPosition - oldPosition); + mOnMoveReturnValue = true; return mOnMoveReturnValue; } @@ -542,7 +532,14 @@ public String[] getColumnNames() { @Override public String getString(int columnIndex) { - Object cell = mRows[mPos].get(columnIndex); + if(mPos < 0){ + return null; + } + var row = mRows[mPos]; + if(row == null) { + return null; + } + Object cell = row.get(columnIndex); return (cell == null) ? null : cell.toString(); } @@ -583,7 +580,7 @@ public boolean isNull(int column) { @Override public int getType(int column) { - return 0; + return FIELD_TYPE_STRING; } public boolean hadCalledOnChange() { diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/CursorJoinerTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/CursorJoinerTest.java similarity index 99% rename from sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/CursorJoinerTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/CursorJoinerTest.java index 494a386..866ff87 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/CursorJoinerTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/CursorJoinerTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.zetetic.database.database_cts; +package net.zetetic.database; import static org.junit.Assert.assertEquals; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/CursorWindowTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/CursorWindowTest.java similarity index 96% rename from sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/CursorWindowTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/CursorWindowTest.java index 02df1b4..2a13ee1 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/CursorWindowTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/CursorWindowTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.zetetic.database.database_cts; +package net.zetetic.database; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -28,9 +28,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; -import net.zetetic.database.CursorWindow; -import net.zetetic.database.MatrixCursor; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -106,6 +103,23 @@ public void testNull() { assertNull(window.getBlob(0, 0)); } + @Test + public void testShouldNotThrowNullPointerExceptionWhenNullValueReturnedFromGetString() { + var window = getOneByOneWindow(); + assertTrue(window.putNull(0, 0)); + var buffer = new CharArrayBuffer(16); + window.copyStringToBuffer(0, 0, buffer); + } + + @Test + public void testShouldSetSizeCopiedToZeroWhenNullValueReturnedFromGetString() { + var window = getOneByOneWindow(); + assertTrue(window.putNull(0, 0)); + var buffer = new CharArrayBuffer(16); + window.copyStringToBuffer(0, 0, buffer); + assertEquals(0, buffer.sizeCopied); + } + @Test public void testEmptyString() { CursorWindow window = getOneByOneWindow(); diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/CursorWrapperTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/CursorWrapperTest.java similarity index 99% rename from sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/CursorWrapperTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/CursorWrapperTest.java index 4372c2f..3e89e5d 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/CursorWrapperTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/CursorWrapperTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.zetetic.database.database_cts; +package net.zetetic.database; import static org.junit.Assert.assertEquals; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/DatabaseCursorTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/DatabaseCursorTest.java similarity index 99% rename from sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/DatabaseCursorTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/DatabaseCursorTest.java index 4d161d1..9f17260 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/DatabaseCursorTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/DatabaseCursorTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.zetetic.database.database_cts; +package net.zetetic.database; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -29,7 +29,7 @@ import android.database.CursorIndexOutOfBoundsException; import android.database.CursorWrapper; import android.database.DataSetObserver; -import net.zetetic.database.DatabaseUtils; + import net.zetetic.database.sqlcipher.SQLiteCursor; import net.zetetic.database.sqlcipher.SQLiteCursorDriver; import net.zetetic.database.sqlcipher.SQLiteDatabase; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/DatabaseUtilsInsertHelperTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/DatabaseUtilsInsertHelperTest.java similarity index 99% rename from sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/DatabaseUtilsInsertHelperTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/DatabaseUtilsInsertHelperTest.java index 1d97d19..238ff8d 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/DatabaseUtilsInsertHelperTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/DatabaseUtilsInsertHelperTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.zetetic.database.database_cts; +package net.zetetic.database; import static org.junit.Assert.assertArrayEquals; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/DatabaseUtilsTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/DatabaseUtilsTest.java similarity index 99% rename from sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/DatabaseUtilsTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/DatabaseUtilsTest.java index a130cf1..aca138d 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/DatabaseUtilsTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/DatabaseUtilsTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.zetetic.database.database_cts; +package net.zetetic.database; import static org.junit.Assert.assertArrayEquals; @@ -36,7 +36,6 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; -import net.zetetic.database.DatabaseUtils; import net.zetetic.database.DatabaseUtils.InsertHelper; import net.zetetic.database.sqlcipher.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteStatement; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/MergeCursorTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/MergeCursorTest.java similarity index 99% rename from sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/MergeCursorTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/MergeCursorTest.java index 4671c7d..8c45eeb 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/MergeCursorTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/MergeCursorTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.zetetic.database.database_cts; +package net.zetetic.database; import static org.junit.Assert.assertEquals; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/README b/sqlcipher/src/androidTest/java/net/zetetic/database/README similarity index 100% rename from sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/README rename to sqlcipher/src/androidTest/java/net/zetetic/database/README diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/AndroidSQLCipherTestCase.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/AndroidSQLCipherTestCase.java similarity index 98% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/AndroidSQLCipherTestCase.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/AndroidSQLCipherTestCase.java index 07bc2ef..270657e 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/AndroidSQLCipherTestCase.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/AndroidSQLCipherTestCase.java @@ -1,4 +1,4 @@ -package net.zetetic.database.sqlcipher_cts; +package net.zetetic.database.sqlcipher; import android.content.Context; import android.icu.text.NumberFormat; @@ -7,16 +7,12 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; -import net.zetetic.database.sqlcipher.SQLiteDatabase; - import org.junit.After; import org.junit.Before; -import org.junit.Test; import org.junit.runner.RunWith; import java.io.File; import java.io.FileOutputStream; -import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.SecureRandom; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/CipherCompatibilityTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/CipherCompatibilityTest.java similarity index 82% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/CipherCompatibilityTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/CipherCompatibilityTest.java index 7f0c2d6..9346571 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/CipherCompatibilityTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/CipherCompatibilityTest.java @@ -1,18 +1,13 @@ -package net.zetetic.database.sqlcipher_cts; +package net.zetetic.database.sqlcipher; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.MatcherAssert.assertThat; import android.database.Cursor; -import net.zetetic.database.sqlcipher.SQLiteConnection; -import net.zetetic.database.sqlcipher.SQLiteDatabase; -import net.zetetic.database.sqlcipher.SQLiteDatabaseHook; - import org.junit.Test; import java.io.File; -import java.io.IOException; public class CipherCompatibilityTest extends AndroidSQLCipherTestCase { diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_android/DatabaseGeneralTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/DatabaseGeneralTest.java similarity index 99% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_android/DatabaseGeneralTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/DatabaseGeneralTest.java index 636aefa..0541c0c 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_android/DatabaseGeneralTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/DatabaseGeneralTest.java @@ -15,7 +15,7 @@ */ // modified from original source see README at the top level of this project -package net.zetetic.database.sqlcipher_android; +package net.zetetic.database.sqlcipher; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; @@ -27,7 +27,6 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import android.annotation.SuppressLint; import android.content.ContentValues; import android.content.Context; import android.database.CharArrayBuffer; @@ -44,12 +43,10 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; -import net.zetetic.database.sqlcipher.SQLiteDatabase; -import net.zetetic.database.sqlcipher.SQLiteStatement; import net.zetetic.database.DefaultDatabaseErrorHandler; -import junit.framework.Assert; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/DatabaseStatementTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/DatabaseStatementTest.java similarity index 98% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/DatabaseStatementTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/DatabaseStatementTest.java index 29e465b..8da26f4 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/DatabaseStatementTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/DatabaseStatementTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.zetetic.database.sqlcipher_cts; +package net.zetetic.database.sqlcipher; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -28,9 +28,6 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; -import net.zetetic.database.sqlcipher.SQLiteDatabase; -import net.zetetic.database.sqlcipher.SQLiteStatement; - import org.junit.After; import org.junit.Before; import org.junit.Test; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/ImportUnencryptedDatabaseTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/ImportUnencryptedDatabaseTest.java similarity index 94% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/ImportUnencryptedDatabaseTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/ImportUnencryptedDatabaseTest.java index 6d56676..9a1f981 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/ImportUnencryptedDatabaseTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/ImportUnencryptedDatabaseTest.java @@ -1,12 +1,10 @@ -package net.zetetic.database.sqlcipher_cts; +package net.zetetic.database.sqlcipher; import static org.hamcrest.Matchers.is; import static org.hamcrest.MatcherAssert.assertThat; import android.database.Cursor; -import net.zetetic.database.sqlcipher.SQLiteDatabase; - import org.junit.Test; import java.io.File; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/JsonCastTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/JsonCastTest.java similarity index 93% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/JsonCastTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/JsonCastTest.java index d9fc707..9b2b6ce 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/JsonCastTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/JsonCastTest.java @@ -1,4 +1,4 @@ -package net.zetetic.database.sqlcipher_cts; +package net.zetetic.database.sqlcipher; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/MigrateDatabaseFrom1xFormatToCurrentFormatTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/MigrateDatabaseFrom1xFormatToCurrentFormatTest.java similarity index 88% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/MigrateDatabaseFrom1xFormatToCurrentFormatTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/MigrateDatabaseFrom1xFormatToCurrentFormatTest.java index a203de5..f66ac96 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/MigrateDatabaseFrom1xFormatToCurrentFormatTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/MigrateDatabaseFrom1xFormatToCurrentFormatTest.java @@ -1,14 +1,10 @@ -package net.zetetic.database.sqlcipher_cts; +package net.zetetic.database.sqlcipher; import static org.hamcrest.Matchers.is; import static org.hamcrest.MatcherAssert.assertThat; import android.database.Cursor; -import net.zetetic.database.sqlcipher.SQLiteConnection; -import net.zetetic.database.sqlcipher.SQLiteDatabase; -import net.zetetic.database.sqlcipher.SQLiteDatabaseHook; - import org.junit.Test; import java.io.File; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/README b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/README similarity index 100% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/README rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/README diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherDatabaseTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLCipherDatabaseTest.java similarity index 98% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherDatabaseTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLCipherDatabaseTest.java index 0e59dfb..c8564b4 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherDatabaseTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLCipherDatabaseTest.java @@ -1,6 +1,7 @@ -package net.zetetic.database.sqlcipher_cts; +package net.zetetic.database.sqlcipher; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsNull.nullValue; import static org.junit.Assert.fail; @@ -10,15 +11,11 @@ import android.database.sqlite.SQLiteException; import android.util.Log; -import net.zetetic.database.sqlcipher.SQLiteCursor; -import net.zetetic.database.sqlcipher.SQLiteDatabase; -import net.zetetic.database.sqlcipher.SQLiteDatabaseConfiguration; -import net.zetetic.database.sqlcipher.SQLiteStatement; - import org.junit.Test; import java.io.File; import java.io.UnsupportedEncodingException; +import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -30,6 +27,14 @@ public class SQLCipherDatabaseTest extends AndroidSQLCipherTestCase { + @Test + public void shouldDeclareBeginTransactionWithListenerReadOnlyOverride() throws NoSuchMethodException { + Method method = SQLiteDatabase.class.getDeclaredMethod( + "beginTransactionWithListenerReadOnly", + android.database.sqlite.SQLiteTransactionListener.class); + assertThat(method, is(notNullValue())); + } + @Test public void testCreateDatabaseConnectionWithStringPassword() { try { diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherOpenHelperTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLCipherOpenHelperTest.java similarity index 92% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherOpenHelperTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLCipherOpenHelperTest.java index 6cd997d..1e26adb 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherOpenHelperTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLCipherOpenHelperTest.java @@ -1,4 +1,4 @@ -package net.zetetic.database.sqlcipher_cts; +package net.zetetic.database.sqlcipher; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.core.Is.is; @@ -7,12 +7,6 @@ import android.content.Context; import android.util.Log; -import net.zetetic.database.sqlcipher.SQLiteConnection; -import net.zetetic.database.sqlcipher.SQLiteDatabase; -import net.zetetic.database.sqlcipher.SQLiteDatabaseHook; -import net.zetetic.database.sqlcipher.SQLiteOpenHelper; -import net.zetetic.database.sqlcipher.SQLiteStatement; - import org.junit.Test; import java.io.File; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherVersionTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLCipherVersionTest.java similarity index 83% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherVersionTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLCipherVersionTest.java index a5ea345..7f45579 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherVersionTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLCipherVersionTest.java @@ -1,4 +1,4 @@ -package net.zetetic.database.sqlcipher_cts; +package net.zetetic.database.sqlcipher; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.StringContains.containsString; @@ -17,6 +17,6 @@ public void shouldExtractLibraryCipherVersion() { cipherVersion = cursor.getString(0); cursor.close(); } - assertThat(cipherVersion, containsString("4.14.0")); + assertThat(cipherVersion, containsString("4.18.0")); } } diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/SQLCipherWALTestScenario.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLCipherWALTestScenario.java similarity index 95% rename from sqlcipher/src/androidTest/java/net/zetetic/database/SQLCipherWALTestScenario.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLCipherWALTestScenario.java index 1540587..d76c198 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/SQLCipherWALTestScenario.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLCipherWALTestScenario.java @@ -1,4 +1,4 @@ -package net.zetetic.database; +package net.zetetic.database.sqlcipher; import static org.junit.Assert.assertEquals; @@ -14,10 +14,6 @@ import org.junit.Test; import org.junit.runner.RunWith; -import net.zetetic.database.sqlcipher.SQLiteConnection; -import net.zetetic.database.sqlcipher.SQLiteDatabase; -import net.zetetic.database.sqlcipher.SQLiteOpenHelper; - import java.io.File; import java.io.FileInputStream; import java.util.Arrays; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteAbortExceptionTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteAbortExceptionTest.java similarity index 95% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteAbortExceptionTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteAbortExceptionTest.java index 51c1873..f7b7e7e 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteAbortExceptionTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteAbortExceptionTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.zetetic.database.sqlcipher_cts; +package net.zetetic.database.sqlcipher; import android.database.sqlite.SQLiteAbortException; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteClosableTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteClosableTest.java similarity index 96% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteClosableTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteClosableTest.java index e1d5e14..e1a61c0 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteClosableTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteClosableTest.java @@ -14,14 +14,12 @@ * limitations under the License. */ -package net.zetetic.database.sqlcipher_cts; +package net.zetetic.database.sqlcipher; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import net.zetetic.database.sqlcipher.SQLiteClosable; - import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteConstraintExceptionTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteConstraintExceptionTest.java similarity index 95% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteConstraintExceptionTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteConstraintExceptionTest.java index 261f865..1d9f97e 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteConstraintExceptionTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteConstraintExceptionTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.zetetic.database.sqlcipher_cts; +package net.zetetic.database.sqlcipher; import android.database.sqlite.SQLiteConstraintException; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteCursorTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteCursorTest.java similarity index 98% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteCursorTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteCursorTest.java index 1fc54cf..3cfe7c7 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteCursorTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteCursorTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.zetetic.database.sqlcipher_cts; +package net.zetetic.database.sqlcipher; import static org.junit.Assert.assertEquals; @@ -33,9 +33,6 @@ import net.zetetic.database.AbstractCursor; import net.zetetic.database.CursorWindow; -import net.zetetic.database.sqlcipher.SQLiteCursor; -import net.zetetic.database.sqlcipher.SQLiteDatabase; -import net.zetetic.database.sqlcipher.SQLiteDirectCursorDriver; import org.junit.After; import org.junit.Before; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDatabaseCorruptExceptionTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteDatabaseCorruptExceptionTest.java similarity index 95% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDatabaseCorruptExceptionTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteDatabaseCorruptExceptionTest.java index 378e08f..09a1928 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDatabaseCorruptExceptionTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteDatabaseCorruptExceptionTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.zetetic.database.sqlcipher_cts; +package net.zetetic.database.sqlcipher; import android.database.sqlite.SQLiteDatabaseCorruptException; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDatabaseTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteDatabaseTest.java similarity index 99% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDatabaseTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteDatabaseTest.java index cccd81f..4b4d75e 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDatabaseTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteDatabaseTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.zetetic.database.sqlcipher_cts; +package net.zetetic.database.sqlcipher; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -31,13 +31,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import net.zetetic.database.DatabaseUtils; -import net.zetetic.database.sqlcipher.SQLiteCursor; -import net.zetetic.database.sqlcipher.SQLiteCursorDriver; -import net.zetetic.database.sqlcipher.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteDatabase.CursorFactory; -import net.zetetic.database.sqlcipher.SQLiteQuery; -import net.zetetic.database.sqlcipher.SQLiteStatement; -import net.zetetic.database.sqlcipher.SQLiteTransactionListener; import org.junit.After; import org.junit.Before; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDiskIOExceptionTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteDiskIOExceptionTest.java similarity index 95% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDiskIOExceptionTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteDiskIOExceptionTest.java index febd2e3..7d074f2 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDiskIOExceptionTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteDiskIOExceptionTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.zetetic.database.sqlcipher_cts; +package net.zetetic.database.sqlcipher; import android.database.sqlite.SQLiteDiskIOException; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDoneExceptionTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteDoneExceptionTest.java similarity index 95% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDoneExceptionTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteDoneExceptionTest.java index 384903a..64c4880 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDoneExceptionTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteDoneExceptionTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.zetetic.database.sqlcipher_cts; +package net.zetetic.database.sqlcipher; import android.database.sqlite.SQLiteDoneException; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteExceptionTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteExceptionTest.java similarity index 95% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteExceptionTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteExceptionTest.java index 92d2c56..6cb88ee 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteExceptionTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteExceptionTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.zetetic.database.sqlcipher_cts; +package net.zetetic.database.sqlcipher; import android.database.sqlite.SQLiteException; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteFtsTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteFtsTest.java similarity index 97% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteFtsTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteFtsTest.java index 4708ddd..7fbff11 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteFtsTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteFtsTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.zetetic.database.sqlcipher_cts; +package net.zetetic.database.sqlcipher; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -23,13 +23,10 @@ import android.content.Context; import android.content.res.Resources; import android.database.Cursor; -import android.test.AndroidTestCase; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; -import net.zetetic.database.sqlcipher.SQLiteDatabase; - import org.junit.After; import org.junit.Before; import org.junit.Test; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteFullExceptionTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteFullExceptionTest.java similarity index 95% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteFullExceptionTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteFullExceptionTest.java index 2e62200..1f62840 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteFullExceptionTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteFullExceptionTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.zetetic.database.sqlcipher_cts; +package net.zetetic.database.sqlcipher; import android.database.sqlite.SQLiteFullException; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteMisuseExceptionTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteMisuseExceptionTest.java similarity index 95% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteMisuseExceptionTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteMisuseExceptionTest.java index 3b91cd5..17e466f 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteMisuseExceptionTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteMisuseExceptionTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.zetetic.database.sqlcipher_cts; +package net.zetetic.database.sqlcipher; import android.database.sqlite.SQLiteMisuseException; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteOpenHelperTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteOpenHelperTest.java similarity index 94% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteOpenHelperTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteOpenHelperTest.java index 5adc5c7..cee20c1 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteOpenHelperTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteOpenHelperTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.zetetic.database.sqlcipher_cts; +package net.zetetic.database.sqlcipher; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -29,12 +29,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; -import net.zetetic.database.sqlcipher.SQLiteCursor; -import net.zetetic.database.sqlcipher.SQLiteCursorDriver; -import net.zetetic.database.sqlcipher.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteDatabase.CursorFactory; -import net.zetetic.database.sqlcipher.SQLiteOpenHelper; -import net.zetetic.database.sqlcipher.SQLiteQuery; import org.junit.Before; import org.junit.Test; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteProgramTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteProgramTest.java similarity index 97% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteProgramTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteProgramTest.java index 5317b03..058e9b0 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteProgramTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteProgramTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.zetetic.database.sqlcipher_cts; +package net.zetetic.database.sqlcipher; import static org.junit.Assert.assertArrayEquals; @@ -31,9 +31,6 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; -import net.zetetic.database.sqlcipher.SQLiteDatabase; -import net.zetetic.database.sqlcipher.SQLiteStatement; - import org.junit.After; import org.junit.Before; import org.junit.Test; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteQueryBuilderTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteQueryBuilderTest.java similarity index 97% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteQueryBuilderTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteQueryBuilderTest.java index 99a7ac1..2258aa5 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteQueryBuilderTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteQueryBuilderTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.zetetic.database.sqlcipher_cts; +package net.zetetic.database.sqlcipher; import android.database.Cursor; @@ -24,12 +24,6 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; -import net.zetetic.database.sqlcipher.SQLiteCursor; -import net.zetetic.database.sqlcipher.SQLiteCursorDriver; -import net.zetetic.database.sqlcipher.SQLiteDatabase; -import net.zetetic.database.sqlcipher.SQLiteQuery; -import net.zetetic.database.sqlcipher.SQLiteQueryBuilder; - import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -55,10 +49,12 @@ public class SQLiteQueryBuilderTest { @Before public void setUp() throws Exception { System.loadLibrary("sqlcipher"); - File f = ApplicationProvider.getApplicationContext().getDatabasePath(DATABASE_FILE); - f.mkdirs(); - if (f.exists()) { f.delete(); } - mDatabase = SQLiteDatabase.openOrCreateDatabase(f,null); + File databasePath = ApplicationProvider.getApplicationContext().getDatabasePath(DATABASE_FILE); + databasePath.mkdirs(); + if (databasePath.exists()) { + databasePath.delete(); + } + mDatabase = SQLiteDatabase.openOrCreateDatabase(databasePath,null); assertNotNull(mDatabase); } diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteQueryTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteQueryTest.java new file mode 100644 index 0000000..1da5321 --- /dev/null +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteQueryTest.java @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2009 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.zetetic.database.sqlcipher; + + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import android.os.CancellationSignal; + +import androidx.sqlite.db.SupportSQLiteProgram; + +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +public class SQLiteQueryTest extends AndroidSQLCipherTestCase { + + private RecordingProgram program; + + @Before + public void setup(){ + program = new RecordingProgram(); + } + + private SQLiteQuery newQuery(String sql) { + try { + var ctor = SQLiteQuery.class.getDeclaredConstructor( + SQLiteDatabase.class, + String.class, + CancellationSignal.class); + ctor.setAccessible(true); + return ctor.newInstance(database, sql, new CancellationSignal()); + } catch (Exception ex){ + throw new RuntimeException(ex); + } + } + + @Test + public void shouldReturnExactStatementWithGetSql() { + var sql = "SELECT ?, ?"; + try (SQLiteQuery query = newQuery(sql)) { + assertEquals(sql, query.getSql()); + } + } + + @Test + public void shouldMatchPlaceholderCountFromGetArgCount() { + try (SQLiteQuery query = newQuery("SELECT ?, ?, ?")) { + assertEquals(3, query.getArgCount()); + } + } + + @Test + public void shouldReturnZeroFromGetArgCountWhenNoPlaceholders() { + try (SQLiteQuery query = newQuery("SELECT 1")) { + assertEquals(0, query.getArgCount()); + } + } + + @Test + public void shouldBindLongValue() { + try (SQLiteQuery query = newQuery("SELECT ?")) { + query.bindLong(1, 42L); + query.bindTo(program); + assertEquals(1, program.calls.size()); + program.calls.get(0).assertLong(1, 42L); + } + } + + @Test + public void shouldBindDoubleValue() { + try (SQLiteQuery query = newQuery("SELECT ?")) { + query.bindDouble(1, 3.14159); + query.bindTo(program); + assertEquals(1, program.calls.size()); + program.calls.get(0).assertDouble(1, 3.14159); + } + } + + @Test + public void shouldBindStringValue() { + try (SQLiteQuery query = newQuery("SELECT ?")) { + query.bindString(1, "hello world"); + query.bindTo(program); + assertEquals(1, program.calls.size()); + program.calls.get(0).assertString(1, "hello world"); + } + } + + @Test + public void shouldBindBlobValue() { + var blob = new byte[] { 1, 2, 3, 4, 5 }; + try (SQLiteQuery query = newQuery("SELECT ?")) { + query.bindBlob(1, blob); + query.bindTo(program); + assertEquals(1, program.calls.size()); + program.calls.get(0).assertBlob(1, blob); + } + } + + @Test + public void shouldBindsNullWhenExplicitlyBound() { + try (SQLiteQuery query = newQuery("SELECT ?")) { + query.bindNull(1); + query.bindTo(program); + assertEquals(1, program.calls.size()); + program.calls.get(0).assertNull(1); + } + } + + @Test + public void shouldBindNullWhenArgLeftUnbound() { + try (SQLiteQuery query = newQuery("SELECT ?")) { + query.bindTo(program); + assertEquals(1, program.calls.size()); + program.calls.get(0).assertNull(1); + } + } + + @Test + public void shouldBindsMultipleArgsInOrderWithMixedTypes() { + var blob = new byte[] { 9, 8, 7 }; + try (SQLiteQuery query = newQuery("SELECT ?, ?, ?, ?, ?")) { + query.bindLong(1, 100L); + query.bindString(2, "middle"); + query.bindDouble(3, 2.5); + query.bindNull(4); + query.bindBlob(5, blob); + query.bindTo(program); + assertEquals(5, program.calls.size()); + program.calls.get(0).assertLong(1, 100L); + program.calls.get(1).assertString(2, "middle"); + program.calls.get(2).assertDouble(3, 2.5); + program.calls.get(3).assertNull(4); + program.calls.get(4).assertBlob(5, blob); + } + } + + @Test + public void shouldPerformNoOpBindWhenStatementHasNoParameters() { + try (SQLiteQuery query = newQuery("SELECT 1")) { + query.bindTo(program); + assertEquals(0, program.calls.size()); + } + } + + @Test + public void shouldBindAllArgsThenBindToTranslatesEachSupportedType() { + var blob = new byte[] { 0x0A, 0x0B }; + try (SQLiteQuery query = newQuery("SELECT ?, ?, ?, ?, ?, ?")) { + query.bindAllArgs(true, (byte) 7, (short) 8, 9, blob, "text"); + query.bindTo(program); + assertEquals(6, program.calls.size()); + program.calls.get(0).assertLong(1, 1L); + program.calls.get(1).assertLong(2, 7L); + program.calls.get(2).assertLong(3, 8L); + program.calls.get(3).assertLong(4, 9L); + program.calls.get(4).assertBlob(5, blob); + program.calls.get(5).assertString(6, "text"); + } + } + + private static final class RecordingProgram implements SupportSQLiteProgram { + final List calls = new ArrayList<>(); + + @Override + public void bindNull(int index) { + calls.add(Call.ofNull(index)); + } + + @Override + public void bindLong(int index, long value) { + calls.add(Call.ofLong(index, value)); + } + + @Override + public void bindDouble(int index, double value) { + calls.add(Call.ofDouble(index, value)); + } + + @Override + public void bindString(int index, String value) { + calls.add(Call.ofString(index, value)); + } + + @Override + public void bindBlob(int index, byte[] value) { + calls.add(Call.ofBlob(index, value)); + } + + @Override + public void clearBindings() { + calls.clear(); + } + + @Override + public void close() {} + } + + private static final class Call { + enum Kind { + NULL, + LONG, + DOUBLE, + STRING, + BLOB + } + + final Kind kind; + final int index; + final long longValue; + final double doubleValue; + final String stringValue; + final byte[] blobValue; + + private Call( + Kind kind, + int index, + long longValue, + double doubleValue, + String stringValue, + byte[] blobValue) { + this.kind = kind; + this.index = index; + this.longValue = longValue; + this.doubleValue = doubleValue; + this.stringValue = stringValue; + this.blobValue = blobValue; + } + + static Call ofNull(int index) { + return new Call(Kind.NULL, index, 0, 0, null, null); + } + + static Call ofLong(int index, long value) { + return new Call(Kind.LONG, index, value, 0, null, null); + } + + static Call ofDouble(int index, double value) { + return new Call(Kind.DOUBLE, index, 0, value, null, null); + } + + static Call ofString(int index, String value) { + return new Call(Kind.STRING, index, 0, 0, value, null); + } + + static Call ofBlob(int index, byte[] value) { + return new Call(Kind.BLOB, index, 0, 0, null, value); + } + + void assertNull(int expectedIndex) { + assertEquals(Kind.NULL, kind); + assertEquals(expectedIndex, index); + } + + void assertLong(int expectedIndex, long expectedValue) { + assertEquals(Kind.LONG, kind); + assertEquals(expectedIndex, index); + assertEquals(expectedValue, longValue); + } + + void assertDouble(int expectedIndex, double expectedValue) { + assertEquals(Kind.DOUBLE, kind); + assertEquals(expectedIndex, index); + assertEquals(expectedValue, doubleValue, 0.0); + } + + void assertString(int expectedIndex, String expectedValue) { + assertEquals(Kind.STRING, kind); + assertEquals(expectedIndex, index); + assertEquals(expectedValue, stringValue); + } + + void assertBlob(int expectedIndex, byte[] expectedValue) { + assertEquals(Kind.BLOB, kind); + assertEquals(expectedIndex, index); + assertArrayEquals(expectedValue, blobValue); + } + } +} diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteStatementTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteStatementTest.java similarity index 98% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteStatementTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteStatementTest.java index 1ae8d8f..bd9e7de 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteStatementTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SQLiteStatementTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.zetetic.database.sqlcipher_cts; +package net.zetetic.database.sqlcipher; import static org.junit.Assert.assertArrayEquals; @@ -28,8 +28,6 @@ import android.content.Context; import android.database.Cursor; import net.zetetic.database.DatabaseUtils; -import net.zetetic.database.sqlcipher.SQLiteDatabase; -import net.zetetic.database.sqlcipher.SQLiteStatement; import android.database.SQLException; import android.database.sqlite.SQLiteDoneException; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SupportHelperTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SupportHelperTest.java similarity index 97% rename from sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SupportHelperTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SupportHelperTest.java index fc6caf2..e8b99e0 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SupportHelperTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/SupportHelperTest.java @@ -1,4 +1,4 @@ -package net.zetetic.database.sqlcipher_cts; +package net.zetetic.database.sqlcipher; import static org.junit.Assert.assertEquals; @@ -11,8 +11,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; -import net.zetetic.database.sqlcipher.SupportHelper; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/driver/Room3SQLCipherDriverTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/driver/Room3SQLCipherDriverTest.java new file mode 100644 index 0000000..c3a4e2e --- /dev/null +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/driver/Room3SQLCipherDriverTest.java @@ -0,0 +1,94 @@ +package net.zetetic.database.sqlcipher.driver; + +import android.content.Context; + +import androidx.room3.Dao; +import androidx.room3.Database; +import androidx.room3.Entity; +import androidx.room3.Insert; +import androidx.room3.PrimaryKey; +import androidx.room3.Query; +import androidx.room3.Room; +import androidx.room3.RoomDatabase; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class Room3SQLCipherDriverTest { + + private Context context; + private SQLCipherDriver driver; + + @Before + public void setup(){ + context = ApplicationProvider.getApplicationContext(); + System.loadLibrary("sqlcipher"); + driver = new SQLCipherDriver( + "password".getBytes(), + null, + null); + } + + @Test + public void shouldTestSQLCipherDriver(){ + var database = Room.databaseBuilder( + context, + UserDatabase.class, + "users.db") + .setDriver(driver) + .build(); + var dao = database.userDao(); + var name = "Nick Parker"; + dao.insert(new User(1, name)); + var result = dao.get(1); + + Assert.assertNotNull(result); + Assert.assertEquals(1, result.id); + Assert.assertEquals(name, result.name); + database.close(); + } + + @Test + public void shouldRetrieveCipherVersion(){ + var connection = driver.open(":memory:"); + var stmt = connection.prepare("PRAGMA cipher_version;"); + stmt.step(); + var version = stmt.getText(0); + Assert.assertEquals("4.18.0 community", version); + connection.close(); + } + + @Entity + public static class User { + @PrimaryKey + public long id; + public String name; + + public User(long id, String name) { + this.id = id; + this.name = name; + } + } + + @Dao + public interface UserDao { + @Insert + void insert(User user); + + @Query("SELECT * FROM User WHERE id = :id") + User get(long id); + } + + @Database( + entities = {User.class}, + version = 1 + ) + public abstract static class UserDatabase extends RoomDatabase { + public abstract UserDao userDao(); + } +} diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/driver/SQLCipherConnectionStatementPrefixTests.kt b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/driver/SQLCipherConnectionStatementPrefixTests.kt new file mode 100644 index 0000000..1e8a81b --- /dev/null +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/driver/SQLCipherConnectionStatementPrefixTests.kt @@ -0,0 +1,73 @@ +package net.zetetic.database.sqlcipher.driver + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertFalse +import junit.framework.Assert.assertNull +import junit.framework.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SQLCipherConnectionStatementPrefixTests { + + @Test + fun shouldExtractPrefixFromPlainStatements() { + assertEquals("SEL", SQLCipherConnection.statementPrefix("SELECT * FROM t")) + assertEquals("BEG", SQLCipherConnection.statementPrefix("BEGIN")) + assertEquals("PRA", SQLCipherConnection.statementPrefix("PRAGMA user_version")) + } + + @Test + fun shouldSkipLeadingWhitespace() { + assertEquals("BEG", SQLCipherConnection.statementPrefix(" \n\t BEGIN IMMEDIATE")) + } + + @Test + fun shouldSkipLineComments() { + assertEquals("BEG", SQLCipherConnection.statementPrefix("-- start txn\nBEGIN IMMEDIATE")) + } + + @Test + fun shouldSkipBlockComments() { + assertEquals("COM", SQLCipherConnection.statementPrefix("/* finish\n the txn */ COMMIT")) + } + + @Test + fun shouldReturnNullForCommentOnlyInput() { + assertNull(SQLCipherConnection.statementPrefix("-- nothing here")) + assertNull(SQLCipherConnection.statementPrefix("/* nothing here */")) + } + + @Test + fun shouldTolerateShortInput() { + assertNull(SQLCipherConnection.statementPrefix("")) + assertNull(SQLCipherConnection.statementPrefix(" ")) + } + + @Test + fun shouldInterceptJournalModeSets() { + assertTrue(journalFor("PRAGMA journal_mode = WAL")) + assertTrue(journalFor("pragma journal_mode=delete")) + assertTrue(journalFor("PRAGMA main.journal_mode = TRUNCATE")) + } + + @Test + fun shouldNotInterceptJournalModeQueries() { + assertFalse(journalFor("PRAGMA journal_mode")) + } + + @Test + fun shouldNotInterceptOtherPragmas() { + assertFalse(journalFor("PRAGMA foreign_keys = ON")) + assertFalse(journalFor("PRAGMA busy_timeout = 3000")) + assertFalse(journalFor("PRAGMA synchronous = NORMAL")) + } + + private fun journalFor(sql: String): Boolean { + val trimmed = sql.trim() + return SQLCipherConnection.isJournalModeSet( + SQLCipherConnection.statementPrefix(trimmed), + trimmed) + } +} \ No newline at end of file diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/driver/SQLCipherConnectionTests.kt b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/driver/SQLCipherConnectionTests.kt new file mode 100644 index 0000000..5eee460 --- /dev/null +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/driver/SQLCipherConnectionTests.kt @@ -0,0 +1,261 @@ +package net.zetetic.database.sqlcipher.driver + +import android.content.Context +import androidx.sqlite.SQLiteConnection +import androidx.sqlite.execSQL +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import kotlinx.coroutines.test.runTest +import net.zetetic.database.sqlcipher.SQLiteDatabase +import org.junit.After +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.io.File + +/** + * Verifying that SQLCipherConnection.prepare() dispatches transaction SQL + * and the journal_mode pragma to the SQLiteSession-aware paths, and + * everything else to the standard SQLCipherStatement. + */ +@RunWith(AndroidJUnit4::class) +class SQLCipherConnectionTests { + + private lateinit var dbFile: File + private lateinit var database: SQLiteDatabase + private lateinit var connection: SQLCipherConnection + + @Before + fun setUp() { + System.loadLibrary("sqlcipher") + val context: Context = InstrumentationRegistry.getInstrumentation().targetContext + dbFile = context.getDatabasePath("test.db").apply { + parentFile?.mkdirs() + } + SQLiteDatabase.deleteDatabase(dbFile) + database = SQLiteDatabase.openOrCreateDatabase( + dbFile, + "test-passphrase", + null, + null, + null) + connection = SQLCipherConnection(database) + exec("CREATE TABLE IF NOT EXISTS t (id INTEGER PRIMARY KEY, v TEXT)") + } + + @After + fun tearDown() { + connection.close() + SQLiteDatabase.deleteDatabase(dbFile) + } + + @Test + fun shouldTrackSessionStateWhenExecutingRawBeginImmediate() { + assertFalse(connection.inTransaction()) + exec("BEGIN IMMEDIATE TRANSACTION") + assertTrue(connection.inTransaction()) + assertTrue(database.inTransaction()) + exec("END TRANSACTION") + assertFalse(connection.inTransaction()) + } + + @Test + fun shouldPersistChangesWhenCommitting() { + exec("BEGIN IMMEDIATE TRANSACTION") + exec("INSERT INTO t (id, v) VALUES (1, 'kept')") + exec("COMMIT") + assertEquals(1L, queryLong("SELECT COUNT(*) FROM t")) + } + + @Test + fun shouldDiscardChangesWhenRollingBack() { + exec("BEGIN IMMEDIATE TRANSACTION") + exec("INSERT INTO t (id, v) VALUES (1, 'discarded')") + exec("ROLLBACK") + assertFalse(connection.inTransaction()) + assertEquals(0L, queryLong("SELECT COUNT(*) FROM t")) + } + + @Test + fun shouldRecognizeBeginRegardlessOfCaseAndLeadingComments() { + exec("-- Room may prepends comments\nbegin immediate transaction") + assertTrue(connection.inTransaction()) + exec("commit") + assertFalse(connection.inTransaction()) + } + + @Test + fun shouldPassSavepointsThroughInsideTransaction() { + exec("BEGIN IMMEDIATE TRANSACTION") + exec("INSERT INTO t (id, v) VALUES (1, 'outer')") + exec("SAVEPOINT sp1") + exec("INSERT INTO t (id, v) VALUES (2, 'inner')") + exec("ROLLBACK TRANSACTION TO SAVEPOINT sp1") + exec("RELEASE SAVEPOINT sp1") + assertTrue("outer transaction should still be open", connection.inTransaction()) + exec("COMMIT") + assertEquals(1L, queryLong("SELECT COUNT(*) FROM t")) + assertEquals(1L, queryLong("SELECT COUNT(*) FROM t WHERE v = 'outer'")) + assertFalse(connection.inTransaction()) + } + + @Test + fun shouldReturnNoRowsWhenSteppingTransactionStatement() { + connection.prepare("BEGIN IMMEDIATE TRANSACTION").use { statement -> + assertFalse("transaction statement should report SQLITE_DONE", statement.step()) + } + exec("ROLLBACK") + } + + @Test + fun shouldEnableWriteAheadLoggingWhenSettingJournalModeWal() { + assertFalse(database.isWriteAheadLoggingEnabled) + connection.prepare("PRAGMA journal_mode = WAL").use { statement -> + assertTrue(statement.step()) + assertEquals("wal", statement.getText(0).lowercase()) + } + assertTrue(database.isWriteAheadLoggingEnabled) + } + + @Test + fun shouldDisableWriteAheadLoggingWhenSettingJournalModeDelete() { + exec("PRAGMA journal_mode = WAL") + assertTrue(database.isWriteAheadLoggingEnabled) + exec("PRAGMA journal_mode = DELETE") + assertFalse(database.isWriteAheadLoggingEnabled) + } + + @Test + fun shouldPreserveTruncateJournalModeAcrossPoolReconfiguration() { + exec("PRAGMA journal_mode = TRUNCATE") + connection.prepare("PRAGMA journal_mode").use { statement -> + assertTrue(statement.step()) + assertEquals("truncate", statement.getText(0).lowercase()) + } + } + + @Test + fun shouldNotToggleWriteAheadLoggingWhenQueryingJournalMode() { + val before = database.isWriteAheadLoggingEnabled + connection.prepare("PRAGMA journal_mode").use { statement -> + assertTrue(statement.step()) + } + assertEquals(before, database.isWriteAheadLoggingEnabled) + } + + @Test + fun shouldRunOtherPragmasOnStandardStatement() { + connection.prepare("PRAGMA user_version").use { statement -> + assertTrue(statement.step()) + assertEquals(0L, statement.getLong(0)) + } + } + + @Test + fun shouldRoundTripDataThroughStandardStatement() { + connection.prepare("INSERT INTO t (id, v) VALUES (?, ?)").use { statement -> + statement.bindLong(1, 42L) + statement.bindText(2, "hello") + assertFalse(statement.step()) + } + connection.prepare("SELECT v FROM t WHERE id = ?").use { statement -> + statement.bindLong(1, 42L) + assertTrue(statement.step()) + assertEquals("hello", statement.getText(0)) + assertFalse(statement.step()) + } + } + + @Test + fun shouldBindAllSupportedTypesAndReadThemBack() { + createUserTable() + val blob = byteArrayOf(0x00, 0x01, -0x80, 0x42) + connection.prepare( + "INSERT INTO user (name, age, email, score, avatar, flag) VALUES (?, ?, ?, ?, ?, ?)" + ).use { stmt -> + stmt.bindText(1, "John Doe") + stmt.bindInt(2, 42) + stmt.bindText(3, "john@doe.com") + stmt.bindDouble(4, 3.14159) + stmt.bindBlob(5, blob) + stmt.bindBoolean(6, true) + assertFalse(stmt.step()) + } + connection.prepare("SELECT id, name, age, email, score, avatar, flag FROM user").use { stmt -> + assertTrue(stmt.step()) + assertEquals(7, stmt.getColumnCount()) + assertTrue(stmt.getLong(0) > 0L) + assertEquals("John Doe", stmt.getText(1)) + assertEquals(42, stmt.getInt(2)) + assertEquals("john@doe.com", stmt.getText(3)) + assertEquals(3.14159, stmt.getDouble(4), 1e-9) + assertArrayEquals(blob, stmt.getBlob(5)) + assertEquals(true, stmt.getBoolean(6)) + + assertEquals("name", stmt.getColumnName(1)) + assertEquals("age", stmt.getColumnName(2)) + assertEquals("email", stmt.getColumnName(3)) + assertEquals("score", stmt.getColumnName(4)) + assertEquals("avatar", stmt.getColumnName(5)) + assertEquals("flag", stmt.getColumnName(6)) + } + } + + @Test + fun shouldRebindStatementAfterResetForStatementReuse() { + createUserTable() + connection.prepare("INSERT INTO user (name) VALUES (?)").use { stmt -> + listOf("one", "two", "three").forEach { name -> + stmt.reset() + stmt.clearBindings() + stmt.bindText(1, name) + assertFalse(stmt.step()) + } + } + connection.prepare("SELECT count(*) FROM user").use { stmt -> + stmt.step() + assertEquals(3L, stmt.getLong(0)) + } + } + + @Test + fun shouldThrowWhenUsingClosedStatement() { + createUserTable() + val stmt = connection.prepare("SELECT count(*) FROM user") + stmt.close() + assertThrows(android.database.SQLException::class.java) { + stmt.step() + } + } + + private fun exec(sql: String) { + connection.prepare(sql).use { it.step() } + } + + private fun queryLong(sql: String): Long = + connection.prepare(sql).use { statement -> + assertTrue("expected a row from: $sql", statement.step()) + statement.getLong(0) + } + + private fun createUserTable(conn: SQLiteConnection = connection) { + connection.execSQL( + """ + CREATE TABLE IF NOT EXISTS user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + age INTEGER, + email TEXT UNIQUE, + score REAL, + avatar BLOB, + flag INTEGER + ) + """.trimIndent() + ) + } +} diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/driver/SupportAPIRoomTest.kt b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/driver/SupportAPIRoomTest.kt new file mode 100644 index 0000000..8d8730f --- /dev/null +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher/driver/SupportAPIRoomTest.kt @@ -0,0 +1,223 @@ +package net.zetetic.database.sqlcipher.driver + +import android.content.Context +import androidx.room3.ColumnInfo +import androidx.room3.Dao +import androidx.room3.Database +import androidx.room3.Delete +import androidx.room3.Entity +import androidx.room3.Insert +import androidx.room3.OnConflictStrategy +import androidx.room3.PrimaryKey +import androidx.room3.Query +import androidx.room3.Room +import androidx.room3.RoomDatabase +import androidx.room3.Upsert +import androidx.room3.support.getSupportWrapper +import androidx.room3.withWriteTransaction +import androidx.sqlite.SQLiteConnection +import androidx.sqlite.SQLiteDriver +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.hamcrest.CoreMatchers.not +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.empty +import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.notNullValue +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.io.File +import java.nio.charset.StandardCharsets +import kotlin.coroutines.cancellation.CancellationException + +@RunWith(AndroidJUnit4::class) +class SupportAPIRoomTest { + private lateinit var driver: SQLiteDriver + private lateinit var db: AppDatabase + private lateinit var userDao: UserDao + private lateinit var databaseFile: File + private lateinit var connection: SQLiteConnection + private val defaultPassword = "user".toByteArray(StandardCharsets.UTF_8) + + @Before + fun setup(){ + setup(defaultPassword, true) + } + + fun setup(password: ByteArray, deleteDatabase: Boolean) { + val context = ApplicationProvider.getApplicationContext() + System.loadLibrary("sqlcipher") + databaseFile = context.getDatabasePath("users.db") + if (deleteDatabase and databaseFile.exists()) { + databaseFile.delete() + } + driver = SQLCipherDriver( + password, + null, + null + ) + connection = driver.open(databaseFile.absolutePath); + db = Room.databaseBuilder( + context, + AppDatabase::class.java, + databaseFile.absolutePath) + .setDriver(driver) + .build() + userDao = db.userDao() + } + + @After + fun after() { + db.close() + databaseFile.delete() + } + + @Test + fun shouldInsertDataViaDao() { + val user = User("John", "Doe") + val uid = userDao.insert(user) + assertThat(uid, not(0L)) + } + + @Test + @Throws(InterruptedException::class) + fun shouldDeleteDataViaDao() = runTest { + val user = User(uid = 1, firstName = "foo", lastName = "bar") + userDao.insert(user) + assertThat(userDao.findById(user.uid), notNullValue()) + userDao.delete(user) + assertThat(userDao.all, `is`(empty())) + } + + @Test + fun shouldQueryDataByParametersViaDao(){ + val user = User(uid = 1, firstName = "foo", lastName = "bar") + userDao.insert(user) + val foundUser = userDao.findByName(user.firstName, user.lastName) + assertThat(foundUser, notNullValue()) + assertThat(foundUser!!.uid, `is`(user.uid)) + assertThat(foundUser.firstName, `is`(user.firstName)) + assertThat(foundUser.lastName, `is`(user.lastName)) + } + + @Test + fun shouldReplaceExistingRowWithReplaceConflictStrategy() = runTest { + val original = User(firstName = "John", lastName = "Doe") + val id = userDao.insert(original) + + val updated = original.copy(uid = id, firstName = "Jane") + userDao.insertOrReplace(updated) + + val all = userDao.all + assertEquals(1, all.size) + with(all.single()) { + assertEquals(id, uid) + assertEquals("Jane", firstName) + assertEquals("Doe", lastName) + } + } + + @Test + fun shouldAllowUpsertBehavior() { + val user = User(firstName = "John", lastName = "Doe") + val uid = userDao.upsert(user) + userDao.upsert(user.copy(uid = uid, firstName = "Jane")) + with(userDao.all.single()) { + assertEquals(uid, uid) + assertEquals("Jane", firstName) + assertEquals("Doe", lastName) + } + } + + @Test + fun shouldRollbackWriteTransactionOnCancellation() = runTest { + try { + db.withWriteTransaction { + userDao.insert(User(firstName = "foo", lastName = "bar")) + throw CancellationException("cancelled mid-transaction") + } + } catch (_: CancellationException) {} + assertEquals(0, userDao.all.count()) + } + + @Test + fun shouldSeeOwnWritesInsideWriteTransaction() = runTest { + db.withWriteTransaction { + userDao.insert(User(firstName = "foo", lastName = "bar")) + assertEquals(1, userDao.all.count()) + } + } + + @Test + fun shouldSupportChangingPasswordWithRoom() = runTest { + val newPassword = "foobar".toByteArray() + userDao.insert( User("foo", "bar")) + db.close() + val connection = driver.open(databaseFile.absolutePath) as SQLCipherConnection + connection.changePassword(newPassword) + connection.close() + setup(newPassword, false) + assertThat(userDao.all.count(), `is`(1)) + } + + @Test + fun shouldVerifyWalModeFromExistingDatabase() = runTest { + connection.prepare("PRAGMA journal_mode = wal;").use { stmt -> + stmt.step(); + val mode = stmt.getText(0) + assertThat(mode, `is`("wal")) + } + connection.close() + setup(defaultPassword, false) + assertThat(db.getSupportWrapper().isWriteAheadLoggingEnabled, `is`(true)) + } + + @Database(entities = [User::class], version = 1, exportSchema = false) + abstract class AppDatabase : RoomDatabase() { + abstract fun userDao(): UserDao + } + + @Entity + data class User( + @ColumnInfo(name = "first_name") val firstName: String, + @ColumnInfo(name = "last_name") val lastName: String, + @PrimaryKey(autoGenerate = true) val uid: Long = 0 + ) + + @Dao + interface UserDao { + @get:Query("SELECT * FROM user") + val all: MutableList + + @Query("SELECT * FROM user WHERE uid IN (:userIds)") + fun loadAllByIds(userIds: IntArray?): MutableList + + @Query( + "SELECT * FROM user WHERE first_name LIKE :first AND " + + "last_name LIKE :last LIMIT 1" + ) + fun findByName(first: String?, last: String?): User? + + @Query("SELECT * FROM user WHERE uid = :userId") + fun findById(userId: Long): User? + + @Insert + fun insert(user: User): Long + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertOrReplace(user: User): Long + + @Upsert + fun upsert(user: User): Long + + @Delete + fun delete(user: User) + + @Query("DELETE FROM user;") + fun deleteAll() + } +} diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/RoomUpsertTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/RoomUpsertTest.java deleted file mode 100644 index a2d6fca..0000000 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/RoomUpsertTest.java +++ /dev/null @@ -1,80 +0,0 @@ -package net.zetetic.database.sqlcipher_cts; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -import android.content.Context; - -import androidx.room.Dao; -import androidx.room.Database; -import androidx.room.Entity; -import androidx.room.PrimaryKey; -import androidx.room.Query; -import androidx.room.Room; -import androidx.room.RoomDatabase; -import androidx.room.Upsert; -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import net.zetetic.database.sqlcipher.SupportOpenHelperFactory; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.File; -import java.nio.charset.StandardCharsets; - -@RunWith(AndroidJUnit4.class) -public class RoomUpsertTest { - private UserDatabase database; - private UserDao userDao; - - @Before - public void before(){ - Context context = ApplicationProvider.getApplicationContext(); - File databaseFile = context.getDatabasePath("upsert.db"); - if(databaseFile.exists()){ - databaseFile.delete(); - } - System.loadLibrary("sqlcipher"); - final byte[] passphrase = "user".getBytes(StandardCharsets.UTF_8); - SupportOpenHelperFactory factory = new SupportOpenHelperFactory(passphrase); - database = Room.databaseBuilder(context, UserDatabase.class, databaseFile.getName()) - .openHelperFactory(factory).build(); - userDao = database.userDao(); - } - - @Test - public void shouldAllowUpsertBehavior(){ - User user = new User(); - user.name = "Foo Bar"; - user.age = 41; - user.id = userDao.upsert(user); - user.age = 42; - userDao.upsert(user); - User[] searchUser = userDao.findById(user.id); - assertThat(searchUser[0].age , is(42)); - } - - @Entity - public static class User { - @PrimaryKey(autoGenerate = true) long id; - String name; - int age; - } - - @Dao - public static abstract class UserDao { - @Upsert - abstract long upsert(User user); - @Query("SELECT * FROM user WHERE id=:id") - abstract User[] findById(long id); - } - - @Database(entities = {User.class}, version = 1, exportSchema = false) - public static abstract class UserDatabase extends RoomDatabase { - abstract UserDao userDao(); - } - -} diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteQueryTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteQueryTest.java deleted file mode 100644 index befb784..0000000 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteQueryTest.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2009 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.zetetic.database.sqlcipher_cts; - -import junit.framework.TestCase; - -public class SQLiteQueryTest extends TestCase { - public void testMethods() { - // cannot obtain an instance of SQLiteQuery - } -} diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SupportAPIRoomTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SupportAPIRoomTest.java deleted file mode 100644 index 1162ed9..0000000 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SupportAPIRoomTest.java +++ /dev/null @@ -1,162 +0,0 @@ -package net.zetetic.database.sqlcipher_cts; - -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -import android.content.Context; -import android.database.Cursor; - -import androidx.room.ColumnInfo; -import androidx.room.Dao; -import androidx.room.Database; -import androidx.room.Delete; -import androidx.room.Entity; -import androidx.room.Insert; -import androidx.room.OnConflictStrategy; -import androidx.room.PrimaryKey; -import androidx.room.Query; -import androidx.room.Room; -import androidx.room.RoomDatabase; -import androidx.sqlite.db.SupportSQLiteDatabase; -import androidx.sqlite.db.SupportSQLiteOpenHelper; -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import net.zetetic.database.sqlcipher.SQLiteDatabase; -import net.zetetic.database.sqlcipher.SupportOpenHelperFactory; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.File; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.UUID; - -@RunWith(AndroidJUnit4.class) -public class SupportAPIRoomTest { - - private AppDatabase db; - private UserDao userDao; - private File databaseFile; - - @Before - public void before(){ - Context context = ApplicationProvider.getApplicationContext(); - System.loadLibrary("sqlcipher"); - databaseFile = context.getDatabasePath("users.db"); - if(databaseFile.exists()){ - databaseFile.delete(); - } - SupportOpenHelperFactory factory = new SupportOpenHelperFactory("user".getBytes(StandardCharsets.UTF_8)); - db = Room.databaseBuilder(context, AppDatabase.class, databaseFile.getAbsolutePath()) - .openHelperFactory(factory).build(); - db.clearAllTables(); - userDao = db.userDao(); - } - - @Test - public void shouldInsertDataViaDao(){ - User user = new User("John", "Doe"); - user.uid = userDao.insert(user); - assertThat(user.uid, is(not(0))); - } - - @Test - public void shouldDeleteDataViaDao(){ - User user = new User("foo", "bar"); - user.uid = 1; - SupportSQLiteOpenHelper helper = db.getOpenHelper(); - SupportSQLiteDatabase database = helper.getWritableDatabase(); - database.execSQL("insert into user values(?,?,?);", - new Object[]{user.uid, user.firstName, user.lastName}); - Cursor cursor = database.query("select count(*) from user;"); - cursor.moveToFirst(); - boolean userInserted = cursor.getInt(0) > 0; - assertThat(userInserted, is(true)); - userDao.delete(user); - cursor = database.query("select count(*) from user;"); - cursor.moveToFirst(); - int existingUsers = cursor.getInt(0); - cursor.close(); - assertThat(existingUsers, is(0)); - } - - @Test - public void shouldQueryDataByParametersViaDao(){ - User user = new User("foo", "bar"); - user.uid = 1; - SupportSQLiteOpenHelper helper = db.getOpenHelper(); - SupportSQLiteDatabase database = helper.getWritableDatabase(); - database.execSQL("insert into user values(?,?,?);", - new Object[]{user.uid, user.firstName, user.lastName}); - User foundUser = userDao.findByName(user.firstName, user.lastName); - assertThat(foundUser.uid, is(user.uid)); - assertThat(foundUser.firstName, is(user.firstName)); - assertThat(foundUser.lastName, is(user.lastName)); - } - - @Test - public void shouldSupportChangingPasswordWithRoom(){ - userDao.insert(new User("foo", "bar")); - SQLiteDatabase database = (SQLiteDatabase)db.getOpenHelper().getWritableDatabase(); - database.changePassword(UUID.randomUUID().toString()); - List users = userDao.getAll(); - assertThat(users.size(), is(1)); - } - - @After - public void after(){ - if(db != null){ - db.close(); - if(databaseFile != null){ - databaseFile.delete(); - } - } - } - - @Database(entities = {User.class}, version = 1, exportSchema = false) - public abstract static class AppDatabase extends RoomDatabase { - public abstract UserDao userDao(); - } - - @Entity - public static class User { - @PrimaryKey(autoGenerate = true) - public long uid; - @ColumnInfo(name = "first_name") - public String firstName; - @ColumnInfo(name = "last_name") - public String lastName; - - public User(String firstName, String lastName) { - this.firstName = firstName; - this.lastName = lastName; - } - } - - @Dao - public interface UserDao { - @Query("SELECT * FROM user") - List getAll(); - - @Query("SELECT * FROM user WHERE uid IN (:userIds)") - List loadAllByIds(int[] userIds); - - @Query("SELECT * FROM user WHERE first_name LIKE :first AND " + - "last_name LIKE :last LIMIT 1") - User findByName(String first, String last); - - @Insert(onConflict = OnConflictStrategy.REPLACE) - long insert(User user); - - @Delete - void delete(User user); - - @Query("DELETE FROM user;") - void deleteAll(); - } -} diff --git a/sqlcipher/src/main/java/net/zetetic/database/CursorWindow.java b/sqlcipher/src/main/java/net/zetetic/database/CursorWindow.java index a604305..8ed83d8 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/CursorWindow.java +++ b/sqlcipher/src/main/java/net/zetetic/database/CursorWindow.java @@ -320,10 +320,14 @@ public void copyStringToBuffer(int row, int column, CharArrayBuffer buffer) { if (buffer == null) { throw new IllegalArgumentException("CharArrayBuffer should not be null"); } - // TODO not as optimal as the original code - char[] chars = getString(row, column).toCharArray(); - buffer.data = chars; - buffer.sizeCopied = chars.length; + var content = getString(row, column); + if(content != null){ + var chars = content.toCharArray(); + buffer.data = chars; + buffer.sizeCopied = chars.length; + } else { + buffer.sizeCopied = 0; + } } /** diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteDatabase.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteDatabase.java index 2fba349..4552792 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteDatabase.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteDatabase.java @@ -405,6 +405,11 @@ public void beginTransactionReadOnly() { beginTransaction(); } + public void beginTransactionWithListenerReadOnly( + android.database.sqlite.SQLiteTransactionListener transactionListener){ + beginTransactionWithListener(transactionListener); + } + /** * Begins a transaction in EXCLUSIVE mode. *

diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteQuery.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteQuery.java index aae818b..c475847 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteQuery.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteQuery.java @@ -25,6 +25,10 @@ import android.os.CancellationSignal; import android.os.OperationCanceledException; +import androidx.annotation.NonNull; +import androidx.sqlite.db.SupportSQLiteProgram; +import androidx.sqlite.db.SupportSQLiteQuery; + import net.zetetic.database.CursorWindow; import net.zetetic.database.Logger; @@ -35,7 +39,7 @@ * This class is not thread-safe. *

*/ -public final class SQLiteQuery extends SQLiteProgram { +public final class SQLiteQuery extends SQLiteProgram implements SupportSQLiteQuery { private static final String TAG = "SQLiteQuery"; private final CancellationSignal mCancellationSignal; @@ -88,4 +92,55 @@ window, startPos, requiredPos, countAllRows, getConnectionFlags(), public String toString() { return "SQLiteQuery: " + getSql(); } + + @NonNull + @Override + public String getSql() { + return super.getSql(); + } + + @Override + public void bindTo(@NonNull SupportSQLiteProgram supportSQLiteProgram) { + Object[] bindArgs = super.getBindArgs(); + if (bindArgs == null) { + return; + } + for (var i = 0; i < bindArgs.length; i++) { + var index = i + 1; + Object arg = bindArgs[i]; + bindArgumentToProgram(supportSQLiteProgram, index, arg); + } + } + + @Override + public int getArgCount() { + var bindArgs = super.getBindArgs(); + return bindArgs == null + ? 0 + : bindArgs.length; + } + + private void bindArgumentToProgram(@NonNull SupportSQLiteProgram program, int index, Object arg) { + if (arg == null) { + program.bindNull(index); + } else if (arg instanceof byte[]) { + program.bindBlob(index, (byte[]) arg); + } else if (arg instanceof Float || arg instanceof Double) { + program.bindDouble(index, ((Number) arg).doubleValue()); + } else if (arg instanceof Boolean) { + var value = ((Boolean) arg) + ? 1L + : 0L; + program.bindLong(index, value); + } else if (arg instanceof Integer || arg instanceof Long + || arg instanceof Short || arg instanceof Byte) { + program.bindLong(index, ((Number) arg).longValue()); + } else if (arg instanceof String) { + program.bindString(index, (String) arg); + } else { + var message = "Cannot bind " + arg + " at index " + index + + " supported types: null, byte[], float, double, int, long, boolean, String"; + throw new IllegalArgumentException(message); + } + } } diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/driver/JournalModeStatement.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/driver/JournalModeStatement.java new file mode 100644 index 0000000..3656bdf --- /dev/null +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/driver/JournalModeStatement.java @@ -0,0 +1,116 @@ +package net.zetetic.database.sqlcipher.driver; + +import androidx.annotation.NonNull; +import androidx.sqlite.SQLiteStatement; + +import net.zetetic.database.sqlcipher.SQLiteDatabase; + +class JournalModeStatement implements SQLiteStatement { + + private final SQLiteDatabase database; + private final SQLiteStatement delegate; + + JournalModeStatement( + SQLiteDatabase database, + SQLiteStatement delegate) { + this.database = database; + this.delegate = delegate; + } + + /** + * Perform journal mode get/set operation. When in WAL + * mode, we dispatch to enableWriteAheadLogging which + * will reconfigure the connection pool accordingly. + */ + @Override + public boolean step() { + var hasRow = delegate.step(); + // The pragma returns the mode actually in effect. + if ("wal".equalsIgnoreCase(delegate.getText(0))) { + database.enableWriteAheadLogging(); + } else { + database.disableWriteAheadLogging(); + } + return hasRow; + } + + @Override public void bindBlob( + int index, + @NonNull byte[] value) { + delegate.bindBlob(index, value); + } + + @Override public void bindDouble( + int index, + double value) { + delegate.bindDouble(index, value); + } + + @Override public void bindLong( + int index, + long value) { + delegate.bindLong(index, value); + } + + @Override public void bindText( + int index, + @NonNull String value) { + delegate.bindText(index, value); + } + + @Override public void bindNull( + int index) { + delegate.bindNull(index); + } + + @Override @NonNull public byte[] getBlob( + int index) { + return delegate.getBlob(index); + } + + @Override public double getDouble( + int index) { + return delegate.getDouble(index); + } + + @Override public long getLong( + int index) { + return delegate.getLong(index); + } + + @Override @NonNull public String getText( + int index) { + return delegate.getText(index); + } + + @Override public boolean isNull( + int index) { + return delegate.isNull(index); + } + + @Override public int getColumnCount() { + return delegate.getColumnCount(); + } + + @Override @NonNull public String getColumnName( + int index) { + return delegate.getColumnName(index); + } + + @Override public int getColumnType( + int index) { + return delegate.getColumnType(index); + } + + @Override public void reset() { + delegate.reset(); + } + + @Override public void clearBindings() { + delegate.clearBindings(); + } + + @Override public void close() { + delegate.close(); + } +} diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/driver/SQLCipherConnection.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/driver/SQLCipherConnection.java new file mode 100644 index 0000000..51de304 --- /dev/null +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/driver/SQLCipherConnection.java @@ -0,0 +1,114 @@ +package net.zetetic.database.sqlcipher.driver; + +import androidx.annotation.NonNull; +import androidx.sqlite.SQLiteConnection; +import androidx.sqlite.SQLiteStatement; + +import net.zetetic.database.sqlcipher.SQLiteDatabase; + +public class SQLCipherConnection implements SQLiteConnection { + private final SQLiteDatabase database; + + public SQLCipherConnection( + SQLiteDatabase database) { + this.database = database; + } + + /** + * Transform SQL string into SQLiteStatement. We preprocess the SQL with the following rules: + * - When a journal mode is set, dispatch the operation, specifically when WAL is used we + * dispatch using the enableWriteAheadLogging which will reconfigure the underlying + * connection pool. + * - For all other statements, we return a default SQLCipherStatement for processing. + */ + @NonNull + @Override + public SQLiteStatement prepare( + @NonNull String sql) { + var trimmed = sql.trim(); + var prefix = statementPrefix(trimmed); + if (prefix != null) { + if (isJournalModeSet(prefix, trimmed)) { + // Run the pragma normally, then let SQLiteDatabase reconfigure the pool + return new JournalModeStatement(database, new SQLCipherStatement(database, sql)); + } + } + return new SQLCipherStatement(database, sql); + } + + @Override + public boolean inTransaction() { + return database.inTransaction(); + } + + public void changePassword( + byte[] newPassword){ + database.changePassword(newPassword); + } + + @Override + public void close() { + database.close(); + } + + static boolean isJournalModeSet( + String prefix, + String sql) { + if (!"PRA".equalsIgnoreCase(prefix)) { + return false; + } + var lower = sql.toLowerCase(); + var index = lower.indexOf("journal_mode"); + // Only a *set* ("PRAGMA journal_mode = WAL") needs special handling; + // a bare query ("PRAGMA journal_mode") can run normally. + return index >= 0 + && lower.indexOf('=', index) >= 0; + } + + /** + * Returns the first 3 significant characters of the statement, skipping + * whitespace and SQL comments (-- line and slash-star block). Port of + * androidx.sqlite.util.SQLStatementParser.getStatementPrefix() + */ + static String statementPrefix( + String statement) { + var limit = statement.length() - 2; + if (limit < 0) { + return null; + } + var index = 0; + while (index >= 0 && index < limit) { + var c = statement.charAt(index); + if (c <= ' ') { + index++; + } else if (c == '-') { + if (statement.charAt(index + 1) != '-') { + break; + } + index = statement.indexOf('\n', index + 2); + if (index < 0) { + return null; + } + index++; + } else if (c == '/') { + if (statement.charAt(index + 1) != '*') { + break; + } + index++; + do { + index = statement.indexOf('*', index + 1); + if (index < 0) { + return null; + } + } while (index + 1 < limit && statement.charAt(index + 1) != '/'); + index += 2; + } else { + break; + } + } + if (index < 0 || index >= statement.length()) { + return null; + } + return statement.substring(index, Math.min(index + 3, statement.length())); + } +} diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/driver/SQLCipherDriver.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/driver/SQLCipherDriver.java new file mode 100644 index 0000000..edf9e34 --- /dev/null +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/driver/SQLCipherDriver.java @@ -0,0 +1,38 @@ +package net.zetetic.database.sqlcipher.driver; + +import androidx.annotation.NonNull; +import androidx.sqlite.SQLiteConnection; +import androidx.sqlite.SQLiteDriver; + +import net.zetetic.database.DatabaseErrorHandler; +import net.zetetic.database.sqlcipher.SQLiteDatabase; +import net.zetetic.database.sqlcipher.SQLiteDatabaseHook; + +public class SQLCipherDriver implements SQLiteDriver { + + private final byte[] passphrase; + private final SQLiteDatabaseHook hook; + private final DatabaseErrorHandler handler; + + public SQLCipherDriver( + byte[] passphrase, + SQLiteDatabaseHook hook, + DatabaseErrorHandler handler){ + this.passphrase = passphrase; + this.hook = hook; + this.handler = handler; + } + + @NonNull + @Override + public SQLiteConnection open( + @NonNull String filename) { + var db = SQLiteDatabase.openOrCreateDatabase(filename, passphrase, null, handler, hook); + return new SQLCipherConnection(db); + } + + @Override + public boolean hasConnectionPool() { + return true; + } +} diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/driver/SQLCipherStatement.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/driver/SQLCipherStatement.java new file mode 100644 index 0000000..2ad3806 --- /dev/null +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/driver/SQLCipherStatement.java @@ -0,0 +1,178 @@ +package net.zetetic.database.sqlcipher.driver; + +import android.database.Cursor; + +import androidx.annotation.NonNull; +import androidx.sqlite.SQLiteStatement; +import android.database.SQLException; + +import net.zetetic.database.sqlcipher.SQLiteDatabase; + +import java.util.LinkedHashMap; +import java.util.Map; + +@SuppressWarnings("resource") +public class SQLCipherStatement implements SQLiteStatement { + + private final SQLiteDatabase database; + private final String sql; + private final Map bindings = new LinkedHashMap<>(); + private Cursor cursor; + private boolean stepped; + private boolean closed; + + public SQLCipherStatement( + SQLiteDatabase database, + String sql) { + this.database = database; + this.sql = sql; + } + + @Override + public void bindBlob( + int index, + @NonNull byte[] value) { + bindings.put(index, value); + } + + @Override + public void bindDouble( + int index, + double value) { + bindings.put(index, value); + } + + @Override + public void bindLong( + int index, + long value) { + bindings.put(index, value); + } + + @Override + public void bindText( + int index, + @NonNull String value) { + bindings.put(index, value); + } + + @Override + public void bindNull( + int index) { + bindings.put(index, null); + } + + @NonNull + @Override + public byte[] getBlob( + int index) { + return requireCursor().getBlob(index); + } + + @Override + public double getDouble( + int index) { + return requireCursor().getDouble(index); + } + + @Override + public long getLong( + int index) { + return requireCursor().getLong(index); + } + + @NonNull + @Override + public String getText( + int index) { + return requireCursor().getString(index); + } + + @Override + public boolean isNull( + int index) { + return requireCursor().isNull(index); + } + + @Override + public int getColumnCount() { + if (cursor == null) { + createCursor(); + } + return cursor.getColumnCount(); + } + + @NonNull + @Override + public String getColumnName( + int index) { + if (cursor == null) { + createCursor(); + } + return cursor.getColumnName(index); + } + + @Override + public int getColumnType( + int index) { + return requireCursor().getType(index); + } + + @Override + public boolean step() { + if(closed){ + throw new SQLException("statement is closed"); + } + if (cursor == null) { + var maxIndex = 0; + for (int key : bindings.keySet()) { + maxIndex = Math.max(maxIndex, key); + } + var args = new Object[maxIndex]; + for (int i = 1; i <= maxIndex; i++) { + args[i - 1] = bindings.get(i); + } + cursor = database.rawQuery(sql, args); + } + stepped = cursor.moveToNext(); + return stepped; + } + + @Override + public void reset() { + if (cursor != null) { + cursor.close(); + cursor = null; + } + stepped = false; + } + + @Override + public void clearBindings() { + bindings.clear(); + } + + @Override + public void close() { + closed = true; + } + + private void createCursor() { + var maxIndex = 0; + for (int key : bindings.keySet()) { + maxIndex = Math.max(maxIndex, key); + } + var args = new Object[maxIndex]; + for (int i = 1; i <= maxIndex; i++) { + args[i - 1] = bindings.get(i); + } + cursor = database.rawQuery(sql, args); + } + + private Cursor requireCursor() { + if (!stepped || cursor == null) { + throw new IllegalStateException("step() must be called before reading column values"); + } + return cursor; + } +} diff --git a/sqlciphertest/build.gradle b/sqlciphertest/build.gradle index b1e5bbf..13ef12e 100644 --- a/sqlciphertest/build.gradle +++ b/sqlciphertest/build.gradle @@ -18,9 +18,6 @@ android { } ndkVersion "21.3.6528147" - - useLibrary 'android.test.base' // for android.test.AndroidTestCase - useLibrary 'android.test.runner' // for android.test.ApplicationTestCase extends AndroidTestCase } dependencies {