#include "quickjs.h" #define WOTA_IMPLEMENTATION #include "wota.h" typedef struct WotaEncodeContext { JSContext *ctx; JSValue visited_stack; WotaBuffer wb; int cycle; JSValue replacer; } WotaEncodeContext; static void wota_stack_push(WotaEncodeContext *enc, JSValueConst 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->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->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 1; } JS_FreeValue(ctx, elem); } return 0; } static JSValue apply_replacer(WotaEncodeContext *enc, JSValueConst holder, JSValueConst key, JSValueConst val) { 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; } static void wota_encode_value(WotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSValueConst key); 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: { 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, replaced); wota_write_text(&enc->wb, str ? str : ""); JS_FreeCString(ctx, str); break; } case JS_TAG_BOOL: 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); 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; } 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); } wota_stack_pop(enc); 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); break; } JS_FreeValue(ctx, replaced); } static char *decode_wota_value(JSContext *ctx, char *data_ptr, char *end_ptr, JSValue *out_val, JSValue holder, JSValue key, JSValue reviver) { 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); *out_val = JS_NewInt64(ctx, val); break; } case WOTA_FLOAT: { 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) *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); *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); *out_val = JS_NewString(ctx, utf8 ? utf8 : ""); if (utf8) free(utf8); break; } case WOTA_ARR: { 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: { 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: 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_encode(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 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; } static const JSCFunctionListEntry js_wota_funcs[] = { JS_CFUNC_DEF("encode", 2, js_wota_encode), JS_CFUNC_DEF("decode", 2, js_wota_decode), }; 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; } #ifdef JS_SHARED_LIBRARY #define JS_INIT_MODULE js_init_module #else #define JS_INIT_MODULE js_init_module_wota #endif 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; } 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; }