diff --git a/source/qjs_nota.c b/source/qjs_nota.c index a212e21c..c6935b9e 100755 --- a/source/qjs_nota.c +++ b/source/qjs_nota.c @@ -5,329 +5,345 @@ #include "nota.h" typedef struct NotaEncodeContext { - JSContext *ctx; - JSValue visitedStack; - NotaBuffer nb; // use the dynamic NotaBuffer - int cycle; + JSContext *ctx; + JSValue visitedStack; + NotaBuffer nb; + int cycle; + JSValue replacer; } NotaEncodeContext; static void nota_stack_push(NotaEncodeContext *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->visitedStack); + JS_SetPropertyInt64(ctx, enc->visitedStack, len, JS_DupValue(ctx, val)); } static void nota_stack_pop(NotaEncodeContext *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->visitedStack); + JS_SetPropertyStr(ctx, enc->visitedStack, "length", JS_NewUint32(ctx, len - 1)); } static int nota_stack_has(NotaEncodeContext *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->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; + } } - return 0; + JS_FreeValue(ctx, elem); + } + return 0; +} + +static JSValue apply_replacer(NotaEncodeContext *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; } JSValue number; -char *js_do_nota_decode(JSContext *js, JSValue *tmp, char *nota) -{ - int type = nota_type(nota); - JSValue ret2; - long long n; - double d; - int b; - char *str; - uint8_t *blob; +char *js_do_nota_decode(JSContext *js, JSValue *tmp, char *nota, JSValue holder, JSValue key, JSValue reviver) { + int type = nota_type(nota); + JSValue ret2; + long long n; + double d; + int b; + char *str; + uint8_t *blob; - switch(type) { - case NOTA_BLOB: - nota = nota_read_blob(&n, (char**)&blob, nota); - *tmp = JS_NewArrayBufferCopy(js, blob, n); - free(blob); - break; - case NOTA_TEXT: + switch(type) { + case NOTA_BLOB: + nota = nota_read_blob(&n, (char**)&blob, nota); + *tmp = JS_NewArrayBufferCopy(js, blob, n); + free(blob); + break; + case NOTA_TEXT: + nota = nota_read_text(&str, nota); + *tmp = JS_NewString(js, str); + free(str); + break; + case NOTA_ARR: + nota = nota_read_array(&n, nota); + *tmp = JS_NewArray(js); + for (int i = 0; i < n; i++) { + nota = js_do_nota_decode(js, &ret2, nota, *tmp, JS_NewInt32(js, i), reviver); + JS_SetPropertyInt64(js, *tmp, i, ret2); + } + break; + case NOTA_REC: + nota = nota_read_record(&n, nota); + *tmp = JS_NewObject(js); + for (int i = 0; i < n; i++) { nota = nota_read_text(&str, nota); - *tmp = JS_NewString(js, str); + JSValue prop_key = JS_NewString(js, str); + nota = js_do_nota_decode(js, &ret2, nota, *tmp, prop_key, reviver); + JS_SetPropertyStr(js, *tmp, str, ret2); + JS_FreeValue(js, prop_key); free(str); - break; - case NOTA_ARR: - nota = nota_read_array(&n, nota); - *tmp = JS_NewArray(js); - for (int i = 0; i < n; i++) { - nota = js_do_nota_decode(js, &ret2, nota); - JS_SetPropertyInt64(js, *tmp, i, ret2); - } - break; - case NOTA_REC: - nota = nota_read_record(&n, nota); - *tmp = JS_NewObject(js); - for (int i = 0; i < n; i++) { - nota = nota_read_text(&str, nota); - nota = js_do_nota_decode(js, &ret2, nota); - JS_SetPropertyStr(js, *tmp, str, ret2); - free(str); - } - break; - case NOTA_INT: - nota = nota_read_int(&n, nota); - *tmp = JS_NewInt64(js,n); - break; - case NOTA_SYM: - nota = nota_read_sym(&b, nota); - switch(b) { - case NOTA_NULL: - *tmp = JS_UNDEFINED; - break; - case NOTA_FALSE: - *tmp = JS_NewBool(js,0); - break; - case NOTA_TRUE: - *tmp = JS_NewBool(js,1); - break; - } - break; - default: - case NOTA_FLOAT: - nota = nota_read_float(&d, nota); - *tmp = JS_NewFloat64(js,d); - break; - } + } + break; + case NOTA_INT: + nota = nota_read_int(&n, nota); + *tmp = JS_NewInt64(js, n); + break; + case NOTA_SYM: + nota = nota_read_sym(&b, nota); + switch(b) { + case NOTA_NULL: *tmp = JS_UNDEFINED; break; + case NOTA_FALSE: *tmp = JS_NewBool(js, 0); break; + case NOTA_TRUE: *tmp = JS_NewBool(js, 1); break; + } + break; + default: + case NOTA_FLOAT: + nota = nota_read_float(&d, nota); + *tmp = JS_NewFloat64(js, d); + break; + } - return nota; + if (!JS_IsUndefined(reviver)) { + JSValue args[2] = { JS_DupValue(js, key), JS_DupValue(js, *tmp) }; + JSValue revived = JS_Call(js, reviver, holder, 2, args); + JS_FreeValue(js, args[0]); + JS_FreeValue(js, args[1]); + if (!JS_IsException(revived)) { + JS_FreeValue(js, *tmp); + *tmp = revived; + } else { + JS_FreeValue(js, revived); + } + } + + return nota; } -static void nota_encode_value(NotaEncodeContext *enc, JSValueConst val); +static void nota_encode_value(NotaEncodeContext *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); -static void encode_object_properties(NotaEncodeContext *enc, JSValueConst val) -{ - JSContext *ctx = enc->ctx; + switch (tag) { + case JS_TAG_INT: { + double d; + JS_ToFloat64(ctx, &d, replaced); + nota_write_number(&enc->nb, d); + break; + } + case JS_TAG_BIG_INT: + case JS_TAG_FLOAT64: + case JS_TAG_BIG_DECIMAL: + case JS_TAG_BIG_FLOAT: { + const char *str = JS_ToCString(ctx, replaced); + nota_write_number_str(&enc->nb, str); + JS_FreeCString(ctx, str); + break; + } + case JS_TAG_STRING: { + const char *str = JS_ToCString(ctx, replaced); + nota_write_text(&enc->nb, str); + JS_FreeCString(ctx, str); + break; + } + case JS_TAG_BOOL: + if (JS_VALUE_GET_BOOL(replaced)) nota_write_sym(&enc->nb, NOTA_TRUE); + else nota_write_sym(&enc->nb, NOTA_FALSE); + break; + case JS_TAG_NULL: + case JS_TAG_UNDEFINED: + nota_write_sym(&enc->nb, NOTA_NULL); + break; + case JS_TAG_OBJECT: { + if (JS_IsArrayBuffer(ctx, replaced)) { + size_t buf_len; + void *buf_data = JS_GetArrayBuffer(ctx, &buf_len, replaced); + nota_write_blob(&enc->nb, (unsigned long long)buf_len * 8, (const char*)buf_data); + break; + } - JSPropertyEnum *ptab; - uint32_t plen; - if (JS_GetOwnPropertyNames(ctx, &ptab, &plen, val, JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK) < 0) { + if (JS_IsArray(ctx, replaced)) { + if (nota_stack_has(enc, replaced)) { + enc->cycle = 1; + break; + } + nota_stack_push(enc, replaced); + int arr_len = JS_ArrayLength(ctx, replaced); + nota_write_array(&enc->nb, 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); + nota_encode_value(enc, elem_val, replaced, elem_key); + JS_FreeValue(ctx, elem_val); + JS_FreeValue(ctx, elem_key); + } + nota_stack_pop(enc); + break; + } + + if (nota_stack_has(enc, replaced)) { + enc->cycle = 1; + break; + } + nota_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)) { + nota_encode_value(enc, result, holder, key); + JS_FreeValue(ctx, result); + } else { + nota_write_sym(&enc->nb, NOTA_NULL); + } + nota_stack_pop(enc); + break; + } + JS_FreeValue(ctx, to_json); + + JSPropertyEnum *ptab; + uint32_t plen; + if (JS_GetOwnPropertyNames(ctx, &ptab, &plen, replaced, JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK) < 0) { nota_write_sym(&enc->nb, NOTA_NULL); - return; - } + nota_stack_pop(enc); + break; + } - nota_write_record(&enc->nb, plen); + uint32_t non_function_count = 0; + for (uint32_t i = 0; i < plen; i++) { + JSValue prop_val = JS_GetProperty(ctx, replaced, ptab[i].atom); + if (!JS_IsFunction(ctx, prop_val)) non_function_count++; + JS_FreeValue(ctx, prop_val); + } - for (uint32_t i = 0; i < plen; i++) { - // property name - const char *propName = JS_AtomToCString(ctx, ptab[i].atom); - nota_write_text(&enc->nb, propName); - JS_FreeCString(ctx, propName); - - // property value - JSValue propVal = JS_GetProperty(ctx, val, ptab[i].atom); - nota_encode_value(enc, propVal); - JS_FreeValue(ctx, propVal); - - // free the atom + nota_write_record(&enc->nb, non_function_count); + for (uint32_t i = 0; i < plen; i++) { + JSValue prop_val = JS_GetProperty(ctx, replaced, 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); + nota_write_text(&enc->nb, prop_name); + nota_encode_value(enc, prop_val, replaced, 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); + nota_stack_pop(enc); + break; } - js_free(ctx, ptab); + default: + nota_write_sym(&enc->nb, NOTA_NULL); + break; + } + JS_FreeValue(ctx, replaced); } -static void nota_encode_value(NotaEncodeContext *enc, JSValueConst val) -{ - JSContext *ctx = enc->ctx; - int tag = JS_VALUE_GET_TAG(val); +void *value2nota(JSContext *ctx, JSValue v) { + NotaEncodeContext enc_s, *enc = &enc_s; + enc->ctx = ctx; + enc->visitedStack = JS_NewArray(ctx); + enc->cycle = 0; + enc->replacer = JS_UNDEFINED; - switch (tag) { - case JS_TAG_INT: { - double d; - JS_ToFloat64(ctx, &d, val); - nota_write_number(&enc->nb, d); - return; - } - case JS_TAG_BIG_INT: - case JS_TAG_FLOAT64: - case JS_TAG_BIG_DECIMAL: - case JS_TAG_BIG_FLOAT: { - const char *str = JS_ToCString(ctx, val); - nota_write_number_str(&enc->nb, str); - JS_FreeCString(ctx, str); - return; - } - - case JS_TAG_STRING: { - const char *str = JS_ToCString(ctx, val); - nota_write_text(&enc->nb, str); - JS_FreeCString(ctx, str); - return; - } - - case JS_TAG_BOOL: { - if (JS_VALUE_GET_BOOL(val)) - nota_write_sym(&enc->nb, NOTA_TRUE); - else - nota_write_sym(&enc->nb, NOTA_FALSE); - return; - } - - case JS_TAG_NULL: - case JS_TAG_UNDEFINED: - nota_write_sym(&enc->nb, NOTA_NULL); - return; - - case JS_TAG_OBJECT: { - if (JS_IsArrayBuffer(ctx, val)) { - size_t bufLen; - void *bufData = JS_GetArrayBuffer(ctx, &bufLen, val); - /* Write as a blob of bits (bufLen * 8). */ - nota_write_blob(&enc->nb, (unsigned long long)bufLen * 8, (const char*)bufData); - return; - } - - if (JS_IsArray(ctx, val)) { - if (nota_stack_has(enc, val)) { - enc->cycle = 1; - return; // bail out - } - nota_stack_push(enc, val); - - int arrLen = JS_ArrayLength(ctx, val); - nota_write_array(&enc->nb, arrLen); - for (int i = 0; i < arrLen; i++) { - JSValue elemVal = JS_GetPropertyUint32(ctx, val, i); - nota_encode_value(enc, elemVal); - JS_FreeValue(ctx, elemVal); - } - - nota_stack_pop(enc); - return; - } - - if (nota_stack_has(enc, val)) { - enc->cycle = 1; - return; // bail out - } - - nota_stack_push(enc, val); - encode_object_properties(enc, val); - nota_stack_pop(enc); - return; - } - - default: - nota_write_sym(&enc->nb, NOTA_NULL); - return; - } -} - -void *value2nota(JSContext *ctx, JSValue v) -{ - NotaEncodeContext enc_s, *enc = &enc_s; - enc->ctx = ctx; - enc->visitedStack = JS_NewArray(ctx); // empty array initially - enc->cycle = 0; - - nota_buffer_init(&enc->nb, 128); - - nota_encode_value(enc, v); - - if (enc->cycle) { - JS_FreeValue(ctx, enc->visitedStack); - nota_buffer_free(&enc->nb); - return NULL; - } + nota_buffer_init(&enc->nb, 128); + nota_encode_value(enc, v, JS_UNDEFINED, JS_NewString(ctx, "")); + if (enc->cycle) { JS_FreeValue(ctx, enc->visitedStack); - - void* dataPtr = enc->nb.data; // pointer to the raw data - enc->nb.data = NULL; nota_buffer_free(&enc->nb); + return NULL; + } - return dataPtr; + JS_FreeValue(ctx, enc->visitedStack); + void *data_ptr = enc->nb.data; + enc->nb.data = NULL; + nota_buffer_free(&enc->nb); + return data_ptr; } -JSValue nota2value(JSContext *js, char *nota) -{ +JSValue nota2value(JSContext *js, char *nota) { if (!nota) return JS_UNDEFINED; JSValue ret; - js_do_nota_decode(js, &ret, nota); + JSValue holder = JS_NewObject(js); + js_do_nota_decode(js, &ret, nota, holder, JS_NewString(js, ""), JS_UNDEFINED); + JS_FreeValue(js, holder); return ret; } -static JSValue js_nota_encode(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) -{ - if (argc < 1) - return JS_ThrowTypeError(ctx, "nota.encode requires 1 argument"); +static JSValue js_nota_encode(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + if (argc < 1) return JS_ThrowTypeError(ctx, "nota.encode requires at least 1 argument"); - NotaEncodeContext enc_s, *enc = &enc_s; - enc->ctx = ctx; - enc->visitedStack = JS_NewArray(ctx); // empty array initially - enc->cycle = 0; + NotaEncodeContext enc_s, *enc = &enc_s; + enc->ctx = ctx; + enc->visitedStack = JS_NewArray(ctx); + enc->cycle = 0; + enc->replacer = (argc > 1 && JS_IsFunction(ctx, argv[1])) ? argv[1] : JS_UNDEFINED; - nota_buffer_init(&enc->nb, 128); - - nota_encode_value(enc, argv[0]); - - if (enc->cycle) { - JS_FreeValue(ctx, enc->visitedStack); - nota_buffer_free(&enc->nb); - return JS_ThrowReferenceError(ctx, "Tried to encode something to nota with a cycle."); - } + nota_buffer_init(&enc->nb, 128); + nota_encode_value(enc, argv[0], JS_UNDEFINED, JS_NewString(ctx, "")); + if (enc->cycle) { JS_FreeValue(ctx, enc->visitedStack); - - size_t totalLen = enc->nb.size; // how many bytes used - void* dataPtr = enc->nb.data; // pointer to the raw data - JSValue ret = JS_NewArrayBufferCopy(ctx, (uint8_t*)dataPtr, totalLen); - nota_buffer_free(&enc->nb); + return JS_ThrowReferenceError(ctx, "Tried to encode something to nota with a cycle."); + } - return ret; + JS_FreeValue(ctx, enc->visitedStack); + size_t total_len = enc->nb.size; + void *data_ptr = enc->nb.data; + JSValue ret = JS_NewArrayBufferCopy(ctx, (uint8_t*)data_ptr, total_len); + + nota_buffer_free(&enc->nb); + return ret; } -JSValue js_nota_decode(JSContext *js, JSValue self, int argc, JSValue *argv) -{ - if (argc < 1) return JS_UNDEFINED; +static JSValue js_nota_decode(JSContext *js, JSValueConst self, int argc, JSValueConst *argv) { + if (argc < 1) return JS_UNDEFINED; - size_t len; - unsigned char *nota = JS_GetArrayBuffer(js, &len, argv[0]); - if (!nota) return JS_UNDEFINED; + size_t len; + unsigned char *nota = JS_GetArrayBuffer(js, &len, argv[0]); + if (!nota) return JS_UNDEFINED; - JSValue ret; - js_do_nota_decode(js, &ret, (char*)nota); - return ret; + JSValue reviver = (argc > 1 && JS_IsFunction(js, argv[1])) ? argv[1] : JS_UNDEFINED; + JSValue ret; + JSValue holder = JS_NewObject(js); + js_do_nota_decode(js, &ret, (char*)nota, holder, JS_NewString(js, ""), reviver); + JS_FreeValue(js, holder); + return ret; } static const JSCFunctionListEntry js_nota_funcs[] = { - JS_CFUNC_DEF("encode", 1, js_nota_encode), - JS_CFUNC_DEF("decode", 1, js_nota_decode), + JS_CFUNC_DEF("encode", 1, js_nota_encode), + JS_CFUNC_DEF("decode", 1, js_nota_decode), }; static int js_nota_init(JSContext *ctx, JSModuleDef *m) { - JS_SetModuleExportList(ctx, m, js_nota_funcs, - sizeof(js_nota_funcs)/sizeof(JSCFunctionListEntry)); - return 0; + JS_SetModuleExportList(ctx, m, js_nota_funcs, sizeof(js_nota_funcs)/sizeof(JSCFunctionListEntry)); + return 0; } -JSValue js_nota_use(JSContext *js) -{ - JSValue export = JS_NewObject(js); - JS_SetPropertyFunctionList(js, export, - js_nota_funcs, - sizeof(js_nota_funcs)/sizeof(JSCFunctionListEntry)); - number = JS_GetPropertyStr(js, JS_GetGlobalObject(js), "Number"); - return export; +JSValue js_nota_use(JSContext *js) { + JSValue export = JS_NewObject(js); + JS_SetPropertyFunctionList(js, export, js_nota_funcs, sizeof(js_nota_funcs)/sizeof(JSCFunctionListEntry)); + number = JS_GetPropertyStr(js, JS_GetGlobalObject(js), "Number"); + return export; } #ifdef JS_SHARED_LIBRARY @@ -337,9 +353,8 @@ JSValue js_nota_use(JSContext *js) #endif JSModuleDef *JS_INIT_MODULE(JSContext *ctx, const char *module_name) { - JSModuleDef *m = JS_NewCModule(ctx, module_name, js_nota_init); - if (!m) return NULL; - JS_AddModuleExportList(ctx, m, js_nota_funcs, - sizeof(js_nota_funcs)/sizeof(JSCFunctionListEntry)); - return m; -} + JSModuleDef *m = JS_NewCModule(ctx, module_name, js_nota_init); + 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/tests/nota.js b/tests/nota.js index 98b4bda1..5b7c78de 100644 --- a/tests/nota.js +++ b/tests/nota.js @@ -3,96 +3,87 @@ 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; + 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(); + return Array.from(new Uint8Array(buffer)) + .map(b => b.toString(16).padStart(2, '0')) + .join('') + .toLowerCase(); } var EPSILON = 1e-12 // Deep comparison function for objects and arrays function deepCompare(expected, actual, path = '') { - if (expected === actual) return { passed: true, messages: [] }; - - // If both are numbers, compare with tolerance - if (typeof expected === 'number' && typeof actual === 'number') { - // e.g. handle NaN specially if you like: - if (isNaN(expected) && isNaN(actual)) { - return { passed: true, messages: [] }; - } - - const diff = Math.abs(expected - actual); - // Pass the test if difference is within EPSILON - if (diff <= EPSILON) { - return { passed: true, messages: [] }; - } - - return { - 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 }; - } + if (expected === actual) return { passed: true, messages: [] }; + 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)}`] + }; } // Extended test cases covering all Nota types from documentation @@ -104,79 +95,100 @@ for (var i = 0; i < 500; i++) { } var testCases = [ - // Integer tests - { 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" }, + // Integer tests + { 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" }, - // Symbol tests - { input: undefined, expectedHex: "70" }, - { input: false, expectedHex: "72" }, - { input: true, expectedHex: "73" }, - // Note: private (78) and system (79) require following records, tested below + // Symbol tests + { input: undefined, expectedHex: "70" }, + { input: false, expectedHex: "72" }, + { input: true, expectedHex: "73" }, - // Floating Point tests - { input: -1.01, expectedHex: "5a65" }, - { input: 98.6, expectedHex: "51875a" }, - { input: -0.5772156649, expectedHex: "d80a95c0b0bd69" }, - { input: -1.00000000000001, expectedHex: "d80e96deb183e98001" }, - { input: -10000000000000, expectedHex: "c80d01" }, + // Floating Point tests + { input: -1.01, expectedHex: "5a65" }, + { input: 98.6, expectedHex: "51875a" }, + { input: -0.5772156649, expectedHex: "d80a95c0b0bd69" }, + { input: -1.00000000000001, expectedHex: "d80e96deb183e98001" }, + { input: -10000000000000, expectedHex: "c80d01" }, - // Text tests - { input: "", expectedHex: "10" }, - { input: "cat", expectedHex: "13636174" }, - { input: "U+1F4A9 「うんち絵文字」 «💩»", - expectedHex: "9014552b314634413920e00ce046e113e06181fa7581cb0781b657e00d20812b87e929813b" }, + // 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" }, // 25 bits example padded to 32 bits + // Blob tests + { input: new Uint8Array([0xFF, 0xAA]).buffer, expectedHex: "8010ffaa" }, + { input: new Uint8Array([0b11110000, 0b11100011, 0b00100000, 0b10000000]).buffer, + expectedHex: "8019f0e32080" }, - { input: testarr, expectedHex: hex }, + { input: testarr, expectedHex: hex }, - // Array tests - { input: [], expectedHex: "20" }, - { input: [1, 2, 3], expectedHex: "23616263" }, - { input: [-1, 0, 1.5], expectedHex: "2369605043" }, + // 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" }, - - // Private prefix test (requires record) - { input: { private: { address: "test" } }, - expectedHex: "317821616464726573731474657374" }, - - // System prefix test (requires record) - { input: { system: { msg: "hello" } }, - expectedHex: "3179216d73671568656c6c6f" }, + // 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" }, + + // Private prefix test + { input: { private: { address: "test" } }, + expectedHex: "317821616464726573731474657374" }, + + // System prefix test + { input: { system: { msg: "hello" } }, + expectedHex: "3179216d73671568656c6c6f" }, - { input: [ { system: {msg: "hello" } }, { - num: 42, - arr: [1, -1, 2.5], - str: "test", - obj: { x: true } - } ], expectedHex: "223179216d73671568656c6c6f34216e756d622a2173747214746573742161727223616965235840216f626a21117873" }, + { input: [{ system: {msg: "hello" } }, { + num: 42, + arr: [1, -1, 2.5], + str: "test", + obj: { x: true } + }], expectedHex: "223179216d73671568656c6c6f34216e756d622a2173747214746573742161727223616965235840216f626a21117873" }, - // Additional edge cases - { input: new Uint8Array([]).buffer, expectedHex: "00" }, // Empty blob - { input: [[]], expectedHex: "2120" }, // Nested empty array - { input: { "": "" }, expectedHex: "311010" }, // Empty string key and value - { input: 1e-10, expectedHex: "d00a01" }, // Small floating point + // 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 @@ -184,79 +196,77 @@ let results = []; let testCount = 0; for (let test of testCases) { - testCount++; - let testName = `Test ${testCount}: ${JSON.stringify(test.input)}`; - let passed = true; - let messages = []; + testCount++; + let testName = `Test ${testCount}: ${JSON.stringify(test.input)}${test.testType ? ` (${test.testType})` : ''}`; + let passed = true; + let messages = []; - // Test encoding - let encoded = nota.encode(test.input); - if (!(encoded instanceof ArrayBuffer)) { - passed = false; - messages.push("Encode should return ArrayBuffer"); - } else { - let encodedHex = bufferToHex(encoded); - if (encodedHex !== test.expectedHex.toLowerCase()) { - messages.push( - `Hex encoding differs (informational): - Expected: ${test.expectedHex} - Got: ${encodedHex}` - ); - } - - // Test decoding - let decoded = nota.decode(encoded); - let expected = test.input; - - // Normalize ArrayBuffer and special cases for comparison - if (expected instanceof ArrayBuffer) { - expected = Array.from(new Uint8Array(expected)); - } - if (decoded instanceof ArrayBuffer) { - decoded = Array.from(new Uint8Array(decoded)); - } - // Handle private/system prefix objects - if (expected && (expected.private || expected.system)) { - const key = expected.private ? 'private' : 'system'; - expected = { [key]: expected[key] }; - } - - const compareResult = deepCompare(expected, decoded); - if (!compareResult.passed) { - passed = false; - messages.push("Decoding failed:"); - messages.push(...compareResult.messages); - } + // Test encoding + let encoded = test.replacer ? nota.encode(test.input, test.replacer) : nota.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}` + ); + } } - // Record result - results.push({ testName, passed, messages }); + // Test decoding + let decoded = test.reviver ? nota.decode(encoded, test.reviver) : nota.decode(encoded); + let expected = test.expected || test.input; - // Print detailed results on first failure - if (!passed) { - console.log(`\nDetailed Failure Report for ${testName}:`); - console.log(`Input: ${JSON.stringify(test.input)}`); - console.log(messages.join("\n")); - console.log(""); + // Normalize ArrayBuffer and special cases for comparison + if (expected instanceof ArrayBuffer) + expected = Array.from(new Uint8Array(expected)); + if (decoded instanceof ArrayBuffer) + decoded = Array.from(new Uint8Array(decoded)); + if (expected && (expected.private || expected.system)) { + const key = expected.private ? 'private' : 'system'; + expected = { [key]: expected[key] }; } + + const compareResult = deepCompare(expected, decoded); + if (!compareResult.passed) { + passed = false; + messages.push("Decoding failed:"); + messages.push(...compareResult.messages); + } + } + + 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(""); + } } // 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(`${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); + console.log("Overall: FAILED"); + os.exit(1); } else { - console.log("Overall: PASSED"); - os.exit(0); -} - + console.log("Overall: PASSED"); + os.exit(0); +} \ No newline at end of file