quickjs-tart

quickjs-based runtime for wallet-core logic
Log | Files | Refs | README | LICENSE

commit d1a531a3ef65fc16f27ebed8106c5a5399562c6d
parent 2989a2317965a1c6da04c96d2dbbc4bdd2852e7a
Author: Florian Dold <dold@taler.net>
Date:   Tue,  1 Sep 2026 21:35:36 +0200

qtart: expose API version and SQLite open options

Diffstat:
Mtart_module.c | 115++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mtests/test_sqlite3_error.js | 37++++++++++++++++++++++++++++++++++++-
2 files changed, 147 insertions(+), 5 deletions(-)

diff --git a/tart_module.c b/tart_module.c @@ -43,6 +43,9 @@ static JSValue make_js_ta_copy(JSContext *ctx, uint8_t *data, size_t size); #define TART_MAX_RSA_MODULUS_BYTES 1024U #define TART_MAX_RSA_EXPONENT_BYTES 8U +/* Increment when the JavaScript-facing native API gains new semantics. */ +#define TART_API_VERSION 1 + static JSValue js_encode_utf8(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { @@ -1843,21 +1846,109 @@ static JSValue throw_sqlite3_error(JSContext *ctx, sqlite3 *db) #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 int sqlite3_open_boolean_option(JSContext *ctx, + JSValueConst options, + const char *name, + int *result) +{ + JSValue value; + + value = JS_GetPropertyStr(ctx, options, name); + if (JS_IsException(value)) { + return -1; + } + if (JS_IsUndefined(value)) { + *result = 0; + } else if (!JS_IsBool(value)) { + JS_FreeValue(ctx, value); + JS_ThrowTypeError(ctx, "sqlite3Open option '%s' must be a boolean", name); + return -1; + } else { + *result = JS_ToBool(ctx, value); + } + JS_FreeValue(ctx, value); + return 0; +} + +static int sqlite_uri_path_char(unsigned char c) +{ + return (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || + c == '/' || c == '-' || c == '.' || c == '_' || c == '~'; +} + +static char *sqlite3_immutable_uri(const char *filename) +{ + static const char hex[] = "0123456789ABCDEF"; + static const char prefix[] = "file:"; + static const char suffix[] = "?immutable=1"; + size_t filename_len; + size_t allocation_size; + char *uri; + char *p; + + filename_len = strlen(filename); + if (filename_len > (SIZE_MAX - sizeof(prefix) - sizeof(suffix)) / 3) { + return NULL; + } + allocation_size = sizeof(prefix) - 1 + filename_len * 3 + sizeof(suffix); + uri = sqlite3_malloc64(allocation_size); + if (!uri) { + return NULL; + } + p = uri; + memcpy(p, prefix, sizeof(prefix) - 1); + p += sizeof(prefix) - 1; + for (size_t i = 0; i < filename_len; i++) { + unsigned char c = (unsigned char)filename[i]; + if (sqlite_uri_path_char(c)) { + *p++ = (char)c; + } else { + *p++ = '%'; + *p++ = hex[c >> 4]; + *p++ = hex[c & 0x0f]; + } + } + memcpy(p, suffix, sizeof(suffix)); + return uri; +} + +// (path: string, options?: { readonly?: boolean, immutable?: boolean }) => 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; + const char *open_filename; + char *immutable_uri = NULL; sqlite3 *sqlite3_db = NULL; + int readonly = 0; + int immutable = 0; + int open_flags; int ret; - if (!JS_IsString(argv[0])) { + if (argc < 1 || !JS_IsString(argv[0])) { ret_val = JS_ThrowTypeError(ctx, "filename argument required"); goto done; } + if (argc > 1 && !JS_IsUndefined(argv[1])) { + if (!JS_IsObject(argv[1])) { + ret_val = JS_ThrowTypeError(ctx, "sqlite3Open options must be an object"); + goto done; + } + if (sqlite3_open_boolean_option(ctx, argv[1], "readonly", &readonly) < 0 || + sqlite3_open_boolean_option(ctx, argv[1], "immutable", &immutable) < 0) { + ret_val = JS_EXCEPTION; + goto done; + } + } + if (immutable) { + readonly = 1; + } + filename = JS_ToCString(ctx, argv[0]); if (!filename) { @@ -1865,8 +1956,22 @@ static JSValue js_sqlite3_open(JSContext *ctx, JSValue this_val, goto done; } + open_filename = filename; + open_flags = readonly + ? SQLITE_OPEN_READONLY + : SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE; + if (immutable) { + immutable_uri = sqlite3_immutable_uri(filename); + if (!immutable_uri) { + ret_val = JS_ThrowOutOfMemory(ctx); + goto done; + } + open_filename = immutable_uri; + open_flags |= SQLITE_OPEN_URI; + } + // fprintf(stderr, "opening sqlite3 db at %s\n", filename), - ret = sqlite3_open_v2(filename, &sqlite3_db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL); + ret = sqlite3_open_v2(open_filename, &sqlite3_db, open_flags, NULL); if (SQLITE_OK != ret) { if (NULL == sqlite3_db) { // sqlite3 was unable to even allocate memory @@ -1891,6 +1996,7 @@ static JSValue js_sqlite3_open(JSContext *ctx, JSValue this_val, JS_SetOpaque(db_obj, sqlite3_db); ret_val = db_obj; done: + sqlite3_free(immutable_uri); JS_FreeCString(ctx, filename); return ret_val; } @@ -2467,6 +2573,7 @@ fail: static const JSCFunctionListEntry tart_talercrypto_funcs[] = { + JS_PROP_INT32_DEF("apiVersion", TART_API_VERSION, 0), JS_CFUNC_DEF("structuredClone", 1, js_structured_clone), JS_CFUNC_DEF("encodeUtf8", 1, js_encode_utf8), JS_CFUNC_DEF("crashForDebug", 0, js_crash_for_debug), @@ -2489,7 +2596,7 @@ 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("sqlite3Open", 2, 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), diff --git a/tests/test_sqlite3_error.js b/tests/test_sqlite3_error.js @@ -1,7 +1,7 @@ import * as os from "os"; import * as tart from "tart"; -const dbPath = `/tmp/qtart-sqlite3-error-${Date.now()}.sqlite3`; +const dbPath = `/tmp/qtart-sqlite3-error-${Date.now()}?.sqlite3`; let db; let stmt; @@ -45,6 +45,8 @@ function expectTypeError(fn, messagePart) { } try { + assert(tart.apiVersion === 1, + `expected Qtart API version 1, got ${tart.apiVersion}`); db = tart.sqlite3Open(dbPath); tart.sqlite3Exec(db, "CREATE TABLE entries (value TEXT UNIQUE)"); tart.sqlite3Exec(db, "INSERT INTO entries VALUES ('one')"); @@ -89,6 +91,39 @@ try { stmt = undefined; tart.sqlite3Close(db); db = undefined; + + expectTypeError( + () => tart.sqlite3Open(dbPath, { readonly: "yes" }), + "option 'readonly' must be a boolean", + ); + + db = tart.sqlite3Open(dbPath, { readonly: true }); + stmt = tart.sqlite3Prepare(db, "SELECT value FROM entries"); + assert(tart.sqlite3StmtGetFirst(stmt)?.value === "one", + "read-only connection must read existing records"); + tart.sqlite3Finalize(stmt); + stmt = undefined; + expectSqlite3Error( + () => tart.sqlite3Exec(db, "INSERT INTO entries VALUES ('two')"), + "SQLITE_READONLY", + "readonly", + ); + tart.sqlite3Close(db); + db = undefined; + + db = tart.sqlite3Open(dbPath, { immutable: true }); + stmt = tart.sqlite3Prepare(db, "SELECT value FROM entries"); + assert(tart.sqlite3StmtGetFirst(stmt)?.value === "one", + "immutable connection must preserve URI-special path characters"); + tart.sqlite3Finalize(stmt); + stmt = undefined; + expectSqlite3Error( + () => tart.sqlite3Exec(db, "INSERT INTO entries VALUES ('two')"), + "SQLITE_READONLY", + "readonly", + ); + tart.sqlite3Close(db); + db = undefined; } finally { if (stmt !== undefined) { tart.sqlite3Finalize(stmt);