diff options
author | Florian Dold <florian@dold.me> | 2023-08-03 23:15:13 +0200 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2023-08-21 14:47:47 +0200 |
commit | 5583045dcff8d2fa2103d8a32aa034bf78b51f4f (patch) | |
tree | 4012053dd97f8093a276467eafa1c6615f7bfe7d | |
parent | 46cdcb6c188180286e303dd08a239bbac70c77d3 (diff) | |
download | quickjs-tart-5583045dcff8d2fa2103d8a32aa034bf78b51f4f.tar.gz quickjs-tart-5583045dcff8d2fa2103d8a32aa034bf78b51f4f.tar.bz2 quickjs-tart-5583045dcff8d2fa2103d8a32aa034bf78b51f4f.zip |
sqlite3 support
-rw-r--r-- | .editorconfig | 2 | ||||
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | meson.build | 5 | ||||
-rw-r--r-- | qtart.c | 6 | ||||
-rw-r--r-- | quickjs/quickjs-libc.c | 10 | ||||
-rw-r--r-- | quickjs/quickjs.c | 51 | ||||
-rw-r--r-- | quickjs/quickjs.h | 3 | ||||
-rw-r--r-- | tart_module.c | 706 | ||||
-rw-r--r-- | test_sqlite3.js | 60 |
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 @@ -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, @@ -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"); |