quickjs-tart

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

commit 7e252316edd93ec69f970a4b7dfb553f54c2ee10
parent 2aa58df6d85a468fffa07e1a45eac3b2912774dd
Author: Florian Dold <florian@dold.me>
Date:   Fri, 11 Nov 2022 20:18:15 +0100

wip

Diffstat:
MMakefile | 4++--
Mprelude.js | 6+++++-
Mqjsc.c | 2++
Mquickjs-libc.c | 209++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mquickjs.c | 27+++++++++++++++++++++++++++
Mquickjs.h | 3+++
Mrepl.js | 2++
7 files changed, 248 insertions(+), 5 deletions(-)

diff --git a/Makefile b/Makefile @@ -174,8 +174,8 @@ QJS_LIB_OBJS+=$(OBJDIR)/libbf.o QJS_OBJS+=$(OBJDIR)/qjscalc.o endif -HOST_LIBS=-lm -ldl -lpthread -LIBS=-lm +HOST_LIBS=-lm -ldl -lpthread -lcurl +LIBS=-lm -lcurl ifndef CONFIG_WIN32 LIBS+=-ldl -lpthread endif diff --git a/prelude.js b/prelude.js @@ -8,13 +8,17 @@ class TextEncoder { class TextDecoder { decode(bytes) { - return _decodeUtf8(bytes.buffer); + if (ArrayBuffer.isView(bytes)) { + return _decodeUtf8(bytes.buffer); + } + return _decodeUtf8(bytes); } } globalThis.TextEncoder = TextEncoder; globalThis.TextDecoder = TextDecoder; globalThis.setTimeout = (f, t) => os.setTimeout(f, t); +globalThis.clearTimeout = (h) => os.clearTimeout(h); globalThis.setImmediate = (f) => os.setTimeout(f, 0); // FIXME: log to the right streams! diff --git a/qjsc.c b/qjsc.c @@ -452,6 +452,8 @@ static int output_executable(const char *out_filename, const char *cfilename, *arg++ = "-lm"; *arg++ = "-ldl"; *arg++ = "-lpthread"; + // FIXME: Make conditional + *arg++ = "-lcurl"; *arg = NULL; if (verbose) { diff --git a/quickjs-libc.c b/quickjs-libc.c @@ -75,6 +75,8 @@ typedef sig_t sighandler_t; - add socket calls */ +#include <curl/curl.h> + typedef struct { struct list_head link; int fd; @@ -2015,6 +2017,202 @@ static void js_os_timer_mark(JSRuntime *rt, JSValueConst val, } } +typedef struct { + DynBuf response_data; + JSValue headers_list; + JSContext *ctx; + uint8_t *readbuf; + size_t readpos; + size_t readlen; +} CurlRequestContext; + +static size_t curl_header_callback(char *buffer, size_t size, + size_t nitems, void *userdata) +{ + CurlRequestContext *rctx = userdata; + size_t sz = size * nitems; + + qjs_array_append_new(rctx->ctx, rctx->headers_list, JS_NewStringLen(rctx->ctx, buffer, sz)); + + return sz; +} + +static size_t curl_write_cb(void *data, size_t size, size_t nmemb, void *userp) +{ + size_t realsize = size * nmemb; + CurlRequestContext *rctx = userp; + + if (0 != dbuf_put(&rctx->response_data, data, realsize)) { + return 0; + } + + return realsize; +} + +size_t read_callback(char *ptr, size_t size, size_t nmemb, void *userdata) +{ + CurlRequestContext *rctx = userdata; + ssize_t src_available = rctx->readlen - rctx->readpos; + size_t dst_available = size * nmemb; + size_t n; + + if (!rctx->readbuf) { + return 0; + } + if (src_available <= 0) { + return 0; + } + n = src_available; + if (dst_available < src_available) { + n = dst_available; + } + memcpy(ptr, rctx->readbuf + rctx->readpos, n); + rctx->readpos += n; + return n; +} + +int expect_property_str_bool(JSContext *ctx, JSValueConst this_val, const char *prop_name) +{ + JSValue prop_val; + BOOL bool_val; + + prop_val = JS_GetPropertyStr(ctx, this_val, prop_name); + if (JS_IsException(prop_val)) { + return -1; + } + bool_val = JS_ToBool(ctx, prop_val); + JS_FreeValue(ctx, prop_val); + return bool_val; +} + +/** + * fetchHttp(url, { method, headers, body }) + */ +static JSValue js_os_fetchHttp(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + const char *req_url = NULL; + JSValue ret_val = JS_UNDEFINED; + JSValue options = JS_UNINITIALIZED; + JSValue method = JS_UNINITIALIZED; + const char *method_str = NULL; + CURLcode res; + CURL *curl = NULL; + CurlRequestContext req_context = { 0 }; + struct curl_slist *headers = NULL; + int debug = FALSE; + + req_context.ctx = ctx; + req_context.headers_list = JS_NewArray(ctx); + dbuf_init(&req_context.response_data); + + req_url = JS_ToCString(ctx, argv[0]); + if (!req_url) { + goto exception; + } + + options = argv[1]; + if (JS_VALUE_GET_TAG (options) == JS_TAG_UNDEFINED) { + method = JS_NewString(ctx, "get"); + } else if (JS_VALUE_GET_TAG(options) == JS_TAG_OBJECT) { + method = JS_GetPropertyStr(ctx, options, "method"); + debug = expect_property_str_bool (ctx, options, "debug"); + } else { + JS_ThrowTypeError(ctx, "invalid options"); + goto exception; + } + + curl = curl_easy_init(); + if (!curl) { + JS_ThrowInternalError(ctx, "unable to init libcurl"); + goto exception; + } + curl_easy_setopt(curl, CURLOPT_URL, req_url); + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, curl_header_callback); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &req_context); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_cb); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &req_context); + if (debug == TRUE) { + curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); + } + + method_str = JS_ToCString(ctx, method); + + if (0 == strcasecmp(method_str, "get")) { + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + headers = curl_slist_append (headers, "Accept: application/json"); + } else if (0 == strcasecmp(method_str, "post")) { + JSValue data; + uint8_t *data_ptr; + size_t data_len; + + data = JS_GetPropertyStr(ctx, options, "data"); + if (JS_IsException(data)) { + goto exception; + } + data_ptr = JS_GetArrayBuffer(ctx, &data_len, data); + if (!data_ptr) { + 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(curl, CURLOPT_POST, 1L); + curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_callback); + curl_easy_setopt(curl, CURLOPT_READDATA, &req_context); + headers = curl_slist_append (headers, "Content-Type: application/json"); + } else { + JS_ThrowTypeError(ctx, "invalid request method"); + goto exception; + } + + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + + res = curl_easy_perform(curl); + if (CURLE_OK != res) { + JS_ThrowInternalError(ctx, "fetch failed"); + goto exception; + } + ret_val = JS_NewObject(ctx); + if (JS_IsException(ret_val)) { + goto exception; + } + { + long resp_code; + + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &resp_code); + JS_SetPropertyStr(ctx, ret_val, "status", JS_NewInt32(ctx, resp_code)); + JS_SetPropertyStr(ctx, ret_val, "headers", req_context.headers_list); + req_context.headers_list = JS_UNINITIALIZED; + // FIXME: Don't copy, own buffer + JS_SetPropertyStr(ctx, + ret_val, + "data", + JS_NewArrayBufferCopy(ctx, + req_context.response_data.buf, + req_context.response_data.size)); + } +done: + if (curl) { + curl_easy_cleanup(curl); + curl = NULL; + } + if (NULL != req_context.readbuf) { + free(req_context.readbuf); + } + JS_FreeCString(ctx, method_str); + dbuf_free(&req_context.response_data); + JS_FreeValue(ctx, method); + JS_FreeValue(ctx, req_context.headers_list); + JS_FreeCString(ctx, req_url); + curl_slist_free_all(headers); + return ret_val; +exception: + ret_val = JS_EXCEPTION; + goto done; +} + static JSValue js_os_setTimeout(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { @@ -3622,6 +3820,7 @@ static const JSCFunctionListEntry js_os_funcs[] = { OS_FLAG(SIGTTOU), #endif JS_CFUNC_DEF("setTimeout", 2, js_os_setTimeout ), + JS_CFUNC_DEF("fetchHttp", 2, js_os_fetchHttp ), JS_CFUNC_DEF("clearTimeout", 1, js_os_clearTimeout ), JS_PROP_STRING_DEF("platform", OS_PLATFORM, 0 ), JS_CFUNC_DEF("getcwd", 0, js_os_getcwd ), @@ -3667,6 +3866,11 @@ static int js_os_init(JSContext *ctx, JSModuleDef *m) JS_NewClassID(&js_os_timer_class_id); JS_NewClass(JS_GetRuntime(ctx), js_os_timer_class_id, &js_os_timer_class); + if (CURLE_OK != curl_global_init (CURL_GLOBAL_DEFAULT)) { + JS_ThrowInternalError(ctx, "unable to init libcurl (global)"); + return -1; + } + #ifdef USE_WORKER { JSRuntime *rt = JS_GetRuntime(ctx); @@ -3738,10 +3942,11 @@ static JSValue js_encode_utf8(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { const char *str; + size_t len; JSValue buf; - str = JS_ToCString(ctx, argv[0]); + str = JS_ToCStringLen2(ctx, &len, argv[0], FALSE); // FIXME: Don't copy buffer but pass destructor function - buf = JS_NewArrayBufferCopy(ctx, (const uint8_t*) str, strlen(str)); + buf = JS_NewArrayBufferCopy(ctx, (const uint8_t*) str, len); JS_FreeCString(ctx, str); return buf; } diff --git a/quickjs.c b/quickjs.c @@ -39011,6 +39011,33 @@ static JSValue js_array_pop(JSContext *ctx, JSValueConst this_val, return JS_EXCEPTION; } +int qjs_array_append_new(JSContext *ctx, JSValue this_val, JSValue item) +{ + JSValue obj; + int64_t len, from, newLen; + + obj = JS_ToObject(ctx, this_val); + if (js_get_length64(ctx, &len, obj)) + goto exception; + newLen = len + 1; + if (newLen > MAX_SAFE_INTEGER) { + JS_ThrowTypeError(ctx, "Array loo long"); + goto exception; + } + from = len; + if (JS_SetPropertyInt64(ctx, obj, from, item) < 0) + goto exception; + if (JS_SetProperty(ctx, obj, JS_ATOM_length, JS_NewInt64(ctx, newLen)) < 0) + goto exception; + + JS_FreeValue(ctx, obj); + return 0; + + exception: + JS_FreeValue(ctx, obj); + return -1; +} + static JSValue js_array_push(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int unshift) { diff --git a/quickjs.h b/quickjs.h @@ -708,6 +708,9 @@ static inline const char *JS_ToCString(JSContext *ctx, JSValueConst val1) } void JS_FreeCString(JSContext *ctx, const char *ptr); + +int qjs_array_append_new (JSContext *ctx, JSValue array, JSValue item); + JSValue JS_NewObjectProtoClass(JSContext *ctx, JSValueConst proto, JSClassID class_id); JSValue JS_NewObjectClass(JSContext *ctx, int class_id); JSValue JS_NewObjectProto(JSContext *ctx, JSValueConst proto); diff --git a/repl.js b/repl.js @@ -45,6 +45,8 @@ import * as os from "os"; var config_numcalc = (typeof os.open === "undefined"); var has_jscalc = (typeof Fraction === "function"); var has_bignum = (typeof BigFloat === "function"); + + var use_history_file = true; var colors = { none: "\x1b[0m",