Skip to content

Commit

Permalink
Support SQL blob marshaling using ArrayBuffer and Base64.
Browse files Browse the repository at this point in the history
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. ArrayBuffer values can be used in statements and these will be converted to a `sqlblob` URI with base64 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 is returned as a `sqlblob` object which can then be unpacked in JavaScript to base64, ArrayBuffer, or binary string.

  tx.executeSql("SELECT foo FROM test_table", [], function(tx, res) {
    var blob = res.rows.item(0).foo;
    if (blob.isBlob) {
      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.
  • Loading branch information
aarononeal committed Jan 1, 2015
1 parent 69163dc commit ec00c6a
Show file tree
Hide file tree
Showing 5 changed files with 285 additions and 165 deletions.
115 changes: 112 additions & 3 deletions SQLitePlugin.coffee.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,65 @@ License for common Javascript: MIT or Apache
### global(s):

txLocks = {}
### 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;
### utility function(s):

Expand All @@ -43,6 +102,50 @@ License for common Javascript: MIT or Apache
return sqlError
# Converts JS types to something that can be marshaled to native.
#
# Cordova handles the basics, but this will convert ArrayBuffer
# instances to a format that the native side will know to treat as
# binary. That is, parse and convert back from base64 to bind as
# a blob.
#
# The specific format is similar to a data URI.
#
# Data URI is not used directly because databases may want to work
# with them as strings.
convertJsToNativeParam = (param) ->
if typeof ArrayBuffer is "undefined"
return param
if param instanceof ArrayBuffer
return "sqlblob:;base64," + arrayBufferToBase64(param)
else
return param
# Converts native types back to JS.
#
# Web SQL does not support Blob binding but the plugin does.
#
# The Cordova layer generally URL or base64 encodes binary data
# to get it to and from the native side. The plugin does this
# itself so it can mark the data as a blob.
#
# When JavaScript reads the returned blob we have the option of
# splitting out the base64 or converting to an ArrayBuffer.
#
# Below makes assumptions about the format based on what the
# native layer passes.
convertNativeToJsParam = (param) ->
if typeof param is "string" && param.indexOf("sqlblob:;base64,") is 0
base64string = param.slice(16)
blob =
toArrayBuffer: () -> base64ToArrayBuffer(base64string)
toBinaryString: () -> atob(base64string)
toBase64: () -> base64string
isBlob: true
return blob
return param
nextTick = window.setImmediate || (fun) ->
window.setTimeout(fun, 0)
return
Expand Down Expand Up @@ -235,6 +338,12 @@ License for common Javascript: MIT or Apache
return
rows = response.rows || []
# Convert parameters if needed
for row in rows
for own colName, colValue of row
row[colName] = convertNativeToJsParam(colValue)
payload =
rows:
item: (i) ->
Expand Down Expand Up @@ -270,7 +379,7 @@ License for common Javascript: MIT or Apache
try
if didSucceed
tx.handleStatementSuccess batchExecutes[index].success, response
else
else
tx.handleStatementFailure batchExecutes[index].error, toSQLError(response)
catch err
txFailure = toSQLError(err) unless txFailure
Expand Down Expand Up @@ -305,7 +414,7 @@ License for common Javascript: MIT or Apache
tropts.push
qid: qid
sql: request.sql
params: request.params
params: (convertJsToNativeParam param for param in request.params)
i++
Expand Down Expand Up @@ -415,7 +524,7 @@ License for common Javascript: MIT or Apache
new SQLitePlugin openargs, okcb, errorcb
deleteDb: (databaseName, success, error) ->
delete SQLitePlugin::openDBs[databaseName]
delete SQLitePlugin::openDBs[databaseName]
onError = (errMessage) -> if !!error then error new Error("error while trying to delete database: " + errMessage)
cordova.exec success, onError, "SQLitePlugin", "delete", [{ path: databaseName }]
Expand Down
4 changes: 2 additions & 2 deletions src/android/org/pgsqlite/SQLitePlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -732,7 +732,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) */
Expand All @@ -755,7 +755,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));
}
Expand Down
8 changes: 3 additions & 5 deletions src/ios/SQLitePlugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

#import <Cordova/CDVPlugin.h>
#import <Cordova/CDVJSON.h>
#import <Cordova/NSData+Base64.h>

#import "AppDelegate.h"

Expand Down Expand Up @@ -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
Loading

0 comments on commit ec00c6a

Please sign in to comment.