summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2023-08-03 23:15:13 +0200
committerFlorian Dold <florian@dold.me>2023-08-21 14:47:47 +0200
commit5583045dcff8d2fa2103d8a32aa034bf78b51f4f (patch)
tree4012053dd97f8093a276467eafa1c6615f7bfe7d
parent46cdcb6c188180286e303dd08a239bbac70c77d3 (diff)
downloadquickjs-tart-5583045dcff8d2fa2103d8a32aa034bf78b51f4f.tar.gz
quickjs-tart-5583045dcff8d2fa2103d8a32aa034bf78b51f4f.tar.bz2
quickjs-tart-5583045dcff8d2fa2103d8a32aa034bf78b51f4f.zip
sqlite3 support
-rw-r--r--.editorconfig2
-rw-r--r--.gitignore1
-rw-r--r--meson.build5
-rw-r--r--qtart.c6
-rw-r--r--quickjs/quickjs-libc.c10
-rw-r--r--quickjs/quickjs.c51
-rw-r--r--quickjs/quickjs.h3
-rw-r--r--tart_module.c706
-rw-r--r--test_sqlite3.js60
9 files changed, 808 insertions, 36 deletions
diff --git a/.editorconfig b/.editorconfig
index b393738..8839ebb 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -54,6 +54,6 @@ cpp_space_after_semicolon=false
cpp_space_remove_around_unary_operator=true
cpp_space_around_binary_operator=insert
cpp_space_around_assignment_operator=insert
-cpp_space_pointer_reference_alignment=left
+cpp_space_pointer_reference_alignment=right
cpp_space_around_ternary_operator=insert
cpp_wrap_preserve_blocks=one_liners
diff --git a/.gitignore b/.gitignore
index cd819ce..8cceb9d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@ tags
# compiled wallet core file
taler-wallet-core-qjs.mjs
+taler-wallet-cli.qtart.mjs
cross/android-*.cross.txt
build-*
diff --git a/meson.build b/meson.build
index ea9fb3d..0d30a28 100644
--- a/meson.build
+++ b/meson.build
@@ -59,6 +59,7 @@ cutils = static_library('cutils', 'quickjs/cutils.c')
quickjs_libc = static_library('quickjs-libc', 'quickjs/quickjs-libc.c', dependencies : curl_dep )
# base JS interpreter
quickjs = static_library('quickjs', 'quickjs/quickjs.c')
+sqlite3 = static_library('sqlite3', 'sqlite3/sqlite3.c')
# avoid warning but compile more slowly on non-cross builds
avoid_cross_warning = true
@@ -134,7 +135,9 @@ prelude = static_library('prelude', prelude_c)
wallet_core = static_library('wallet_core', wallet_core_c)
# Taler runtime ("tart") loadable JavaScript module
-tart = static_library('tart', 'tart_module.c', dependencies : [
+tart = static_library('tart', 'tart_module.c',
+ link_with : [sqlite3],
+ dependencies : [
m_dep,
mbedcrypto_dep,
mbedtls_dep,
diff --git a/qtart.c b/qtart.c
index 1b17c1c..c670b46 100644
--- a/qtart.c
+++ b/qtart.c
@@ -457,12 +457,14 @@ int main(int argc, char **argv)
if (!empty_run) {
js_std_add_helpers(ctx, argc - optind, argv + optind);
- /* make 'std' and 'os' visible to non module code */
+ /* make 'std', 'os' and 'tart' visible to non module code */
if (load_std) {
const char *str = "import * as std from 'std';\n"
"import * as os from 'os';\n"
+ "import * as tart from 'tart';\n"
"globalThis.std = std;\n"
- "globalThis.os = os;\n";
+ "globalThis.os = os;\n"
+ "globalThis.tart = tart;\n";
eval_buf(ctx, str, strlen(str), "<input>", JS_EVAL_TYPE_MODULE);
}
diff --git a/quickjs/quickjs-libc.c b/quickjs/quickjs-libc.c
index 4c20d68..b89c8c3 100644
--- a/quickjs/quickjs-libc.c
+++ b/quickjs/quickjs-libc.c
@@ -2397,7 +2397,8 @@ static JSValue js_os_fetchHttp(JSContext *ctx, JSValueConst this_val,
} else if (0 == strcasecmp(method_str, "delete")) {
curl_easy_setopt(req_context->curl, CURLOPT_HTTPGET, 1L);
curl_easy_setopt(req_context->curl, CURLOPT_CUSTOMREQUEST, "DELETE");
- } else if (0 == strcasecmp(method_str, "post")) {
+ } else if ((0 == strcasecmp(method_str, "post")) ||
+ (0 == strcasecmp(method_str, "put"))) {
JSValue data;
uint8_t *data_ptr;
size_t data_len;
@@ -2408,17 +2409,20 @@ static JSValue js_os_fetchHttp(JSContext *ctx, JSValueConst this_val,
}
data_ptr = JS_GetArrayBuffer(ctx, &data_len, data);
if (!data_ptr) {
- goto exception;
+ goto exception;
}
req_context->readbuf = malloc(data_len);
memcpy(req_context->readbuf, data_ptr, data_len);
req_context->readlen = data_len;
JS_FreeValue(ctx, data);
curl_easy_setopt(req_context->curl, CURLOPT_POST, 1L);
+ if (0 == strcasecmp(method_str, "put")) {
+ curl_easy_setopt(req_context->curl, CURLOPT_CUSTOMREQUEST, "PUT");
+ }
curl_easy_setopt(req_context->curl, CURLOPT_READFUNCTION, read_callback);
curl_easy_setopt(req_context->curl, CURLOPT_READDATA, req_context);
if (!req_context->client_has_content_type_header) {
- req_context->headers = curl_slist_append(req_context->headers, "Content-Type: application/json");
+ req_context->headers = curl_slist_append(req_context->headers, "Content-Type: application/json");
}
} else {
JS_ThrowTypeError(ctx, "invalid request method");
diff --git a/quickjs/quickjs.c b/quickjs/quickjs.c
index 8b125cf..bae5541 100644
--- a/quickjs/quickjs.c
+++ b/quickjs/quickjs.c
@@ -51913,6 +51913,24 @@ JSValue JS_GetTypedArrayBuffer(JSContext *ctx, JSValueConst obj,
return JS_DupValue(ctx, JS_MKPTR(JS_TAG_OBJECT, ta->buffer));
}
+BOOL JS_IsArrayBuffer(JSValueConst obj)
+{
+ JSObject *p;
+
+ if (JS_VALUE_GET_TAG(obj) != JS_TAG_OBJECT)
+ return FALSE;
+ p = JS_VALUE_GET_OBJ(obj);
+ if (p->class_id == JS_CLASS_ARRAY_BUFFER ||
+ p->class_id == JS_CLASS_SHARED_ARRAY_BUFFER) {
+ return TRUE;
+ }
+ if (p->class_id >= JS_CLASS_UINT8C_ARRAY &&
+ p->class_id <= JS_CLASS_FLOAT64_ARRAY) {
+ return TRUE;
+ }
+ return FALSE;
+}
+
/* return NULL if exception. WARNING: any JS call can detach the
buffer and render the returned pointer invalid */
uint8_t *JS_GetArrayBuffer(JSContext *ctx, size_t *psize, JSValueConst obj)
@@ -53308,22 +53326,23 @@ static int typed_array_init(JSContext *ctx, JSValueConst obj,
JSValue JS_NewTypedArray(JSContext *ctx, JSValue array_buf, size_t bytes_per_element)
{
- JSValue obj;
- JSObject *p = JS_VALUE_GET_OBJ(array_buf);
- JSArrayBuffer *abuf = NULL;
- if (p->class_id != JS_CLASS_ARRAY_BUFFER) {
- return JS_ThrowTypeError(ctx, "expected array buffer");
- }
- abuf = p->u.array_buffer;
- if (abuf->detached) {
- return JS_ThrowTypeErrorDetachedArrayBuffer(ctx);
- }
- obj = JS_NewObjectClass(ctx, JS_CLASS_UINT8_ARRAY);
- if (typed_array_init(ctx, obj, array_buf, 0, abuf->byte_length)) {
- JS_FreeValue(ctx, obj);
- return JS_EXCEPTION;
- }
- return obj;
+ JSValue obj;
+ JSObject *p = JS_VALUE_GET_OBJ(array_buf);
+ JSArrayBuffer *abuf = NULL;
+
+ if (p->class_id != JS_CLASS_ARRAY_BUFFER) {
+ return JS_ThrowTypeError(ctx, "expected array buffer");
+ }
+ abuf = p->u.array_buffer;
+ if (abuf->detached) {
+ return JS_ThrowTypeErrorDetachedArrayBuffer(ctx);
+ }
+ obj = JS_NewObjectClass(ctx, JS_CLASS_UINT8_ARRAY);
+ if (typed_array_init(ctx, obj, array_buf, 0, abuf->byte_length)) {
+ JS_FreeValue(ctx, obj);
+ return JS_EXCEPTION;
+ }
+ return obj;
}
diff --git a/quickjs/quickjs.h b/quickjs/quickjs.h
index 4791541..c1cf768 100644
--- a/quickjs/quickjs.h
+++ b/quickjs/quickjs.h
@@ -191,7 +191,7 @@ static inline JS_BOOL JS_VALUE_IS_NAN(JSValue v)
tag = JS_VALUE_GET_TAG(v);
return tag == (JS_NAN >> 32);
}
-
+
#else /* !JS_NAN_BOXING */
typedef union JSValueUnion {
@@ -821,6 +821,7 @@ JSValue JS_NewArrayBuffer(JSContext *ctx, uint8_t *buf, size_t len,
JSValue JS_NewTypedArray(JSContext *ctx, JSValue array_buf, size_t bytes_per_element);
JSValue JS_NewArrayBufferCopy(JSContext *ctx, const uint8_t *buf, size_t len);
void JS_DetachArrayBuffer(JSContext *ctx, JSValueConst obj);
+JS_BOOL JS_IsArrayBuffer(JSValueConst obj);
uint8_t *JS_GetArrayBuffer(JSContext *ctx, size_t *psize, JSValueConst obj);
JSValue JS_GetTypedArrayBuffer(JSContext *ctx, JSValueConst obj,
size_t *pbyte_offset,
diff --git a/tart_module.c b/tart_module.c
index 50431aa..66fbec9 100644
--- a/tart_module.c
+++ b/tart_module.c
@@ -29,6 +29,8 @@
#include <arpa/inet.h>
+#include "sqlite3/sqlite3.h"
+
static JSValue js_encode_utf8(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv)
{
@@ -1366,29 +1368,693 @@ static JSValue js_talercrypto_hash_state_update(JSContext *ctx, JSValue this_val
static JSValue js_talercrypto_hash_state_finish(JSContext *ctx, JSValue this_val,
int argc, JSValueConst *argv)
{
- JSValue state = argv[0];
- TART_HashState *hstate;
- uint8_t hashval[crypto_hash_sha512_BYTES];
+ JSValue state = argv[0];
+ TART_HashState *hstate;
+ uint8_t hashval[crypto_hash_sha512_BYTES];
- hstate = JS_GetOpaque(state, js_hash_state_class_id);
+ hstate = JS_GetOpaque(state, js_hash_state_class_id);
- if (!hstate) {
- return JS_ThrowTypeError(ctx, "expected HashState");
+ if (!hstate) {
+ return JS_ThrowTypeError(ctx, "expected HashState");
+ }
+
+ if (hstate->finalized) {
+ return JS_ThrowTypeError(ctx, "already finalized");
+ }
+
+ if (0 != crypto_hash_sha512_final(&hstate->h, hashval)) {
+ return JS_ThrowInternalError(ctx, "hashing failed");
+ }
+
+ hstate->finalized = TRUE;
+
+ return make_js_ta_copy(ctx, hashval, crypto_hash_sha512_BYTES);
+}
+
+static JSClassID js_sqlite3_database_class_id;
+static JSClassID js_sqlite3_statement_class_id;
+
+static void js_sqlite3_database_finalizer(JSRuntime *rt, JSValue val)
+{
+ sqlite3 *sqlite3_db;
+ sqlite3_db = JS_GetOpaque(val, js_sqlite3_database_class_id);
+ (void)sqlite3_close_v2(sqlite3_db);
+ JS_SetOpaque(val, NULL);
+}
+
+static void js_sqlite3_statement_finalizer(JSRuntime *rt, JSValue val)
+{
+ sqlite3_stmt *stmt;
+
+ stmt = JS_GetOpaque(val, js_sqlite3_statement_class_id);
+ // FIXME: Check error code and warn?
+ sqlite3_finalize(stmt);
+ JS_SetOpaque(val, NULL);
+}
+
+static JSClassDef js_sqlite3_database_class = {
+ .class_name = "Sqlite3Database",
+ .finalizer = js_sqlite3_database_finalizer,
+};
+
+static JSClassDef js_sqlite3_statement_class = {
+ .class_name = "Sqlite3Statement",
+ .finalizer = js_sqlite3_statement_finalizer,
+};
+
+#define ERRCASE(c) case c: return #c
+
+const char *translate_sqlite3_err_to_string(int errcode)
+{
+ switch (errcode) {
+ ERRCASE(SQLITE_OK);
+ ERRCASE(SQLITE_ERROR);
+ ERRCASE(SQLITE_INTERNAL);
+ ERRCASE(SQLITE_PERM);
+ ERRCASE(SQLITE_ABORT);
+ ERRCASE(SQLITE_BUSY);
+ ERRCASE(SQLITE_LOCKED);
+ ERRCASE(SQLITE_NOMEM);
+ ERRCASE(SQLITE_READONLY);
+ ERRCASE(SQLITE_INTERRUPT);
+ ERRCASE(SQLITE_IOERR);
+ ERRCASE(SQLITE_CORRUPT);
+ ERRCASE(SQLITE_NOTFOUND);
+ ERRCASE(SQLITE_FULL);
+ ERRCASE(SQLITE_CANTOPEN);
+ ERRCASE(SQLITE_PROTOCOL);
+ ERRCASE(SQLITE_EMPTY);
+ ERRCASE(SQLITE_SCHEMA);
+ ERRCASE(SQLITE_TOOBIG);
+ ERRCASE(SQLITE_CONSTRAINT);
+ ERRCASE(SQLITE_MISMATCH);
+ ERRCASE(SQLITE_MISUSE);
+ ERRCASE(SQLITE_NOLFS);
+ ERRCASE(SQLITE_AUTH);
+ ERRCASE(SQLITE_FORMAT);
+ ERRCASE(SQLITE_RANGE);
+ ERRCASE(SQLITE_NOTADB);
+ ERRCASE(SQLITE_NOTICE);
+ ERRCASE(SQLITE_WARNING);
+ ERRCASE(SQLITE_ROW);
+ ERRCASE(SQLITE_DONE);
+ ERRCASE(SQLITE_ERROR_MISSING_COLLSEQ);
+ ERRCASE(SQLITE_ERROR_RETRY);
+ ERRCASE(SQLITE_ERROR_SNAPSHOT);
+ ERRCASE(SQLITE_IOERR_READ);
+ ERRCASE(SQLITE_IOERR_SHORT_READ);
+ ERRCASE(SQLITE_IOERR_WRITE);
+ ERRCASE(SQLITE_IOERR_FSYNC);
+ ERRCASE(SQLITE_IOERR_DIR_FSYNC);
+ ERRCASE(SQLITE_IOERR_TRUNCATE);
+ ERRCASE(SQLITE_IOERR_FSTAT);
+ ERRCASE(SQLITE_IOERR_UNLOCK);
+ ERRCASE(SQLITE_IOERR_RDLOCK);
+ ERRCASE(SQLITE_IOERR_DELETE);
+ ERRCASE(SQLITE_IOERR_BLOCKED);
+ ERRCASE(SQLITE_IOERR_NOMEM);
+ ERRCASE(SQLITE_IOERR_ACCESS);
+ ERRCASE(SQLITE_IOERR_CHECKRESERVEDLOCK);
+ ERRCASE(SQLITE_IOERR_LOCK);
+ ERRCASE(SQLITE_IOERR_CLOSE);
+ ERRCASE(SQLITE_IOERR_DIR_CLOSE);
+ ERRCASE(SQLITE_IOERR_SHMOPEN);
+ ERRCASE(SQLITE_IOERR_SHMSIZE);
+ ERRCASE(SQLITE_IOERR_SHMLOCK);
+ ERRCASE(SQLITE_IOERR_SHMMAP);
+ ERRCASE(SQLITE_IOERR_SEEK);
+ ERRCASE(SQLITE_IOERR_DELETE_NOENT);
+ ERRCASE(SQLITE_IOERR_MMAP);
+ ERRCASE(SQLITE_IOERR_GETTEMPPATH);
+ ERRCASE(SQLITE_IOERR_CONVPATH);
+ ERRCASE(SQLITE_IOERR_VNODE);
+ ERRCASE(SQLITE_IOERR_AUTH);
+ ERRCASE(SQLITE_IOERR_BEGIN_ATOMIC);
+ ERRCASE(SQLITE_IOERR_COMMIT_ATOMIC);
+ ERRCASE(SQLITE_IOERR_ROLLBACK_ATOMIC);
+ ERRCASE(SQLITE_IOERR_DATA);
+ ERRCASE(SQLITE_IOERR_CORRUPTFS);
+ ERRCASE(SQLITE_LOCKED_SHAREDCACHE);
+ ERRCASE(SQLITE_LOCKED_VTAB);
+ ERRCASE(SQLITE_BUSY_RECOVERY);
+ ERRCASE(SQLITE_BUSY_SNAPSHOT);
+ ERRCASE(SQLITE_BUSY_TIMEOUT);
+ ERRCASE(SQLITE_CANTOPEN_NOTEMPDIR);
+ ERRCASE(SQLITE_CANTOPEN_ISDIR);
+ ERRCASE(SQLITE_CANTOPEN_FULLPATH);
+ ERRCASE(SQLITE_CANTOPEN_CONVPATH);
+ ERRCASE(SQLITE_CANTOPEN_DIRTYWAL);
+ ERRCASE(SQLITE_CANTOPEN_SYMLINK);
+ ERRCASE(SQLITE_CORRUPT_VTAB);
+ ERRCASE(SQLITE_CORRUPT_SEQUENCE);
+ ERRCASE(SQLITE_CORRUPT_INDEX);
+ ERRCASE(SQLITE_READONLY_RECOVERY);
+ ERRCASE(SQLITE_READONLY_CANTLOCK);
+ ERRCASE(SQLITE_READONLY_ROLLBACK);
+ ERRCASE(SQLITE_READONLY_DBMOVED);
+ ERRCASE(SQLITE_READONLY_CANTINIT);
+ ERRCASE(SQLITE_READONLY_DIRECTORY);
+ ERRCASE(SQLITE_ABORT_ROLLBACK);
+ ERRCASE(SQLITE_CONSTRAINT_CHECK);
+ ERRCASE(SQLITE_CONSTRAINT_COMMITHOOK);
+ ERRCASE(SQLITE_CONSTRAINT_FOREIGNKEY);
+ ERRCASE(SQLITE_CONSTRAINT_FUNCTION);
+ ERRCASE(SQLITE_CONSTRAINT_NOTNULL);
+ ERRCASE(SQLITE_CONSTRAINT_PRIMARYKEY);
+ ERRCASE(SQLITE_CONSTRAINT_TRIGGER);
+ ERRCASE(SQLITE_CONSTRAINT_UNIQUE);
+ ERRCASE(SQLITE_CONSTRAINT_VTAB);
+ ERRCASE(SQLITE_CONSTRAINT_ROWID);
+ ERRCASE(SQLITE_CONSTRAINT_PINNED);
+ ERRCASE(SQLITE_CONSTRAINT_DATATYPE);
+ ERRCASE(SQLITE_NOTICE_RECOVER_WAL);
+ ERRCASE(SQLITE_NOTICE_RECOVER_ROLLBACK);
+ ERRCASE(SQLITE_NOTICE_RBU);
+ ERRCASE(SQLITE_WARNING_AUTOINDEX);
+ ERRCASE(SQLITE_AUTH_USER);
+ ERRCASE(SQLITE_OK_LOAD_PERMANENTLY);
+ ERRCASE(SQLITE_OK_SYMLINK);
+ default:
+ return "SQLITE_UNKNOWN_ERRCODE";
+ }
+}
+
+
+static JSValue throw_sqlite3_error(JSContext *ctx, sqlite3 *db)
+{
+ JSValue obj;
+ char *errmsg;
+
+ obj = JS_NewError(ctx);
+ if (JS_IsException(obj)) {
+ /* out of memory: throw JS_NULL to avoid recursing */
+ obj = JS_NULL;
+ goto done;
+ }
+
+ JS_DefinePropertyValueStr(
+ ctx,
+ obj,
+ "message",
+ JS_NewString(ctx, sqlite3_errmsg(db)),
+ JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE);
+
+ JS_DefinePropertyValueStr(
+ ctx,
+ obj,
+ "code",
+ JS_NewString(ctx, translate_sqlite3_err_to_string(sqlite3_errcode(db))),
+ JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE);
+
+done:
+ return JS_Throw(ctx, obj);
+}
+
+
+#define MAX_SAFE_INTEGER (((int64_t)1 << 53) - 1)
+#define MIN_SAFE_INTEGER (-(((int64_t)1 << 53) - 1))
+
+// (path: string, options?: { readonly?: boolean = false }) => Sqlite3Database
+static JSValue js_sqlite3_open(JSContext *ctx, JSValue this_val,
+ int argc, JSValueConst *argv)
+{
+ JSValue ret_val = JS_UNINITIALIZED;
+ JSValue db_obj = JS_UNINITIALIZED;
+ const char *filename = NULL;
+ sqlite3 *sqlite3_db = NULL;
+ int ret;
+
+ if (!JS_IsString(argv[0])) {
+ ret_val = JS_ThrowTypeError(ctx, "filename argument required");
+ goto done;
+ }
+
+ filename = JS_ToCString(ctx, argv[0]);
+
+ if (!filename) {
+ ret_val = JS_ThrowTypeError(ctx, "filename argument required");
+ goto done;
+ }
+
+ ret = sqlite3_open_v2(filename, &sqlite3_db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
+ if (SQLITE_OK != ret) {
+ (void)sqlite3_close_v2(sqlite3_db);
+ ret_val = JS_ThrowTypeError(ctx, "unable to open database");
+ goto done;
+ }
+ db_obj = JS_NewObjectClass(ctx, js_sqlite3_database_class_id);
+ JS_SetOpaque(db_obj, sqlite3_db);
+ ret_val = db_obj;
+done:
+ JS_FreeCString(ctx, filename);
+ return ret_val;
+}
+
+// (handle: Sqlite3Database) -> undefined
+static JSValue js_sqlite3_close(JSContext *ctx, JSValue this_val,
+ int argc, JSValueConst *argv)
+{
+ JSValue db_handle = argv[0];
+ sqlite3 *sqlite3_db;
+
+ sqlite3_db = JS_GetOpaque(db_handle, js_sqlite3_database_class_id);
+
+ if (!sqlite3_db) {
+ return JS_ThrowTypeError(ctx, "invalid sqlite3 database handle");
}
- if (hstate->finalized) {
- return JS_ThrowTypeError(ctx, "already finalized");
+ (void) sqlite3_close_v2(sqlite3_db);
+ JS_SetOpaque(db_handle, NULL);
+ return JS_UNDEFINED;
+}
+
+// (handle: Sqlite3Database, stmt: string) => Sqlite3Statement
+static JSValue js_sqlite3_prepare(JSContext *ctx, JSValue this_val,
+ int argc, JSValueConst *argv)
+{
+ JSValue db_handle = argv[0];
+ JSValue stmt_str = argv[1];
+ JSValue ret_val = JS_UNDEFINED;
+ JSValue stmt_obj = JS_UNDEFINED;
+ int ret;
+ sqlite3 *sqlite3_db;
+ sqlite3_stmt *stmt;
+ const char *stmt_cstr;
+ const char *tail;
+
+ sqlite3_db = JS_GetOpaque(db_handle, js_sqlite3_database_class_id);
+
+ if (!sqlite3_db) {
+ return JS_ThrowTypeError(ctx, "invalid sqlite3 database handle");
}
- if (0 != crypto_hash_sha512_final(&hstate->h, hashval)) {
- return JS_ThrowInternalError(ctx, "hashing failed");
+ stmt_cstr = JS_ToCString(ctx, stmt_str);
+
+ if (!stmt_cstr) {
+ ret_val = JS_ThrowTypeError(ctx, "invalid prepared statement, string expected");
+ goto done;
+ }
+
+ ret = sqlite3_prepare_v3(sqlite3_db, stmt_cstr, strlen(stmt_cstr), 0, &stmt, &tail);
+ if (SQLITE_OK != ret) {
+ ret_val = JS_ThrowTypeError(ctx, "unable to prepare");
+ goto done;
+ }
+
+ stmt_obj = JS_NewObjectClass(ctx, js_sqlite3_statement_class_id);
+ JS_SetOpaque(stmt_obj, stmt);
+ ret_val = stmt_obj;
+done:
+ JS_FreeCString(ctx, stmt_cstr);
+ return ret_val;
+}
+
+
+static JSValue js_sqlite3_finalize(JSContext *ctx, JSValue this_val,
+ int argc, JSValueConst *argv)
+{
+ sqlite3_stmt *stmt;
+
+ stmt = JS_GetOpaque(argv[0], js_sqlite3_statement_class_id);
+ if (!stmt) {
+ return JS_ThrowTypeError(ctx, "unable to finalize (not a statement)");
}
+ // FIXME: Check error code and warn?
+ sqlite3_finalize(stmt);
+ JS_SetOpaque(argv[0], NULL);
+ return JS_UNDEFINED;
+}
+
+static int sql_exec_cb(void *cls, int numcol, char **res, char **colnames) {
+ printf("got row with %d columns\n", numcol);
+ return 0;
+}
+
+static JSValue js_sqlite3_exec(JSContext *ctx, JSValue this_val,
+ int argc, JSValueConst *argv)
+{
+ sqlite3 *sqlite3_db;
+ sqlite3_stmt *stmt;
+ JSValue db_handle = argv[0];
+ JSValue stmt_str = argv[1];
+ const char *stmt_cstr;
+ int res;
+ char *errmsg = NULL;
+ JSValue ret_val = JS_UNDEFINED;
+
+ sqlite3_db = JS_GetOpaque(db_handle, js_sqlite3_database_class_id);
+ if (!sqlite3_db) {
+ JS_ThrowTypeError(ctx, "invalid sqlite3 database handle");
+ goto exception;
+ }
+ stmt_cstr = JS_ToCString(ctx, stmt_str);
+ if (!stmt_cstr) {
+ goto exception;
+ }
+ res = sqlite3_exec(sqlite3_db, stmt_cstr, &sql_exec_cb, NULL, &errmsg);
+ if (SQLITE_OK != res) {
+ // FIXME: throw!
+ printf("got error: %s\n", errmsg);
+ }
+done:
+ sqlite3_free(errmsg);
+ JS_FreeCString(ctx, stmt_cstr);
+ return JS_UNDEFINED;
+exception:
+ ret_val = JS_EXCEPTION;
+}
+
+static int find_param_index(sqlite3_stmt *stmt, const char *name)
+{
+ int param_index;
+ char *prefixed_name = malloc(strlen(name) + 2);
+ memcpy(prefixed_name + 1, name, strlen(name) + 1);
+
+ prefixed_name[0] = '$';
+ param_index = sqlite3_bind_parameter_index(stmt, prefixed_name);
+ if (param_index) {
+ goto done;
+ }
+ prefixed_name[0] = ':';
+ param_index = sqlite3_bind_parameter_index(stmt, prefixed_name);
+ if (param_index) {
+ goto done;
+ }
+ prefixed_name[0] = '@';
+ param_index = sqlite3_bind_parameter_index(stmt, prefixed_name);
+ if (param_index) {
+ goto done;
+ }
+done:
+ free(prefixed_name);
+ return param_index;
+}
+
+static int bind_from_object(JSContext *ctx, sqlite3_stmt *stmt, JSValueConst obj)
+{
+ JSValue val;
+ int i;
+ int len = 0; /* len of property table */
+ JSPropertyEnum *tab;
+ const char *key = NULL;
+ int retval = 0;
+
+ if (JS_IsUndefined(obj)) {
+ return 0;
+ }
+
+ if (JS_GetOwnPropertyNames(ctx, &tab, &len, obj,
+ JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) < 0) {
+ JS_ThrowTypeError(ctx, "can't get property names");
+ return -1;
+ }
+
+ for(i = 0; i < len; i++) {
+ int param_index;
+ val = JS_GetProperty(ctx, obj, tab[i].atom);
+ if (JS_IsException(val))
+ goto fail;
+ key = JS_AtomToCString(ctx, tab[i].atom);
+ if (!key) {
+ goto fail;
+ }
+ param_index = find_param_index(stmt, key);
+ if (0 == param_index) {
+ // JS_ThrowTypeError(ctx, "unable to bind, named param '%s' not found", key);
+ //goto fail;
+ // We ignore parameters that are bound but not used.
+ goto next;
+ }
+ if (JS_IsNull(val)) {
+ sqlite3_bind_null(stmt, param_index);
+ goto next;
+ }
+ if (JS_IsString(val)) {
+ const char *cstr;
+ cstr = JS_ToCString(ctx, val);
+ sqlite3_bind_text(stmt, param_index, cstr, strlen(cstr), SQLITE_TRANSIENT);
+ JS_FreeCString(ctx, cstr);
+ goto next;
+ }
+ if (JS_IsNumber(val)) {
+ int64_t n;
+ JS_ToInt64(ctx, &n, val);
+ sqlite3_bind_int64(stmt, param_index, n);
+ goto next;
+ }
+ if (JS_IsArrayBuffer(val)) {
+ uint8_t *data;
+ size_t size;
+ data = JS_GetArrayBuffer(ctx, &size, val);
+ if (!data) {
+ goto fail;
+ }
+ sqlite3_bind_blob(stmt, param_index, data, size, SQLITE_TRANSIENT);
+ goto next;
+ }
+ JS_ThrowTypeError(ctx, "unable to bind, unsupported type for arg %s", key);
+ goto fail;
+next:
+ JS_FreeCString(ctx, key);
+ JS_FreeValue(ctx, val);
+ }
+done:
+ for (i = 0; i < len; i++) {
+ JS_FreeAtom(ctx, tab[i].atom);
+ }
+ js_free(ctx, tab);
+ return retval;
+fail:
+ retval = -1;
+ goto done;
+}
+
+
+static JSValue js_sqlite3_stmt_run(JSContext *ctx, JSValue this_val,
+ int argc, JSValueConst *argv)
+{
+ JSValue ret_val = JS_UNDEFINED;
+ JSValue stmt_handle = argv[0];
+ sqlite3_stmt *stmt;
+ sqlite3 *db;
+ int sqlret;
+
+ stmt = JS_GetOpaque(stmt_handle, js_sqlite3_statement_class_id);
+ if (!stmt) {
+ ret_val = JS_ThrowTypeError(ctx, "invalid sqlite3 database handle");
+ goto done;
+ }
+ db = sqlite3_db_handle(stmt);
+ sqlret = sqlite3_reset(stmt);
+ if (SQLITE_OK != sqlret) {
+ ret_val = JS_ThrowTypeError(ctx, "failed to reset");
+ goto done;
+ }
+ sqlret = sqlite3_clear_bindings(stmt);
+ if (SQLITE_OK != sqlret) {
+ ret_val = JS_ThrowTypeError(ctx, "failed to clear bindings");
+ goto done;
+ }
+ if (argc > 1) {
+ if (0 != bind_from_object(ctx, stmt, argv[1])) {
+ ret_val = JS_EXCEPTION;
+ goto done;
+ }
+ }
+ while (1) {
+ sqlret = sqlite3_step(stmt);
+ switch (sqlret) {
+ case SQLITE_ROW:
+ break;
+ case SQLITE_DONE: {
+ JSValue rowid_val;
+ ret_val = JS_NewObject(ctx);
+ sqlite3_int64 rowid = sqlite3_last_insert_rowid(db);
+ if (rowid >= MIN_SAFE_INTEGER && rowid <= MAX_SAFE_INTEGER) {
+ rowid_val = JS_NewInt64(ctx, rowid);
+ } else {
+ rowid_val = JS_NewBigInt64(ctx, rowid);
+ }
+ JS_SetPropertyStr(ctx, ret_val, "lastInsertRowid", rowid_val);
+ goto done;
+ }
+ default:
+ ret_val = throw_sqlite3_error(ctx, db);
+ goto done;
+ }
+ }
+done:
+ return ret_val;
+}
+
+
+static int extract_result_row(JSContext *ctx, sqlite3_stmt *stmt, JSValueConst target)
+{
+ int colcount;
+ int i;
+
+ colcount = sqlite3_column_count(stmt);
+
+ for (i = 0; i < colcount; i++) {
+ const char *colname = sqlite3_column_name(stmt, i);
+ int coltype = sqlite3_column_type(stmt, i);
+ switch (coltype) {
+ case SQLITE_INTEGER: {
+ int64_t val = sqlite3_column_int64(stmt, i);
+ if (val > MAX_SAFE_INTEGER || val < MIN_SAFE_INTEGER) {
+ JS_SetPropertyStr(ctx, target, colname, JS_NewBigInt64(ctx, val));
+ } else {
+ JS_SetPropertyStr(ctx, target, colname, JS_NewInt64(ctx, val));
+ }
+ break;
+ }
+ case SQLITE_FLOAT: {
+ double val = sqlite3_column_double(stmt, i);
+ JS_SetPropertyStr(ctx, target, colname, JS_NewFloat64(ctx, val));
+ break;
+ }
+ case SQLITE_BLOB: {
+ JSValue abuf;
+ const uint8_t *blobdata = sqlite3_column_blob(stmt, i);
+ size_t bloblen = sqlite3_column_bytes(stmt, i);
+ abuf = JS_NewArrayBufferCopy(ctx, blobdata, bloblen);
+ JS_SetPropertyStr(ctx, target, colname, JS_NewTypedArray(ctx, abuf, 1));
+ break;
+ }
+ case SQLITE_NULL:
+ JS_SetPropertyStr(ctx, target, colname, JS_NULL);
+ break;
+ case SQLITE_TEXT:
+ const char *text = sqlite3_column_text(stmt, i);
+ JS_SetPropertyStr(ctx, target, colname, JS_NewString(ctx, text));
+ break;
+ default:
+ JS_ThrowInternalError(ctx, "unexpected type from DB");
+ return -1;
+ }
+ }
+ return 0;
+}
- hstate->finalized = TRUE;
- return make_js_ta_copy(ctx, hashval, crypto_hash_sha512_BYTES);
+static JSValue js_sqlite3_stmt_get_all(JSContext *ctx, JSValue this_val,
+ int argc, JSValueConst *argv)
+{
+ JSValue ret_val = JS_UNDEFINED;
+ JSValue stmt_handle = argv[0];
+ sqlite3_stmt *stmt;
+ sqlite3 *db;
+ int sqlret;
+ JSValue rows_array = JS_UNDEFINED;
+
+ stmt = JS_GetOpaque(stmt_handle, js_sqlite3_statement_class_id);
+ if (!stmt) {
+ ret_val = JS_ThrowTypeError(ctx, "invalid sqlite3 database handle");
+ goto done;
+ }
+ db = sqlite3_db_handle(stmt);
+ sqlret = sqlite3_reset(stmt);
+ if (SQLITE_OK != sqlret) {
+ ret_val = JS_ThrowTypeError(ctx, "failed to reset");
+ goto done;
+ }
+ sqlret = sqlite3_clear_bindings(stmt);
+ if (SQLITE_OK != sqlret) {
+ ret_val = JS_ThrowTypeError(ctx, "failed to clear bindings");
+ goto done;
+ }
+ if (argc > 1) {
+ if (0 != bind_from_object(ctx, stmt, argv[1])) {
+ ret_val = JS_EXCEPTION;
+ goto done;
+ }
+ }
+ rows_array = JS_NewArray(ctx);
+ while (1) {
+ sqlret = sqlite3_step(stmt);
+ switch (sqlret) {
+ case SQLITE_ROW: {
+ JSValue row_obj = JS_NewObject(ctx);
+ if (0 != extract_result_row(ctx, stmt, row_obj)) {
+ goto fail;
+ }
+ qjs_array_append_new(ctx, rows_array, row_obj);
+ break;
+ }
+ case SQLITE_DONE: {
+ ret_val = JS_DupValue(ctx, rows_array);
+ goto done;
+ }
+ default:
+ ret_val = throw_sqlite3_error(ctx, db);
+ goto done;
+ }
+ }
+done:
+ JS_FreeValue(ctx, rows_array);
+ return ret_val;
+fail:
+ ret_val = JS_EXCEPTION;
+ goto done;
}
+static JSValue js_sqlite3_stmt_get_first(JSContext *ctx, JSValue this_val,
+ int argc, JSValueConst *argv)
+{
+ JSValue ret_val = JS_UNDEFINED;
+ JSValue stmt_handle = argv[0];
+ sqlite3_stmt *stmt;
+ sqlite3 *db;
+ int sqlret;
+
+ stmt = JS_GetOpaque(stmt_handle, js_sqlite3_statement_class_id);
+ if (!stmt) {
+ ret_val = JS_ThrowTypeError(ctx, "invalid sqlite3 database handle");
+ goto done;
+ }
+ db = sqlite3_db_handle(stmt);
+ sqlret = sqlite3_reset(stmt);
+ if (SQLITE_OK != sqlret) {
+ ret_val = JS_ThrowTypeError(ctx, "failed to reset");
+ goto done;
+ }
+ sqlret = sqlite3_clear_bindings(stmt);
+ if (SQLITE_OK != sqlret) {
+ ret_val = JS_ThrowTypeError(ctx, "failed to clear bindings");
+ goto done;
+ }
+ if (argc > 1) {
+ if (0 != bind_from_object(ctx, stmt, argv[1])) {
+ ret_val = JS_EXCEPTION;
+ goto done;
+ }
+ }
+ while (1) {
+ sqlret = sqlite3_step(stmt);
+ switch (sqlret) {
+ case SQLITE_ROW: {
+ JSValue row_obj = JS_NewObject(ctx);
+ if (0 != extract_result_row(ctx, stmt, row_obj)) {
+ goto fail;
+ }
+ ret_val = row_obj;
+ goto done;
+ }
+ case SQLITE_DONE: {
+ ret_val = JS_UNDEFINED;
+ goto done;
+ }
+ default:
+ ret_val = throw_sqlite3_error(ctx, db);
+ goto done;
+ }
+ }
+done:
+ return ret_val;
+fail:
+ ret_val = JS_EXCEPTION;
+ goto done;
+}
static const JSCFunctionListEntry tart_talercrypto_funcs[] = {
@@ -1413,6 +2079,14 @@ static const JSCFunctionListEntry tart_talercrypto_funcs[] = {
JS_CFUNC_DEF("rsaBlind", 3, js_talercrypto_rsa_blind),
JS_CFUNC_DEF("rsaUnblind", 3, js_talercrypto_rsa_unblind),
JS_CFUNC_DEF("rsaVerify", 3, js_talercrypto_rsa_verify),
+ JS_CFUNC_DEF("sqlite3Open", 1, js_sqlite3_open),
+ JS_CFUNC_DEF("sqlite3Close", 1, js_sqlite3_close),
+ JS_CFUNC_DEF("sqlite3Prepare", 2, js_sqlite3_prepare),
+ JS_CFUNC_DEF("sqlite3Finalize", 1, js_sqlite3_finalize),
+ JS_CFUNC_DEF("sqlite3Exec", 2, js_sqlite3_exec),
+ JS_CFUNC_DEF("sqlite3StmtRun", 2, js_sqlite3_stmt_run),
+ JS_CFUNC_DEF("sqlite3StmtGetFirst", 2, js_sqlite3_stmt_get_first),
+ JS_CFUNC_DEF("sqlite3StmtGetAll", 2, js_sqlite3_stmt_get_all),
};
static int tart_talercrypto_init(JSContext *ctx, JSModuleDef *m)
@@ -1421,6 +2095,14 @@ static int tart_talercrypto_init(JSContext *ctx, JSModuleDef *m)
JS_NewClassID(&js_hash_state_class_id);
JS_NewClass(JS_GetRuntime(ctx), js_hash_state_class_id, &js_hash_state_class);
+ /* create the Sqlite3Database class*/
+ JS_NewClassID(&js_sqlite3_database_class_id);
+ JS_NewClass(JS_GetRuntime(ctx), js_sqlite3_database_class_id, &js_sqlite3_database_class);
+
+ /* create the Sqlite3Statement class*/
+ JS_NewClassID(&js_sqlite3_statement_class_id);
+ JS_NewClass(JS_GetRuntime(ctx), js_sqlite3_statement_class_id, &js_sqlite3_statement_class);
+
return JS_SetModuleExportList(ctx, m, tart_talercrypto_funcs,
countof(tart_talercrypto_funcs));
}
diff --git a/test_sqlite3.js b/test_sqlite3.js
new file mode 100644
index 0000000..3e1f742
--- /dev/null
+++ b/test_sqlite3.js
@@ -0,0 +1,60 @@
+import * as tart from "tart";
+import * as std from "std";
+
+function expectThrows(f) {
+ try {
+ f();
+ } catch (e) {
+ return e;
+ }
+ throw Error("expected exception, but function did not throw");
+}
+
+function expectStrictEquals(actual, expected) {
+ if (actual !== expected) {
+ throw Error();
+ }
+}
+
+const db = tart.sqlite3Open(":memory:");
+
+tart.sqlite3Exec(db, "create table foo ( name string unique, age number)");
+
+let res;
+
+const stmt1 = tart.sqlite3Prepare(db, "insert into foo(name, age) values ($name, $value)")
+res = tart.sqlite3StmtRun(stmt1, { name: "foo", value: 42 });
+console.log("stmt1 res:", res.lastInsertRowid);
+res = tart.sqlite3StmtRun(stmt1, { name: "bar", value: 10 });
+console.log("stmt1 res:", res.lastInsertRowid);
+
+const stmt2 = tart.sqlite3Prepare(db, "select * from foo")
+res = tart.sqlite3StmtGetAll(stmt2);
+
+console.log("result:", JSON.stringify(res));
+
+const stmt3 = tart.sqlite3Prepare(db, "select * from foo")
+res = tart.sqlite3StmtGetFirst(stmt3);
+
+console.log("result:", JSON.stringify(res));
+
+tart.sqlite3Exec(db, "create table bla ( name string unique, data blob)");
+const stmt4 = tart.sqlite3Prepare(db, "insert into bla(name, data) values ($name, $value)")
+const d = new Uint8Array(4);
+d[0] = 42;
+d[1] = 43;
+d[2] = 44;
+d[3] = 46;
+res = tart.sqlite3StmtRun(stmt4, { name: "v1", value: d });
+
+tart.sqlite3Exec(db, "create table t ( name string unique, data blob)");
+const stmt5 = tart.sqlite3Prepare(db, "insert into t(name, data) values ($name, $value)")
+
+res = tart.sqlite3StmtRun(stmt4, { name: "n1", value: "v1" });
+res = tart.sqlite3StmtRun(stmt4, { name: "n2", value: "v2" });
+
+const exc = expectThrows(() => {
+ res = tart.sqlite3StmtRun(stmt4, { name: "n2", value: "v3" });
+});
+
+expectStrictEquals(exc.code, "SQLITE_CONSTRAINT");