From c4cf95aeb01fa73a91d17ea37c06945bbd7e6c32 Mon Sep 17 00:00:00 2001 From: Andrea Donetti Date: Tue, 26 May 2026 10:20:56 -0600 Subject: [PATCH 1/5] perf: reuse CloudSync tickets in wasm fetch Read CloudSync ticket response headers in the wasm network adapter and send the cached ticket on later API requests while preserving bearer authentication. Use fetch->numBytes when copying loaded response bodies, report statusText with strlen(), and handle null fetch results to keep error lengths and failures accurate. --- wasm.c | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 96 insertions(+), 11 deletions(-) diff --git a/wasm.c b/wasm.c index 2dc490e..e7463c3 100644 --- a/wasm.c +++ b/wasm.c @@ -8,6 +8,8 @@ #include #include +#include +#include #include #include #include "sqlite3.h" @@ -59,9 +61,71 @@ char *substr(const char *start, const char *end) { return out; } +static bool wasm_header_name_equals(const char *start, const char *end, const char *name) { + size_t name_len = strlen(name); + if ((size_t)(end - start) != name_len) return false; + + for (size_t i = 0; i < name_len; i++) { + if (tolower((unsigned char)start[i]) != tolower((unsigned char)name[i])) return false; + } + + return true; +} + +static char *wasm_response_header_dup(emscripten_fetch_t *fetch, const char *name) { + if (!fetch || !name) return NULL; + + size_t headers_len = emscripten_fetch_get_response_headers_length(fetch); + if (headers_len == 0) return NULL; + + char *headers = (char *)malloc(headers_len + 1); + if (!headers) return NULL; + + if (emscripten_fetch_get_response_headers(fetch, headers, headers_len + 1) == 0) { + free(headers); + return NULL; + } + + char *value = NULL; + const char *line = headers; + while (*line) { + const char *line_end = strchr(line, '\n'); + if (!line_end) line_end = line + strlen(line); + + const char *colon = memchr(line, ':', (size_t)(line_end - line)); + if (colon && wasm_header_name_equals(line, colon, name)) { + const char *value_start = colon + 1; + const char *value_end = line_end; + + while (value_start < value_end && (*value_start == ' ' || *value_start == '\t')) value_start++; + while (value_end > value_start && (value_end[-1] == '\r' || value_end[-1] == '\n' || value_end[-1] == ' ' || value_end[-1] == '\t')) value_end--; + + size_t value_len = (size_t)(value_end - value_start); + value = (char *)malloc(value_len + 1); + if (value) { + memcpy(value, value_start, value_len); + value[value_len] = 0; + } + break; + } + + line = (*line_end == '\n') ? line_end + 1 : line_end; + } + + free(headers); + return value; +} + +static void wasm_request_headers_free(char **header_keys, int allocated_keys, const char **headers) { + for (int i = 0; i < allocated_keys; i++) free(header_keys[i]); + free(header_keys); + free(headers); +} + NETWORK_RESULT network_receive_buffer (network_data *data, const char *endpoint, const char *authentication, bool zero_terminated, bool is_post_request, char *json_payload, const char **extra_headers, int nextra_headers) { char *buffer = NULL; size_t blen = 0; + bool using_ticket = network_data_should_use_ticket(data, endpoint, authentication); emscripten_fetch_attr_t attr; emscripten_fetch_attr_init(&attr); @@ -75,7 +139,7 @@ NETWORK_RESULT network_receive_buffer (network_data *data, const char *endpoint, attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY | EMSCRIPTEN_FETCH_SYNCHRONOUS | EMSCRIPTEN_FETCH_REPLACE; // Prepare header array (alternating key, value, NULL-terminated) - int max_header_pairs = nextra_headers + 4; + int max_header_pairs = nextra_headers + 5; const char **headers = (const char **)calloc((size_t)(max_header_pairs * 2 + 1), sizeof(char *)); char **header_keys = (char **)calloc((size_t)(nextra_headers > 0 ? nextra_headers : 1), sizeof(char *)); if (!headers || !header_keys) { @@ -97,9 +161,7 @@ NETWORK_RESULT network_receive_buffer (network_data *data, const char *endpoint, size_t klen = (size_t)(colon - header); char *key = (char *)malloc(klen + 1); if (!key) { - for (int j = 0; j < allocated_keys; j++) free(header_keys[j]); - free(header_keys); - free(headers); + wasm_request_headers_free(header_keys, allocated_keys, headers); return (NETWORK_RESULT){CLOUDSYNC_NETWORK_ERROR, NULL, 0, NULL, NULL}; } memcpy(key, header, klen); @@ -131,6 +193,15 @@ NETWORK_RESULT network_receive_buffer (network_data *data, const char *endpoint, headers[h++] = auth_header; } + // CloudSync session ticket + char ticket_header[AUTH_HEADER_MAXSIZE]; + if (using_ticket) { + char *ticket = network_data_get_ticket(data); + snprintf(ticket_header, sizeof(ticket_header), "%s", ticket); + headers[h++] = CLOUDSYNC_HEADER_TICKET; + headers[h++] = ticket_header; + } + // Content-Type for JSON if (json_payload) { headers[h++] = "Content-Type"; @@ -148,10 +219,25 @@ NETWORK_RESULT network_receive_buffer (network_data *data, const char *endpoint, emscripten_fetch_t *fetch = emscripten_fetch(&attr, endpoint); // Blocks here until the operation is complete. NETWORK_RESULT result = {0, NULL, 0, NULL, NULL}; + if (!fetch) { + result.code = CLOUDSYNC_NETWORK_ERROR; + wasm_request_headers_free(header_keys, allocated_keys, headers); + return result; + } if(fetch->readyState == 4){ - buffer = fetch->data; - blen = fetch->totalBytes; + buffer = (char *)fetch->data; + blen = fetch->numBytes; + } + + if (fetch->status < 400) { + char *ticket = wasm_response_header_dup(fetch, CLOUDSYNC_HEADER_TICKET); + if (ticket && ticket[0]) { + char *expires_at = wasm_response_header_dup(fetch, CLOUDSYNC_HEADER_TICKET_EXPIRES_AT); + network_data_update_ticket(data, ticket, expires_at); + free(expires_at); + } + free(ticket); } if (fetch->status >= 200 && fetch->status < 300) { @@ -168,9 +254,9 @@ NETWORK_RESULT network_receive_buffer (network_data *data, const char *endpoint, } else result.code = CLOUDSYNC_NETWORK_OK; } else { result.code = CLOUDSYNC_NETWORK_ERROR; - if (fetch->statusText && fetch->statusText[0]) { + if (fetch->statusText[0]) { result.buffer = strdup(fetch->statusText); - result.blen = sizeof(fetch->statusText); + result.blen = strlen(fetch->statusText); result.xfree = free; } else if (blen > 0 && buffer) { char *buf = (char*)malloc(blen + 1); @@ -186,9 +272,7 @@ NETWORK_RESULT network_receive_buffer (network_data *data, const char *endpoint, // cleanup emscripten_fetch_close(fetch); - for (int i = 0; i < allocated_keys; i++) free(header_keys[i]); - free(header_keys); - free(headers); + wasm_request_headers_free(header_keys, allocated_keys, headers); return result; } @@ -223,6 +307,7 @@ bool network_send_buffer(network_data *data, const char *endpoint, const char *a attr.requestDataSize = blob_size; emscripten_fetch_t *fetch = emscripten_fetch(&attr, endpoint); // Blocks here until the operation is complete. + if (!fetch) return false; if (fetch->status >= 200 && fetch->status < 300) result = true; emscripten_fetch_close(fetch); From 6f01d83568404bc4af28b06313f93ac04a9dc738 Mon Sep 17 00:00:00 2001 From: Andrea Donetti Date: Tue, 26 May 2026 10:31:52 -0600 Subject: [PATCH 2/5] fix: harden wasm CloudSync ticket handling Gate ticket response header reads to completed HTTP responses and use the CloudSync session token buffer size for cached tickets. Skip overlong cached tickets instead of truncating them and guard fetch response sizes before casting to size_t. --- wasm.c | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/wasm.c b/wasm.c index e7463c3..0366d4f 100644 --- a/wasm.c +++ b/wasm.c @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -194,12 +195,14 @@ NETWORK_RESULT network_receive_buffer (network_data *data, const char *endpoint, } // CloudSync session ticket - char ticket_header[AUTH_HEADER_MAXSIZE]; + char ticket_header[CLOUDSYNC_SESSION_TOKEN_MAXSIZE]; if (using_ticket) { char *ticket = network_data_get_ticket(data); - snprintf(ticket_header, sizeof(ticket_header), "%s", ticket); - headers[h++] = CLOUDSYNC_HEADER_TICKET; - headers[h++] = ticket_header; + if (strlen(ticket) < sizeof(ticket_header)) { + snprintf(ticket_header, sizeof(ticket_header), "%s", ticket); + headers[h++] = CLOUDSYNC_HEADER_TICKET; + headers[h++] = ticket_header; + } } // Content-Type for JSON @@ -226,11 +229,17 @@ NETWORK_RESULT network_receive_buffer (network_data *data, const char *endpoint, } if(fetch->readyState == 4){ + if (fetch->numBytes > SIZE_MAX) { + result.code = CLOUDSYNC_NETWORK_ERROR; + emscripten_fetch_close(fetch); + wasm_request_headers_free(header_keys, allocated_keys, headers); + return result; + } buffer = (char *)fetch->data; - blen = fetch->numBytes; + blen = (size_t)fetch->numBytes; } - if (fetch->status < 400) { + if (fetch->status >= 200 && fetch->status < 400) { char *ticket = wasm_response_header_dup(fetch, CLOUDSYNC_HEADER_TICKET); if (ticket && ticket[0]) { char *expires_at = wasm_response_header_dup(fetch, CLOUDSYNC_HEADER_TICKET_EXPIRES_AT); From e1b60cabb81bc10641ea557a005cf0d142805b04 Mon Sep 17 00:00:00 2001 From: Andrea Donetti Date: Tue, 26 May 2026 10:47:47 -0600 Subject: [PATCH 3/5] build: include wasm wrapper version in package Define the sqlite-wasm wrapper version in wasm.c so wrapper-level changes can be tracked alongside module versions. Extract and validate SQLITEAI_WASM_WRAPPER_VERSION during build, producing versions such as 3.50.4-wasm.1.0.0-sync.1.0.20-vector.1.0.0-memory.1.2.2. --- build.sh | 8 +++++++- wasm.c | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/build.sh b/build.sh index 0e0f3d1..59637f2 100755 --- a/build.sh +++ b/build.sh @@ -54,5 +54,11 @@ cp modules/sqlite-wasm/tsconfig.json sqlite-wasm/. PKG=sqlite-wasm/package.json TMP=sqlite-wasm/package.tmp.json +WASM_VERSION="$(sed -n 's/^#define SQLITEAI_WASM_WRAPPER_VERSION[[:space:]]*"\([^"]*\)".*/\1/p' wasm.c)" -jq --arg version "$(cat modules/sqlite/VERSION)-sync.$(cd modules/sqlite-sync && make version)-vector.$(cd modules/sqlite-vector && make version)-memory.$(cd modules/sqlite-memory && make version)" '.version = $version' "$PKG" > "$TMP" && mv "$TMP" "$PKG" +if ! [[ "$WASM_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Missing or invalid SQLITEAI_WASM_WRAPPER_VERSION in wasm.c: $WASM_VERSION" + exit 1 +fi + +jq --arg version "$(cat modules/sqlite/VERSION)-wasm.$WASM_VERSION-sync.$(cd modules/sqlite-sync && make version)-vector.$(cd modules/sqlite-vector && make version)-memory.$(cd modules/sqlite-memory && make version)" '.version = $version' "$PKG" > "$TMP" && mv "$TMP" "$PKG" diff --git a/wasm.c b/wasm.c index 0366d4f..58f23e0 100644 --- a/wasm.c +++ b/wasm.c @@ -50,6 +50,7 @@ // MARK: - WASM - +#define SQLITEAI_WASM_WRAPPER_VERSION "1.0.0" #define AUTH_HEADER_MAXSIZE 4096 char *substr(const char *start, const char *end) { From a47222cb23ab83155c19377dc5812c7e62b00092 Mon Sep 17 00:00:00 2001 From: Andrea Donetti Date: Tue, 26 May 2026 11:05:23 -0600 Subject: [PATCH 4/5] fix: expose wasm wrapper version at runtime Register a wasm_version() SQL function backed by SQLITEAI_WASM_WRAPPER_VERSION so generated package versions can be matched by runtime checks. Update the browser version smoke test to include the wasm wrapper segment between SQLite and module versions. --- sqlite-wasm/test/demo.js | 7 +++++++ wasm.c | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/sqlite-wasm/test/demo.js b/sqlite-wasm/test/demo.js index fcd760a..f1e2cbc 100644 --- a/sqlite-wasm/test/demo.js +++ b/sqlite-wasm/test/demo.js @@ -21,6 +21,13 @@ testString += version; }, }); + db.exec({ + sql: 'select wasm_version();', + rowMode: 'array', + callback: function (version) { + testString += `-wasm.${version}`; + }, + }); db.exec({ sql: 'select cloudsync_version();', rowMode: 'array', diff --git a/wasm.c b/wasm.c index 58f23e0..41aaafb 100644 --- a/wasm.c +++ b/wasm.c @@ -766,9 +766,22 @@ void dbmem_remote_engine_free (dbmem_remote_engine_t *engine) { // MARK: - +static void sqliteai_wasm_version(sqlite3_context *context, int argc, sqlite3_value **argv) { + (void)argc; + (void)argv; + sqlite3_result_text(context, SQLITEAI_WASM_WRAPPER_VERSION, -1, SQLITE_STATIC); +} + +static int sqlite3_wasm_wrapper_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi) { + (void)pzErrMsg; + (void)pApi; + return sqlite3_create_function(db, "wasm_version", 0, SQLITE_UTF8 | SQLITE_DETERMINISTIC, NULL, sqliteai_wasm_version, NULL, NULL); +} + int sqlite3_wasm_extra_init(const char *z) { fprintf(stderr, "%s: %s()\n", __FILE__, __func__); int rc = SQLITE_OK; + rc = sqlite3_auto_extension((void *) sqlite3_wasm_wrapper_init); rc = sqlite3_auto_extension((void *) sqlite3_cloudsync_init); rc = sqlite3_auto_extension((void *) sqlite3_vector_init); rc = sqlite3_auto_extension((void *) sqlite3_memory_init); From 289af6534ffd3dea02246f65ab38bc8965201d17 Mon Sep 17 00:00:00 2001 From: Andrea Donetti Date: Tue, 26 May 2026 11:14:39 -0600 Subject: [PATCH 5/5] fix: remove wasm init stderr log Drop the sqlite3_wasm_extra_init startup fprintf because Emscripten routes stderr to console.error in browsers. This prevents successful package initialization from appearing as a JavaScript console error. --- wasm.c | 1 - 1 file changed, 1 deletion(-) diff --git a/wasm.c b/wasm.c index 41aaafb..e9fb6af 100644 --- a/wasm.c +++ b/wasm.c @@ -779,7 +779,6 @@ static int sqlite3_wasm_wrapper_init(sqlite3 *db, char **pzErrMsg, const sqlite3 } int sqlite3_wasm_extra_init(const char *z) { - fprintf(stderr, "%s: %s()\n", __FILE__, __func__); int rc = SQLITE_OK; rc = sqlite3_auto_extension((void *) sqlite3_wasm_wrapper_init); rc = sqlite3_auto_extension((void *) sqlite3_cloudsync_init);