add wota replacer and reviver
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
var json = {}
|
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.
|
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.
|
||||||
|
|||||||
@@ -1,5 +1,25 @@
|
|||||||
var nota = this
|
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.
|
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.
|
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.
|
||||||
|
|||||||
@@ -357,4 +357,4 @@ JSModuleDef *JS_INIT_MODULE(JSContext *ctx, const char *module_name) {
|
|||||||
if (!m) return NULL;
|
if (!m) return NULL;
|
||||||
JS_AddModuleExportList(ctx, m, js_nota_funcs, sizeof(js_nota_funcs)/sizeof(JSCFunctionListEntry));
|
JS_AddModuleExportList(ctx, m, js_nota_funcs, sizeof(js_nota_funcs)/sizeof(JSCFunctionListEntry));
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,370 +1,323 @@
|
|||||||
//
|
|
||||||
// qjs_wota.c
|
|
||||||
//
|
|
||||||
#include "quickjs.h"
|
#include "quickjs.h"
|
||||||
|
|
||||||
/* We define WOTA_IMPLEMENTATION so wota.h includes its implementation. */
|
|
||||||
#define WOTA_IMPLEMENTATION
|
#define WOTA_IMPLEMENTATION
|
||||||
#include "wota.h"
|
#include "wota.h"
|
||||||
|
|
||||||
typedef struct WotaEncodeContext {
|
typedef struct WotaEncodeContext {
|
||||||
JSContext *ctx;
|
JSContext *ctx;
|
||||||
JSValue visitedStack; /* array for cycle detection */
|
JSValue visited_stack;
|
||||||
WotaBuffer wb; /* dynamic buffer for building Wota data */
|
WotaBuffer wb;
|
||||||
int cycle;
|
int cycle;
|
||||||
|
JSValue replacer;
|
||||||
} WotaEncodeContext;
|
} WotaEncodeContext;
|
||||||
|
|
||||||
/* ----------------------------------------------------------------
|
|
||||||
CYCLE DETECTION (to avoid infinite recursion on circular objects)
|
|
||||||
---------------------------------------------------------------- */
|
|
||||||
static void wota_stack_push(WotaEncodeContext *enc, JSValueConst val)
|
static void wota_stack_push(WotaEncodeContext *enc, JSValueConst val)
|
||||||
{
|
{
|
||||||
JSContext *ctx = enc->ctx;
|
JSContext *ctx = enc->ctx;
|
||||||
int len = JS_ArrayLength(ctx, enc->visitedStack);
|
int len = JS_ArrayLength(ctx, enc->visited_stack);
|
||||||
JS_SetPropertyInt64(ctx, enc->visitedStack, len, JS_DupValue(ctx, val));
|
JS_SetPropertyInt64(ctx, enc->visited_stack, len, JS_DupValue(ctx, val));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void wota_stack_pop(WotaEncodeContext *enc)
|
static void wota_stack_pop(WotaEncodeContext *enc)
|
||||||
{
|
{
|
||||||
JSContext *ctx = enc->ctx;
|
JSContext *ctx = enc->ctx;
|
||||||
int len = JS_ArrayLength(ctx, enc->visitedStack);
|
int len = JS_ArrayLength(ctx, enc->visited_stack);
|
||||||
JS_SetPropertyStr(ctx, enc->visitedStack, "length", JS_NewUint32(ctx, len - 1));
|
JS_SetPropertyStr(ctx, enc->visited_stack, "length", JS_NewUint32(ctx, len - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
static int wota_stack_has(WotaEncodeContext *enc, JSValueConst val)
|
static int wota_stack_has(WotaEncodeContext *enc, JSValueConst val)
|
||||||
{
|
{
|
||||||
JSContext *ctx = enc->ctx;
|
JSContext *ctx = enc->ctx;
|
||||||
int len = JS_ArrayLength(ctx, enc->visitedStack);
|
int len = JS_ArrayLength(ctx, enc->visited_stack);
|
||||||
for (int i = 0; i < len; i++) {
|
for (int i = 0; i < len; i++) {
|
||||||
JSValue elem = JS_GetPropertyUint32(ctx, enc->visitedStack, i);
|
JSValue elem = JS_GetPropertyUint32(ctx, enc->visited_stack, i);
|
||||||
if (JS_IsObject(elem) && JS_IsObject(val)) {
|
if (JS_IsObject(elem) && JS_IsObject(val))
|
||||||
if (JS_VALUE_GET_OBJ(elem) == JS_VALUE_GET_OBJ(val)) {
|
if (JS_VALUE_GET_OBJ(elem) == JS_VALUE_GET_OBJ(val)) {
|
||||||
JS_FreeValue(ctx, elem);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
JS_FreeValue(ctx, elem);
|
JS_FreeValue(ctx, elem);
|
||||||
}
|
return 1;
|
||||||
return 0;
|
}
|
||||||
|
JS_FreeValue(ctx, elem);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Forward declaration */
|
static JSValue apply_replacer(WotaEncodeContext *enc, JSValueConst holder, JSValueConst key, JSValueConst val)
|
||||||
static void wota_encode_value(WotaEncodeContext *enc, JSValueConst val);
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------
|
|
||||||
Encode object properties => Wota record
|
|
||||||
---------------------------------------------------------------- */
|
|
||||||
static void encode_object_properties(WotaEncodeContext *enc, JSValueConst val)
|
|
||||||
{
|
{
|
||||||
JSContext *ctx = enc->ctx;
|
if (JS_IsUndefined(enc->replacer)) return JS_DupValue(enc->ctx, val);
|
||||||
|
JSValue args[2] = { JS_DupValue(enc->ctx, key), JS_DupValue(enc->ctx, val) };
|
||||||
JSPropertyEnum *ptab;
|
JSValue result = JS_Call(enc->ctx, enc->replacer, holder, 2, args);
|
||||||
uint32_t plen;
|
JS_FreeValue(enc->ctx, args[0]);
|
||||||
if (JS_GetOwnPropertyNames(ctx, &ptab, &plen, val,
|
JS_FreeValue(enc->ctx, args[1]);
|
||||||
JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK) < 0) {
|
if (JS_IsException(result)) return JS_DupValue(enc->ctx, val);
|
||||||
wota_write_sym(&enc->wb, WOTA_NULL);
|
return result;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------------------
|
static void wota_encode_value(WotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSValueConst key);
|
||||||
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);
|
|
||||||
|
|
||||||
switch(tag) {
|
static void encode_object_properties(WotaEncodeContext *enc, JSValueConst val, JSValueConst holder)
|
||||||
case JS_TAG_INT:
|
{
|
||||||
{
|
JSContext *ctx = enc->ctx;
|
||||||
double d;
|
JSPropertyEnum *ptab;
|
||||||
JS_ToFloat64(ctx, &d, val);
|
uint32_t plen;
|
||||||
wota_write_number(&enc->wb, d);
|
if (JS_GetOwnPropertyNames(ctx, &ptab, &plen, val, JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK) < 0) {
|
||||||
return;
|
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_FLOAT64:
|
||||||
case JS_TAG_BIG_INT:
|
case JS_TAG_BIG_INT:
|
||||||
case JS_TAG_BIG_DECIMAL:
|
case JS_TAG_BIG_DECIMAL:
|
||||||
case JS_TAG_BIG_FLOAT:
|
case JS_TAG_BIG_FLOAT: {
|
||||||
{
|
double d;
|
||||||
/* Convert to double if possible. If it's out of double range,
|
if (JS_ToFloat64(ctx, &d, replaced) < 0) {
|
||||||
you might need a fallback. QuickJS can handle bigint, etc.
|
wota_write_sym(&enc->wb, WOTA_NULL);
|
||||||
But let's just do double. */
|
break;
|
||||||
double d;
|
}
|
||||||
if (JS_ToFloat64(ctx, &d, val) < 0) {
|
wota_write_number(&enc->wb, d);
|
||||||
wota_write_sym(&enc->wb, WOTA_NULL);
|
break;
|
||||||
return;
|
|
||||||
}
|
|
||||||
wota_write_number(&enc->wb, d);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
case JS_TAG_STRING:
|
case JS_TAG_STRING: {
|
||||||
{
|
const char *str = JS_ToCString(ctx, replaced);
|
||||||
const char *str = JS_ToCString(ctx, val);
|
wota_write_text(&enc->wb, str ? str : "");
|
||||||
if (!str) {
|
JS_FreeCString(ctx, str);
|
||||||
wota_write_text(&enc->wb, "");
|
break;
|
||||||
return;
|
|
||||||
}
|
|
||||||
wota_write_text(&enc->wb, str);
|
|
||||||
JS_FreeCString(ctx, str);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
case JS_TAG_BOOL:
|
case JS_TAG_BOOL:
|
||||||
{
|
wota_write_sym(&enc->wb, JS_VALUE_GET_BOOL(replaced) ? WOTA_TRUE : WOTA_FALSE);
|
||||||
if (JS_VALUE_GET_BOOL(val)) {
|
break;
|
||||||
wota_write_sym(&enc->wb, WOTA_TRUE);
|
|
||||||
} else {
|
|
||||||
wota_write_sym(&enc->wb, WOTA_FALSE);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case JS_TAG_NULL:
|
case JS_TAG_NULL:
|
||||||
case JS_TAG_UNDEFINED:
|
case JS_TAG_UNDEFINED:
|
||||||
wota_write_sym(&enc->wb, WOTA_NULL);
|
wota_write_sym(&enc->wb, WOTA_NULL);
|
||||||
return;
|
break;
|
||||||
|
case JS_TAG_OBJECT: {
|
||||||
case JS_TAG_OBJECT:
|
if (JS_IsArrayBuffer(ctx, replaced)) {
|
||||||
{
|
size_t buf_len;
|
||||||
/* Check if it's an ArrayBuffer => blob */
|
void *buf_data = JS_GetArrayBuffer(ctx, &buf_len, replaced);
|
||||||
if (JS_IsArrayBuffer(ctx, val)) {
|
wota_write_blob(&enc->wb, (unsigned long long)buf_len * 8, (const char *)buf_data);
|
||||||
size_t bufLen;
|
break;
|
||||||
void *bufData = JS_GetArrayBuffer(ctx, &bufLen, val);
|
}
|
||||||
wota_write_blob(&enc->wb, (unsigned long long)bufLen * 8,
|
if (JS_IsArray(ctx, replaced)) {
|
||||||
(const char *)bufData);
|
if (wota_stack_has(enc, replaced)) {
|
||||||
return;
|
enc->cycle = 1;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
wota_stack_push(enc, replaced);
|
||||||
if (JS_IsArray(ctx, val)) {
|
int arr_len = JS_ArrayLength(ctx, replaced);
|
||||||
if (wota_stack_has(enc, val)) {
|
wota_write_array(&enc->wb, arr_len);
|
||||||
enc->cycle = 1;
|
for (int i = 0; i < arr_len; i++) {
|
||||||
return;
|
JSValue elem_val = JS_GetPropertyUint32(ctx, replaced, i);
|
||||||
}
|
JSValue elem_key = JS_NewInt32(ctx, i);
|
||||||
wota_stack_push(enc, val);
|
wota_encode_value(enc, elem_val, replaced, elem_key);
|
||||||
|
JS_FreeValue(ctx, elem_val);
|
||||||
int arrLen = JS_ArrayLength(ctx, val);
|
JS_FreeValue(ctx, elem_key);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 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);
|
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:
|
default:
|
||||||
wota_write_sym(&enc->wb, WOTA_NULL);
|
wota_write_sym(&enc->wb, WOTA_NULL);
|
||||||
return;
|
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)
|
||||||
Public JS function: wota.encode(value) => ArrayBuffer
|
|
||||||
---------------------------------------------------------------- */
|
|
||||||
static JSValue js_wota_encode(JSContext *ctx, JSValueConst this_val,
|
|
||||||
int argc, JSValueConst *argv)
|
|
||||||
{
|
{
|
||||||
if (argc < 1) {
|
if ((end_ptr - data_ptr) < 8) {
|
||||||
return JS_ThrowTypeError(ctx, "wota.encode requires 1 argument");
|
*out_val = JS_UNDEFINED;
|
||||||
}
|
return data_ptr;
|
||||||
WotaEncodeContext enc_s, *enc = &enc_s;
|
}
|
||||||
enc->ctx = ctx;
|
uint64_t first_word = *(uint64_t *)data_ptr;
|
||||||
enc->visitedStack = JS_NewArray(ctx);
|
int type = (int)(first_word & 0xffU);
|
||||||
enc->cycle = 0;
|
switch (type) {
|
||||||
|
|
||||||
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) {
|
|
||||||
case WOTA_INT: {
|
case WOTA_INT: {
|
||||||
long long val;
|
long long val;
|
||||||
data_ptr = wota_read_int(&val, data_ptr);
|
data_ptr = wota_read_int(&val, data_ptr);
|
||||||
*outVal = JS_NewInt64(ctx, val);
|
*out_val = JS_NewInt64(ctx, val);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case WOTA_FLOAT: {
|
case WOTA_FLOAT: {
|
||||||
double d;
|
double d;
|
||||||
data_ptr = wota_read_float(&d, data_ptr);
|
data_ptr = wota_read_float(&d, data_ptr);
|
||||||
*outVal = JS_NewFloat64(ctx, d);
|
*out_val = JS_NewFloat64(ctx, d);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case WOTA_SYM: {
|
case WOTA_SYM: {
|
||||||
int scode;
|
int scode;
|
||||||
data_ptr = wota_read_sym(&scode, data_ptr);
|
data_ptr = wota_read_sym(&scode, data_ptr);
|
||||||
if (scode == WOTA_NULL) {
|
if (scode == WOTA_NULL) *out_val = JS_UNDEFINED;
|
||||||
*outVal = JS_UNDEFINED;
|
else if (scode == WOTA_FALSE) *out_val = JS_NewBool(ctx, 0);
|
||||||
} else if (scode == WOTA_FALSE) {
|
else if (scode == WOTA_TRUE) *out_val = JS_NewBool(ctx, 1);
|
||||||
*outVal = JS_NewBool(ctx, 0);
|
else *out_val = JS_UNDEFINED;
|
||||||
} else if (scode == WOTA_TRUE) {
|
break;
|
||||||
*outVal = JS_NewBool(ctx, 1);
|
|
||||||
} else {
|
|
||||||
// other symbol codes => undefined or handle them
|
|
||||||
*outVal = JS_UNDEFINED;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case WOTA_BLOB: {
|
case WOTA_BLOB: {
|
||||||
long long blen;
|
long long blen;
|
||||||
char *bdata = NULL;
|
char *bdata = NULL;
|
||||||
data_ptr = wota_read_blob(&blen, &bdata, data_ptr);
|
data_ptr = wota_read_blob(&blen, &bdata, data_ptr);
|
||||||
if (bdata) {
|
*out_val = bdata ? JS_NewArrayBufferCopy(ctx, (uint8_t *)bdata, (size_t)blen) : JS_NewArrayBufferCopy(ctx, NULL, 0);
|
||||||
*outVal = JS_NewArrayBufferCopy(ctx, (uint8_t*)bdata, (size_t)blen);
|
if (bdata) free(bdata);
|
||||||
free(bdata);
|
break;
|
||||||
} else {
|
|
||||||
*outVal = JS_NewArrayBufferCopy(ctx, NULL, 0);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case WOTA_TEXT: {
|
case WOTA_TEXT: {
|
||||||
char *utf8 = NULL;
|
char *utf8 = NULL;
|
||||||
data_ptr = wota_read_text(&utf8, data_ptr);
|
data_ptr = wota_read_text(&utf8, data_ptr);
|
||||||
if (utf8) {
|
*out_val = JS_NewString(ctx, utf8 ? utf8 : "");
|
||||||
*outVal = JS_NewString(ctx, utf8);
|
if (utf8) free(utf8);
|
||||||
free(utf8);
|
break;
|
||||||
} else {
|
|
||||||
*outVal = JS_NewString(ctx, "");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case WOTA_ARR: {
|
case WOTA_ARR: {
|
||||||
// Recursively decode the array
|
long long c;
|
||||||
long long c;
|
data_ptr = wota_read_array(&c, data_ptr);
|
||||||
data_ptr = wota_read_array(&c, data_ptr);
|
JSValue arr = JS_NewArray(ctx);
|
||||||
JSValue arr = JS_NewArray(ctx);
|
for (long long i = 0; i < c; i++) {
|
||||||
for (long long i = 0; i < c; i++) {
|
JSValue elem_val = JS_UNDEFINED;
|
||||||
JSValue elemVal = JS_UNDEFINED;
|
data_ptr = decode_wota_value(ctx, data_ptr, end_ptr, &elem_val, arr, JS_NewInt32(ctx, i), reviver);
|
||||||
data_ptr = decode_wota_value(ctx, data_ptr, end_ptr, &elemVal);
|
JS_SetPropertyUint32(ctx, arr, i, elem_val);
|
||||||
JS_SetPropertyUint32(ctx, arr, i, elemVal);
|
}
|
||||||
}
|
*out_val = arr;
|
||||||
*outVal = arr;
|
break;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case WOTA_REC: {
|
case WOTA_REC: {
|
||||||
// Recursively decode the record
|
long long c;
|
||||||
long long c;
|
data_ptr = wota_read_record(&c, data_ptr);
|
||||||
data_ptr = wota_read_record(&c, data_ptr);
|
JSValue obj = JS_NewObject(ctx);
|
||||||
JSValue obj = JS_NewObject(ctx);
|
for (long long i = 0; i < c; i++) {
|
||||||
for (long long i = 0; i < c; i++) {
|
char *tkey = NULL;
|
||||||
// read the key
|
data_ptr = wota_read_text(&tkey, data_ptr);
|
||||||
char *tkey = NULL;
|
JSValue prop_key = tkey ? JS_NewString(ctx, tkey) : JS_UNDEFINED;
|
||||||
data_ptr = wota_read_text(&tkey, data_ptr);
|
JSValue sub_val = JS_UNDEFINED;
|
||||||
// read the value
|
data_ptr = decode_wota_value(ctx, data_ptr, end_ptr, &sub_val, obj, prop_key, reviver);
|
||||||
JSValue subVal = JS_UNDEFINED;
|
if (tkey) JS_SetPropertyStr(ctx, obj, tkey, sub_val);
|
||||||
data_ptr = decode_wota_value(ctx, data_ptr, end_ptr, &subVal);
|
else JS_FreeValue(ctx, sub_val);
|
||||||
|
JS_FreeValue(ctx, prop_key);
|
||||||
if (tkey) {
|
if (tkey) free(tkey);
|
||||||
JS_SetPropertyStr(ctx, obj, tkey, subVal);
|
}
|
||||||
free(tkey);
|
*out_val = obj;
|
||||||
} else {
|
break;
|
||||||
JS_FreeValue(ctx, subVal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*outVal = obj;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
// unknown => skip
|
data_ptr += 8;
|
||||||
data_ptr += 8;
|
*out_val = JS_UNDEFINED;
|
||||||
*outVal = JS_UNDEFINED;
|
break;
|
||||||
break;
|
}
|
||||||
}
|
if (!JS_IsUndefined(reviver)) {
|
||||||
|
JSValue args[2] = { JS_DupValue(ctx, key), JS_DupValue(ctx, *out_val) };
|
||||||
return data_ptr;
|
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,
|
static JSValue js_wota_encode(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
|
||||||
int argc, JSValueConst *argv)
|
|
||||||
{
|
{
|
||||||
if (argc < 1) return JS_UNDEFINED;
|
if (argc < 1) return JS_ThrowTypeError(ctx, "wota.encode requires at least 1 argument");
|
||||||
|
WotaEncodeContext enc_s, *enc = &enc_s;
|
||||||
size_t len;
|
enc->ctx = ctx;
|
||||||
uint8_t *buf = JS_GetArrayBuffer(ctx, &len, argv[0]);
|
enc->visited_stack = JS_NewArray(ctx);
|
||||||
if (!buf) {
|
enc->cycle = 0;
|
||||||
return JS_UNDEFINED;
|
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, ""));
|
||||||
char *data_ptr = (char *)buf;
|
if (enc->cycle) {
|
||||||
char *end_ptr = data_ptr + len;
|
JS_FreeValue(ctx, enc->visited_stack);
|
||||||
|
wota_buffer_free(&enc->wb);
|
||||||
JSValue result = JS_UNDEFINED;
|
return JS_ThrowReferenceError(ctx, "Cannot encode cyclic object with wota");
|
||||||
decode_wota_value(ctx, data_ptr, end_ptr, &result);
|
}
|
||||||
return result;
|
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[] = {
|
static const JSCFunctionListEntry js_wota_funcs[] = {
|
||||||
JS_CFUNC_DEF("encode", 1, js_wota_encode),
|
JS_CFUNC_DEF("encode", 1, js_wota_encode),
|
||||||
JS_CFUNC_DEF("decode", 1, js_wota_decode),
|
JS_CFUNC_DEF("decode", 1, js_wota_decode),
|
||||||
};
|
};
|
||||||
|
|
||||||
/* For module usage */
|
|
||||||
static int js_wota_init(JSContext *ctx, JSModuleDef *m)
|
static int js_wota_init(JSContext *ctx, JSModuleDef *m)
|
||||||
{
|
{
|
||||||
JS_SetModuleExportList(ctx, m, js_wota_funcs,
|
JS_SetModuleExportList(ctx, m, js_wota_funcs, sizeof(js_wota_funcs)/sizeof(js_wota_funcs[0]));
|
||||||
sizeof(js_wota_funcs)/sizeof(js_wota_funcs[0]));
|
return 0;
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef JS_SHARED_LIBRARY
|
#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 *JS_INIT_MODULE(JSContext *ctx, const char *module_name)
|
||||||
{
|
{
|
||||||
JSModuleDef *m = JS_NewCModule(ctx, module_name, js_wota_init);
|
JSModuleDef *m = JS_NewCModule(ctx, module_name, js_wota_init);
|
||||||
if (!m) return NULL;
|
if (!m) return NULL;
|
||||||
JS_AddModuleExportList(ctx, m, js_wota_funcs,
|
JS_AddModuleExportList(ctx, m, js_wota_funcs, sizeof(js_wota_funcs)/sizeof(js_wota_funcs[0]));
|
||||||
sizeof(js_wota_funcs)/sizeof(js_wota_funcs[0]));
|
return m;
|
||||||
return m;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* An optional helper function if you're using it outside the module system */
|
|
||||||
JSValue js_wota_use(JSContext *ctx)
|
JSValue js_wota_use(JSContext *ctx)
|
||||||
{
|
{
|
||||||
JSValue exports = JS_NewObject(ctx);
|
JSValue exports = JS_NewObject(ctx);
|
||||||
JS_SetPropertyFunctionList(ctx, exports,
|
JS_SetPropertyFunctionList(ctx, exports, js_wota_funcs, sizeof(js_wota_funcs)/sizeof(js_wota_funcs[0]));
|
||||||
js_wota_funcs,
|
return exports;
|
||||||
sizeof(js_wota_funcs)/sizeof(js_wota_funcs[0]));
|
|
||||||
return exports;
|
|
||||||
}
|
}
|
||||||
|
|||||||
420
tests/wota.js
420
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 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;
|
var EPSILON = 1e-12;
|
||||||
|
|
||||||
/* Deep comparison function (for JS values).
|
// Deep comparison function for objects and arrays
|
||||||
Compares numbers within EPSILON, compares arrays and objects recursively, etc. */
|
|
||||||
function deepCompare(expected, actual, path = '') {
|
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') {
|
||||||
if (typeof expected === 'number' && typeof actual === 'number') {
|
if (isNaN(expected) && isNaN(actual))
|
||||||
// Handle NaN:
|
return { passed: true, messages: [] };
|
||||||
if (isNaN(expected) && isNaN(actual)) {
|
const diff = Math.abs(expected - actual);
|
||||||
return { passed: true, messages: [] };
|
if (diff <= EPSILON)
|
||||||
}
|
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
|
|
||||||
return {
|
return {
|
||||||
passed: false,
|
passed: false,
|
||||||
messages: [
|
messages: [
|
||||||
`Value mismatch at ${path}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`
|
`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 = [
|
// Test cases covering Wota types and replacer/reviver functionality
|
||||||
// Basic numbers
|
var testarr = [];
|
||||||
0,
|
var hex = "a374";
|
||||||
1,
|
for (var i = 0; i < 500; i++) {
|
||||||
-1,
|
testarr.push(1);
|
||||||
2023,
|
hex += "61";
|
||||||
1e46,
|
}
|
||||||
2.5e120,
|
|
||||||
2e120,
|
|
||||||
4.3e-7,
|
|
||||||
42,
|
|
||||||
1.5,
|
|
||||||
-0.123456,
|
|
||||||
|
|
||||||
// Integer boundaries (56-bit limit in Wota spec)
|
var testCases = [
|
||||||
// Wota can store -2^55 through 2^55 - 1 as an INT
|
// Integer tests (WOTA_INT up to 56-bit)
|
||||||
-(2**55),
|
{ input: 0, expectedHex: "60" },
|
||||||
(2**55) - 1,
|
{ 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
|
// Symbol tests
|
||||||
-(2**55) - 1,
|
{ input: undefined, expectedHex: "70" },
|
||||||
(2**55),
|
{ input: false, expectedHex: "72" },
|
||||||
|
{ input: true, expectedHex: "73" },
|
||||||
|
|
||||||
// Infinity and NaN
|
// Floating Point tests (WOTA_FLOAT)
|
||||||
Infinity,
|
{ input: -1.01, expectedHex: "5a65" },
|
||||||
-Infinity,
|
{ input: 98.6, expectedHex: "51875a" },
|
||||||
NaN,
|
{ 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
|
// Text tests
|
||||||
true,
|
{ input: "", expectedHex: "10" },
|
||||||
false,
|
{ input: "cat", expectedHex: "13636174" },
|
||||||
|
{ input: "U+1F4A9 「うんち絵文字」 «💩»",
|
||||||
|
expectedHex: "9014552b314634413920e00ce046e113e06181fa7581cb0781b657e00d20812b87e929813b" },
|
||||||
|
|
||||||
// undefined (WOTA_NULL)
|
// Blob tests
|
||||||
undefined,
|
{ input: new Uint8Array([0xFF, 0xAA]).buffer, expectedHex: "8010ffaa" },
|
||||||
|
{ input: new Uint8Array([0b11110000, 0b11100011, 0b00100000, 0b10000000]).buffer,
|
||||||
|
expectedHex: "8019f0e32080" },
|
||||||
|
|
||||||
// A couple strings
|
// Large array test
|
||||||
"Hello, Wota!",
|
{ input: testarr, expectedHex: hex },
|
||||||
"",
|
|
||||||
"Emoji test: \u{1f600}\u{1f64f}", // 😀🙏
|
|
||||||
|
|
||||||
// An ArrayBuffer (binary blob)
|
// Array tests
|
||||||
new Uint8Array([0xde, 0xad, 0xbe, 0xef]).buffer,
|
{ input: [], expectedHex: "20" },
|
||||||
// empty blob
|
{ input: [1, 2, 3], expectedHex: "23616263" },
|
||||||
new Uint8Array([]).buffer,
|
{ input: [-1, 0, 1.5], expectedHex: "2369605043" },
|
||||||
|
|
||||||
// Arrays
|
// Record tests
|
||||||
[],
|
{ input: {}, expectedHex: "30" },
|
||||||
[1,2,3],
|
{ input: { a: 1, b: 2 }, expectedHex: "32116161116262" },
|
||||||
[true, false, undefined, "test", -999],
|
|
||||||
// Nested array
|
|
||||||
[[[]]],
|
|
||||||
|
|
||||||
// Objects / Records
|
// Complex nested structures
|
||||||
{},
|
{ input: {
|
||||||
{a:1, b:2.2, c:"3", d:false},
|
num: 42,
|
||||||
{ nested: { arr: [1, { x: "y" }] } },
|
arr: [1, -1, 2.5],
|
||||||
// Symbol-like keys in JS (just unusual keys)
|
str: "test",
|
||||||
{ "": 123, "foo": "bar" },
|
obj: { x: true }
|
||||||
|
},
|
||||||
|
expectedHex: "34216e756d622a2173747214746573742161727223616965235840216f626a21117873" },
|
||||||
|
|
||||||
// Larger array length test (not extreme, just a demonstration)
|
// Additional edge cases
|
||||||
Array.from({length: 10}, (_, i) => i),
|
{ input: new Uint8Array([]).buffer, expectedHex: "00" },
|
||||||
// Some deeper nesting
|
{ input: [[]], expectedHex: "2120" },
|
||||||
{
|
{ input: { "": "" }, expectedHex: "311010" },
|
||||||
deep: {
|
{ input: 1e-10, expectedHex: "d00a01" },
|
||||||
deeper: {
|
|
||||||
arr: [ { level: "three" }, [ "and four?" ] ]
|
// 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:
|
// Test decoding
|
||||||
decoded = wota.decode( wota.encode(input) )
|
let decoded = test.reviver ? wota.decode(encoded, test.reviver) : wota.decode(encoded);
|
||||||
and compare decoded vs. original.
|
let expected = test.expected || test.input;
|
||||||
*/
|
|
||||||
let results = [];
|
|
||||||
let passCount = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < testCases.length; i++) {
|
// Normalize ArrayBuffer for comparison
|
||||||
let input = testCases[i];
|
if (expected instanceof ArrayBuffer)
|
||||||
let testName = `Test ${i+1}: ${JSON.stringify(input)}`;
|
expected = Array.from(new Uint8Array(expected));
|
||||||
let passed = true;
|
if (decoded instanceof ArrayBuffer)
|
||||||
let messages = [];
|
decoded = Array.from(new Uint8Array(decoded));
|
||||||
|
|
||||||
try {
|
const compareResult = deepCompare(expected, decoded);
|
||||||
let encoded = wota.encode(input);
|
if (!compareResult.passed) {
|
||||||
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) {
|
|
||||||
passed = false;
|
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 });
|
results.push({ testName, passed, messages });
|
||||||
if (passed) passCount++;
|
|
||||||
|
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
|
// Summary
|
||||||
for (let r of results) {
|
console.log("\nTest Summary:");
|
||||||
console.log(`${r.testName}: ${r.passed ? "PASS" : "FAIL"}`);
|
results.forEach(result => {
|
||||||
if (!r.passed && r.messages.length > 0) {
|
console.log(`${result.testName} - ${result.passed ? "Passed" : "Failed"}`);
|
||||||
for (let m of r.messages) {
|
if (!result.passed)
|
||||||
console.log(" ", m);
|
console.log(result.messages);
|
||||||
}
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\nOverall: ${passCount}/${results.length} tests passed.`);
|
let passedCount = results.filter(r => r.passed).length;
|
||||||
if (passCount < results.length) {
|
console.log(`\nResult: ${passedCount}/${testCount} tests passed`);
|
||||||
os.exit(1);
|
|
||||||
|
if (passedCount < testCount) {
|
||||||
|
console.log("Overall: FAILED");
|
||||||
|
os.exit(1);
|
||||||
} else {
|
} else {
|
||||||
os.exit(0);
|
console.log("Overall: PASSED");
|
||||||
|
os.exit(0);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user