Skip to content

Commit fae2c3e

Browse files
Use 8 MB CursorWindow by default, adjustable with static methods on SQLiteCursor class
1 parent e4c99d9 commit fae2c3e

File tree

3 files changed

+117
-11
lines changed

3 files changed

+117
-11
lines changed

sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherDatabaseTest.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
import static org.hamcrest.MatcherAssert.assertThat;
44
import static org.hamcrest.core.Is.is;
5+
import static org.hamcrest.core.IsNull.notNullValue;
56
import static org.hamcrest.core.IsNull.nullValue;
7+
import static org.junit.Assert.fail;
68

79
import android.content.ContentValues;
810
import android.database.Cursor;
911

12+
import net.zetetic.database.sqlcipher.SQLiteCursor;
1013
import net.zetetic.database.sqlcipher.SQLiteDatabase;
1114
import net.zetetic.database.sqlcipher.SQLiteDatabaseConfiguration;
1215
import net.zetetic.database.sqlcipher.SQLiteDatabaseCorruptException;
@@ -18,6 +21,7 @@
1821

1922
import java.io.File;
2023
import java.nio.charset.StandardCharsets;
24+
import java.util.Arrays;
2125

2226
public class SQLCipherDatabaseTest extends AndroidSQLCipherTestCase {
2327

@@ -451,4 +455,54 @@ public void shouldReportErrorAfterDatabaseCloseWhenCheckingTransactionState(){
451455
database.close();
452456
database.inTransaction();
453457
}
458+
459+
@Test
460+
public void shouldRetrieveLargeSingleRowResultFromCursor(){
461+
try {
462+
int id = 1;
463+
byte[] queriedData = null;
464+
int size = 256;
465+
SQLiteCursor.setCursorWindowSize(size);
466+
byte[] data = generateRandomBytes(size);
467+
database.execSQL("create table t1(a,b);");
468+
database.execSQL("insert into t1(a,b) values(?,?);", new Object[]{id, data});
469+
Cursor cursor = database.rawQuery("select b from t1 where a = ?;", new Object[]{id});
470+
if(cursor != null && cursor.moveToFirst()){
471+
queriedData = cursor.getBlob(0);
472+
cursor.close();
473+
}
474+
assertThat(Arrays.equals(queriedData, data), is(true));
475+
} finally {
476+
SQLiteCursor.resetCursorWindowSize();
477+
}
478+
}
479+
480+
@Test
481+
public void shouldAllowCursorWindowToResize(){
482+
try {
483+
Cursor cursor;
484+
int id = 1, extra = 100, size = 256;
485+
byte[] tooLargeQueriedData = null;
486+
SQLiteCursor.setCursorWindowSize(size);
487+
byte[] tooLargeData = generateRandomBytes(size + extra);
488+
database.execSQL("create table t1(a,b);");
489+
database.execSQL("insert into t1(a,b) values(?,?);", new Object[]{id, tooLargeData});
490+
try {
491+
cursor = database.rawQuery("select b from t1 where a = ?;", new Object[]{id});
492+
if(cursor != null && cursor.moveToFirst()) {
493+
fail("CursorWindow should be too small to fill query results");
494+
}
495+
} catch (Exception ex){
496+
SQLiteCursor.setCursorWindowSize(size + extra);
497+
cursor = database.rawQuery("select b from t1 where a = ?;", new Object[]{id});
498+
if(cursor != null && cursor.moveToFirst()) {
499+
tooLargeQueriedData = cursor.getBlob(0);
500+
cursor.close();
501+
}
502+
}
503+
assertThat(Arrays.equals(tooLargeQueriedData, tooLargeData), is(true));
504+
} finally {
505+
SQLiteCursor.resetCursorWindowSize();
506+
}
507+
}
454508
}

sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteCursor.java

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@
2020

2121
package net.zetetic.database.sqlcipher;
2222

23+
import android.annotation.SuppressLint;
2324
import android.database.AbstractWindowedCursor;
2425
import android.database.CursorWindow;
2526
import net.zetetic.database.DatabaseUtils;
2627

27-
import android.os.StrictMode;
28+
import android.os.Build;
2829
import android.util.Log;
2930

31+
import java.lang.reflect.Field;
3032
import java.util.HashMap;
3133
import java.util.Map;
3234

@@ -40,6 +42,10 @@
4042
public class SQLiteCursor extends AbstractWindowedCursor {
4143
static final String TAG = "SQLiteCursor";
4244
static final int NO_COUNT = -1;
45+
private static final int CURSOR_WINDOW_EXTRA = 512;
46+
private static boolean CURSOR_WINDOW_NEEDS_RECREATED = false;
47+
private static final int DEFAULT_CURSOR_WINDOW_SIZE = (int)(8 * Math.pow(1024, 2));
48+
public static int PREFERRED_CURSOR_WINDOW_SIZE = DEFAULT_CURSOR_WINDOW_SIZE;
4349

4450
/** The name of the table to edit */
4551
private final String mEditTable;
@@ -131,21 +137,52 @@ public int getCount() {
131137
return mCount;
132138
}
133139

140+
public static void setCursorWindowSize(int size) {
141+
PREFERRED_CURSOR_WINDOW_SIZE = size;
142+
CURSOR_WINDOW_NEEDS_RECREATED = true;
143+
}
144+
145+
public static void resetCursorWindowSize() {
146+
PREFERRED_CURSOR_WINDOW_SIZE = DEFAULT_CURSOR_WINDOW_SIZE;
147+
CURSOR_WINDOW_NEEDS_RECREATED = true;
148+
}
149+
134150
/*
135151
** The AbstractWindowClass contains protected methods clearOrCreateWindow() and
136152
** closeWindow(), which are used by the android.database.sqlite.* version of this
137153
** class. But, since they are marked with "@hide", the following replacement
138154
** versions are required.
139155
*/
140-
private void awc_clearOrCreateWindow(String name){
141-
CursorWindow win = getWindow();
142-
if( win==null ){
143-
win = new CursorWindow(name);
144-
setWindow(win);
145-
}else{
146-
win.clear();
156+
private void awc_clearOrCreateWindow(String name) {
157+
int cursorWindowAllocationSize = PREFERRED_CURSOR_WINDOW_SIZE + CURSOR_WINDOW_EXTRA;
158+
if (CURSOR_WINDOW_NEEDS_RECREATED) {
159+
awc_closeWindow();
160+
CURSOR_WINDOW_NEEDS_RECREATED = false;
161+
}
162+
CursorWindow win = getWindow();
163+
if ( win==null ) {
164+
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
165+
win = new CursorWindow(name, cursorWindowAllocationSize);
166+
} else {
167+
try {
168+
@SuppressLint("DiscouragedPrivateApi")
169+
Field field = CursorWindow.class.getDeclaredField("sCursorWindowSize");
170+
if (field != null) {
171+
field.setAccessible(true);
172+
field.set(null, cursorWindowAllocationSize);
173+
Log.i(TAG, String.format("Set CursorWindow allocation size to %s", cursorWindowAllocationSize));
174+
}
175+
} catch (Exception ex) {
176+
Log.e(TAG, "Failed to override CursorWindow allocation size", ex);
177+
}
178+
win = new CursorWindow(name);
179+
}
180+
setWindow(win);
181+
}else{
182+
win.clear();
147183
}
148184
}
185+
149186
private void awc_closeWindow(){
150187
setWindow(null);
151188
}

sqlcipher/src/main/jni/sqlcipher/android_database_SQLiteConnection.cpp

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
#include <JNIHelp.h>
2525
#include "ALog-priv.h"
2626

27-
2827
#include <sys/mman.h>
2928
#include <string.h>
3029
#include <unistd.h>
@@ -755,7 +754,7 @@ static jlong nativeExecuteForCursorWindow(
755754
int nRow;
756755
jboolean bOk;
757756
int iStart; /* First row copied to CursorWindow */
758-
757+
int rc_step;
759758
/* Locate all required CursorWindow methods. */
760759
cls = pEnv->FindClass("android/database/CursorWindow");
761760
for(i=0; i<(sizeof(aMethod)/sizeof(struct CWMethod)); i++){
@@ -775,7 +774,7 @@ static jlong nativeExecuteForCursorWindow(
775774

776775
nRow = 0;
777776
iStart = startPos;
778-
while( sqlite3_step(pStmt)==SQLITE_ROW ){
777+
while( (rc_step = sqlite3_step(pStmt))==SQLITE_ROW ){
779778
/* Only copy in rows that occur at or after row index iStart. */
780779
if( nRow>=iStart && bOk ){
781780
bOk = copyRowToWindow(pEnv, win, (nRow - iStart), pStmt, aMethod);
@@ -811,6 +810,22 @@ static jlong nativeExecuteForCursorWindow(
811810
return 0;
812811
}
813812

813+
if( bOk==0 && countAllRows==0 && nRow <= iRowRequired ) {
814+
char *msg = sqlite3_mprintf("Row too big to fit into CursorWindow requiredPos=%d, totalRows=%d",
815+
iRowRequired, nRow);
816+
throw_sqlite3_exception(pEnv, pConnection->db, msg);
817+
sqlite3_free(msg);
818+
return 0;
819+
}
820+
821+
if ( rc_step != SQLITE_DONE && rc_step != SQLITE_ROW ) {
822+
char *msg = sqlite3_mprintf("Unexpected result code=%d while stepping through result",
823+
rc_step);
824+
throw_sqlite3_exception(pEnv, pConnection->db, msg);
825+
sqlite3_free(msg);
826+
return 0;
827+
}
828+
814829
jlong lRet = jlong(iStart) << 32 | jlong(nRow);
815830
return lRet;
816831
}

0 commit comments

Comments
 (0)