details: https://github.com/nginx/njs/commit/447d66d41d41504db976e900d94e75a90d388265 branches: master commit: 447d66d41d41504db976e900d94e75a90d388265 user: Dmitry Volyntsev <xei...@nginx.com> date: Fri, 10 Jan 2025 23:20:36 -0800 description: QuickJS: added TextDecoder and TextEncoder.
--- src/qjs.c | 568 +++++++++++++++++++++++++++++++++++++++++++++++ src/qjs.h | 11 +- src/qjs_buffer.c | 4 +- src/test/njs_unit_test.c | 122 ---------- test/text_decoder.t.js | 151 +++++++++++++ test/text_encoder.t.js | 87 ++++++++ 6 files changed, 814 insertions(+), 129 deletions(-) diff --git a/src/qjs.c b/src/qjs.c index 487a03fc..e21e6568 100644 --- a/src/qjs.c +++ b/src/qjs.c @@ -19,6 +19,26 @@ typedef struct { } qjs_signal_entry_t; +typedef enum { + QJS_ENCODING_UTF8, +} qjs_encoding_t; + + +typedef struct { + qjs_encoding_t encoding; + int fatal; + int ignore_bom; + + njs_unicode_decode_t ctx; +} qjs_text_decoder_t; + + +typedef struct { + njs_str_t name; + qjs_encoding_t encoding; +} qjs_encoding_label_t; + + extern char **environ; @@ -32,6 +52,26 @@ static JSValue qjs_process_kill(JSContext *ctx, JSValueConst this_val, static JSValue qjs_process_pid(JSContext *ctx, JSValueConst this_val); static JSValue qjs_process_ppid(JSContext *ctx, JSValueConst this_val); +static int qjs_add_intrinsic_text_decoder(JSContext *cx, JSValueConst global); +static JSValue qjs_text_decoder_to_string_tag(JSContext *ctx, + JSValueConst this_val); +static JSValue qjs_text_decoder_decode(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv); +static JSValue qjs_text_decoder_encoding(JSContext *ctx, JSValueConst this_val); +static JSValue qjs_text_decoder_fatal(JSContext *ctx, JSValueConst this_val); +static JSValue qjs_text_decoder_ignore_bom(JSContext *ctx, + JSValueConst this_val); +static void qjs_text_decoder_finalizer(JSRuntime *rt, JSValue val); + +static int qjs_add_intrinsic_text_encoder(JSContext *cx, JSValueConst global); +static JSValue qjs_text_encoder_to_string_tag(JSContext *ctx, + JSValueConst this_val); +static JSValue qjs_text_encoder_encode(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv); +static JSValue qjs_text_encoder_encode_into(JSContext *ctx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue qjs_text_encoder_encoding(JSContext *ctx, JSValueConst this_val); + /* P1990 signals from `man 7 signal` are supported */ static qjs_signal_entry_t qjs_signals_table[] = { @@ -58,10 +98,35 @@ static qjs_signal_entry_t qjs_signals_table[] = { }; +static qjs_encoding_label_t qjs_encoding_labels[] = +{ + { njs_str("utf-8"), QJS_ENCODING_UTF8 }, + { njs_str("utf8") , QJS_ENCODING_UTF8 }, + { njs_null_str, 0 } +}; + + static const JSCFunctionListEntry qjs_global_proto[] = { JS_CGETSET_DEF("njs", qjs_njs_getter, NULL), }; +static const JSCFunctionListEntry qjs_text_decoder_proto[] = { + JS_CGETSET_DEF("[Symbol.toStringTag]", qjs_text_decoder_to_string_tag, + NULL), + JS_CFUNC_DEF("decode", 1, qjs_text_decoder_decode), + JS_CGETSET_DEF("encoding", qjs_text_decoder_encoding, NULL), + JS_CGETSET_DEF("fatal", qjs_text_decoder_fatal, NULL), + JS_CGETSET_DEF("ignoreBOM", qjs_text_decoder_ignore_bom, NULL), +}; + +static const JSCFunctionListEntry qjs_text_encoder_proto[] = { + JS_CGETSET_DEF("[Symbol.toStringTag]", qjs_text_encoder_to_string_tag, + NULL), + JS_CFUNC_DEF("encode", 1, qjs_text_encoder_encode), + JS_CFUNC_DEF("encodeInto", 1, qjs_text_encoder_encode_into), + JS_CGETSET_DEF("encoding", qjs_text_encoder_encoding, NULL), +}; + static const JSCFunctionListEntry qjs_njs_proto[] = { JS_CGETSET_DEF("[Symbol.toStringTag]", qjs_njs_to_string_tag, NULL), JS_PROP_STRING_DEF("version", NJS_VERSION, JS_PROP_C_W_E), @@ -80,6 +145,12 @@ static const JSCFunctionListEntry qjs_process_proto[] = { }; +static JSClassDef qjs_text_decoder_class = { + "TextDecoder", + .finalizer = qjs_text_decoder_finalizer, +}; + + JSContext * qjs_new_context(JSRuntime *rt, qjs_module_t **addons) { @@ -121,6 +192,14 @@ qjs_new_context(JSRuntime *rt, qjs_module_t **addons) global_obj = JS_GetGlobalObject(ctx); + if (qjs_add_intrinsic_text_decoder(ctx, global_obj) < 0) { + return NULL; + } + + if (qjs_add_intrinsic_text_encoder(ctx, global_obj) < 0) { + return NULL; + } + JS_SetPropertyFunctionList(ctx, global_obj, qjs_global_proto, njs_nitems(qjs_global_proto)); @@ -393,6 +472,495 @@ qjs_process_object(JSContext *ctx, int argc, const char **argv) } +static int +qjs_text_decoder_encoding_arg(JSContext *cx, int argc, JSValueConst *argv, + qjs_text_decoder_t *td) +{ + njs_str_t str; + qjs_encoding_label_t *label; + + if (argc < 1) { + td->encoding = QJS_ENCODING_UTF8; + return 0; + } + + str.start = (u_char *) JS_ToCStringLen(cx, &str.length, argv[0]); + if (str.start == NULL) { + JS_ThrowOutOfMemory(cx); + return -1; + } + + for (label = &qjs_encoding_labels[0]; label->name.length != 0; label++) { + if (njs_strstr_eq(&str, &label->name)) { + td->encoding = label->encoding; + JS_FreeCString(cx, (char *) str.start); + return 0; + } + } + + JS_ThrowTypeError(cx, "The \"%.*s\" encoding is not supported", + (int) str.length, str.start); + JS_FreeCString(cx, (char *) str.start); + + return -1; +} + + +static int +qjs_text_decoder_options(JSContext *cx, int argc, JSValueConst *argv, + qjs_text_decoder_t *td) +{ + JSValue val; + + if (argc < 2) { + td->fatal = 0; + td->ignore_bom = 0; + + return 0; + } + + val = JS_GetPropertyStr(cx, argv[1], "fatal"); + if (JS_IsException(val)) { + return -1; + } + + td->fatal = JS_ToBool(cx, val); + JS_FreeValue(cx, val); + + val = JS_GetPropertyStr(cx, argv[1], "ignoreBOM"); + if (JS_IsException(val)) { + return -1; + } + + td->ignore_bom = JS_ToBool(cx, val); + JS_FreeValue(cx, val); + + return 0; +} + + +static JSValue +qjs_text_decoder_ctor(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + JSValue obj; + qjs_text_decoder_t *td; + + obj = JS_NewObjectClass(cx, QJS_CORE_CLASS_ID_TEXT_DECODER); + if (JS_IsException(obj)) { + return JS_EXCEPTION; + } + + td = js_mallocz(cx, sizeof(qjs_text_decoder_t)); + if (td == NULL) { + JS_ThrowOutOfMemory(cx); + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } + + if (qjs_text_decoder_encoding_arg(cx, argc, argv, td) < 0) { + js_free(cx, td); + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } + + if (qjs_text_decoder_options(cx, argc, argv, td) < 0) { + js_free(cx, td); + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } + + njs_utf8_decode_init(&td->ctx); + + JS_SetOpaque(obj, td); + + return obj; +} + + +static int +qjs_add_intrinsic_text_decoder(JSContext *cx, JSValueConst global) +{ + JSValue ctor, proto; + + if (JS_NewClass(JS_GetRuntime(cx), QJS_CORE_CLASS_ID_TEXT_DECODER, + &qjs_text_decoder_class) < 0) + { + return -1; + } + + proto = JS_NewObject(cx); + if (JS_IsException(proto)) { + return -1; + } + + JS_SetPropertyFunctionList(cx, proto, qjs_text_decoder_proto, + njs_nitems(qjs_text_decoder_proto)); + + JS_SetClassProto(cx, QJS_CORE_CLASS_ID_TEXT_DECODER, proto); + + ctor = JS_NewCFunction2(cx, qjs_text_decoder_ctor, "TextDecoder", 2, + JS_CFUNC_constructor, 0); + if (JS_IsException(ctor)) { + return -1; + } + + JS_SetConstructor(cx, ctor, proto); + + return JS_SetPropertyStr(cx, global, "TextDecoder", ctor); +} + + +static JSValue +qjs_text_decoder_to_string_tag(JSContext *ctx, JSValueConst this_val) +{ + return JS_NewString(ctx, "TextDecoder"); +} + + +static JSValue +qjs_text_decoder_decode(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + int stream; + size_t size; + u_char *dst; + JSValue ret; + ssize_t length; + njs_str_t data; + const u_char *end; + qjs_text_decoder_t *td; + njs_unicode_decode_t ctx; + + td = JS_GetOpaque(this_val, QJS_CORE_CLASS_ID_TEXT_DECODER); + if (td == NULL) { + return JS_ThrowInternalError(cx, "'this' is not a TextDecoder"); + } + + ret = qjs_typed_array_data(cx, argv[0], &data); + if (JS_IsException(ret)) { + return ret; + } + + stream = 0; + + if (argc > 1) { + ret = JS_GetPropertyStr(cx, argv[1], "stream"); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + + stream = JS_ToBool(cx, ret); + JS_FreeValue(cx, ret); + } + + ctx = td->ctx; + end = data.start + data.length; + + if (data.start != NULL && !td->ignore_bom) { + data.start += njs_utf8_bom(data.start, end); + } + + length = njs_utf8_stream_length(&ctx, data.start, end - data.start, !stream, + td->fatal, &size); + + if (length == -1) { + return JS_ThrowTypeError(cx, "The encoded data was not valid"); + } + + dst = js_malloc(cx, size + 1); + if (dst == NULL) { + JS_ThrowOutOfMemory(cx); + return JS_EXCEPTION; + } + + (void) njs_utf8_stream_encode(&td->ctx, data.start, end, dst, !stream, 0); + + ret = JS_NewStringLen(cx, (const char *) dst, size); + js_free(cx, dst); + + if (!stream) { + njs_utf8_decode_init(&td->ctx); + } + + return ret; +} + + +static JSValue +qjs_text_decoder_encoding(JSContext *ctx, JSValueConst this_val) +{ + qjs_text_decoder_t *td; + + td = JS_GetOpaque(this_val, QJS_CORE_CLASS_ID_TEXT_DECODER); + if (td == NULL) { + return JS_ThrowInternalError(ctx, "'this' is not a TextDecoder"); + } + + switch (td->encoding) { + case QJS_ENCODING_UTF8: + return JS_NewString(ctx, "utf-8"); + } + + return JS_UNDEFINED; +} + + +static JSValue +qjs_text_decoder_fatal(JSContext *ctx, JSValueConst this_val) +{ + qjs_text_decoder_t *td; + + td = JS_GetOpaque(this_val, QJS_CORE_CLASS_ID_TEXT_DECODER); + if (td == NULL) { + return JS_ThrowInternalError(ctx, "'this' is not a TextDecoder"); + } + + return JS_NewBool(ctx, td->fatal); +} + + +static JSValue +qjs_text_decoder_ignore_bom(JSContext *ctx, JSValueConst this_val) +{ + qjs_text_decoder_t *td; + + td = JS_GetOpaque(this_val, QJS_CORE_CLASS_ID_TEXT_DECODER); + if (td == NULL) { + return JS_ThrowInternalError(ctx, "'this' is not a TextDecoder"); + } + + return JS_NewBool(ctx, td->ignore_bom); +} + + +static void +qjs_text_decoder_finalizer(JSRuntime *rt, JSValue val) +{ + qjs_text_decoder_t *td; + + td = JS_GetOpaque(val, QJS_CORE_CLASS_ID_TEXT_DECODER); + if (td != NULL) { + js_free_rt(rt, td); + } +} + + +static JSValue +qjs_text_encoder_ctor(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + JSValue obj; + + obj = JS_NewObjectClass(cx, QJS_CORE_CLASS_ID_TEXT_ENCODER); + if (JS_IsException(obj)) { + return JS_EXCEPTION; + } + + JS_SetOpaque(obj, (void *) 1); + + return obj; +} + + +static int +qjs_add_intrinsic_text_encoder(JSContext *cx, JSValueConst global) +{ + JSValue ctor, proto; + + proto = JS_NewObject(cx); + if (JS_IsException(proto)) { + return -1; + } + + JS_SetPropertyFunctionList(cx, proto, qjs_text_encoder_proto, + njs_nitems(qjs_text_encoder_proto)); + + JS_SetClassProto(cx, QJS_CORE_CLASS_ID_TEXT_ENCODER, proto); + + ctor = JS_NewCFunction2(cx, qjs_text_encoder_ctor, "TextEncoder", 0, + JS_CFUNC_constructor, 0); + if (JS_IsException(ctor)) { + return -1; + } + + JS_SetConstructor(cx, ctor, proto); + + return JS_SetPropertyStr(cx, global, "TextEncoder", ctor); +} + + +static JSValue +qjs_text_encoder_to_string_tag(JSContext *ctx, JSValueConst this_val) +{ + return JS_NewString(ctx, "TextEncoder"); +} + + +static JSValue +qjs_text_encoder_encoding(JSContext *ctx, JSValueConst this_val) +{ + return JS_NewString(ctx, "utf-8"); +} + + +static JSValue +qjs_text_encoder_encode(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + void *te; + JSValue len, ta, ret; + njs_str_t utf8, dst; + + te = JS_GetOpaque(this_val, QJS_CORE_CLASS_ID_TEXT_ENCODER); + if (te == NULL) { + return JS_ThrowInternalError(cx, "'this' is not a TextEncoder"); + } + + if (!JS_IsString(argv[0])) { + return JS_ThrowTypeError(cx, "The input argument must be a string"); + } + + utf8.start = (u_char *) JS_ToCStringLen(cx, &utf8.length, argv[0]); + if (utf8.start == NULL) { + return JS_EXCEPTION; + } + + len = JS_NewInt64(cx, utf8.length); + + ta = qjs_new_uint8_array(cx, 1, &len); + if (JS_IsException(ta)) { + JS_FreeCString(cx, (char *) utf8.start); + return ta; + } + + ret = qjs_typed_array_data(cx, ta, &dst); + if (JS_IsException(ret)) { + JS_FreeCString(cx, (char *) utf8.start); + return ret; + } + + memcpy(dst.start, utf8.start, utf8.length); + JS_FreeCString(cx, (char *) utf8.start); + + return ta; +} + + +static int +qjs_is_uint8_array(JSContext *cx, JSValueConst value) +{ + int ret; + JSValue ctor, global; + + global = JS_GetGlobalObject(cx); + + ctor = JS_GetPropertyStr(cx, global, "Uint8Array"); + if (JS_IsException(ctor)) { + JS_FreeValue(cx, global); + return -1; + } + + ret = JS_IsInstanceOf(cx, value, ctor); + JS_FreeValue(cx, ctor); + JS_FreeValue(cx, global); + + return ret; +} + + +static JSValue +qjs_text_encoder_encode_into(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + int read, written; + void *te; + size_t size; + u_char *to, *to_end; + JSValue ret; + uint32_t cp; + njs_str_t utf8, dst; + const u_char *start, *end; + njs_unicode_decode_t ctx; + + te = JS_GetOpaque(this_val, QJS_CORE_CLASS_ID_TEXT_ENCODER); + if (te == NULL) { + return JS_ThrowInternalError(cx, "'this' is not a TextEncoder"); + } + + if (!JS_IsString(argv[0])) { + return JS_ThrowTypeError(cx, "The input argument must be a string"); + } + + ret = qjs_typed_array_data(cx, argv[1], &dst); + if (JS_IsException(ret)) { + return ret; + } + + if (!qjs_is_uint8_array(cx, argv[1])) { + return JS_ThrowTypeError(cx, "The output argument must be a" + " Uint8Array"); + } + + utf8.start = (u_char *) JS_ToCStringLen(cx, &utf8.length, argv[0]); + if (utf8.start == NULL) { + return JS_EXCEPTION; + } + + read = 0; + written = 0; + + start = utf8.start; + end = start + utf8.length; + + to = dst.start; + to_end = to + dst.length; + + njs_utf8_decode_init(&ctx); + + while (start < end) { + cp = njs_utf8_decode(&ctx, &start, end); + + if (cp > NJS_UNICODE_MAX_CODEPOINT) { + cp = NJS_UNICODE_REPLACEMENT; + } + + size = njs_utf8_size(cp); + + if (to + size > to_end) { + break; + } + + read += (cp > 0xFFFF) ? 2 : 1; + written += size; + + to = njs_utf8_encode(to, cp); + } + + JS_FreeCString(cx, (char *) utf8.start); + + ret = JS_NewObject(cx); + if (JS_IsException(ret)) { + return ret; + } + + if (JS_DefinePropertyValueStr(cx, ret, "read", JS_NewInt32(cx, read), + JS_PROP_C_W_E) < 0) + { + JS_FreeValue(cx, ret); + return JS_EXCEPTION; + } + + if (JS_DefinePropertyValueStr(cx, ret, "written", JS_NewInt32(cx, written), + JS_PROP_C_W_E) < 0) + { + JS_FreeValue(cx, ret); + return JS_EXCEPTION; + } + + return ret; +} + int qjs_to_bytes(JSContext *ctx, qjs_bytes_t *bytes, JSValueConst value) { diff --git a/src/qjs.h b/src/qjs.h index dec6419d..76bf5c3d 100644 --- a/src/qjs.h +++ b/src/qjs.h @@ -36,10 +36,12 @@ #define QJS_CORE_CLASS_ID_OFFSET 64 #define QJS_CORE_CLASS_ID_BUFFER (QJS_CORE_CLASS_ID_OFFSET) #define QJS_CORE_CLASS_ID_UINT8_ARRAY_CTOR (QJS_CORE_CLASS_ID_OFFSET + 1) -#define QJS_CORE_CLASS_ID_FS_STATS (QJS_CORE_CLASS_ID_OFFSET + 2) -#define QJS_CORE_CLASS_ID_FS_DIRENT (QJS_CORE_CLASS_ID_OFFSET + 3) -#define QJS_CORE_CLASS_ID_FS_FILEHANDLE (QJS_CORE_CLASS_ID_OFFSET + 4) -#define QJS_CORE_CLASS_ID_LAST (QJS_CORE_CLASS_ID_OFFSET + 5) +#define QJS_CORE_CLASS_ID_TEXT_DECODER (QJS_CORE_CLASS_ID_OFFSET + 2) +#define QJS_CORE_CLASS_ID_TEXT_ENCODER (QJS_CORE_CLASS_ID_OFFSET + 3) +#define QJS_CORE_CLASS_ID_FS_STATS (QJS_CORE_CLASS_ID_OFFSET + 4) +#define QJS_CORE_CLASS_ID_FS_DIRENT (QJS_CORE_CLASS_ID_OFFSET + 5) +#define QJS_CORE_CLASS_ID_FS_FILEHANDLE (QJS_CORE_CLASS_ID_OFFSET + 6) +#define QJS_CORE_CLASS_ID_LAST (QJS_CORE_CLASS_ID_OFFSET + 7) typedef JSModuleDef *(*qjs_addon_init_pt)(JSContext *ctx, const char *name); @@ -53,6 +55,7 @@ typedef struct { JSContext *qjs_new_context(JSRuntime *rt, qjs_module_t **addons); +JSValue qjs_new_uint8_array(JSContext *ctx, int argc, JSValueConst *argv); JSValue qjs_buffer_alloc(JSContext *ctx, size_t size); JSValue qjs_buffer_create(JSContext *ctx, u_char *start, size_t size); JSValue qjs_buffer_chb_alloc(JSContext *ctx, njs_chb_t *chain); diff --git a/src/qjs_buffer.c b/src/qjs_buffer.c index 9f451e26..3652a07a 100644 --- a/src/qjs_buffer.c +++ b/src/qjs_buffer.c @@ -90,8 +90,6 @@ static int qjs_hex_encode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst); static size_t qjs_hex_encode_length(JSContext *ctx, const njs_str_t *src); static int qjs_hex_decode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst); static size_t qjs_hex_decode_length(JSContext *ctx, const njs_str_t *src); -static JSValue qjs_new_uint8_array(JSContext *ctx, int argc, - JSValueConst *argv); static JSModuleDef *qjs_buffer_init(JSContext *ctx, const char *name); @@ -2465,7 +2463,7 @@ qjs_buffer_chb_alloc(JSContext *ctx, njs_chb_t *chain) } -static JSValue +JSValue qjs_new_uint8_array(JSContext *ctx, int argc, JSValueConst *argv) { JSValue ret; diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c index d6ae4ed8..2e9a5379 100644 --- a/src/test/njs_unit_test.c +++ b/src/test/njs_unit_test.c @@ -19339,128 +19339,6 @@ static njs_unit_test_t njs_test[] = { njs_str("var t = \"123\"; t = parseInt(t); t"), njs_str("123") }, - /* TextEncoder. */ - - { njs_str("var en = new TextEncoder(); typeof en.encode()"), - njs_str("object") }, - - { njs_str("var en = new TextEncoder(); en.encode()"), - njs_str("") }, - - { njs_str("var en = new TextEncoder(); var res = en.encode('α'); res"), - njs_str("206,177") }, - - { njs_str("var en = new TextEncoder(); var res = en.encode('α1α'); res[2]"), - njs_str("49") }, - - { njs_str("var en = new TextEncoder(); en.encoding"), - njs_str("utf-8") }, - - { njs_str("TextEncoder.prototype.encode.apply({}, [])"), - njs_str("TypeError: \"this\" is not a TextEncoder") }, - - { njs_str("var en = new TextEncoder();" - "var utf8 = new Uint8Array(5);" - "var res = en.encodeInto('ααααα', utf8); njs.dump(res)"), - njs_str("{read:2,written:4}") }, - - { njs_str("var en = new TextEncoder();" - "var utf8 = new Uint8Array(10);" - "var res = en.encodeInto('ααααα', utf8); njs.dump(res)"), - njs_str("{read:5,written:10}") }, - - { njs_str("var en = new TextEncoder();" - "var utf8 = new Uint8Array(10);" - "en.encodeInto('ααααα', utf8.subarray(2)); utf8[0]"), - njs_str("0") }, - - { njs_str("TextEncoder.prototype.encodeInto.apply({}, [])"), - njs_str("TypeError: \"this\" is not a TextEncoder") }, - - { njs_str("(new TextEncoder()).encodeInto('', 0.12) "), - njs_str("TypeError: The \"destination\" argument must be an instance of Uint8Array") }, - - /* TextDecoder. */ - - { njs_str("var de = new TextDecoder();" - "var u8arr = new Uint8Array([240, 160, 174, 183]);" - "var u16arr = new Uint16Array(u8arr.buffer);" - "var u32arr = new Uint32Array(u8arr.buffer);" - "[u8arr, u16arr, u32arr].map(v=>de.decode(v)).join(',')"), - njs_str("𠮷,𠮷,𠮷") }, - - { njs_str("var de = new TextDecoder();" - "[new Uint8Array([240, 160]), " - " new Uint8Array([174]), " - " new Uint8Array([183])].map(v=>de.decode(v, {stream: 1}))[2]"), - njs_str("𠮷") }, - - { njs_str("var de = new TextDecoder();" - "de.decode(new Uint8Array([240, 160]), {stream: 1});" - "de.decode(new Uint8Array([174]), {stream: 1});" - "de.decode(new Uint8Array([183]))"), - njs_str("𠮷") }, - - { njs_str("var de = new TextDecoder();" - "de.decode(new Uint8Array([240, 160]), {stream: 1});" - "de.decode()"), - njs_str("�") }, - - { njs_str("var de = new TextDecoder('utf-8', {fatal: true});" - "de.decode(new Uint8Array([240, 160]))"), - njs_str("TypeError: The encoded data was not valid") }, - - { njs_str("var de = new TextDecoder('utf-8', {fatal: false});" - "de.decode(new Uint8Array([240, 160]))"), - njs_str("�") }, - - { njs_str("var en = new TextEncoder();" - "var de = new TextDecoder('utf-8', {ignoreBOM: true});" - "en.encode(de.decode(new Uint8Array([239, 187, 191, 50])))"), - njs_str("239,187,191,50") }, - - { njs_str("var en = new TextEncoder();" - "var de = new TextDecoder('utf-8', {ignoreBOM: false});" - "en.encode(de.decode(new Uint8Array([239, 187, 191, 50])))"), - njs_str("50") }, - - { njs_str("var en = new TextEncoder(); var de = new TextDecoder();" - "en.encode(de.decode(new Uint8Array([239, 187, 191, 50])))"), - njs_str("50") }, - - { njs_str("var de = new TextDecoder(); de.decode('')"), - njs_str("TypeError: The \"input\" argument must be an instance of TypedArray") }, - - { njs_str("var de = new TextDecoder({})"), - njs_str("RangeError: The \"[object Object]\" encoding is not supported") }, - - { njs_str("var de = new TextDecoder('foo')"), - njs_str("RangeError: The \"foo\" encoding is not supported") }, - - { njs_str("var de = new TextDecoder(); de.encoding"), - njs_str("utf-8") }, - - { njs_str("var de = new TextDecoder(); de.fatal"), - njs_str("false") }, - - { njs_str("var de = new TextDecoder(); de.ignoreBOM"), - njs_str("false") }, - - { njs_str("TextDecoder.prototype.decode.apply({}, new Uint8Array([1]))"), - njs_str("TypeError: \"this\" is not a TextDecoder") }, - - { njs_str("var de = new TextDecoder();" - "var buf = new Uint32Array([1,2,3]).buffer;" - "var en = new TextEncoder();" - "njs.dump(new Uint32Array(en.encode(de.decode(buf)).buffer))"), - njs_str("Uint32Array [1,2,3]") }, - - { njs_str("var de = new TextDecoder();" - "var buf = new Uint32Array([1,2,3]).subarray(1,2);" - "var en = new TextEncoder();" - "njs.dump(new Uint32Array(en.encode(de.decode(buf)).buffer))"), - njs_str("Uint32Array [2]") }, - /* let */ { njs_str("let x"), diff --git a/test/text_decoder.t.js b/test/text_decoder.t.js new file mode 100644 index 00000000..2eb879c0 --- /dev/null +++ b/test/text_decoder.t.js @@ -0,0 +1,151 @@ +/*--- +includes: [runTsuite.js, compareArray.js] +flags: [async] +---*/ + +function p(args, default_opts) { + let params = merge({}, default_opts); + params = merge(params, args); + + return params; +} + +let stream_tsuite = { + name: "TextDecoder() stream tests", + T: async (params) => { + let td = new TextDecoder('utf-8'); + + if (td.encoding !== 'utf-8') { + throw Error(`unexpected encoding "${td.encoding}" != "utf-8"`); + } + + if (td.fatal !== false) { + throw Error(`unexpected fatal "${td.fatal}" != "false"`); + } + + if (td.ignoreBOM !== false) { + throw Error(`unexpected ignoreBOM "${td.ignoreBOM}" != "false"`); + } + + let chunks = []; + for (var i = 0; i < params.chunks.length; i++) { + let r = td.decode(params.chunks[i], { stream: (i != params.chunks.length - 1) }); + chunks.push(r); + } + + if (!compareArray(chunks, params.expected)) { + throw Error(`unexpected output "${chunks.join('|')}" != "${params.expected.join('|')}"`); + } + + return 'SUCCESS'; + }, + + prepare_args: p, + opts: {}, + + tests: [ + { chunks: [new Uint8Array([0xF0, 0x9F, 0x8C, 0x9F])], + expected: ['🌟'] }, + // BOM is ignored + { chunks: [new Uint8Array([0xEF, 0xBB, 0xBF, 0xF0, 0x9F, 0x8C, 0x9F])], + expected: ['🌟'] }, + { chunks: [(new Uint8Array([0xF0, 0x9F, 0x8C, 0x9F])).buffer], + expected: ['🌟'] }, + { chunks: [new Uint32Array((new Uint8Array([0xF0, 0x9F, 0x8C, 0x9F])).buffer)], + expected: ['🌟'] }, + { chunks: [new Uint8Array((new Uint8Array([0x00, 0xF0, 0x9F, 0x8C, 0x9F, 0x00])).buffer, 1, 4)], + expected: ['🌟'] }, + { chunks: [new Uint8Array([0xF0, 0x9F]), new Uint8Array([0x8C, 0x9F])], + expected: ['', '🌟'] }, + { chunks: [new Uint8Array([0xF0, 0xA0]), new Uint8Array([0xAE]), new Uint8Array([0xB7])], + expected: ['', '', '𠮷'] }, + { chunks: [new Uint8Array([0xF0, 0xA0]), new Uint8Array([])], + expected: ['', '�'] }, + { chunks: [''], + exception: 'TypeError: TypeError: not a TypedArray' }, + ], +}; + +let fatal_tsuite = { + name: "TextDecoder() fatal tests", + T: async (params) => { + let td = new TextDecoder('utf8', {fatal: true, ignoreBOM: true}); + + if (td.encoding !== 'utf-8') { + throw Error(`unexpected encoding "${td.encoding}" != "utf-8"`); + } + + if (td.fatal !== true) { + throw Error(`unexpected fatal "${td.fatal}" != "true"`); + } + + if (td.ignoreBOM !== true) { + throw Error(`unexpected ignoreBOM "${td.ignoreBOM}" != "true"`); + } + + let chunks = []; + for (var i = 0; i < params.chunks.length; i++) { + let r = td.decode(params.chunks[i]); + chunks.push(r); + } + + if (!compareArray(chunks, params.expected)) { + throw Error(`unexpected output "${chunks.join('|')}" != "${params.expected.join('|')}"`); + } + + return 'SUCCESS'; + }, + + prepare_args: p, + opts: {}, + + tests: [ + { chunks: [new Uint8Array([0xF0, 0xA0, 0xAE, 0xB7])], + expected: ['𠮷'] }, + { chunks: [new Uint8Array([0xF0, 0xA0, 0xAE])], + exception: 'Error: The encoded data was not valid' }, + { chunks: [new Uint8Array([0xF0, 0xA0])], + exception: 'Error: The encoded data was not valid' }, + { chunks: [new Uint8Array([0xF0])], + exception: 'Error: The encoded data was not valid' }, + ], +}; + +let ignoreBOM_tsuite = { + name: "TextDecoder() ignoreBOM tests", + T: async (params) => { + let td = new TextDecoder('utf8', params.opts); + let te = new TextEncoder(); + + let res = te.encode(td.decode(params.value)); + + if (!compareArray(res, params.expected)) { + throw Error(`unexpected output "${res}" != "${params.expected}"`); + } + + return 'SUCCESS'; + }, + + prepare_args: p, + opts: {}, + + tests: [ + { value: new Uint8Array([239, 187, 191, 50]), + opts: {ignoreBOM: true}, + expected: [239, 187, 191, 50] }, + { value: new Uint8Array([239, 187, 191, 50]), + opts: {ignoreBOM: false}, + expected: [50] }, + { value: new Uint8Array([239, 187, 191, 50]), + opts: {}, + expected: [50] }, + ], +}; + + +run([ + stream_tsuite, + fatal_tsuite, + ignoreBOM_tsuite, +]) +.then($DONE, $DONE); diff --git a/test/text_encoder.t.js b/test/text_encoder.t.js new file mode 100644 index 00000000..e790ae37 --- /dev/null +++ b/test/text_encoder.t.js @@ -0,0 +1,87 @@ + +/*--- +includes: [runTsuite.js, compareArray.js] +flags: [async] +---*/ + +function p(args, default_opts) { + let params = merge({}, default_opts); + params = merge(params, args); + + return params; +} + +let encode_tsuite = { + name: "TextEncoder() encode tests", + T: async (params) => { + let te = new TextEncoder(); + + if (te.encoding !== 'utf-8') { + throw Error(`unexpected encoding "${td.encoding}" != "utf-8"`); + } + + let res = te.encode(params.value); + + if (!(res instanceof Uint8Array)) { + throw Error(`unexpected result "${res}" is not Uint8Array`); + } + + if (!compareArray(Array.from(res), params.expected)) { + throw Error(`unexpected output "${res}" != "${params.expected}"`); + } + + return 'SUCCESS'; + }, + + prepare_args: p, + opts: {}, + + tests: [ + { value: "", expected: [] }, + { value: "abc", expected: [97, 98, 99] }, + { value: "α1α", expected: [206, 177, 49, 206, 177] }, + { value: 0.12, exception: 'TypeError: TextEncoder.prototype.encode requires a string' }, + ], +}; + +let encodeinto_tsuite = { + name: "TextEncoder() encodeInto tests", + T: async (params) => { + let te = new TextEncoder(); + + let res = te.encodeInto(params.value, params.dest); + + if (res.written !== params.expected.length) { + throw Error(`unexpected written "${res.written}" != "${params.expected.length}"`); + } + + if (res.read !== params.read) { + throw Error(`unexpected read "${res.read}" != "${params.read}"`); + } + + if (!compareArray(Array.from(params.dest).slice(0, res.written), params.expected)) { + throw Error(`unexpected output "${res}" != "${params.expected}"`); + } + + return 'SUCCESS'; + }, + + prepare_args: p, + opts: {}, + + tests: [ + { value: "", dest: new Uint8Array(4), expected: [], read: 0 }, + { value: "aα", dest: new Uint8Array(3), expected: [97, 206, 177], read: 2 }, + { value: "αααα", dest: new Uint8Array(4), expected: [206, 177, 206, 177], read: 2 }, + { value: "αααα", dest: new Uint8Array(5), expected: [206, 177, 206, 177], read: 2 }, + { value: "αααα", dest: new Uint8Array(6), expected: [206, 177, 206, 177, 206, 177], read: 3 }, + { value: "", dest: 0.12, exception: 'TypeError: TextEncoder.prototype.encodeInto requires a string' }, + { value: 0.12, exception: 'TypeError: TextEncoder.prototype.encodeInto requires a string' }, + ], +}; + +run([ + encode_tsuite, + encodeinto_tsuite, +]) +.then($DONE, $DONE); _______________________________________________ nginx-devel mailing list nginx-devel@nginx.org https://mailman.nginx.org/mailman/listinfo/nginx-devel