add wota replacer and reviver

This commit is contained in:
2025-03-13 15:21:01 -05:00
parent 1332af93ab
commit eb3a41be69
5 changed files with 510 additions and 507 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -1,369 +1,322 @@
//
// 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 */
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));
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));
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);
int len = JS_ArrayLength(ctx, enc->visited_stack);
for (int i = 0; i < len; i++) {
JSValue elem = JS_GetPropertyUint32(ctx, enc->visitedStack, i);
if (JS_IsObject(elem) && JS_IsObject(val)) {
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;
}
/* Forward declaration */
static void wota_encode_value(WotaEncodeContext *enc, JSValueConst val);
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;
}
/* ----------------------------------------------------------------
Encode object properties => Wota record
---------------------------------------------------------------- */
static void encode_object_properties(WotaEncodeContext *enc, JSValueConst val)
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) {
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);
uint32_t non_function_count = 0;
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);
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);
}
/* ----------------------------------------------------------------
Main dispatcher for any JS value => Wota
---------------------------------------------------------------- */
static void wota_encode_value(WotaEncodeContext *enc, JSValueConst val)
static void wota_encode_value(WotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSValueConst key)
{
JSContext *ctx = enc->ctx;
int tag = JS_VALUE_GET_TAG(val);
JSValue replaced = apply_replacer(enc, holder, key, val);
int tag = JS_VALUE_GET_TAG(replaced);
switch (tag) {
case JS_TAG_INT:
{
case JS_TAG_INT: {
double d;
JS_ToFloat64(ctx, &d, val);
JS_ToFloat64(ctx, &d, replaced);
wota_write_number(&enc->wb, d);
return;
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. */
case JS_TAG_BIG_FLOAT: {
double d;
if (JS_ToFloat64(ctx, &d, val) < 0) {
if (JS_ToFloat64(ctx, &d, replaced) < 0) {
wota_write_sym(&enc->wb, WOTA_NULL);
return;
break;
}
wota_write_number(&enc->wb, d);
return;
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);
case JS_TAG_STRING: {
const char *str = JS_ToCString(ctx, replaced);
wota_write_text(&enc->wb, str ? str : "");
JS_FreeCString(ctx, str);
return;
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;
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, val)) {
if (wota_stack_has(enc, val)) {
if (JS_IsArray(ctx, replaced)) {
if (wota_stack_has(enc, replaced)) {
enc->cycle = 1;
return;
break;
}
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_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);
return;
break;
}
/* Otherwise => record */
if (wota_stack_has(enc, val)) {
if (wota_stack_has(enc, replaced)) {
enc->cycle = 1;
return;
break;
}
wota_stack_push(enc, val);
encode_object_properties(enc, val);
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);
return;
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;
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;
*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);
*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);
*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;
}
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);
}
*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, "");
}
*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);
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);
}
*outVal = arr;
*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);
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);
}
}
*outVal = obj;
*out_val = obj;
break;
}
default:
// unknown => skip
data_ptr += 8;
*outVal = JS_UNDEFINED;
*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;
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;
decode_wota_value(ctx, data_ptr, end_ptr, &result);
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),
};
/* 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]));
JS_SetModuleExportList(ctx, m, js_wota_funcs, sizeof(js_wota_funcs)/sizeof(js_wota_funcs[0]));
return 0;
}
@@ -377,17 +330,13 @@ 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]));
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]));
JS_SetPropertyFunctionList(ctx, exports, js_wota_funcs, sizeof(js_wota_funcs)/sizeof(js_wota_funcs[0]));
return exports;
}

View File

@@ -1,201 +1,225 @@
//
// 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');
// A small epsilon for floating comparisons
// 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();
}
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)) {
if (isNaN(expected) && isNaN(actual))
return { passed: true, messages: [] };
}
const diff = Math.abs(expected - actual);
if (diff <= EPSILON) {
if (diff <= EPSILON)
return { passed: true, messages: [] };
}
return {
passed: false,
messages: [
`Number mismatch at ${path}: expected ${expected}, got ${actual}.`,
`Difference ${diff} > epsilon ${EPSILON}.`
`Value mismatch at ${path}: expected ${expected}, got ${actual}`,
`Difference of ${diff} is larger than tolerance ${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);
const expArray = Array.from(new Uint8Array(expected));
const actArray = Array.from(new Uint8Array(actual));
return deepCompare(expArray, actArray, path);
}
// If one is an ArrayBuffer, convert for array comparison
if (actual instanceof ArrayBuffer) {
actual = [...new Uint8Array(actual)];
}
if (actual instanceof ArrayBuffer)
actual = Array.from(new Uint8Array(actual));
// Compare arrays
if (Array.isArray(expected) && Array.isArray(actual)) {
if (expected.length !== actual.length) {
if (expected.length !== actual.length)
return {
passed: false,
messages: [
`Array length mismatch at ${path}: expected ${expected.length}, got ${actual.length}`
]
messages: [`Array length mismatch at ${path}: expected ${expected.length}, got ${actual.length}`]
};
}
let subMessages = [];
let messages = [];
for (let i = 0; i < expected.length; i++) {
let r = deepCompare(expected[i], actual[i], `${path}[${i}]`);
if (!r.passed) subMessages.push(...r.messages);
const result = deepCompare(expected[i], actual[i], `${path}[${i}]`);
if (!result.passed) messages.push(...result.messages);
}
return { passed: subMessages.length === 0, messages: subMessages };
return { passed: messages.length === 0, messages };
}
// 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)) {
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 key mismatch at ${path}:\n expected keys: ${expKeys}\n actual keys: ${actKeys}`
]
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);
}
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 };
return { passed: messages.length === 0, messages };
}
// Fallback: primitive mismatch
return {
passed: false,
messages: [
`Value mismatch at ${path}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`
]
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,
// 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,
// Larger than 56-bit => stored as float
-(2**55) - 1,
(2**55),
// Infinity and NaN
Infinity,
-Infinity,
NaN,
// Booleans
true,
false,
// undefined (WOTA_NULL)
undefined,
// A couple strings
"Hello, Wota!",
"",
"Emoji test: \u{1f600}\u{1f64f}", // 😀🙏
// An ArrayBuffer (binary blob)
new Uint8Array([0xde, 0xad, 0xbe, 0xef]).buffer,
// empty blob
new Uint8Array([]).buffer,
// Arrays
[],
[1,2,3],
[true, false, undefined, "test", -999],
// Nested array
[[[]]],
// 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" },
// 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?" ] ]
}
// 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";
}
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
// Symbol tests
{ input: undefined, expectedHex: "70" },
{ input: false, expectedHex: "72" },
{ input: true, expectedHex: "73" },
// 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
// Text tests
{ input: "", expectedHex: "10" },
{ input: "cat", expectedHex: "13636174" },
{ input: "U+1F4A9 「うんち絵文字」 «💩»",
expectedHex: "9014552b314634413920e00ce046e113e06181fa7581cb0781b657e00d20812b87e929813b" },
// Blob tests
{ input: new Uint8Array([0xFF, 0xAA]).buffer, expectedHex: "8010ffaa" },
{ input: new Uint8Array([0b11110000, 0b11100011, 0b00100000, 0b10000000]).buffer,
expectedHex: "8019f0e32080" },
// Large array test
{ input: testarr, expectedHex: hex },
// Array tests
{ input: [], expectedHex: "20" },
{ input: [1, 2, 3], expectedHex: "23616263" },
{ input: [-1, 0, 1.5], expectedHex: "2369605043" },
// Record tests
{ input: {}, expectedHex: "30" },
{ input: { a: 1, b: 2 }, expectedHex: "32116161116262" },
// Complex nested structures
{ input: {
num: 42,
arr: [1, -1, 2.5],
str: "test",
obj: { x: true }
},
expectedHex: "34216e756d622a2173747214746573742161727223616965235840216f626a21117873" },
// 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' }
];
/* Well just do a round-trip test:
decoded = wota.decode( wota.encode(input) )
and compare decoded vs. original.
*/
// Run tests and collect results
let results = [];
let passCount = 0;
let testCount = 0;
for (let i = 0; i < testCases.length; i++) {
let input = testCases[i];
let testName = `Test ${i+1}: ${JSON.stringify(input)}`;
for (let test of testCases) {
testCount++;
let testName = `Test ${testCount}: ${JSON.stringify(test.input)}${test.testType ? ` (${test.testType})` : ''}`;
let passed = true;
let messages = [];
try {
let encoded = wota.encode(input);
// Test encoding
let encoded = test.replacer ? wota.encode(test.input, test.replacer) : wota.encode(test.input);
if (!(encoded instanceof ArrayBuffer)) {
passed = false;
messages.push("wota.encode did not return an ArrayBuffer!");
messages.push("Encode should return ArrayBuffer");
} else {
let decoded = wota.decode(encoded);
if (test.expectedHex) {
let encodedHex = bufferToHex(encoded);
if (encodedHex !== test.expectedHex.toLowerCase()) {
messages.push(
`Hex encoding differs (informational):
Expected: ${test.expectedHex}
Got: ${encodedHex}`
);
}
}
let compareResult = deepCompare(input, decoded, "");
// Test decoding
let decoded = test.reviver ? wota.decode(encoded, test.reviver) : wota.decode(encoded);
let expected = test.expected || test.input;
// Normalize ArrayBuffer for comparison
if (expected instanceof ArrayBuffer)
expected = Array.from(new Uint8Array(expected));
if (decoded instanceof ArrayBuffer)
decoded = Array.from(new Uint8Array(decoded));
const compareResult = deepCompare(expected, decoded);
if (!compareResult.passed) {
passed = false;
messages.push(`Roundtrip mismatch for input=${JSON.stringify(input)}`);
messages.push("Decoding failed:");
messages.push(...compareResult.messages);
}
}
@@ -205,22 +229,32 @@ for (let i = 0; i < testCases.length; i++) {
}
results.push({ testName, passed, messages });
if (passed) passCount++;
}
// 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);
}
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("");
}
}
console.log(`\nOverall: ${passCount}/${results.length} tests passed.`);
if (passCount < results.length) {
// Summary
console.log("\nTest Summary:");
results.forEach(result => {
console.log(`${result.testName} - ${result.passed ? "Passed" : "Failed"}`);
if (!result.passed)
console.log(result.messages);
});
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 {
console.log("Overall: PASSED");
os.exit(0);
}