From 88a5bed0b9220c37d52b459009e90b2ab0a4926b Mon Sep 17 00:00:00 2001 From: Aaron Oneal Date: Tue, 30 Dec 2014 17:21:07 -0800 Subject: [PATCH 01/11] Fix test runner to use correct plugin id. The plugin id used here must match the one in plugin.xml or the test runner will fail to remove it when deployed. --- bin/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/test.sh b/bin/test.sh index 8a780023d..11e7596e6 100755 --- a/bin/test.sh +++ b/bin/test.sh @@ -49,6 +49,6 @@ cp -r ../src ../plugin.xml ../www ../.plugin # update the plugin, run the test app cordova platform add $platform -cordova plugin rm com.phonegap.plugins.sqlite +cordova plugin rm com.brodysoft.sqlitePlugin cordova plugin add ../.plugin cordova run $platform From 8c3bdccfe19ba9c488884283acdb2ad1427995de Mon Sep 17 00:00:00 2001 From: Aaron Oneal Date: Tue, 30 Dec 2014 17:26:47 -0800 Subject: [PATCH 02/11] Fix database close conditions and unit test. The test was closing the database in the middle of an execute transaction. This would cause the transaction to fail because at the exit of the transaction handler a commit would be issued on a closed database. This fix moves the close to the success handler of the transaction so that the final close occurs after the transaction ends. Additionally, the db variable is now obtained from the open callback instead of the function result, which is a more consistent pattern for async. Finally, the close method was updated to fail if an attempt is made to call it while a transaction is active and a test was added to confirm this behavior. --- SQLitePlugin.coffee.md | 4 ++++ test-www/www/index.html | 38 ++++++++++++++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/SQLitePlugin.coffee.md b/SQLitePlugin.coffee.md index 67a470ef9..1a31248a8 100644 --- a/SQLitePlugin.coffee.md +++ b/SQLitePlugin.coffee.md @@ -128,6 +128,10 @@ License for common Javascript: MIT or Apache #console.log "SQLitePlugin.prototype.close" if @dbname of @openDBs + if txLocks[@dbname] && txLocks[@dbname].inProgress + error(new Error('database cannot be closed while a transaction is in progress')) + return + delete @openDBs[@dbname] cordova.exec success, error, "SQLitePlugin", "close", [ { path: @dbname } ] diff --git a/test-www/www/index.html b/test-www/www/index.html index b66d0a7b6..580aa3627 100755 --- a/test-www/www/index.html +++ b/test-www/www/index.html @@ -1232,6 +1232,33 @@ start(1); }); }); + + if (!isWebSql) test (suiteName + ' database.close fails in transaction', function () { + stop(1); + + var dbName = "Database-Close-fail"; + var db = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE); + + db.readTransaction(function(tx) { + tx.executeSql('SELECT 1', [], function(tx, results) { + // close database - need to run tests directly in callbacks as nothing is guarenteed to be queued after a close + db.close(function () { + ok(false, 'expect close to fail during transaction'); + start(1); + }, function (error) { + ok(true, 'expect close to fail during transaction'); + start(1); + }); + start(1); + }, function(error) { + ok(false, error); + start(2); + }); + }, function(error) { + ok(false, error); + start(2); + }); + }); if (!isWebSql) test(suiteName + ' open same database twice works', function () { @@ -1275,15 +1302,14 @@ stop(1); var dbName = "Database-Close-and-Reopen"; - var db = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE, function () { + openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE, function (db) { db.close(function () { - db = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE, function () { + openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE, function (db) { db.close(function () { - db = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE, function () { + openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE, function (db) { db.readTransaction(function (tx) { tx.executeSql('SELECT 1', [], function (tx, results) { ok(true, 'database re-opened succesfully'); - db.close(); start(1); }, function (error) { ok(false, error.message); @@ -1292,6 +1318,10 @@ }, function (error) { ok(false, error.message); start(1); + }, function(tx) { + // close on transaction success not while executing + // or commit will fail + db.close(); }); }, function (error) { ok(false, error.message); From 1c7e1c703b83ea895ac820cca7b4881879987582 Mon Sep 17 00:00:00 2001 From: Aaron Oneal Date: Tue, 30 Dec 2014 17:51:40 -0800 Subject: [PATCH 03/11] Propagate statement failures to transaction failures. According to the W3C spec, if a transaction failure callback *does not return false* then the error is propagated to a transaction failure. This means that if a transaction returns true, but more importantly, if it has no return value (undefined) then the error must propagate to the transaction. This fix explicitly requires false to correct the propagation. A subsequent fix will address the fact that the original statement error is not propagated and is replaced here, but that is unrelated to this issue. --- SQLitePlugin.coffee.md | 2 +- test-www/www/index.html | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/SQLitePlugin.coffee.md b/SQLitePlugin.coffee.md index 1a31248a8..553618f31 100644 --- a/SQLitePlugin.coffee.md +++ b/SQLitePlugin.coffee.md @@ -233,7 +233,7 @@ License for common Javascript: MIT or Apache SQLitePluginTransaction::handleStatementFailure = (handler, response) -> if !handler throw new Error "a statement with no error handler failed: " + response.message - if handler(this, response) + if handler(this, response) isnt false throw new Error "a statement error callback did not return false" return diff --git a/test-www/www/index.html b/test-www/www/index.html index 580aa3627..ceedff1f5 100755 --- a/test-www/www/index.html +++ b/test-www/www/index.html @@ -509,11 +509,12 @@ }, function(tx, err) { start(); ok(!!err.message, "should report a valid error message"); + return false; }); }); }, function(err) { start(); - ok(false, "transaction was supposed to succeed"); + ok(false, "transaction was supposed to succeed: " + err.message); }, function() { db.transaction(function(tx) { tx.executeSql("select count(*) as cnt from test_table", [], function(tx, res) { From 7a065fb48302095e15246355ecc82e830a96b0c2 Mon Sep 17 00:00:00 2001 From: Aaron Oneal Date: Tue, 30 Dec 2014 18:08:48 -0800 Subject: [PATCH 04/11] Fix error types on open, close, and deleteDb. The aforementioned methods receive errors as string messages from the native layer, so these strings must be wrapped in a regular `Error` type before passing on to API callbacks. This fix also makes 2 minor changes to open: 1) remove a semicolon per doc convention 2) only call the success callback if there is one (in case the default handler that logs is ever removed) --- SQLitePlugin.coffee.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/SQLitePlugin.coffee.md b/SQLitePlugin.coffee.md index 553618f31..34a59475a 100644 --- a/SQLitePlugin.coffee.md +++ b/SQLitePlugin.coffee.md @@ -111,17 +111,18 @@ License for common Javascript: MIT or Apache return SQLitePlugin::open = (success, error) -> - onSuccess = () => success this + onSuccess = () => if !!success then success this + onError = (errMessage) -> if !!error then error new Error(errMessage) unless @dbname of @openDBs @openDBs[@dbname] = true - cordova.exec onSuccess, error, "SQLitePlugin", "open", [ @openargs ] + cordova.exec onSuccess, onError, "SQLitePlugin", "open", [ @openargs ] else ### for a re-open run onSuccess async so that the openDatabase return value can be used in the success handler as an alternative to the handler's db argument ### - nextTick () -> onSuccess(); + nextTick () -> onSuccess() return SQLitePlugin::close = (success, error) -> @@ -133,9 +134,8 @@ License for common Javascript: MIT or Apache return delete @openDBs[@dbname] - - cordova.exec success, error, "SQLitePlugin", "close", [ { path: @dbname } ] - + onError = (errMessage) -> if !!error then error new Error(errMessage) + cordova.exec success, onError, "SQLitePlugin", "close", [ { path: @dbname } ] return SQLitePlugin::executeSql = (statement, params, success, error) -> @@ -399,7 +399,8 @@ License for common Javascript: MIT or Apache deleteDb: (databaseName, success, error) -> delete SQLitePlugin::openDBs[databaseName] - cordova.exec success, error, "SQLitePlugin", "delete", [{ path: databaseName }] + onError = (errMessage) -> if !!error then error new Error("error while trying to delete database: " + errMessage) + cordova.exec success, onError, "SQLitePlugin", "delete", [{ path: databaseName }] ### Exported API: From 1cf85118f51b1a893b4cd084cbf28c8d7ba49901 Mon Sep 17 00:00:00 2001 From: Aaron Oneal Date: Tue, 30 Dec 2014 18:21:23 -0800 Subject: [PATCH 05/11] Fix transaction and statement errors to conform to SQLError. Sometimes the native layer returns errors as {message:string, code:number} and other times as just a string. This fix wraps key places to ensure that errors returned through the API always conform to SQLError per the API contract documented in the W3C spec. In short, errors consistently have a message and code now and propagate from statement failure up to the transaction. --- SQLitePlugin.coffee.md | 43 ++++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/SQLitePlugin.coffee.md b/SQLitePlugin.coffee.md index 34a59475a..ec3bfb849 100644 --- a/SQLitePlugin.coffee.md +++ b/SQLitePlugin.coffee.md @@ -20,6 +20,29 @@ License for common Javascript: MIT or Apache ### utility function(s): + # Errors returned to callbacks must conform to `SqlError` with a code and message. + # Some errors are of type `Error` or `string` and must be converted. + toSQLError = (error, code) -> + sqlError = error + code = 0 if !code # unknown by default + + if !sqlError + sqlError = new Error "a plugin had an error but provided no response" + sqlError.code = code + + if typeof sqlError is "string" + sqlError = new Error error + sqlError.code = code + + if !sqlError.code && sqlError.message + sqlError.code = code + + if !sqlError.code && !sqlError.message + sqlError = new Error "an unknown error was returned: " + JSON.stringify(sqlError) + sqlError.code = code + + return sqlError + nextTick = window.setImmediate || (fun) -> window.setTimeout(fun, 0) return @@ -87,14 +110,14 @@ License for common Javascript: MIT or Apache SQLitePlugin::transaction = (fn, error, success) -> if !@openDBs[@dbname] - error('database not open') + error(toSQLError('database not open')) return @addTransaction new SQLitePluginTransaction(this, fn, error, success, true, false) return SQLitePlugin::readTransaction = (fn, error, success) -> if !@openDBs[@dbname] - error('database not open') + error(toSQLError('database not open')) return @addTransaction new SQLitePluginTransaction(this, fn, error, success, true, true) return @@ -174,7 +197,7 @@ License for common Javascript: MIT or Apache if txlock @executeSql "BEGIN", [], null, (tx, err) -> - throw new Error("unable to begin transaction: " + err.message) + throw toSQLError("unable to begin transaction: " + err.message, err.code) return @@ -189,7 +212,7 @@ License for common Javascript: MIT or Apache txLocks[@db.dbname].inProgress = false @db.startNextTransaction() if @error - @error err + @error toSQLError(err) return SQLitePluginTransaction::executeSql = (sql, values, success, error) -> @@ -232,9 +255,9 @@ License for common Javascript: MIT or Apache SQLitePluginTransaction::handleStatementFailure = (handler, response) -> if !handler - throw new Error "a statement with no error handler failed: " + response.message + throw toSQLError("a statement with no error handler failed: " + response.message, response.code) if handler(this, response) isnt false - throw new Error "a statement error callback did not return false" + throw toSQLError("a statement error callback did not return false: " + response.message, response.code) return SQLitePluginTransaction::run = -> @@ -252,9 +275,9 @@ License for common Javascript: MIT or Apache if didSucceed tx.handleStatementSuccess batchExecutes[index].success, response else - tx.handleStatementFailure batchExecutes[index].error, response + tx.handleStatementFailure batchExecutes[index].error, toSQLError(response) catch err - txFailure = err unless txFailure + txFailure = toSQLError(err) unless txFailure if --waiting == 0 if txFailure @@ -325,7 +348,7 @@ License for common Javascript: MIT or Apache failed = (tx, err) -> txLocks[tx.db.dbname].inProgress = false tx.db.startNextTransaction() - if tx.error then tx.error new Error("error while trying to roll back: " + err.message) + if tx.error then tx.error toSQLError("error while trying to roll back: " + err.message, err.code) return @finalized = true @@ -351,7 +374,7 @@ License for common Javascript: MIT or Apache failed = (tx, err) -> txLocks[tx.db.dbname].inProgress = false tx.db.startNextTransaction() - if tx.error then tx.error new Error("error while trying to commit: " + err.message) + if tx.error then tx.error toSQLError("error while trying to commit: " + err.message, err.code) return @finalized = true From 4f1433cb7741735c27674431ed00c5c97b86d97b Mon Sep 17 00:00:00 2001 From: Aaron Oneal Date: Tue, 30 Dec 2014 19:03:43 -0800 Subject: [PATCH 06/11] Fix to prevent double marshaling of data. Moving data between JavaScript and native is extremely slow, especially for binary which Cordova has to base64 encode and then transmit over XHR or as an URL parameter. The last thing we want to do is send more data. For some reason SQL statements and their parameters were being sent twice because they were duplicated to a `query` member. That member was only used by the iOS plugin, so this fix removes it and updates the iOS plugin to use the sql and params members like the others. Additionally, the iOS plugin is updated to handle unsupported types during binding and a test was added to verify. --- SQLitePlugin.coffee.md | 2 -- src/ios/SQLitePlugin.m | 34 ++++++++++++++++++++-------------- test-www/www/index.html | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 16 deletions(-) diff --git a/SQLitePlugin.coffee.md b/SQLitePlugin.coffee.md index ec3bfb849..9a2ff9e57 100644 --- a/SQLitePlugin.coffee.md +++ b/SQLitePlugin.coffee.md @@ -308,8 +308,6 @@ License for common Javascript: MIT or Apache tropts.push qid: qid - # for ios version: - query: [request.sql].concat(request.params) sql: request.sql params: request.params diff --git a/src/ios/SQLitePlugin.m b/src/ios/SQLitePlugin.m index 4c61d7f3c..5d69b19cd 100755 --- a/src/ios/SQLitePlugin.m +++ b/src/ios/SQLitePlugin.m @@ -352,24 +352,24 @@ -(void) executeSql: (CDVInvokedUrlCommand*)command -(CDVPluginResult*) executeSqlWithDict: (NSMutableDictionary*)options andArgs: (NSMutableDictionary*)dbargs { NSString *dbPath = [self getDBPath:[dbargs objectForKey:@"dbname"]]; - - NSMutableArray *query_parts = [options objectForKey:@"query"]; - NSString *query = [query_parts objectAtIndex:0]; - if (dbPath == NULL) { return [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"You must specify database path"]; } - if (query == NULL) { - return [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"You must specify a query to execute"]; + + NSString *sql = [options objectForKey:@"sql"]; + if (sql == NULL) { + return [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"You must specify a sql query to execute"]; } + NSMutableArray *params = [options objectForKey:@"params"]; // optional + NSValue *dbPointer = [openDBs objectForKey:dbPath]; if (dbPointer == NULL) { return [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"No such database, you must open it first"]; } sqlite3 *db = [dbPointer pointerValue]; - const char *sql_stmt = [query UTF8String]; + const char *sql_stmt = [sql UTF8String]; NSDictionary *error = nil; sqlite3_stmt *statement; int result, i, column_type, count; @@ -392,9 +392,9 @@ -(CDVPluginResult*) executeSqlWithDict: (NSMutableDictionary*)options andArgs: ( if (sqlite3_prepare_v2(db, sql_stmt, -1, &statement, NULL) != SQLITE_OK) { error = [SQLitePlugin captureSQLiteErrorFromDb:db]; keepGoing = NO; - } else { - for (int b = 1; b < query_parts.count; b++) { - [self bindStatement:statement withArg:[query_parts objectAtIndex:b] atIndex:b]; + } else if(params != NULL) { + for (int b = 0; b < params.count; b++) { + [self bindStatement:statement withArg:[params objectAtIndex:b] atIndex:(b+1)]; } } @@ -489,13 +489,19 @@ -(void)bindStatement:(sqlite3_stmt *)statement withArg:(NSObject *)arg atIndex:( } else if (strcmp(numberType, @encode(double)) == 0) { sqlite3_bind_double(statement, argIndex, [numberArg doubleValue]); } else { - sqlite3_bind_text(statement, argIndex, [[NSString stringWithFormat:@"%@", arg] UTF8String], -1, SQLITE_TRANSIENT); + sqlite3_bind_text(statement, argIndex, [[arg description] UTF8String], -1, SQLITE_TRANSIENT); } } else { // NSString - NSString *stringArg = (NSString *)arg; + NSString *stringArg; + + if ([arg isKindOfClass:[NSString class]]) { + stringArg = (NSString *)arg; + } else { + stringArg = [arg description]; // convert to text + } + NSData *data = [stringArg dataUsingEncoding:NSUTF8StringEncoding]; - - sqlite3_bind_text(statement, argIndex, data.bytes, data.length, SQLITE_TRANSIENT); + sqlite3_bind_text(statement, argIndex, data.bytes, data.length, SQLITE_TRANSIENT); } } diff --git a/test-www/www/index.html b/test-www/www/index.html index ceedff1f5..9879adb07 100755 --- a/test-www/www/index.html +++ b/test-www/www/index.html @@ -592,6 +592,40 @@ }); }); + // This test shows that the plugin does not throw an error when trying to serialize + // an unsupported parameter type. Blob becomes an empty dictionary on iOS, for example, + // and so this verifies the type is converted to a string and continues. Web SQL does + // the same but on the JavaScript side and converts to a string like `[object Blob]`. + if (typeof ArrayBuffer !== "undefined") test(suiteName + "unsupported parameter type as string", function() { + var db = openDatabase("Blob-test.db", "1.0", "Demo", DEFAULT_SIZE); + ok(!!db, "db object"); + stop(1); + + db.transaction(function(tx) { + ok(!!tx, "tx object"); + + var buffer = new ArrayBuffer(5); + var view = new Uint8Array(buffer); + view[0] = 'h'.charCodeAt(); + view[1] = 'e'.charCodeAt(); + view[2] = 'l'.charCodeAt(); + view[3] = 'l'.charCodeAt(); + view[4] = 'o'.charCodeAt(); + var blob = new Blob([view.buffer], { type:"application/octet-stream" }); + + tx.executeSql('DROP TABLE IF EXISTS test_table'); + tx.executeSql('CREATE TABLE IF NOT EXISTS test_table (foo blob)'); + tx.executeSql('INSERT INTO test_table VALUES (?)', [blob], function(tx, res) { + ok(true, "insert as string succeeds"); + start(1); + }); + start(1); + }, function(err) { + ok(false, "transaction does not serialize real data but still should not fail: " + err.message); + start(2); + }); + }); + test(suiteName + "readTransaction should throw on modification", function() { stop(); var db = openDatabase("Database-readonly", "1.0", "Demo", DEFAULT_SIZE); From c19698b127d414cfccd9d97e8e134e5d72202b64 Mon Sep 17 00:00:00 2001 From: Aaron Oneal Date: Tue, 30 Dec 2014 20:32:20 -0800 Subject: [PATCH 07/11] Fix iOS regex args check. The regex function for iOS was reading arguments before checking that the correct number were specified. --- src/ios/SQLitePlugin.m | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/ios/SQLitePlugin.m b/src/ios/SQLitePlugin.m index 5d69b19cd..7a5893e05 100755 --- a/src/ios/SQLitePlugin.m +++ b/src/ios/SQLitePlugin.m @@ -134,10 +134,12 @@ static int base64_encode_blockend(char* code_out, static void sqlite_regexp(sqlite3_context* context, int argc, sqlite3_value** values) { int ret; regex_t regex; - char* reg = (char*)sqlite3_value_text(values[0]); - char* text = (char*)sqlite3_value_text(values[1]); + char* reg; + char* text; - if ( argc != 2 || reg == 0 || text == 0) { + if ( argc != 2 + || (reg = (char*)sqlite3_value_text(values[0])) == 0 + || (text = (char*)sqlite3_value_text(values[1])) == 0) { sqlite3_result_error(context, "SQL function regexp() called with invalid arguments.\n", -1); return; } From 6d5f4427463de16e57c631a56bd2df2d2122f518 Mon Sep 17 00:00:00 2001 From: Aaron Oneal Date: Tue, 30 Dec 2014 20:57:26 -0800 Subject: [PATCH 08/11] Fix truncation in iOS query result string encoding. This fix enables end-to-end encoding of binary data using strings. Strings with Unicode values like `\u0000` were being truncated because the string constructor was using a null terminator to determine size instead of the stored byte length. The fix uses a different constructor and the stored byte length so that the full length string is returned. --- src/ios/SQLitePlugin.m | 7 +++++- test-www/www/index.html | 50 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/ios/SQLitePlugin.m b/src/ios/SQLitePlugin.m index 7a5893e05..8f092fe48 100755 --- a/src/ios/SQLitePlugin.m +++ b/src/ios/SQLitePlugin.m @@ -419,7 +419,12 @@ -(CDVPluginResult*) executeSqlWithDict: (NSMutableDictionary*)options andArgs: ( columnValue = [NSNumber numberWithDouble: sqlite3_column_double(statement, i)]; break; case SQLITE_TEXT: - columnValue = [NSString stringWithUTF8String:(char *)sqlite3_column_text(statement, i)]; + columnValue = [[NSString alloc] initWithBytes:(char *)sqlite3_column_text(statement, i) + length:sqlite3_column_bytes(statement, i) + encoding:NSUTF8StringEncoding]; +#if !__has_feature(objc_arc) + [columnValue autorelease]; +#endif break; case SQLITE_BLOB: //LIBB64 diff --git a/test-www/www/index.html b/test-www/www/index.html index 9879adb07..024fc848d 100755 --- a/test-www/www/index.html +++ b/test-www/www/index.html @@ -902,6 +902,56 @@ }); } + if (!/MSIE/.test(navigator.userAgent)) test(suiteName + ' returns unicode correctly', function () { + stop(); + + var dbName = "Database-Unicode"; + var db = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE); + + db.transaction(function (tx) { + tx.executeSql('DROP TABLE IF EXISTS test', [], function () { + tx.executeSql('CREATE TABLE test (name, id)', [], function() { + tx.executeSql('INSERT INTO test VALUES (?, "id1")', ['\u0000foo'], function () { + tx.executeSql('SELECT name FROM test', [], function (tx, res) { + var name = res.rows.item(0).name; + + var expected = [ + '\u0000foo' + ]; + + // There is a bug in WebKit and Chromium where strings are created + // using methods that rely on '\0' for termination instead of + // the specified byte length. + // + // https://bugs.webkit.org/show_bug.cgi?id=137637 + // + // For now we expect this test to fail there, but when it is fixed + // we would like to know, so the test is coded to fail if it starts + // working there. + if(!isWebSql) { + ok(expected.indexOf(name) !== -1, 'field value: ' + + JSON.stringify(name) + ' should be in ' + + JSON.stringify(expected)); + + equal(name.length, 4, 'length of field === 4'); + } else { + ok(expected.indexOf(name) === -1, 'field value: ' + + JSON.stringify(name) + ' should not be in this until a bug is fixed ' + + JSON.stringify(expected)); + + equal(name.length, 0, 'length of field === 0'); + } + start(); + }) + }); + }); + }); + }, function(err) { + ok(false, 'unexpected error: ' + err.message); + }, function () { + }); + }); + test(suiteName + "syntax error", function() { var db = openDatabase("Syntax-error-test.db", "1.0", "Demo", DEFAULT_SIZE); ok(!!db, "db object"); From ca69cae8cb983360f44ec536bf576ca2ad1acc37 Mon Sep 17 00:00:00 2001 From: Aaron Oneal Date: Wed, 31 Dec 2014 00:18:51 -0800 Subject: [PATCH 09/11] Fix warning regarding test runner viewport format. Parameters use `,` not `;`. --- test-www/www/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-www/www/index.html b/test-www/www/index.html index 024fc848d..a9f94bbf0 100755 --- a/test-www/www/index.html +++ b/test-www/www/index.html @@ -1,7 +1,7 @@ - + SQLitePlugin test From 37bd8598baccd2a37c8f5c91175aee71aa616917 Mon Sep 17 00:00:00 2001 From: Aaron Oneal Date: Thu, 1 Jan 2015 18:36:36 -0800 Subject: [PATCH 10/11] Fix executeSql to throw on finalized transactions. If executeSql is called on a finalized transaction it should throw. If it does not, timing errors get a lot harder to debug. ES6 promises, for example, generally execute on the next tick and then it becomes unclear why a statement fails to execute. --- SQLitePlugin.coffee.md | 15 +++++++++++++-- test-www/www/index.html | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/SQLitePlugin.coffee.md b/SQLitePlugin.coffee.md index 9a2ff9e57..63507f89d 100644 --- a/SQLitePlugin.coffee.md +++ b/SQLitePlugin.coffee.md @@ -217,6 +217,17 @@ License for common Javascript: MIT or Apache SQLitePluginTransaction::executeSql = (sql, values, success, error) -> + if @finalized + throw {message: 'InvalidStateError: DOM Exception 11: This transaction is already finalized. Transactions are committed after its success or failure handlers are called. If you are using a Promise to handle callbacks, be aware that implementations following the A+ standard adhere to run-to-completion semantics and so Promise resolution occurs on a subsequent tick and therefore after the transaction commits.', code: 11} + return + + @_executeSqlInternal(sql, values, success, error) + return + + # This method performs the actual execute but does not check for + # finalization since it is used to execute COMMIT and ROLLBACK. + SQLitePluginTransaction::_executeSqlInternal = (sql, values, success, error) -> + if @readOnly && READ_ONLY_REGEX.test(sql) @handleStatementFailure(error, {message: 'invalid sql for a read-only transaction'}) return @@ -352,7 +363,7 @@ License for common Javascript: MIT or Apache @finalized = true if @txlock - @executeSql "ROLLBACK", [], succeeded, failed + @_executeSqlInternal "ROLLBACK", [], succeeded, failed @run() else succeeded(tx) @@ -378,7 +389,7 @@ License for common Javascript: MIT or Apache @finalized = true if @txlock - @executeSql "COMMIT", [], succeeded, failed + @_executeSqlInternal "COMMIT", [], succeeded, failed @run() else succeeded(tx) diff --git a/test-www/www/index.html b/test-www/www/index.html index a9f94bbf0..4ab0ab854 100755 --- a/test-www/www/index.html +++ b/test-www/www/index.html @@ -550,6 +550,38 @@ }); }); }); + + test(suiteName + "executeSql fails outside transaction", function() { + withTestTable(function(db) { + expect(5); + ok(!!db, "db ok"); + var txg; + stop(2); + db.transaction(function(tx) { + ok(!!tx, "tx ok"); + txg = tx; + tx.executeSql("insert into test_table (data, data_num) VALUES (?,?)", ['test', null], function(tx, res) { + equal(res.rowsAffected, 1, 'row inserted'); + }); + start(1); + }, function(err) { + ok(false, err); + start(1); + }, function() { + // this simulates what would happen if a Promise ran on the next tick + // and invoked an execute on the transaction + try { + txg.executeSql("select count(*) as cnt from test_table", [], null, null); + ok(false, "executeSql should have thrown but continued instead"); + } catch(err) { + ok(!!err.message, "error had valid message"); + ok(/InvalidStateError|SQLTransaction/.test(err.message), + "execute must throw InvalidStateError; actual error: " + err.message); + } + start(1); + }); + }); + }); test(suiteName + "all columns should be included in result set (including 'null' columns)", function() { withTestTable(function(db) { From b62bdff92f68494c6b2aaafc8de5406de85d5997 Mon Sep 17 00:00:00 2001 From: Aaron Oneal Date: Wed, 31 Dec 2014 19:38:08 -0800 Subject: [PATCH 11/11] Improve SQL blob marshaling. Web SQL doesn't actually support storing binary data in SQL blobs using the SQLite blob methods. It serializes binary data using strings with the SQLite text methods which results in data not being stored as a blob. This can be problematic in Cordova if you need to work with existing SQLite databases. This change adds more robust support for binary serialization. `SQLBlob` objects can be used in statements and these will be converted to a `sqlblob` URI with base64 or URL encoded data that is unpacked on the native side and stored as a proper binary blob. When reading a blob back from the native side, the previous behavior was to return the blob as base64. Now, the blob can be decoded as a `SQLBlob` object which can then be unpacked in JavaScript to base64, ArrayBuffer, or binary string. tx.executeSql('INSERT INTO test_table VALUES (?)', [new SQLBlob(arrayBuffer)]); tx.executeSql("SELECT foo FROM test_table", [], function(tx, res) { var blob = new SQLBlob(res.rows.item(0).foo); blob.toBase64(); blob.toBinaryString(); blob.toArrayBuffer(); }); Tests were added to verify the behavior and to demonstrate usage. Only iOS and Android were updated. Windows Phone did not previously have blob support; saving for a future update. --- SQLBlob.coffee.md | 295 +++++++++++++++++++++ SQLitePlugin.coffee.md | 3 +- bin/test.sh | 2 +- plugin.xml | 4 + src/android/org/pgsqlite/SQLitePlugin.java | 28 +- src/ios/SQLitePlugin.h | 8 +- src/ios/SQLitePlugin.m | 189 +++---------- test-www/www/index.html | 266 +++++++++++++++++++ www/SQLBlob.js | 271 +++++++++++++++++++ 9 files changed, 902 insertions(+), 164 deletions(-) create mode 100644 SQLBlob.coffee.md create mode 100644 www/SQLBlob.js diff --git a/SQLBlob.coffee.md b/SQLBlob.coffee.md new file mode 100644 index 000000000..f49ea75c5 --- /dev/null +++ b/SQLBlob.coffee.md @@ -0,0 +1,295 @@ +# SQLBlob in Markdown (litcoffee) + +## Top-level objects + +### Root window object + + root = @ + +### Base64 conversion + + # Adapted from: base64-arraybuffer + # https://github.com/niklasvh/base64-arraybuffer + # Copyright (c) 2012 Niklas von Hertzen + # Licensed under the MIT license. + + BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + + arrayBufferToBase64 = (arraybuffer) -> + bytes = new Uint8Array(arraybuffer) + len = bytes.length + base64 = "" + + i = 0 + while i < len + base64 += BASE64_CHARS[bytes[i] >> 2] + base64 += BASE64_CHARS[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)] + base64 += BASE64_CHARS[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)] + base64 += BASE64_CHARS[bytes[i + 2] & 63] + i += 3 + + if (len % 3) is 2 + base64 = base64.substring(0, base64.length - 1) + "=" + else if (len % 3) is 1 + base64 = base64.substring(0, base64.length - 2) + "==" + + return base64 + + # This direct conversion should be faster than atob() and array copy + base64ToArrayBuffer = (base64) -> + bufferLength = base64.length * 0.75 + len = base64.length + p = 0 + + if base64[base64.length - 1] is "=" + bufferLength-- + + if base64[base64.length - 2] is "=" + bufferLength-- + + arraybuffer = new ArrayBuffer(bufferLength) + bytes = new Uint8Array(arraybuffer) + + i = 0 + while i < len + encoded1 = BASE64_CHARS.indexOf(base64[i]) + encoded2 = BASE64_CHARS.indexOf(base64[i+1]) + encoded3 = BASE64_CHARS.indexOf(base64[i+2]) + encoded4 = BASE64_CHARS.indexOf(base64[i+3]) + + bytes[p++] = (encoded1 << 2) | (encoded2 >> 4) + bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2) + bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63) + i += 4 + + return arraybuffer + +### Binary string conversion + +Binary string is a JavaScript term that basically means treat the string as a byte array. + + # Each byte from the buffer is transferred into a JavaScript UCS-2 string. + arrayBufferToBinaryString = (buffer) -> + binary = "" + bytes = new Uint8Array(buffer) + length = bytes.byteLength + i = 0 + while i < length + # code will be < 256 but expanded to 2 bytes + # when stored in a JavaScript string + binary += String.fromCharCode(bytes[i]) + ++i + return binary + + # The binary string is UCS-2 encoded, but all characters are required to be < 256. + # That is, the string must represent a series of bytes. Because UCS-2 is a 2 byte + # format, it's possible a multi-byte character was added and that this isn't + # a true "binary string", so the method throws if encountered. + binaryStringToArrayBuffer = (binary) -> + length = binary.length + buffer = new ArrayBuffer(length) + bytes = new Uint8Array(buffer) + i = 0 + while i < length + code = binary.charCodeAt(i) + if code > 255 + throw new Error("a multibyte character was encountered in the provided string which indicates it was not encoded as a binary string") + bytes[i] = code + ++i + return bytes.buffer + +### SQLBlob + +#### Summary + +SQLite determines column types for data rows during insert based on the type used to bind the value to the statement, not the type specified in the table schema. The latter is only an informational tag to indicate expected binding. + +Browser implementations convert between JS and SQL types when binding statements: + +* JS number -> SQLite integer | float +* JS UCS-2 string -> SQLite text + +Values never serialize to SQLite blobs and at best will be serialized as text. Some implementations serialize text using UTF-8 and some UTF-16. + +In a Web SQL environment, it's up to the caller to determine how to encode binary into a string format that can be serialized and deserialized given those conversions. + +One can use a "binary string" which is a JS UCS-2 string where every real byte of data has an extra byte of padding internally, but unfortunately not all browser implementations support all Unicode values from end to end. Some interpret `\u0000` as a null terminator when reading back the string from their internal SQLite implementations which results in truncated data. + +Additionally, Cordova invokes `JSON.stringify()` before sending data to the native layer. See the [JSON spec](http://json.org/) for what that does to strings; generally any Unicode character is allowed except `"` and `\` which will be escaped. A JSON parser like `JSON.parse()` will handle unescaping. + +Even though browser Web SQL implementations only persist binary data as text, it is useful in Cordova scenarios to be able to consume or produce databases that conform to external specifications that make use of blobs and raw binary data. + +The `SQLBlob` type below abstracts these problems by making it: + +1. Easy to serialize binary data to and from an ArrayBuffer, base64 string, or binary string. +2. Possible to recognize binary data in the Cordova plugin so it can be stored as a binary blob. +3. Possible to write the same code for persisting binary data whether using the Cordova plugin or Web SQL in a browser. + +#### SQLBlob object is defined by a constructor function and prototype member functions + + class SQLBlob + # This prefix allows a Cordova native SQL plugin to recognize binary data. + SQLBLOB_URL_PREFIX = "sqlblob:" + DATA_URL_PREFIX = "data:" + SQLBLOB_URL_BASE64_ENCODING = ";base64" + SQLBLOB_URL_BASE64_PREFIX = SQLBLOB_URL_PREFIX + SQLBLOB_URL_BASE64_ENCODING + + # The blob's value is internally and externally represented as + # a SQLBlob URL which is nearly identical to a data URL: + # sqlblob:[][;charset=][;base64], + # + # If ";base64" is part of the URL then data is base64 encoded, + # otherwise it is URL encoded (percent encoded UTF-8). + # + # The former is generally better for binary and the latter for text. + # There is an encoding option to specify the default representation. + # "auto": Prefer base64 for ArrayBuffer and BinaryString, pass through encoding for URLs + # "base64": Always base64 encode + # "url": Always url encode + + @_value # the sqlblob URL with base64 or url encoded data + @_commaIndex # the comma index in the sqlblob URL separating the prefix and data regions + @_options # options like encoding + + constructor: (obj, options = { encoding: "auto" }) -> + @_options = options + + if options.encoding isnt "auto" and options.encoding isnt "url" and options.encoding isnt "base64" + throw new Error("Unknown encoding (must be 'auto', 'url', or 'base64'): " + options.encoding) + + # allow null or undefined as a passthrough + if !obj + @_value = obj + return + + if obj instanceof ArrayBuffer + if options.encoding is "base64" or options.encoding is "auto" + @_value = SQLBLOB_URL_BASE64_PREFIX + "," + arrayBufferToBase64(obj) + @_commaIndex = SQLBLOB_URL_BASE64_PREFIX.length; + else if options.encoding is "url" + # convert to percent encoded UTF-8 (good for most text, not so good for binary) + @_value = SQLBLOB_URL_PREFIX + "," + encodeURIComponent(arrayBufferToBinaryString(obj)); + @_commaIndex = SQLBLOB_URL_PREFIX.length; + else if typeof obj is "string" + # Decode SQLBlob or Data URL if detected. + # Slice is faster than indexOf. + startsWithSqlBlob = obj.slice(0, SQLBLOB_URL_PREFIX.length) is SQLBLOB_URL_PREFIX + startsWithData = obj.slice(0, DATA_URL_PREFIX.length) is DATA_URL_PREFIX + + # verify supported format + if not startsWithSqlBlob and not startsWithData + throw new Error("Only 'sqlblob' and 'data' URI strings are supported") + + # convert data to sqlblob + if startsWithData + obj = SQLBLOB_URL_PREFIX + obj.slice(DATA_URL_PREFIX.length) + + # find comma dividing prefix and data regions + @_commaIndex = commaIndex = obj.indexOf(",") + throw new Error("Missing comma in SQLBlob URL") if commaIndex is -1 + + # test for base64 + isBase64 = obj.slice(0, commaIndex).indexOf(SQLBLOB_URL_BASE64_ENCODING) isnt -1 + + # assign value + if options.encoding is "auto" + @_value = obj # save the sqlblob verbatim + else if options.encoding is "base64" + if isBase64 + @_value = obj # save the sqlblob verbatim + else + # take the percent encoded UTF-8, unescape it to get a byte string, then base64 encode + prefix = obj.slice(0, commaIndex) + SQLBLOB_URL_BASE64_ENCODING + "," + @_commaIndex = prefix.length - 1; + data = obj.slice(commaIndex + 1) + # use unescape here to decode to binary rather than interpret the bytes as UTF-8 + @_value = prefix + window.btoa(unescape(data)) + else if options.encoding is "url" + if not isBase64 + @_value = obj # save the url encoded sqlblob verbatim + else + # decode the base64 to binary, escape to convert bytes back into percent encoding + prefix = obj.slice(0, commaIndex + 1).replace(SQLBLOB_URL_BASE64_ENCODING, "") + @_commaIndex = prefix.length - 1; + data = obj.slice(commaIndex + 1) + @_value = prefix + encodeURIComponent(window.atob(data)) + else + throw new Error("unsupported object type (must be ArrayBuffer or string): " + typeof obj) + # TODO: Blob with FileReader + + return + + Object.defineProperties @prototype, + isBase64: + get: -> @_value.slice(0, @_commaIndex).indexOf(SQLBLOB_URL_BASE64_ENCODING) isnt -1 + + toString: () -> + return @_value # already string + + # This is for JavaScript automatic type conversion and used + # by Web SQL for serialization. + valueOf: () -> + return @_value + + toJSON: () -> + return @_value + + toArrayBuffer: () -> + return @_value if !@_value + + data = @_value.slice(@_commaIndex + 1) + + if @isBase64 + return base64ToArrayBuffer(data) + else + return binaryStringToArrayBuffer(unescape(data)) + + toBase64: () -> + return @_value if !@_value + + data = @_value.slice(@_commaIndex + 1) + + if @isBase64 + return data + else + return window.btoa(unescape(data)) + + toBinaryString: () -> + return @_value if !@_value + + data = @_value.slice(@_commaIndex + 1) + + if @isBase64 + return window.atob(data) + else + return unescape(data) + + toUnicodeString: () -> + return @_value if !@_value + + data = @_value.slice(@_commaIndex + 1) + + if @isBase64 + return decodeURIComponent(escape(window.atob(data))) + else + return decodeURIComponent(data) + + @createFromBase64: (base64, options) -> + return new SQLBlob(SQLBLOB_URL_BASE64_PREFIX + "," + base64, options) + + # All character codes must be < 256 as the string is used in place of a byte array. + @createFromBinaryString: (binary, options = { encoding: "auto" }) -> + if options.encoding is "base64" or options.encoding is "auto" + return new SQLBlob(SQLBLOB_URL_BASE64_PREFIX + "," + window.btoa(binary), options) + else if options.encoding is "url" + return new SQLBlob(SQLBLOB_URL_PREFIX + "," + encodeURIComponent(binary), options) + + # Unicode chars are converted to UTF-8 and percent encoded. If "url" encoding is not + # specified as an option, then the constructor used below will complete the + # conversion of the UTF-8 encoded string to base64. + @createFromUnicodeString: (text, options = { encoding: "auto" }) -> + return new SQLBlob(SQLBLOB_URL_PREFIX + "," + encodeURIComponent(text), options) + +### Exported API + + root.SQLBlob = SQLBlob \ No newline at end of file diff --git a/SQLitePlugin.coffee.md b/SQLitePlugin.coffee.md index 63507f89d..f06c43552 100644 --- a/SQLitePlugin.coffee.md +++ b/SQLitePlugin.coffee.md @@ -320,7 +320,8 @@ License for common Javascript: MIT or Apache tropts.push qid: qid sql: request.sql - params: request.params + # only primitives are supported by Web SQL so call valueOf + params: ((if p then p.valueOf() else p) for p in request.params) i++ diff --git a/bin/test.sh b/bin/test.sh index 11e7596e6..fda37cd3b 100755 --- a/bin/test.sh +++ b/bin/test.sh @@ -34,7 +34,7 @@ if [[ $? != 0 ]]; then # run from the bin/ directory fi # compile coffeescript -coffee --no-header -cl -o ../www ../SQLitePlugin.coffee.md +coffee --no-header -cl -o ../www ../SQLitePlugin.coffee.md ../SQLBlob.coffee.md if [[ $? != 0 ]]; then echo "coffeescript compilation failed" diff --git a/plugin.xml b/plugin.xml index 74ef146a1..7be5f406c 100644 --- a/plugin.xml +++ b/plugin.xml @@ -18,6 +18,10 @@ + + + + diff --git a/src/android/org/pgsqlite/SQLitePlugin.java b/src/android/org/pgsqlite/SQLitePlugin.java index b2c2e2601..425eecc67 100755 --- a/src/android/org/pgsqlite/SQLitePlugin.java +++ b/src/android/org/pgsqlite/SQLitePlugin.java @@ -28,6 +28,7 @@ import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaPlugin; +import org.apache.http.util.EncodingUtils; import org.json.JSONArray; import org.json.JSONException; @@ -633,7 +634,28 @@ private void bindArgsToStatement(SQLiteStatement myStatement, JSONArray sqlArgs) } else if (sqlArgs.isNull(i)) { myStatement.bindNull(i + 1); } else { - myStatement.bindString(i + 1, sqlArgs.getString(i)); + String text = sqlArgs.getString(i); + + int commaIndex; + if(text.startsWith("sqlblob:") && (commaIndex = text.indexOf(',')) != -1) { + String[] mimeParts = text.substring(0, commaIndex).split(";"); + String contentType = mimeParts.length > 0 ? mimeParts[0] : null; + boolean base64 = false; + for (int j = 1; j < mimeParts.length; ++j) { + if ("base64".equalsIgnoreCase(mimeParts[j])) { + base64 = true; + } + } + String dataText = text.substring(commaIndex + 1); + + byte[] data = base64 ? Base64.decode(dataText, Base64.DEFAULT) : + EncodingUtils.getBytes(dataText, "UTF-8"); + + myStatement.bindBlob(i + 1, data); + } + else { + myStatement.bindString(i + 1, text); + } } } } @@ -732,7 +754,7 @@ private void bindPostHoneycomb(JSONObject row, String key, Cursor cur, int i) th row.put(key, cur.getDouble(i)); break; case Cursor.FIELD_TYPE_BLOB: - row.put(key, new String(Base64.encode(cur.getBlob(i), Base64.DEFAULT))); + row.put(key, "sqlblob:;base64,".concat(new String(Base64.encode(cur.getBlob(i), Base64.DEFAULT)))); break; case Cursor.FIELD_TYPE_STRING: default: /* (not expected) */ @@ -755,7 +777,7 @@ private void bindPreHoneycomb(JSONObject row, String key, Cursor cursor, int i) } else if (cursorWindow.isFloat(pos, i)) { row.put(key, cursor.getDouble(i)); } else if (cursorWindow.isBlob(pos, i)) { - row.put(key, new String(Base64.encode(cursor.getBlob(i), Base64.DEFAULT))); + row.put(key, "sqlblob:;base64,".concat(new String(Base64.encode(cursor.getBlob(i), Base64.DEFAULT)))); } else { // string row.put(key, cursor.getString(i)); } diff --git a/src/ios/SQLitePlugin.h b/src/ios/SQLitePlugin.h index 073af33df..e5b902ffc 100755 --- a/src/ios/SQLitePlugin.h +++ b/src/ios/SQLitePlugin.h @@ -12,6 +12,7 @@ #import #import +#import #import "AppDelegate.h" @@ -56,9 +57,6 @@ typedef int WebSQLError; +(int)mapSQLiteErrorCode:(int)code; -// LIBB64 -+(id) getBlobAsBase64String:(const char*) blob_chars - withlength: (int) blob_length; -// LIBB64---END - ++(NSString*)getBlobAsBase64String:(const char*) blob_chars + withlength:(int) blob_length; @end diff --git a/src/ios/SQLitePlugin.m b/src/ios/SQLitePlugin.m index 8f092fe48..e581e18cc 100755 --- a/src/ios/SQLitePlugin.m +++ b/src/ios/SQLitePlugin.m @@ -9,128 +9,6 @@ #import "SQLitePlugin.h" #include - -//LIBB64 -typedef enum -{ - step_A, step_B, step_C -} base64_encodestep; - -typedef struct -{ - base64_encodestep step; - char result; - int stepcount; -} base64_encodestate; - -static void base64_init_encodestate(base64_encodestate* state_in) -{ - state_in->step = step_A; - state_in->result = 0; - state_in->stepcount = 0; -} - -static char base64_encode_value(char value_in) -{ - static const char* encoding = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - if (value_in > 63) return '='; - return encoding[(int)value_in]; -} - -static int base64_encode_block(const char* plaintext_in, - int length_in, - char* code_out, - base64_encodestate* state_in, - int line_length) -{ - const char* plainchar = plaintext_in; - const char* const plaintextend = plaintext_in + length_in; - char* codechar = code_out; - char result; - char fragment; - - result = state_in->result; - - switch (state_in->step) - { - while (1) - { - case step_A: - if (plainchar == plaintextend) - { - state_in->result = result; - state_in->step = step_A; - return codechar - code_out; - } - fragment = *plainchar++; - result = (fragment & 0x0fc) >> 2; - *codechar++ = base64_encode_value(result); - result = (fragment & 0x003) << 4; - case step_B: - if (plainchar == plaintextend) - { - state_in->result = result; - state_in->step = step_B; - return codechar - code_out; - } - fragment = *plainchar++; - result |= (fragment & 0x0f0) >> 4; - *codechar++ = base64_encode_value(result); - result = (fragment & 0x00f) << 2; - case step_C: - if (plainchar == plaintextend) - { - state_in->result = result; - state_in->step = step_C; - return codechar - code_out; - } - fragment = *plainchar++; - result |= (fragment & 0x0c0) >> 6; - *codechar++ = base64_encode_value(result); - result = (fragment & 0x03f) >> 0; - *codechar++ = base64_encode_value(result); - - if(line_length > 0) - { - ++(state_in->stepcount); - if (state_in->stepcount == line_length/4) - { - *codechar++ = '\n'; - state_in->stepcount = 0; - } - } - } - } - /* control should not reach here */ - return codechar - code_out; -} - -static int base64_encode_blockend(char* code_out, - base64_encodestate* state_in) -{ - char* codechar = code_out; - - switch (state_in->step) - { - case step_B: - *codechar++ = base64_encode_value(state_in->result); - *codechar++ = '='; - *codechar++ = '='; - break; - case step_C: - *codechar++ = base64_encode_value(state_in->result); - *codechar++ = '='; - break; - case step_A: - break; - } - *codechar++ = '\n'; - - return codechar - code_out; -} - -//LIBB64---END - static void sqlite_regexp(sqlite3_context* context, int argc, sqlite3_value** values) { int ret; regex_t regex; @@ -217,7 +95,7 @@ -(void)open: (CDVInvokedUrlCommand*)command // const char *key = [@"your_key_here" UTF8String]; // if(key != NULL) sqlite3_key(db, key, strlen(key)); - sqlite3_create_function(db, "regexp", 2, SQLITE_ANY, NULL, &sqlite_regexp, NULL, NULL); + sqlite3_create_function(db, "regexp", 2, SQLITE_ANY, NULL, &sqlite_regexp, NULL, NULL); // Attempt to read the SQLite master table (test for SQLCipher version): if(sqlite3_exec(db, (const char*)"SELECT count(*) FROM sqlite_master;", NULL, NULL, NULL) == SQLITE_OK) { @@ -427,10 +305,8 @@ -(CDVPluginResult*) executeSqlWithDict: (NSMutableDictionary*)options andArgs: ( #endif break; case SQLITE_BLOB: - //LIBB64 columnValue = [SQLitePlugin getBlobAsBase64String: sqlite3_column_blob(statement, i) - withlength: sqlite3_column_bytes(statement, i) ]; - //LIBB64---END + withLength: sqlite3_column_bytes(statement, i)]; break; case SQLITE_FLOAT: columnValue = [NSNumber numberWithFloat: sqlite3_column_double(statement, i)]; @@ -506,9 +382,25 @@ -(void)bindStatement:(sqlite3_stmt *)statement withArg:(NSObject *)arg atIndex:( } else { stringArg = [arg description]; // convert to text } - - NSData *data = [stringArg dataUsingEncoding:NSUTF8StringEncoding]; - sqlite3_bind_text(statement, argIndex, data.bytes, data.length, SQLITE_TRANSIENT); + + // If the string is a sqlblob URI then decode it and store the binary directly. + // + // A sqlblob URI is formatted similar to a data URI which makes it easy to convert: + // sqlblob:[][;charset=][;base64], + // + // The reason the `sqlblob` prefix is used instead of `data` is because + // applications may want to use data URI strings directly, so the + // `sqlblob` prefix disambiguates the desired behavior. + if([stringArg hasPrefix:@"sqlblob:"]) { + // convert to data URI, decode, store as blob + stringArg = [stringArg stringByReplacingCharactersInRange:NSMakeRange(0,7) withString:@"data"]; + NSData *data = [NSData dataWithContentsOfURL: [NSURL URLWithString:stringArg]]; + sqlite3_bind_blob(statement, argIndex, data.bytes, data.length, SQLITE_TRANSIENT); + } + else { + NSData *data = [stringArg dataUsingEncoding:NSUTF8StringEncoding]; + sqlite3_bind_text(statement, argIndex, data.bytes, data.length, SQLITE_TRANSIENT); + } } } @@ -574,32 +466,21 @@ +(int)mapSQLiteErrorCode:(int)code } } -+(id) getBlobAsBase64String:(const char*) blob_chars - withlength: (int) blob_length ++(NSString*)getBlobAsBase64String:(const char*)blob_chars + withLength:(int)blob_length { - base64_encodestate b64state; - - base64_init_encodestate(&b64state); - - //2* ensures 3 bytes -> 4 Base64 characters + null for NSString init - char* code = malloc (2*blob_length*sizeof(char)); - - int codelength; - int endlength; - - codelength = base64_encode_block(blob_chars,blob_length,code,&b64state,0); - - endlength = base64_encode_blockend(&code[codelength], &b64state); - - //Adding in a null in order to use initWithUTF8String, expecting null terminated char* string - code[codelength+endlength] = '\0'; - - NSString* result = [NSString stringWithUTF8String: code]; - - free(code); - - return result; + size_t outputLength = 0; + char* outputBuffer = CDVNewBase64Encode(blob_chars, blob_length, true, &outputLength); + + NSString* result = [[NSString alloc] initWithBytesNoCopy:outputBuffer + length:outputLength + encoding:NSASCIIStringEncoding + freeWhenDone:YES]; +#if !__has_feature(objc_arc) + [result autorelease]; +#endif + + return [@"sqlblob:;base64," stringByAppendingString:result]; } - @end diff --git a/test-www/www/index.html b/test-www/www/index.html index 4ab0ab854..6f4532990 100755 --- a/test-www/www/index.html +++ b/test-www/www/index.html @@ -10,6 +10,7 @@ +