diff --git a/scripts/modules/json.js b/scripts/modules/json.js index 4ba440b9..f1b2b399 100644 --- a/scripts/modules/json.js +++ b/scripts/modules/json.js @@ -1,8 +1,8 @@ var json = {} -json.encode = function(val,space,replacer,whitelist) +json.encode = function(val,replacer,space = 1,whitelist) { - return JSON.stringify(val, replacer, space ? 1 : 0) + return JSON.stringify(val, replacer, space) } json.encode[prosperon.DOC] = `Produce a JSON text from a Javascript object. If a record value, at any level, contains a json() method, it will be called, and the value it returns (usually a simpler record) will be JSONified. diff --git a/scripts/modules/nota.js b/scripts/modules/nota.js index 277d30c4..b7f4a1cc 100644 --- a/scripts/modules/nota.js +++ b/scripts/modules/nota.js @@ -1,5 +1,25 @@ var nota = this +var json = use('json') + +var encode = nota.encode + +function nota_tostring() +{ + return json.encode(nota.decode(this)) +} + +var nota_obj = { + toString: nota_tostring +} + +nota.encode = function(obj, replacer) +{ + var result = encode(obj,replacer) + result.toString = nota_tostring + return result +} + nota.encode[prosperon.DOC] = `Convert a JavaScript value into a NOTA-encoded ArrayBuffer. This function serializes JavaScript values (such as numbers, strings, booleans, arrays, objects, or ArrayBuffers) into the NOTA binary format. The resulting ArrayBuffer can be stored or transmitted and later decoded back into a JavaScript value. diff --git a/source/qjs_nota.c b/source/qjs_nota.c index c6935b9e..4531b5f4 100755 --- a/source/qjs_nota.c +++ b/source/qjs_nota.c @@ -357,4 +357,4 @@ JSModuleDef *JS_INIT_MODULE(JSContext *ctx, const char *module_name) { if (!m) return NULL; JS_AddModuleExportList(ctx, m, js_nota_funcs, sizeof(js_nota_funcs)/sizeof(JSCFunctionListEntry)); return m; -} \ No newline at end of file +} diff --git a/source/qjs_wota.c b/source/qjs_wota.c index e91b359d..5c1979e1 100644 --- a/source/qjs_wota.c +++ b/source/qjs_wota.c @@ -1,370 +1,323 @@ -// -// qjs_wota.c -// #include "quickjs.h" -/* We define WOTA_IMPLEMENTATION so wota.h includes its implementation. */ #define WOTA_IMPLEMENTATION #include "wota.h" typedef struct WotaEncodeContext { - JSContext *ctx; - JSValue visitedStack; /* array for cycle detection */ - WotaBuffer wb; /* dynamic buffer for building Wota data */ - int cycle; + JSContext *ctx; + JSValue visited_stack; + WotaBuffer wb; + int cycle; + JSValue replacer; } WotaEncodeContext; -/* ---------------------------------------------------------------- - CYCLE DETECTION (to avoid infinite recursion on circular objects) - ---------------------------------------------------------------- */ static void wota_stack_push(WotaEncodeContext *enc, JSValueConst val) { - JSContext *ctx = enc->ctx; - int len = JS_ArrayLength(ctx, enc->visitedStack); - JS_SetPropertyInt64(ctx, enc->visitedStack, len, JS_DupValue(ctx, val)); + JSContext *ctx = enc->ctx; + int len = JS_ArrayLength(ctx, enc->visited_stack); + JS_SetPropertyInt64(ctx, enc->visited_stack, len, JS_DupValue(ctx, val)); } static void wota_stack_pop(WotaEncodeContext *enc) { - JSContext *ctx = enc->ctx; - int len = JS_ArrayLength(ctx, enc->visitedStack); - JS_SetPropertyStr(ctx, enc->visitedStack, "length", JS_NewUint32(ctx, len - 1)); + JSContext *ctx = enc->ctx; + int len = JS_ArrayLength(ctx, enc->visited_stack); + JS_SetPropertyStr(ctx, enc->visited_stack, "length", JS_NewUint32(ctx, len - 1)); } static int wota_stack_has(WotaEncodeContext *enc, JSValueConst val) { - JSContext *ctx = enc->ctx; - int len = JS_ArrayLength(ctx, enc->visitedStack); - for (int i = 0; i < len; i++) { - JSValue elem = JS_GetPropertyUint32(ctx, enc->visitedStack, i); - if (JS_IsObject(elem) && JS_IsObject(val)) { - if (JS_VALUE_GET_OBJ(elem) == JS_VALUE_GET_OBJ(val)) { - JS_FreeValue(ctx, elem); - return 1; - } - } + JSContext *ctx = enc->ctx; + int len = JS_ArrayLength(ctx, enc->visited_stack); + for (int i = 0; i < len; i++) { + JSValue elem = JS_GetPropertyUint32(ctx, enc->visited_stack, i); + if (JS_IsObject(elem) && JS_IsObject(val)) + if (JS_VALUE_GET_OBJ(elem) == JS_VALUE_GET_OBJ(val)) { JS_FreeValue(ctx, elem); - } - return 0; + return 1; + } + JS_FreeValue(ctx, elem); + } + return 0; } -/* Forward declaration */ -static void wota_encode_value(WotaEncodeContext *enc, JSValueConst val); - -/* ---------------------------------------------------------------- - Encode object properties => Wota record - ---------------------------------------------------------------- */ -static void encode_object_properties(WotaEncodeContext *enc, JSValueConst val) +static JSValue apply_replacer(WotaEncodeContext *enc, JSValueConst holder, JSValueConst key, JSValueConst val) { - JSContext *ctx = enc->ctx; - - JSPropertyEnum *ptab; - uint32_t plen; - if (JS_GetOwnPropertyNames(ctx, &ptab, &plen, val, - JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK) < 0) { - wota_write_sym(&enc->wb, WOTA_NULL); - return; - } - - wota_write_record(&enc->wb, plen); - - for (uint32_t i = 0; i < plen; i++) { - /* property name => TEXT */ - const char *propName = JS_AtomToCString(ctx, ptab[i].atom); - wota_write_text(&enc->wb, propName); - JS_FreeCString(ctx, propName); - - /* property value => wota_encode_value */ - JSValue propVal = JS_GetProperty(ctx, val, ptab[i].atom); - wota_encode_value(enc, propVal); - JS_FreeValue(ctx, propVal); - - JS_FreeAtom(ctx, ptab[i].atom); - } - js_free(ctx, ptab); + if (JS_IsUndefined(enc->replacer)) return JS_DupValue(enc->ctx, val); + JSValue args[2] = { JS_DupValue(enc->ctx, key), JS_DupValue(enc->ctx, val) }; + JSValue result = JS_Call(enc->ctx, enc->replacer, holder, 2, args); + JS_FreeValue(enc->ctx, args[0]); + JS_FreeValue(enc->ctx, args[1]); + if (JS_IsException(result)) return JS_DupValue(enc->ctx, val); + return result; } -/* ---------------------------------------------------------------- - Main dispatcher for any JS value => Wota - ---------------------------------------------------------------- */ -static void wota_encode_value(WotaEncodeContext *enc, JSValueConst val) -{ - JSContext *ctx = enc->ctx; - int tag = JS_VALUE_GET_TAG(val); +static void wota_encode_value(WotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSValueConst key); - switch(tag) { - case JS_TAG_INT: - { - double d; - JS_ToFloat64(ctx, &d, val); - wota_write_number(&enc->wb, d); - return; +static void encode_object_properties(WotaEncodeContext *enc, JSValueConst val, JSValueConst holder) +{ + JSContext *ctx = enc->ctx; + JSPropertyEnum *ptab; + uint32_t plen; + if (JS_GetOwnPropertyNames(ctx, &ptab, &plen, val, JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK) < 0) { + wota_write_sym(&enc->wb, WOTA_NULL); + return; + } + uint32_t non_function_count = 0; + for (uint32_t i = 0; i < plen; i++) { + JSValue prop_val = JS_GetProperty(ctx, val, ptab[i].atom); + if (!JS_IsFunction(ctx, prop_val)) non_function_count++; + JS_FreeValue(ctx, prop_val); + } + wota_write_record(&enc->wb, non_function_count); + for (uint32_t i = 0; i < plen; i++) { + JSValue prop_val = JS_GetProperty(ctx, val, ptab[i].atom); + if (!JS_IsFunction(ctx, prop_val)) { + const char *prop_name = JS_AtomToCString(ctx, ptab[i].atom); + JSValue prop_key = JS_AtomToValue(ctx, ptab[i].atom); + wota_write_text(&enc->wb, prop_name); + wota_encode_value(enc, prop_val, val, prop_key); + JS_FreeCString(ctx, prop_name); + JS_FreeValue(ctx, prop_key); + } + JS_FreeValue(ctx, prop_val); + JS_FreeAtom(ctx, ptab[i].atom); + } + js_free(ctx, ptab); +} + +static void wota_encode_value(WotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSValueConst key) +{ + JSContext *ctx = enc->ctx; + JSValue replaced = apply_replacer(enc, holder, key, val); + int tag = JS_VALUE_GET_TAG(replaced); + switch (tag) { + case JS_TAG_INT: { + double d; + JS_ToFloat64(ctx, &d, replaced); + wota_write_number(&enc->wb, d); + break; } case JS_TAG_FLOAT64: case JS_TAG_BIG_INT: case JS_TAG_BIG_DECIMAL: - case JS_TAG_BIG_FLOAT: - { - /* Convert to double if possible. If it's out of double range, - you might need a fallback. QuickJS can handle bigint, etc. - But let's just do double. */ - double d; - if (JS_ToFloat64(ctx, &d, val) < 0) { - wota_write_sym(&enc->wb, WOTA_NULL); - return; - } - wota_write_number(&enc->wb, d); - return; + case JS_TAG_BIG_FLOAT: { + double d; + if (JS_ToFloat64(ctx, &d, replaced) < 0) { + wota_write_sym(&enc->wb, WOTA_NULL); + break; + } + wota_write_number(&enc->wb, d); + break; } - case JS_TAG_STRING: - { - const char *str = JS_ToCString(ctx, val); - if (!str) { - wota_write_text(&enc->wb, ""); - return; - } - wota_write_text(&enc->wb, str); - JS_FreeCString(ctx, str); - return; + case JS_TAG_STRING: { + const char *str = JS_ToCString(ctx, replaced); + wota_write_text(&enc->wb, str ? str : ""); + JS_FreeCString(ctx, str); + break; } case JS_TAG_BOOL: - { - if (JS_VALUE_GET_BOOL(val)) { - wota_write_sym(&enc->wb, WOTA_TRUE); - } else { - wota_write_sym(&enc->wb, WOTA_FALSE); - } - return; - } + wota_write_sym(&enc->wb, JS_VALUE_GET_BOOL(replaced) ? WOTA_TRUE : WOTA_FALSE); + break; case JS_TAG_NULL: case JS_TAG_UNDEFINED: - wota_write_sym(&enc->wb, WOTA_NULL); - return; - - case JS_TAG_OBJECT: - { - /* Check if it's an ArrayBuffer => blob */ - if (JS_IsArrayBuffer(ctx, val)) { - size_t bufLen; - void *bufData = JS_GetArrayBuffer(ctx, &bufLen, val); - wota_write_blob(&enc->wb, (unsigned long long)bufLen * 8, - (const char *)bufData); - return; + wota_write_sym(&enc->wb, WOTA_NULL); + break; + case JS_TAG_OBJECT: { + if (JS_IsArrayBuffer(ctx, replaced)) { + size_t buf_len; + void *buf_data = JS_GetArrayBuffer(ctx, &buf_len, replaced); + wota_write_blob(&enc->wb, (unsigned long long)buf_len * 8, (const char *)buf_data); + break; + } + if (JS_IsArray(ctx, replaced)) { + if (wota_stack_has(enc, replaced)) { + enc->cycle = 1; + break; } - - if (JS_IsArray(ctx, val)) { - if (wota_stack_has(enc, val)) { - enc->cycle = 1; - return; - } - wota_stack_push(enc, val); - - int arrLen = JS_ArrayLength(ctx, val); - wota_write_array(&enc->wb, arrLen); - for (int i = 0; i < arrLen; i++) { - JSValue elemVal = JS_GetPropertyUint32(ctx, val, i); - wota_encode_value(enc, elemVal); - JS_FreeValue(ctx, elemVal); - } - wota_stack_pop(enc); - return; + wota_stack_push(enc, replaced); + int arr_len = JS_ArrayLength(ctx, replaced); + wota_write_array(&enc->wb, arr_len); + for (int i = 0; i < arr_len; i++) { + JSValue elem_val = JS_GetPropertyUint32(ctx, replaced, i); + JSValue elem_key = JS_NewInt32(ctx, i); + wota_encode_value(enc, elem_val, replaced, elem_key); + JS_FreeValue(ctx, elem_val); + JS_FreeValue(ctx, elem_key); } - - /* Otherwise => record */ - if (wota_stack_has(enc, val)) { - enc->cycle = 1; - return; - } - wota_stack_push(enc, val); - encode_object_properties(enc, val); wota_stack_pop(enc); - return; + break; + } + if (wota_stack_has(enc, replaced)) { + enc->cycle = 1; + break; + } + wota_stack_push(enc, replaced); + JSValue to_json = JS_GetPropertyStr(ctx, replaced, "toJSON"); + if (JS_IsFunction(ctx, to_json)) { + JSValue result = JS_Call(ctx, to_json, replaced, 0, NULL); + JS_FreeValue(ctx, to_json); + if (!JS_IsException(result)) { + wota_encode_value(enc, result, holder, key); + JS_FreeValue(ctx, result); + } else + wota_write_sym(&enc->wb, WOTA_NULL); + wota_stack_pop(enc); + break; + } + JS_FreeValue(ctx, to_json); + encode_object_properties(enc, replaced, holder); + wota_stack_pop(enc); + break; } default: - wota_write_sym(&enc->wb, WOTA_NULL); - return; - } + wota_write_sym(&enc->wb, WOTA_NULL); + break; + } + JS_FreeValue(ctx, replaced); } -/* ---------------------------------------------------------------- - Public JS function: wota.encode(value) => ArrayBuffer - ---------------------------------------------------------------- */ -static JSValue js_wota_encode(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv) +static char *decode_wota_value(JSContext *ctx, char *data_ptr, char *end_ptr, JSValue *out_val, JSValue holder, JSValue key, JSValue reviver) { - if (argc < 1) { - return JS_ThrowTypeError(ctx, "wota.encode requires 1 argument"); - } - WotaEncodeContext enc_s, *enc = &enc_s; - enc->ctx = ctx; - enc->visitedStack = JS_NewArray(ctx); - enc->cycle = 0; - - wota_buffer_init(&enc->wb, 16); - - wota_encode_value(enc, argv[0]); - - if (enc->cycle) { - JS_FreeValue(ctx, enc->visitedStack); - wota_buffer_free(&enc->wb); - return JS_ThrowTypeError(ctx, "Cannot encode cyclic object with wota"); - } - - JS_FreeValue(ctx, enc->visitedStack); - - /* Prepare the ArrayBuffer result */ - size_t word_count = enc->wb.size; - size_t total_bytes = word_count * sizeof(uint64_t); - uint8_t *raw = (uint8_t *)enc->wb.data; - - JSValue ret = JS_NewArrayBufferCopy(ctx, raw, total_bytes); - - wota_buffer_free(&enc->wb); - return ret; -} - -// A dedicated function that decodes one Wota value from data_ptr, -// returns a JSValue, and advances data_ptr. -// We return the updated pointer (like wota_read_* functions do). -static char* decode_wota_value(JSContext *ctx, char *data_ptr, char *end_ptr, JSValue *outVal) { - if ((end_ptr - data_ptr) < 8) { - // Not enough data to read; just set undefined - *outVal = JS_UNDEFINED; - return data_ptr; - } - uint64_t first_word = *(uint64_t *)data_ptr; - int type = (int)(first_word & 0xffU); - - switch (type) { + if ((end_ptr - data_ptr) < 8) { + *out_val = JS_UNDEFINED; + return data_ptr; + } + uint64_t first_word = *(uint64_t *)data_ptr; + int type = (int)(first_word & 0xffU); + switch (type) { case WOTA_INT: { - long long val; - data_ptr = wota_read_int(&val, data_ptr); - *outVal = JS_NewInt64(ctx, val); - break; + long long val; + data_ptr = wota_read_int(&val, data_ptr); + *out_val = JS_NewInt64(ctx, val); + break; } case WOTA_FLOAT: { - double d; - data_ptr = wota_read_float(&d, data_ptr); - *outVal = JS_NewFloat64(ctx, d); - break; + double d; + data_ptr = wota_read_float(&d, data_ptr); + *out_val = JS_NewFloat64(ctx, d); + break; } case WOTA_SYM: { - int scode; - data_ptr = wota_read_sym(&scode, data_ptr); - if (scode == WOTA_NULL) { - *outVal = JS_UNDEFINED; - } else if (scode == WOTA_FALSE) { - *outVal = JS_NewBool(ctx, 0); - } else if (scode == WOTA_TRUE) { - *outVal = JS_NewBool(ctx, 1); - } else { - // other symbol codes => undefined or handle them - *outVal = JS_UNDEFINED; - } - break; + int scode; + data_ptr = wota_read_sym(&scode, data_ptr); + if (scode == WOTA_NULL) *out_val = JS_UNDEFINED; + else if (scode == WOTA_FALSE) *out_val = JS_NewBool(ctx, 0); + else if (scode == WOTA_TRUE) *out_val = JS_NewBool(ctx, 1); + else *out_val = JS_UNDEFINED; + break; } case WOTA_BLOB: { - long long blen; - char *bdata = NULL; - data_ptr = wota_read_blob(&blen, &bdata, data_ptr); - if (bdata) { - *outVal = JS_NewArrayBufferCopy(ctx, (uint8_t*)bdata, (size_t)blen); - free(bdata); - } else { - *outVal = JS_NewArrayBufferCopy(ctx, NULL, 0); - } - break; + long long blen; + char *bdata = NULL; + data_ptr = wota_read_blob(&blen, &bdata, data_ptr); + *out_val = bdata ? JS_NewArrayBufferCopy(ctx, (uint8_t *)bdata, (size_t)blen) : JS_NewArrayBufferCopy(ctx, NULL, 0); + if (bdata) free(bdata); + break; } case WOTA_TEXT: { - char *utf8 = NULL; - data_ptr = wota_read_text(&utf8, data_ptr); - if (utf8) { - *outVal = JS_NewString(ctx, utf8); - free(utf8); - } else { - *outVal = JS_NewString(ctx, ""); - } - break; + char *utf8 = NULL; + data_ptr = wota_read_text(&utf8, data_ptr); + *out_val = JS_NewString(ctx, utf8 ? utf8 : ""); + if (utf8) free(utf8); + break; } case WOTA_ARR: { - // Recursively decode the array - long long c; - data_ptr = wota_read_array(&c, data_ptr); - JSValue arr = JS_NewArray(ctx); - for (long long i = 0; i < c; i++) { - JSValue elemVal = JS_UNDEFINED; - data_ptr = decode_wota_value(ctx, data_ptr, end_ptr, &elemVal); - JS_SetPropertyUint32(ctx, arr, i, elemVal); - } - *outVal = arr; - break; + long long c; + data_ptr = wota_read_array(&c, data_ptr); + JSValue arr = JS_NewArray(ctx); + for (long long i = 0; i < c; i++) { + JSValue elem_val = JS_UNDEFINED; + data_ptr = decode_wota_value(ctx, data_ptr, end_ptr, &elem_val, arr, JS_NewInt32(ctx, i), reviver); + JS_SetPropertyUint32(ctx, arr, i, elem_val); + } + *out_val = arr; + break; } case WOTA_REC: { - // Recursively decode the record - long long c; - data_ptr = wota_read_record(&c, data_ptr); - JSValue obj = JS_NewObject(ctx); - for (long long i = 0; i < c; i++) { - // read the key - char *tkey = NULL; - data_ptr = wota_read_text(&tkey, data_ptr); - // read the value - JSValue subVal = JS_UNDEFINED; - data_ptr = decode_wota_value(ctx, data_ptr, end_ptr, &subVal); - - if (tkey) { - JS_SetPropertyStr(ctx, obj, tkey, subVal); - free(tkey); - } else { - JS_FreeValue(ctx, subVal); - } - } - *outVal = obj; - break; + long long c; + data_ptr = wota_read_record(&c, data_ptr); + JSValue obj = JS_NewObject(ctx); + for (long long i = 0; i < c; i++) { + char *tkey = NULL; + data_ptr = wota_read_text(&tkey, data_ptr); + JSValue prop_key = tkey ? JS_NewString(ctx, tkey) : JS_UNDEFINED; + JSValue sub_val = JS_UNDEFINED; + data_ptr = decode_wota_value(ctx, data_ptr, end_ptr, &sub_val, obj, prop_key, reviver); + if (tkey) JS_SetPropertyStr(ctx, obj, tkey, sub_val); + else JS_FreeValue(ctx, sub_val); + JS_FreeValue(ctx, prop_key); + if (tkey) free(tkey); + } + *out_val = obj; + break; } default: - // unknown => skip - data_ptr += 8; - *outVal = JS_UNDEFINED; - break; - } - - return data_ptr; + data_ptr += 8; + *out_val = JS_UNDEFINED; + break; + } + if (!JS_IsUndefined(reviver)) { + JSValue args[2] = { JS_DupValue(ctx, key), JS_DupValue(ctx, *out_val) }; + JSValue revived = JS_Call(ctx, reviver, holder, 2, args); + JS_FreeValue(ctx, args[0]); + JS_FreeValue(ctx, args[1]); + if (!JS_IsException(revived)) { + JS_FreeValue(ctx, *out_val); + *out_val = revived; + } else + JS_FreeValue(ctx, revived); + } + return data_ptr; } -static JSValue js_wota_decode(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv) +static JSValue js_wota_encode(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { - if (argc < 1) return JS_UNDEFINED; - - size_t len; - uint8_t *buf = JS_GetArrayBuffer(ctx, &len, argv[0]); - if (!buf) { - return JS_UNDEFINED; - } - - char *data_ptr = (char *)buf; - char *end_ptr = data_ptr + len; - - JSValue result = JS_UNDEFINED; - decode_wota_value(ctx, data_ptr, end_ptr, &result); - return result; + if (argc < 1) return JS_ThrowTypeError(ctx, "wota.encode requires at least 1 argument"); + WotaEncodeContext enc_s, *enc = &enc_s; + enc->ctx = ctx; + enc->visited_stack = JS_NewArray(ctx); + enc->cycle = 0; + enc->replacer = (argc > 1 && JS_IsFunction(ctx, argv[1])) ? argv[1] : JS_UNDEFINED; + wota_buffer_init(&enc->wb, 16); + wota_encode_value(enc, argv[0], JS_UNDEFINED, JS_NewString(ctx, "")); + if (enc->cycle) { + JS_FreeValue(ctx, enc->visited_stack); + wota_buffer_free(&enc->wb); + return JS_ThrowReferenceError(ctx, "Cannot encode cyclic object with wota"); + } + JS_FreeValue(ctx, enc->visited_stack); + size_t total_bytes = enc->wb.size * sizeof(uint64_t); + JSValue ret = JS_NewArrayBufferCopy(ctx, (uint8_t *)enc->wb.data, total_bytes); + wota_buffer_free(&enc->wb); + return ret; +} + +static JSValue js_wota_decode(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) +{ + if (argc < 1) return JS_UNDEFINED; + size_t len; + uint8_t *buf = JS_GetArrayBuffer(ctx, &len, argv[0]); + if (!buf) return JS_UNDEFINED; + JSValue reviver = (argc > 1 && JS_IsFunction(ctx, argv[1])) ? argv[1] : JS_UNDEFINED; + char *data_ptr = (char *)buf; + char *end_ptr = data_ptr + len; + JSValue result = JS_UNDEFINED; + JSValue holder = JS_NewObject(ctx); + decode_wota_value(ctx, data_ptr, end_ptr, &result, holder, JS_NewString(ctx, ""), reviver); + JS_FreeValue(ctx, holder); + return result; } -/* ---------------------------------------------------------------- - Expose wota.encode / wota.decode to QuickJS - ---------------------------------------------------------------- */ static const JSCFunctionListEntry js_wota_funcs[] = { - JS_CFUNC_DEF("encode", 1, js_wota_encode), - JS_CFUNC_DEF("decode", 1, js_wota_decode), + JS_CFUNC_DEF("encode", 1, js_wota_encode), + JS_CFUNC_DEF("decode", 1, js_wota_decode), }; -/* For module usage */ static int js_wota_init(JSContext *ctx, JSModuleDef *m) { - JS_SetModuleExportList(ctx, m, js_wota_funcs, - sizeof(js_wota_funcs)/sizeof(js_wota_funcs[0])); - return 0; + JS_SetModuleExportList(ctx, m, js_wota_funcs, sizeof(js_wota_funcs)/sizeof(js_wota_funcs[0])); + return 0; } #ifdef JS_SHARED_LIBRARY @@ -375,19 +328,15 @@ static int js_wota_init(JSContext *ctx, JSModuleDef *m) JSModuleDef *JS_INIT_MODULE(JSContext *ctx, const char *module_name) { - JSModuleDef *m = JS_NewCModule(ctx, module_name, js_wota_init); - if (!m) return NULL; - JS_AddModuleExportList(ctx, m, js_wota_funcs, - sizeof(js_wota_funcs)/sizeof(js_wota_funcs[0])); - return m; + JSModuleDef *m = JS_NewCModule(ctx, module_name, js_wota_init); + if (!m) return NULL; + JS_AddModuleExportList(ctx, m, js_wota_funcs, sizeof(js_wota_funcs)/sizeof(js_wota_funcs[0])); + return m; } -/* An optional helper function if you're using it outside the module system */ JSValue js_wota_use(JSContext *ctx) { - JSValue exports = JS_NewObject(ctx); - JS_SetPropertyFunctionList(ctx, exports, - js_wota_funcs, - sizeof(js_wota_funcs)/sizeof(js_wota_funcs[0])); - return exports; + JSValue exports = JS_NewObject(ctx); + JS_SetPropertyFunctionList(ctx, exports, js_wota_funcs, sizeof(js_wota_funcs)/sizeof(js_wota_funcs[0])); + return exports; } diff --git a/tests/wota.js b/tests/wota.js index 3963545e..cbc04673 100644 --- a/tests/wota.js +++ b/tests/wota.js @@ -1,226 +1,260 @@ -// -// wota.js -// -// A test script that exercises wota.encode() / wota.decode() in QuickJS. -// - -// Load the Wota module. If you compiled qjs_wota.c as a module named "wota.so", -// then in QuickJS you might do: var wota = use('wota'); -var os = use('os'); +var os = use('os'); + +// Helper function to convert hex string to ArrayBuffer +function hexToBuffer(hex) { + let bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) + bytes[i / 2] = parseInt(hex.substr(i, 2), 16); + return bytes.buffer; +} + +// Helper function to convert ArrayBuffer to hex string +function bufferToHex(buffer) { + return Array.from(new Uint8Array(buffer)) + .map(b => b.toString(16).padStart(2, '0')) + .join('') + .toLowerCase(); +} -// A small epsilon for floating comparisons var EPSILON = 1e-12; -/* Deep comparison function (for JS values). - Compares numbers within EPSILON, compares arrays and objects recursively, etc. */ +// Deep comparison function for objects and arrays function deepCompare(expected, actual, path = '') { - // Shortcut: if strictly equal, fine - if (expected === actual) { - return { passed: true, messages: [] }; - } + if (expected === actual) return { passed: true, messages: [] }; - // Compare numbers with tolerance - if (typeof expected === 'number' && typeof actual === 'number') { - // Handle NaN: - if (isNaN(expected) && isNaN(actual)) { - return { passed: true, messages: [] }; - } - const diff = Math.abs(expected - actual); - if (diff <= EPSILON) { - return { passed: true, messages: [] }; - } - return { - passed: false, - messages: [ - `Number mismatch at ${path}: expected ${expected}, got ${actual}.`, - `Difference ${diff} > epsilon ${EPSILON}.` - ] - }; - } - - // Compare ArrayBuffers by contents - if (expected instanceof ArrayBuffer && actual instanceof ArrayBuffer) { - const eArr = new Uint8Array(expected); - const aArr = new Uint8Array(actual); - return deepCompare([...eArr], [...aArr], path); - } - - // If one is an ArrayBuffer, convert for array comparison - if (actual instanceof ArrayBuffer) { - actual = [...new Uint8Array(actual)]; - } - - // Compare arrays - if (Array.isArray(expected) && Array.isArray(actual)) { - if (expected.length !== actual.length) { - return { - passed: false, - messages: [ - `Array length mismatch at ${path}: expected ${expected.length}, got ${actual.length}` - ] - }; - } - let subMessages = []; - for (let i = 0; i < expected.length; i++) { - let r = deepCompare(expected[i], actual[i], `${path}[${i}]`); - if (!r.passed) subMessages.push(...r.messages); - } - return { passed: subMessages.length === 0, messages: subMessages }; - } - - // Compare objects - if (expected && typeof expected === 'object' && - actual && typeof actual === 'object') { - let expKeys = Object.keys(expected).sort(); - let actKeys = Object.keys(actual).sort(); - // Quick JSON-based check on key sets - if (JSON.stringify(expKeys) !== JSON.stringify(actKeys)) { - return { - passed: false, - messages: [ - `Object key mismatch at ${path}:\n expected keys: ${expKeys}\n actual keys: ${actKeys}` - ] - }; - } - let subMessages = []; - for (let k of expKeys) { - let r = deepCompare(expected[k], actual[k], path ? `${path}.${k}` : k); - if (!r.passed) subMessages.push(...r.messages); - } - return { passed: subMessages.length === 0, messages: subMessages }; - } - - // Fallback: primitive mismatch + if (typeof expected === 'number' && typeof actual === 'number') { + if (isNaN(expected) && isNaN(actual)) + return { passed: true, messages: [] }; + const diff = Math.abs(expected - actual); + if (diff <= EPSILON) + return { passed: true, messages: [] }; return { - passed: false, - messages: [ - `Value mismatch at ${path}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}` - ] + passed: false, + messages: [ + `Value mismatch at ${path}: expected ${expected}, got ${actual}`, + `Difference of ${diff} is larger than tolerance ${EPSILON}` + ] }; + } + + if (expected instanceof ArrayBuffer && actual instanceof ArrayBuffer) { + const expArray = Array.from(new Uint8Array(expected)); + const actArray = Array.from(new Uint8Array(actual)); + return deepCompare(expArray, actArray, path); + } + + if (actual instanceof ArrayBuffer) + actual = Array.from(new Uint8Array(actual)); + + if (Array.isArray(expected) && Array.isArray(actual)) { + if (expected.length !== actual.length) + return { + passed: false, + messages: [`Array length mismatch at ${path}: expected ${expected.length}, got ${actual.length}`] + }; + let messages = []; + for (let i = 0; i < expected.length; i++) { + const result = deepCompare(expected[i], actual[i], `${path}[${i}]`); + if (!result.passed) messages.push(...result.messages); + } + return { passed: messages.length === 0, messages }; + } + + if (typeof expected === 'object' && expected !== null && + typeof actual === 'object' && actual !== null) { + const expKeys = Object.keys(expected).sort(); + const actKeys = Object.keys(actual).sort(); + if (JSON.stringify(expKeys) !== JSON.stringify(actKeys)) + return { + passed: false, + messages: [`Object keys mismatch at ${path}: expected ${expKeys}, got ${actKeys}`] + }; + let messages = []; + for (let key of expKeys) { + const result = deepCompare(expected[key], actual[key], `${path}.${key}`); + if (!result.passed) messages.push(...result.messages); + } + return { passed: messages.length === 0, messages }; + } + + return { + passed: false, + messages: [`Value mismatch at ${path}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`] + }; } -let testCases = [ - // Basic numbers - 0, - 1, - -1, - 2023, - 1e46, - 2.5e120, - 2e120, - 4.3e-7, - 42, - 1.5, - -0.123456, +// Test cases covering Wota types and replacer/reviver functionality +var testarr = []; +var hex = "a374"; +for (var i = 0; i < 500; i++) { + testarr.push(1); + hex += "61"; +} - // Integer boundaries (56-bit limit in Wota spec) - // Wota can store -2^55 through 2^55 - 1 as an INT - -(2**55), - (2**55) - 1, +var testCases = [ + // Integer tests (WOTA_INT up to 56-bit) + { input: 0, expectedHex: "60" }, + { input: 2023, expectedHex: "e08f67" }, + { input: -1, expectedHex: "69" }, + { input: 7, expectedHex: "67" }, + { input: -7, expectedHex: "6f" }, + { input: 1023, expectedHex: "e07f" }, + { input: -1023, expectedHex: "ef7f" }, + { input: 2**55 - 1, expectedHex: "e0ffffffffffffff" }, // Max 56-bit int + { input: -(2**55), expectedHex: "e000000000000000" }, // Min 56-bit int - // Larger than 56-bit => stored as float - -(2**55) - 1, - (2**55), + // Symbol tests + { input: undefined, expectedHex: "70" }, + { input: false, expectedHex: "72" }, + { input: true, expectedHex: "73" }, - // Infinity and NaN - Infinity, - -Infinity, - NaN, + // Floating Point tests (WOTA_FLOAT) + { input: -1.01, expectedHex: "5a65" }, + { input: 98.6, expectedHex: "51875a" }, + { input: -0.5772156649, expectedHex: "d80a95c0b0bd69" }, + { input: -1.00000000000001, expectedHex: "d80e96deb183e98001" }, + { input: -10000000000000, expectedHex: "c80d01" }, + { input: 2**55, expectedHex: "d80e01" }, // Beyond 56-bit, stored as float - // Booleans - true, - false, + // Text tests + { input: "", expectedHex: "10" }, + { input: "cat", expectedHex: "13636174" }, + { input: "U+1F4A9 πÇîπüåπéôπüíτ╡╡µûçσ¡ùπÇì ┬½≡ƒÆ⌐┬╗", + expectedHex: "9014552b314634413920e00ce046e113e06181fa7581cb0781b657e00d20812b87e929813b" }, - // undefined (WOTA_NULL) - undefined, + // Blob tests + { input: new Uint8Array([0xFF, 0xAA]).buffer, expectedHex: "8010ffaa" }, + { input: new Uint8Array([0b11110000, 0b11100011, 0b00100000, 0b10000000]).buffer, + expectedHex: "8019f0e32080" }, - // A couple strings - "Hello, Wota!", - "", - "Emoji test: \u{1f600}\u{1f64f}", // 😀🙏 + // Large array test + { input: testarr, expectedHex: hex }, - // An ArrayBuffer (binary blob) - new Uint8Array([0xde, 0xad, 0xbe, 0xef]).buffer, - // empty blob - new Uint8Array([]).buffer, + // Array tests + { input: [], expectedHex: "20" }, + { input: [1, 2, 3], expectedHex: "23616263" }, + { input: [-1, 0, 1.5], expectedHex: "2369605043" }, - // Arrays - [], - [1,2,3], - [true, false, undefined, "test", -999], - // Nested array - [[[]]], + // Record tests + { input: {}, expectedHex: "30" }, + { input: { a: 1, b: 2 }, expectedHex: "32116161116262" }, - // Objects / Records - {}, - {a:1, b:2.2, c:"3", d:false}, - { nested: { arr: [1, { x: "y" }] } }, - // Symbol-like keys in JS (just unusual keys) - { "": 123, "foo": "bar" }, + // Complex nested structures + { input: { + num: 42, + arr: [1, -1, 2.5], + str: "test", + obj: { x: true } + }, + expectedHex: "34216e756d622a2173747214746573742161727223616965235840216f626a21117873" }, - // Larger array length test (not extreme, just a demonstration) - Array.from({length: 10}, (_, i) => i), - // Some deeper nesting - { - deep: { - deeper: { - arr: [ { level: "three" }, [ "and four?" ] ] + // Additional edge cases + { input: new Uint8Array([]).buffer, expectedHex: "00" }, + { input: [[]], expectedHex: "2120" }, + { input: { "": "" }, expectedHex: "311010" }, + { input: 1e-10, expectedHex: "d00a01" }, + + // Replacer tests + { input: { a: 1, b: 2 }, + replacer: (key, value) => typeof value === 'number' ? value * 2 : value, + expected: { a: 2, b: 4 }, + testType: 'replacer' }, + + { input: { x: "test", y: 5 }, + replacer: (key, value) => key === 'x' ? value + "!" : value, + expected: { x: "test!", y: 5 }, + testType: 'replacer' }, + + // Reviver tests + { input: { a: 1, b: 2 }, + reviver: (key, value) => typeof value === 'number' ? value * 3 : value, + expected: { a: 3, b: 6 }, + testType: 'reviver' }, + + { input: { x: "test", y: 10 }, + reviver: (key, value) => key === 'y' ? value + 1 : value, + expected: { x: "test", y: 11 }, + testType: 'reviver' } +]; + +// Run tests and collect results +let results = []; +let testCount = 0; + +for (let test of testCases) { + testCount++; + let testName = `Test ${testCount}: ${JSON.stringify(test.input)}${test.testType ? ` (${test.testType})` : ''}`; + let passed = true; + let messages = []; + + try { + // Test encoding + let encoded = test.replacer ? wota.encode(test.input, test.replacer) : wota.encode(test.input); + if (!(encoded instanceof ArrayBuffer)) { + passed = false; + messages.push("Encode should return ArrayBuffer"); + } else { + if (test.expectedHex) { + let encodedHex = bufferToHex(encoded); + if (encodedHex !== test.expectedHex.toLowerCase()) { + messages.push( + `Hex encoding differs (informational): + Expected: ${test.expectedHex} + Got: ${encodedHex}` + ); } } - }, - ]; -/* We’ll just do a round-trip test: - decoded = wota.decode( wota.encode(input) ) - and compare decoded vs. original. -*/ -let results = []; -let passCount = 0; + // Test decoding + let decoded = test.reviver ? wota.decode(encoded, test.reviver) : wota.decode(encoded); + let expected = test.expected || test.input; -for (let i = 0; i < testCases.length; i++) { - let input = testCases[i]; - let testName = `Test ${i+1}: ${JSON.stringify(input)}`; - let passed = true; - let messages = []; + // Normalize ArrayBuffer for comparison + if (expected instanceof ArrayBuffer) + expected = Array.from(new Uint8Array(expected)); + if (decoded instanceof ArrayBuffer) + decoded = Array.from(new Uint8Array(decoded)); - try { - let encoded = wota.encode(input); - if (!(encoded instanceof ArrayBuffer)) { - passed = false; - messages.push("wota.encode did not return an ArrayBuffer!"); - } else { - let decoded = wota.decode(encoded); - - let compareResult = deepCompare(input, decoded, ""); - if (!compareResult.passed) { - passed = false; - messages.push(`Roundtrip mismatch for input=${JSON.stringify(input)}`); - messages.push(...compareResult.messages); - } - } - } catch(e) { + const compareResult = deepCompare(expected, decoded); + if (!compareResult.passed) { passed = false; - messages.push(`Exception thrown: ${e}`); + messages.push("Decoding failed:"); + messages.push(...compareResult.messages); + } } + } catch (e) { + passed = false; + messages.push(`Exception thrown: ${e}`); + } - results.push({ testName, passed, messages }); - if (passed) passCount++; + results.push({ testName, passed, messages }); + + if (!passed) { + console.log(`\nDetailed Failure Report for ${testName}:`); + console.log(`Input: ${JSON.stringify(test.input)}`); + if (test.replacer) console.log(`Replacer: ${test.replacer.toString()}`); + if (test.reviver) console.log(`Reviver: ${test.reviver.toString()}`); + console.log(messages.join("\n")); + console.log(""); + } } -// Print a summary -for (let r of results) { - console.log(`${r.testName}: ${r.passed ? "PASS" : "FAIL"}`); - if (!r.passed && r.messages.length > 0) { - for (let m of r.messages) { - console.log(" ", m); - } - } -} +// Summary +console.log("\nTest Summary:"); +results.forEach(result => { + console.log(`${result.testName} - ${result.passed ? "Passed" : "Failed"}`); + if (!result.passed) + console.log(result.messages); +}); -console.log(`\nOverall: ${passCount}/${results.length} tests passed.`); -if (passCount < results.length) { - os.exit(1); +let passedCount = results.filter(r => r.passed).length; +console.log(`\nResult: ${passedCount}/${testCount} tests passed`); + +if (passedCount < testCount) { + console.log("Overall: FAILED"); + os.exit(1); } else { - os.exit(0); + console.log("Overall: PASSED"); + os.exit(0); }