[njs] QuickJS: added TextDecoder and TextEncoder.

noreply at nginx.com noreply at nginx.com
Thu Jan 23 00:06:02 UTC 2025


details:   https://github.com/nginx/njs/commit/447d66d41d41504db976e900d94e75a90d388265
branches:  master
commit:    447d66d41d41504db976e900d94e75a90d388265
user:      Dmitry Volyntsev <xeioex at 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);


More information about the nginx-devel mailing list