[njs] QuickJS: added querystring module.

noreply at nginx.com noreply at nginx.com
Sat Feb 8 02:15:02 UTC 2025


details:   https://github.com/nginx/njs/commit/f289dcb99a9e4c9b72ca8d1c60659a43e58547cd
branches:  master
commit:    f289dcb99a9e4c9b72ca8d1c60659a43e58547cd
user:      Dmitry Volyntsev <xeioex at nginx.com>
date:      Wed, 29 Jan 2025 21:35:08 -0800
description:
QuickJS: added querystring module.


---
 auto/qjs_modules                   |    6 +
 external/qjs_query_string_module.c | 1009 ++++++++++++++++++++++++++++++++++++
 src/test/njs_unit_test.c           |  354 -------------
 test/harness/compareObjects.js     |   10 +-
 test/querystring.t.mjs             |  253 +++++++++
 5 files changed, 1277 insertions(+), 355 deletions(-)

diff --git a/auto/qjs_modules b/auto/qjs_modules
index 50ce9476..d82f9d9f 100644
--- a/auto/qjs_modules
+++ b/auto/qjs_modules
@@ -13,6 +13,12 @@ njs_module_srcs=external/qjs_fs_module.c
 
 . auto/qjs_module
 
+njs_module_name=qjs_query_string_module
+njs_module_incs=
+njs_module_srcs=external/qjs_query_string_module.c
+
+. auto/qjs_module
+
 if [ $NJS_OPENSSL = YES -a $NJS_HAVE_OPENSSL = YES ]; then
 	njs_module_name=qjs_webcrypto_module
 	njs_module_incs=
diff --git a/external/qjs_query_string_module.c b/external/qjs_query_string_module.c
new file mode 100644
index 00000000..85d2fcb3
--- /dev/null
+++ b/external/qjs_query_string_module.c
@@ -0,0 +1,1009 @@
+
+/*
+ * Copyright (C) Dmitry Volyntsev
+ * Copyright (C) F5, Inc.
+ */
+
+
+#include <qjs.h>
+
+static JSValue qjs_query_string_parse(JSContext *ctx, JSValueConst this_val,
+    int argc, JSValueConst *argv);
+static JSValue qjs_query_string_stringify(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv);
+static JSValue qjs_query_string_escape(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv);
+static JSValue qjs_query_string_unescape(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv);
+static JSValue qjs_query_string_decode(JSContext *cx, const u_char *start,
+    size_t size);
+static int qjs_query_string_encode(njs_chb_t *chain, njs_str_t *str);
+static JSValue qjs_query_string_parser(JSContext *cx, u_char *query,
+    u_char *end, njs_str_t *sep, njs_str_t *eq, JSValue decode,
+    unsigned max_keys);
+static JSModuleDef *qjs_querystring_init(JSContext *ctx, const char *name);
+
+
+static const JSCFunctionListEntry  qjs_querystring_export[] = {
+    JS_CFUNC_DEF("decode", 4, qjs_query_string_parse),
+    JS_CFUNC_DEF("encode", 4, qjs_query_string_stringify),
+    JS_CFUNC_DEF("escape", 1, qjs_query_string_escape),
+    JS_CFUNC_DEF("parse", 4, qjs_query_string_parse),
+    JS_CFUNC_DEF("stringify", 4, qjs_query_string_stringify),
+    JS_CFUNC_DEF("unescape", 1, qjs_query_string_unescape),
+};
+
+
+qjs_module_t  qjs_query_string_module = {
+    .name = "querystring",
+    .init = qjs_querystring_init,
+};
+
+
+static JSValue
+qjs_query_string_parse(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv)
+{
+    int64_t    max_keys;
+    JSValue    options, ret, decode, native;
+    njs_str_t  str, sep, eq;
+
+    sep.start = NULL;
+    eq.start = NULL;
+    str.start = NULL;
+
+    max_keys = 1000;
+    decode = JS_UNDEFINED;
+
+    if (!JS_IsNullOrUndefined(argv[1])) {
+        sep.start = (u_char *) JS_ToCStringLen(cx, &sep.length, argv[1]);
+        if (sep.start == NULL) {
+            return JS_EXCEPTION;
+        }
+    }
+
+    if (!JS_IsNullOrUndefined(argv[2])) {
+        eq.start = (u_char *) JS_ToCStringLen(cx, &eq.length, argv[2]);
+        if (eq.start == NULL) {
+            JS_FreeCString(cx, (char *) sep.start);
+            return JS_EXCEPTION;
+        }
+    }
+
+    options = argv[3];
+    if (JS_IsObject(options)) {
+        ret = JS_GetPropertyStr(cx, options, "maxKeys");
+        if (JS_IsException(ret)) {
+            goto fail;
+        }
+
+        if (!JS_IsUndefined(ret)) {
+            if (JS_ToInt64(cx, &max_keys, ret) < 0) {
+                JS_FreeValue(cx, ret);
+                goto fail;
+            }
+
+            JS_FreeValue(cx, ret);
+
+            if (max_keys < 0) {
+                max_keys = INT64_MAX;
+            }
+        }
+
+        decode = JS_GetPropertyStr(cx, options, "decodeURIComponent");
+        if (JS_IsException(decode)) {
+            goto fail;
+        }
+
+        if (!JS_IsUndefined(decode) && !JS_IsFunction(cx, decode)) {
+            JS_ThrowTypeError(cx, "option decodeURIComponent is not "
+                              "a function");
+            goto fail;
+        }
+    }
+
+    if (JS_IsNullOrUndefined(decode)) {
+        decode = JS_GetPropertyStr(cx, this_val, "unescape");
+        if (JS_IsException(decode)) {
+            goto fail;
+        }
+
+        if (!JS_IsFunction(cx, decode)) {
+            JS_ThrowTypeError(cx, "QueryString.unescape is not a function");
+            goto fail;
+        }
+
+        native = JS_GetPropertyStr(cx, decode, "native");
+        if (JS_IsException(native)) {
+            goto fail;
+        }
+
+        if (JS_IsBool(native)) {
+            JS_FreeValue(cx, decode);
+            decode = JS_NULL;
+        }
+    }
+
+    str.start = (u_char *) JS_ToCStringLen(cx, &str.length, argv[0]);
+    if (str.start == NULL) {
+        goto fail;
+    }
+
+    ret = qjs_query_string_parser(cx, str.start, str.start + str.length,
+                                  sep.start ? &sep : NULL,
+                                  eq.start ? &eq : NULL, decode, max_keys);
+
+    JS_FreeValue(cx, decode);
+
+    if (sep.start != NULL) {
+        JS_FreeCString(cx, (char *) sep.start);
+    }
+
+    if (eq.start != NULL) {
+        JS_FreeCString(cx, (char *) eq.start);
+    }
+
+    JS_FreeCString(cx, (char *) str.start);
+
+    return ret;
+
+fail:
+
+    JS_FreeValue(cx, decode);
+
+    if (sep.start != NULL) {
+        JS_FreeCString(cx, (char *) sep.start);
+    }
+
+    if (eq.start != NULL) {
+        JS_FreeCString(cx, (char *) eq.start);
+    }
+
+    if (str.start != NULL) {
+        JS_FreeCString(cx, (char *) str.start);
+    }
+
+    return JS_EXCEPTION;
+}
+
+
+static JSValue
+qjs_query_string_decode(JSContext *cx, const u_char *start, size_t size)
+{
+    u_char                *dst;
+    JSValue               ret;
+    uint32_t              cp;
+    njs_chb_t             chain;
+    const u_char          *p, *end;
+    njs_unicode_decode_t  ctx;
+
+    static const int8_t  hex[256]
+        njs_aligned(32) =
+    {
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+         0,  1,  2,  3,  4,  5,  6,  7,  8,  9, -1, -1, -1, -1, -1, -1,
+        -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+    };
+
+    NJS_CHB_CTX_INIT(&chain, cx);
+    njs_utf8_decode_init(&ctx);
+
+    cp = 0;
+
+    p = start;
+    end = p + size;
+
+    while (p < end) {
+        if (*p == '%' && end - p > 2 && hex[p[1]] >= 0 && hex[p[2]] >= 0) {
+            cp = njs_utf8_consume(&ctx, (hex[p[1]] << 4) | hex[p[2]]);
+            p += 3;
+
+        } else {
+            if (*p == '+') {
+                cp = ' ';
+                p++;
+
+            } else {
+                cp = njs_utf8_decode(&ctx, &p, end);
+            }
+        }
+
+        if (cp > NJS_UNICODE_MAX_CODEPOINT) {
+            if (cp == NJS_UNICODE_CONTINUE) {
+                continue;
+            }
+
+            cp = NJS_UNICODE_REPLACEMENT;
+        }
+
+        dst = njs_chb_reserve(&chain, 4);
+        if (dst == NULL) {
+            JS_ThrowOutOfMemory(cx);
+            return JS_EXCEPTION;
+        }
+
+        njs_chb_written(&chain, njs_utf8_encode(dst, cp) - dst);
+    }
+
+    if (cp == NJS_UNICODE_CONTINUE) {
+        dst = njs_chb_reserve(&chain, 3);
+        if (dst == NULL) {
+            JS_ThrowOutOfMemory(cx);
+            return JS_EXCEPTION;
+        }
+
+        njs_chb_written(&chain,
+                        njs_utf8_encode(dst, NJS_UNICODE_REPLACEMENT) - dst);
+    }
+
+
+    ret = qjs_string_create_chb(cx, &chain);
+
+    njs_chb_destroy(&chain);
+
+    return ret;
+}
+
+
+static JSValue
+qjs_query_string_escape(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv)
+{
+    JSValue    ret;
+    njs_str_t  str;
+    njs_chb_t  chain;
+
+    str.start = (u_char *) JS_ToCStringLen(cx, &str.length, argv[0]);
+    if (str.start == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    NJS_CHB_CTX_INIT(&chain, cx);
+
+    if (qjs_query_string_encode(&chain, &str) < 0) {
+        JS_FreeCString(cx, (char *) str.start);
+        njs_chb_destroy(&chain);
+        return JS_EXCEPTION;
+    }
+
+    ret = qjs_string_create_chb(cx, &chain);
+
+    njs_chb_destroy(&chain);
+
+    JS_FreeCString(cx, (char *) str.start);
+
+    return ret;
+}
+
+
+static JSValue
+qjs_query_string_unescape(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv)
+{
+    u_char     *p, *end;
+    JSValue    ret;
+    njs_str_t  str;
+
+    str.start = (u_char *) JS_ToCStringLen(cx, &str.length, argv[0]);
+    if (str.start == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    p = str.start;
+    end = p + str.length;
+
+    ret = qjs_query_string_decode(cx, p, end - p);
+
+    JS_FreeCString(cx, (char *) str.start);
+
+    return ret;
+}
+
+
+static u_char *
+qjs_query_string_match(u_char *p, u_char *end, const njs_str_t *v)
+{
+    size_t  length;
+
+    length = v->length;
+
+    if (length == 1) {
+        p = njs_strlchr(p, end, v->start[0]);
+
+        if (p == NULL) {
+            p = end;
+        }
+
+        return p;
+    }
+
+    while (p <= (end - length)) {
+        if (memcmp(p, v->start, length) == 0) {
+            return p;
+        }
+
+        p++;
+    }
+
+    return end;
+}
+
+
+static int
+qjs_query_string_append(JSContext *cx, JSValue object, const u_char *key,
+    size_t key_size, const u_char *val, size_t val_size, JSValue decoder)
+{
+    JSAtom    prop;
+    JSValue   name, value, prev, length, ret;
+    uint32_t  len;
+
+    if (JS_IsNullOrUndefined(decoder)) {
+        name = qjs_query_string_decode(cx, key, key_size);
+        if (JS_IsException(name)) {
+            return -1;
+        }
+
+        value = qjs_query_string_decode(cx, val, val_size);
+        if (JS_IsException(value)) {
+            JS_FreeValue(cx, name);
+            return -1;
+        }
+
+    } else {
+        name = JS_NewStringLen(cx, (const char *) key, key_size);
+        if (JS_IsException(name)) {
+            return -1;
+        }
+
+        ret = JS_Call(cx, decoder, JS_UNDEFINED, 1, &name);
+        JS_FreeValue(cx, name);
+        if (JS_IsException(ret)) {
+            return -1;
+        }
+
+        name = ret;
+
+        value = JS_NewStringLen(cx, (const char *) val, val_size);
+        if (JS_IsException(value)) {
+            return -1;
+        }
+
+        ret = JS_Call(cx, decoder, JS_UNDEFINED, 1, &value);
+        JS_FreeValue(cx, value);
+        if (JS_IsException(ret)) {
+            JS_FreeValue(cx, name);
+            return -1;
+        }
+
+        value = ret;
+    }
+
+    prop = JS_ValueToAtom(cx, name);
+    JS_FreeValue(cx, name);
+    if (prop == JS_ATOM_NULL) {
+        JS_FreeValue(cx, value);
+        return -1;
+    }
+
+    prev = JS_GetProperty(cx, object, prop);
+    if (JS_IsException(prev)) {
+        JS_FreeAtom(cx, prop);
+        JS_FreeValue(cx, value);
+        return -1;
+    }
+
+    if (JS_IsUndefined(prev)) {
+        if (JS_SetProperty(cx, object, prop, value) < 0) {
+            goto exception;
+        }
+
+    } else if (JS_IsArray(cx, prev)) {
+        length = JS_GetPropertyStr(cx, prev, "length");
+
+        if (JS_ToUint32(cx, &len, length) < 0) {
+            JS_FreeValue(cx, length);
+            goto exception;
+        }
+
+        JS_FreeValue(cx, length);
+
+        if (JS_SetPropertyUint32(cx, prev, len, value) < 0) {
+            goto exception;
+        }
+
+        JS_FreeValue(cx, prev);
+
+    } else {
+        ret = JS_NewArray(cx);
+        if (JS_IsException(ret)) {
+            goto exception;
+        }
+
+        if (JS_SetPropertyUint32(cx, ret, 0, prev) < 0) {
+            JS_FreeValue(cx, ret);
+            goto exception;
+        }
+
+        prev = JS_UNDEFINED;
+
+        if (JS_SetPropertyUint32(cx, ret, 1, value) < 0) {
+            JS_FreeValue(cx, ret);
+            goto exception;
+        }
+
+        value = JS_UNDEFINED;
+
+        if (JS_SetProperty(cx, object, prop, ret) < 0) {
+            JS_FreeValue(cx, ret);
+            goto exception;
+        }
+    }
+
+    JS_FreeAtom(cx, prop);
+
+    return 0;
+
+exception:
+
+    JS_FreeAtom(cx, prop);
+    JS_FreeValue(cx, prev);
+    JS_FreeValue(cx, value);
+
+    return -1;
+}
+
+
+static JSValue
+qjs_query_string_parser(JSContext *cx, u_char *query, u_char *end,
+    njs_str_t *sep, njs_str_t *eq, JSValue decode, unsigned max_keys)
+{
+    size_t     size;
+    u_char     *part, *key, *val;
+    JSValue    obj;
+    unsigned   count;
+    njs_str_t  sep_val, eq_val;
+
+    if (sep == NULL || sep->length == 0) {
+        sep = &sep_val;
+        sep->start = (u_char *) "&";
+        sep->length = 1;
+    }
+
+    if (eq == NULL || eq->length == 0) {
+        eq = &eq_val;
+        eq->start = (u_char *) "=";
+        eq->length = 1;
+    }
+
+    obj = JS_NewObject(cx);
+    if (JS_IsException(obj)) {
+        return JS_EXCEPTION;
+    }
+
+    count = 0;
+
+    key = query;
+
+    while (key < end) {
+        if (count++ == max_keys) {
+            break;
+        }
+
+        part = qjs_query_string_match(key, end, sep);
+
+        if (part == key) {
+            goto next;
+        }
+
+        val = qjs_query_string_match(key, part, eq);
+
+        size = val - key;
+
+        if (val != part) {
+            val += eq->length;
+        }
+
+        if (qjs_query_string_append(cx, obj, key, size, val, part - val,
+                                     decode) < 0)
+        {
+            JS_FreeValue(cx, obj);
+            return JS_EXCEPTION;
+        }
+
+next:
+
+        key = part + sep->length;
+    }
+
+    return obj;
+}
+
+
+static inline int
+qjs_need_escape(const uint32_t *escape, uint32_t byte)
+{
+    return ((escape[byte >> 5] & ((uint32_t) 1 << (byte & 0x1f))) != 0);
+}
+
+
+static inline u_char *
+qjs_string_encode(const uint32_t *escape, size_t size, const u_char *src,
+    u_char *dst)
+{
+    uint8_t              byte;
+    static const u_char  hex[16] = "0123456789ABCDEF";
+
+    do {
+        byte = *src++;
+
+        if (qjs_need_escape(escape, byte)) {
+            *dst++ = '%';
+            *dst++ = hex[byte >> 4];
+            *dst++ = hex[byte & 0xf];
+
+        } else {
+            *dst++ = byte;
+        }
+
+        size--;
+
+    } while (size != 0);
+
+    return dst;
+}
+
+
+static int
+qjs_query_string_encode(njs_chb_t *chain, njs_str_t *str)
+{
+    size_t  size;
+    u_char  *p, *start, *end;
+
+    static const uint32_t  escape[] = {
+        0xffffffff,  /* 1111 1111 1111 1111  1111 1111 1111 1111 */
+
+                     /* ?>=< ;:98 7654 3210  /.-, +*)( '&%$ #"!  */
+        0xfc00987d,  /* 1111 1100 0000 0000  1001 1000 0111 1101 */
+
+                     /* _^]\ [ZYX WVUT SRQP  ONML KJIH GFED CBA@ */
+        0x78000001,  /* 0111 1000 0000 0000  0000 0000 0000 0001 */
+
+                     /*  ~}| {zyx wvut srqp  onml kjih gfed cba` */
+        0xb8000001,  /* 1011 1000 0000 0000  0000 0000 0000 0001 */
+
+        0xffffffff,  /* 1111 1111 1111 1111  1111 1111 1111 1111 */
+        0xffffffff,  /* 1111 1111 1111 1111  1111 1111 1111 1111 */
+        0xffffffff,  /* 1111 1111 1111 1111  1111 1111 1111 1111 */
+        0xffffffff,  /* 1111 1111 1111 1111  1111 1111 1111 1111 */
+    };
+
+    if (chain->error) {
+        return -1;
+    }
+
+    if (str->length == 0) {
+        return 0;
+    }
+
+    p = str->start;
+    end = p + str->length;
+
+    size = str->length;
+    while (p < end) {
+        if (qjs_need_escape(escape, *p++)) {
+            size += 2;
+        }
+    }
+
+    start = njs_chb_reserve(chain, size);
+    if (start == NULL) {
+        return -1;
+    }
+
+    if (size == str->length) {
+        memcpy(start, str->start, str->length);
+        njs_chb_written(chain, str->length);
+        return 0;
+    }
+
+    qjs_string_encode(escape, str->length, str->start, start);
+
+    njs_chb_written(chain, size);
+
+    return 0;
+}
+
+
+static inline int
+qjs_query_string_encoder_call(JSContext *cx, njs_chb_t *chain,
+    JSValue encoder, JSValue value)
+{
+    int        rc;
+    JSValue    ret;
+    njs_str_t  str;
+
+    if (JS_IsNullOrUndefined(encoder)) {
+        str.start = (u_char *) JS_ToCStringLen(cx, &str.length, value);
+        if (str.start == NULL) {
+            return -1;
+        }
+
+        rc = qjs_query_string_encode(chain, &str);
+        JS_FreeCString(cx, (char *) str.start);
+        return rc;
+    }
+
+    ret = JS_Call(cx, encoder, JS_UNDEFINED, 1, &value);
+    if (JS_IsException(ret)) {
+        return -1;
+    }
+
+    str.start = (u_char *) JS_ToCStringLen(cx, &str.length, ret);
+    JS_FreeValue(cx, ret);
+    if (str.start == NULL) {
+        return -1;
+    }
+
+    njs_chb_append_str(chain, &str);
+
+    JS_FreeCString(cx, (char *) str.start);
+
+    return 0;
+}
+
+
+static inline int
+qjs_query_string_push(JSContext *cx, njs_chb_t *chain, JSValue key,
+    JSValue value, njs_str_t *eq, JSValue encoder)
+{
+    if (qjs_query_string_encoder_call(cx, chain, encoder, key) < 0) {
+        return -1;
+    }
+
+    njs_chb_append(chain, eq->start, eq->length);
+
+    if (JS_IsNumber(value)
+        || JS_IsBool(value)
+        || JS_IsString(value))
+    {
+        return qjs_query_string_encoder_call(cx, chain, encoder, value);
+    }
+
+    return 0;
+}
+
+
+static inline int
+qjs_query_string_push_array(JSContext *cx, njs_chb_t *chain, JSValue key,
+    JSValue array, njs_str_t *eq, njs_str_t *sep, JSValue encoder)
+{
+    int       rc;
+    JSValue   val, len;
+    uint32_t  i, length;
+
+    len = JS_GetPropertyStr(cx, array, "length");
+    if (JS_IsException(len)) {
+        return -1;
+    }
+
+    if (JS_ToUint32(cx, &length, len) < 0) {
+        JS_FreeValue(cx, len);
+        return -1;
+    }
+
+    JS_FreeValue(cx, len);
+
+    for (i = 0; i < length; i++) {
+        if (chain->last != NULL) {
+            njs_chb_append(chain, sep->start, sep->length);
+        }
+
+        val = JS_GetPropertyUint32(cx, array, i);
+        if (JS_IsException(val)) {
+            return -1;
+        }
+
+        rc = qjs_query_string_push(cx, chain, key, val, eq, encoder);
+        JS_FreeValue(cx, val);
+        if (rc != 0) {
+            return -1;
+        }
+    }
+
+    return 0;
+}
+
+
+static void
+qjs_free_prop_enum(JSContext *cx, JSPropertyEnum *tab, uint32_t len)
+{
+    uint32_t  i;
+
+    for (i = 0; i < len; i++) {
+        JS_FreeAtom(cx, tab[i].atom);
+    }
+
+    js_free(cx, tab);
+}
+
+
+static JSValue
+qjs_query_string_stringify_internal(JSContext *cx, JSValue obj, njs_str_t *sep,
+    njs_str_t *eq, JSValue encoder)
+{
+    int             rc;
+    uint32_t        n, length;
+    JSValue         key, val, ret;
+    njs_str_t       sep_val, eq_val;
+    njs_chb_t       chain;
+    JSPropertyEnum  *ptab;
+
+    if (!JS_IsObject(obj)) {
+        return JS_NewString(cx, "");
+    }
+
+    if (JS_GetOwnPropertyNames(cx, &ptab, &length, obj,
+                               JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY)
+        < 0)
+    {
+        return JS_EXCEPTION;
+    }
+
+    if (sep == NULL || sep->length == 0) {
+        sep = &sep_val;
+        sep->start = (u_char *) "&";
+        sep->length = 1;
+    }
+
+    if (eq == NULL || eq->length == 0) {
+        eq = &eq_val;
+        eq->start = (u_char *) "=";
+        eq->length = 1;
+    }
+
+    NJS_CHB_CTX_INIT(&chain, cx);
+
+    for (n = 0; n < length; n++) {
+        val = JS_GetProperty(cx, obj, ptab[n].atom);
+        if (JS_IsException(val)) {
+            goto fail;
+        }
+
+        if (JS_IsArray(cx, val)) {
+            key = JS_AtomToString(cx, ptab[n].atom);
+            if (JS_IsException(key)) {
+                JS_FreeValue(cx, val);
+                goto fail;
+            }
+
+            rc = qjs_query_string_push_array(cx, &chain, key, val, eq, sep,
+                                             encoder);
+            JS_FreeValue(cx, key);
+            JS_FreeValue(cx, val);
+            if (rc != 0) {
+                goto fail;
+            }
+
+            continue;
+        }
+
+        if (n != 0) {
+            njs_chb_append(&chain, sep->start, sep->length);
+        }
+
+        key = JS_AtomToString(cx, ptab[n].atom);
+        if (JS_IsException(key)) {
+            JS_FreeValue(cx, val);
+            goto fail;
+        }
+
+        rc = qjs_query_string_push(cx, &chain, key, val, eq, encoder);
+        JS_FreeValue(cx, key);
+        JS_FreeValue(cx, val);
+        if (rc != 0) {
+            goto fail;
+        }
+    }
+
+    if (ptab != NULL) {
+        qjs_free_prop_enum(cx, ptab, length);
+    }
+
+    ret = qjs_string_create_chb(cx, &chain);
+
+    njs_chb_destroy(&chain);
+
+    return ret;
+
+fail:
+
+    if (ptab != NULL) {
+        qjs_free_prop_enum(cx, ptab, length);
+    }
+
+    njs_chb_destroy(&chain);
+
+    return JS_EXCEPTION;
+}
+
+
+static JSValue
+qjs_query_string_stringify(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv)
+{
+    JSValue    options, ret, encode, native;
+    njs_str_t  sep, eq;
+
+    sep.start = NULL;
+    eq.start = NULL;
+
+    encode = JS_UNDEFINED;
+
+    if (!JS_IsNullOrUndefined(argv[1])) {
+        sep.start = (u_char *) JS_ToCStringLen(cx, &sep.length, argv[1]);
+        if (sep.start == NULL) {
+            return JS_EXCEPTION;
+        }
+    }
+
+    if (!JS_IsNullOrUndefined(argv[2])) {
+        eq.start = (u_char *) JS_ToCStringLen(cx, &eq.length, argv[2]);
+        if (eq.start == NULL) {
+            JS_FreeCString(cx, (char *) sep.start);
+            return JS_EXCEPTION;
+        }
+    }
+
+    options = argv[3];
+    if (JS_IsObject(options)) {
+        encode = JS_GetPropertyStr(cx, options, "encodeURIComponent");
+        if (JS_IsException(encode)) {
+            return JS_EXCEPTION;
+        }
+
+        if (!JS_IsUndefined(encode) && !JS_IsFunction(cx, encode)) {
+            JS_ThrowTypeError(cx, "option encodeURIComponent is not "
+                              "a function");
+            goto fail;
+        }
+    }
+
+    if (JS_IsNullOrUndefined(encode)) {
+        encode = JS_GetPropertyStr(cx, this_val, "escape");
+        if (JS_IsException(encode)) {
+            goto fail;
+        }
+
+        if (!JS_IsFunction(cx, encode)) {
+            JS_ThrowTypeError(cx, "QueryString.escape is not a function");
+            goto fail;
+        }
+
+        native = JS_GetPropertyStr(cx, encode, "native");
+        if (JS_IsException(native)) {
+            goto fail;
+        }
+
+        if (JS_IsBool(native)) {
+            JS_FreeValue(cx, encode);
+            encode = JS_NULL;
+        }
+    }
+
+    ret = qjs_query_string_stringify_internal(cx, argv[0],
+                                              sep.start ? &sep : NULL,
+                                              eq.start ? &eq : NULL, encode);
+
+    JS_FreeValue(cx, encode);
+
+    if (sep.start != NULL) {
+        JS_FreeCString(cx, (char *) sep.start);
+    }
+
+    if (eq.start != NULL) {
+        JS_FreeCString(cx, (char *) eq.start);
+    }
+
+    return ret;
+
+fail:
+
+    JS_FreeValue(cx, encode);
+
+    if (sep.start != NULL) {
+        JS_FreeCString(cx, (char *) sep.start);
+    }
+
+    if (eq.start != NULL) {
+        JS_FreeCString(cx, (char *) eq.start);
+    }
+
+    return JS_EXCEPTION;
+}
+
+
+static int
+qjs_querystring_module_init(JSContext *ctx, JSModuleDef *m)
+{
+    int      rc;
+    JSValue  proto, method;
+
+    proto = JS_NewObject(ctx);
+    if (JS_IsException(proto)) {
+        return -1;
+    }
+
+    JS_SetPropertyFunctionList(ctx, proto, qjs_querystring_export,
+                               njs_nitems(qjs_querystring_export));
+
+    method = JS_GetPropertyStr(ctx, proto, "escape");
+    if (JS_IsException(method)) {
+        return -1;
+    }
+
+    /* Marking the default "escape" function for the fast path. */
+
+    if (JS_SetPropertyStr(ctx, method, "native", JS_NewBool(ctx, 1)) < 0) {
+        JS_FreeValue(ctx, method);
+        return -1;
+    }
+
+    JS_FreeValue(ctx, method);
+
+    method = JS_GetPropertyStr(ctx, proto, "unescape");
+    if (JS_IsException(method)) {
+        return -1;
+    }
+
+    /* Marking the default "unescape" function for the fast path. */
+
+    if (JS_SetPropertyStr(ctx, method, "native", JS_NewBool(ctx, 1)) < 0) {
+        JS_FreeValue(ctx, method);
+        return -1;
+    }
+
+    JS_FreeValue(ctx, method);
+
+    rc = JS_SetModuleExport(ctx, m, "default", proto);
+    if (rc != 0) {
+        return -1;
+    }
+
+    return JS_SetModuleExportList(ctx, m, qjs_querystring_export,
+                                  njs_nitems(qjs_querystring_export));
+}
+
+
+static JSModuleDef *
+qjs_querystring_init(JSContext *ctx, const char *name)
+{
+    int          rc;
+    JSModuleDef  *m;
+
+    m = JS_NewCModule(ctx, name, qjs_querystring_module_init);
+    if (m == NULL) {
+        return NULL;
+    }
+
+    JS_AddModuleExport(ctx, m, "default");
+    rc = JS_AddModuleExportList(ctx, m, qjs_querystring_export,
+                                njs_nitems(qjs_querystring_export));
+    if (rc != 0) {
+        return NULL;
+    }
+
+    return m;
+}
diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c
index 81fee436..e99a6d8a 100644
--- a/src/test/njs_unit_test.c
+++ b/src/test/njs_unit_test.c
@@ -20491,354 +20491,6 @@ static njs_unit_test_t  njs_crypto_module_test[] =
       njs_str("TypeError: \"this\" is not a hash object") },
 };
 
-static njs_unit_test_t  njs_querystring_module_test[] =
-{
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('baz=fuz');"
-              "njs.dump(obj)"),
-      njs_str("{baz:'fuz'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('baz=');"
-              "njs.dump(obj)"),
-      njs_str("{baz:''}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('baz=fuz&muz=tax');"
-              "njs.dump(obj)"),
-      njs_str("{baz:'fuz',muz:'tax'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('baz=fuz&');"
-              "njs.dump(obj)"),
-      njs_str("{baz:'fuz'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('&baz=fuz');"
-              "njs.dump(obj)"),
-      njs_str("{baz:'fuz'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('&&&&&baz=fuz');"
-              "njs.dump(obj)"),
-      njs_str("{baz:'fuz'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('=fuz');"
-              "njs.dump(obj)"),
-      njs_str("{:'fuz'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('=fuz=');"
-              "njs.dump(obj)"),
-      njs_str("{:'fuz='}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('===fu=z');"
-              "njs.dump(obj)"),
-      njs_str("{:'==fu=z'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('baz=fuz&baz=tax');"
-              "njs.dump(obj)"),
-      njs_str("{baz:['fuz','tax']}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('freespace');"
-              "njs.dump(obj)"),
-      njs_str("{freespace:''}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('name&value=12');"
-              "njs.dump(obj)"),
-      njs_str("{name:'',value:'12'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('baz=fuz&muz=tax', 'fuz');"
-              "njs.dump(obj)"),
-      njs_str("{baz:'',&muz:'tax'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('baz=fuz&muz=tax', '');"
-              "njs.dump(obj)"),
-      njs_str("{baz:'fuz',muz:'tax'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('baz=fuz&muz=tax', null);"
-              "njs.dump(obj)"),
-      njs_str("{baz:'fuz',muz:'tax'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('baz=fuz&muz=tax', undefined);"
-              "njs.dump(obj)"),
-      njs_str("{baz:'fuz',muz:'tax'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('baz=fuz123muz=tax', 123);"
-              "njs.dump(obj)"),
-      njs_str("{baz:'fuz',muz:'tax'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('baz=fuzαααmuz=tax', 'ααα');"
-              "njs.dump(obj)"),
-      njs_str("{baz:'fuz',muz:'tax'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('baz=fuz&muz=tax', '=');"
-              "njs.dump(obj)"),
-      njs_str("{baz:'',fuz&muz:'',tax:''}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('baz=fuz&muz=tax', null, 'fuz');"
-              "njs.dump(obj)"),
-      njs_str("{baz=:'',muz=tax:''}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('baz=fuz&muz=tax', null, '&');"
-              "njs.dump(obj)"),
-      njs_str("{baz=fuz:'',muz=tax:''}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('baz123fuz&muz123tax', null, 123);"
-              "njs.dump(obj)"),
-      njs_str("{baz:'fuz',muz:'tax'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('bazαααfuz&muzαααtax', null, 'ααα');"
-              "njs.dump(obj)"),
-      njs_str("{baz:'fuz',muz:'tax'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('baz=fuz&muz=tax', null, null, {maxKeys: 1});"
-              "njs.dump(obj)"),
-      njs_str("{baz:'fuz'}") },
-
-    { njs_str("var qs = require('querystring'); var out = [];"
-              "var obj = qs.parse('baz=fuz&muz=tax', null, null, {decodeURIComponent: (key) => {out.push(key)}});"
-              "out.join('; ');"),
-      njs_str("baz; fuz; muz; tax") },
-
-    { njs_str("var qs = require('querystring'); var i = 0;"
-              "var obj = qs.parse('baz=fuz&muz=tax', null, null, {decodeURIComponent: (key) => 'α' + i++});"
-              "njs.dump(obj);"),
-      njs_str("{α0:'α1',α2:'α3'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "qs.parse('baz=fuz&muz=tax', null, null, {decodeURIComponent: 123});"),
-      njs_str("TypeError: option decodeURIComponent is not a function") },
-
-    { njs_str("var qs = require('querystring');"
-              "qs.unescape = 123;"
-              "qs.parse('baz=fuz&muz=tax');"),
-    njs_str("TypeError: QueryString.unescape is not a function") },
-
-    { njs_str("var qs = require('querystring'); var out = [];"
-              "qs.unescape = (key) => {out.push(key)};"
-              "qs.parse('baz=fuz&muz=tax');"
-              "out.join('; ');"),
-      njs_str("baz; fuz; muz; tax") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('ba%32z=f%32uz');"
-              "njs.dump(obj)"),
-      njs_str("{ba2z:'f2uz'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('ba%32z=f%32uz');"
-              "njs.dump(obj)"),
-      njs_str("{ba2z:'f2uz'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('ba%F0%9F%92%A9z=f%F0%9F%92%A9uz');"
-              "njs.dump(obj)"),
-      njs_str("{ba💩z:'f💩uz'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('======');"
-              "njs.dump(obj)"),
-      njs_str("{:'====='}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('baz=%F0%9F%A9');"
-              "njs.dump(obj)"),
-      njs_str("{baz:'�'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('baz=αααααα%\x00\x01\x02αααα');"
-              "njs.dump(obj)"),
-      njs_str("{baz:'αααααα%\\u0000\\u0001\\u0002αααα'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('baz=%F6α');"
-              "njs.dump(obj)"),
-      njs_str("{baz:'�α'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('baz=%F6');"
-              "njs.dump(obj)"),
-      njs_str("{baz:'�'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('baz=%FG');"
-              "njs.dump(obj)"),
-      njs_str("{baz:'%FG'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('baz=%F');"
-              "njs.dump(obj)"),
-      njs_str("{baz:'%F'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('baz=%');"
-              "njs.dump(obj)"),
-      njs_str("{baz:'%'}") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = qs.parse('ba+z=f+uz');"
-              "njs.dump(obj)"),
-      njs_str("{ba z:'f uz'}") },
-
-
-    { njs_str("var qs = require('querystring');"
-              "qs.parse('X='+'α'.repeat(33)).X.length"),
-      njs_str("33") },
-
-    { njs_str("var qs = require('querystring');"
-              "var x = qs.parse('X='+'α1'.repeat(33)).X;"
-              "[x.length, x[33], x[34]]"),
-      njs_str("66,1,α") },
-
-    { njs_str("var qs = require('querystring');"
-              "qs.stringify({'baz': 'fuz'})"),
-      njs_str("baz=fuz") },
-
-    { njs_str("var qs = require('querystring');"
-              "qs.stringify({'baz': 'fuz', 'muz': 'tax'})"),
-      njs_str("baz=fuz&muz=tax") },
-
-    { njs_str("var qs = require('querystring');"
-              "qs.stringify({'baαz': 'fαuz', 'muαz': 'tαax'});"),
-      njs_str("ba%CE%B1z=f%CE%B1uz&mu%CE%B1z=t%CE%B1ax") },
-
-    { njs_str("var qs = require('querystring');"
-              "qs.stringify({'baz': ['fuz', 'tax']})"),
-      njs_str("baz=fuz&baz=tax") },
-
-    { njs_str("var qs = require('querystring');"
-              njs_declare_sparse_array("arr", 2)
-              "arr[0] = 0; arr[1] = 1.5;"
-              "qs.stringify({'baz': arr})"),
-      njs_str("baz=0&baz=1.5") },
-
-    { njs_str("var qs = require('querystring'); var out = [];"
-              "qs.stringify({'baz': 'fuz', 'muz': 'tax'}, null, null, {encodeURIComponent: (key) => {out.push(key)}});"
-              "out.join('; ')"),
-      njs_str("baz; fuz; muz; tax") },
-
-    { njs_str("var qs = require('querystring');"
-              "qs.stringify({'baz': 'fuz', 'muz': 'tax'}, null, null, {encodeURIComponent: 123});"
-              "out.join('; ')"),
-      njs_str("TypeError: option encodeURIComponent is not a function") },
-
-    { njs_str("var qs = require('querystring');"
-              "qs.escape = 123;"
-              "qs.stringify({'baz': 'fuz', 'muz': 'tax'})"),
-      njs_str("TypeError: QueryString.escape is not a function") },
-
-    { njs_str("var qs = require('querystring'); var out = [];"
-              "qs.escape = (key) => {out.push(key)};"
-              "qs.stringify({'baz': 'fuz', 'muz': 'tax'});"
-              "out.join('; ')"),
-      njs_str("baz; fuz; muz; tax") },
-
-    { njs_str("var qs = require('querystring');"
-              "qs.stringify({'baz': 'fuz', 'muz': 'tax'}, '****')"),
-      njs_str("baz=fuz****muz=tax") },
-
-    { njs_str("var qs = require('querystring');"
-              "qs.stringify({'baz': 'fuz', 'muz': 'tax'}, null, '^^^^')"),
-      njs_str("baz^^^^fuz&muz^^^^tax") },
-
-    { njs_str("var qs = require('querystring');"
-              "var obj = {A:'α'}; obj['δ'] = 'D';"
-              "var s = qs.stringify(obj,'γ=','&β'); [s, s.length]"),
-      njs_str("A&β%CE%B1γ=%CE%B4&βD,20") },
-
-    { njs_str("var qs = require('querystring');"
-              "qs.stringify({'baz': 'fuz', 'muz': 'tax'}, '', '')"),
-      njs_str("baz=fuz&muz=tax") },
-
-    { njs_str("var qs = require('querystring');"
-              "qs.stringify({'baz': 'fuz', 'muz': 'tax'}, undefined, undefined)"),
-      njs_str("baz=fuz&muz=tax") },
-
-    { njs_str("var qs = require('querystring');"
-              "qs.stringify({'baz': 'fuz', 'muz': 'tax'}, '?', '/')"),
-      njs_str("baz/fuz?muz/tax") },
-
-    { njs_str("var qs = require('querystring');"
-              "qs.stringify('123')"),
-      njs_str("") },
-
-    { njs_str("var qs = require('querystring');"
-              "qs.stringify(123)"),
-      njs_str("") },
-
-    { njs_str("var qs = require('querystring');"
-              "qs.stringify({X:{toString(){return 3}}})"),
-      njs_str("X=") },
-
-    { njs_str("var qs = require('querystring');"
-              "qs.stringify({ name: undefined, age: 12 })"),
-      njs_str("name=&age=12") },
-
-    { njs_str("var qs = require('querystring');"
-              "qs.stringify(Object.create({ name: undefined, age: 12 }))"),
-      njs_str("") },
-
-    { njs_str("var qs = require('querystring');"
-              "qs.stringify([])"),
-      njs_str("") },
-
-    { njs_str("var qs = require('querystring');"
-              "qs.stringify(['','',''])"),
-      njs_str("0=&1=&2=") },
-
-    { njs_str("var qs = require('querystring');"
-              "qs.stringify([undefined, null, Symbol(), Object(0), Object('test'), Object(false),,,])"),
-      njs_str("0=&1=&2=&3=&4=&5=") },
-
-#if 0
-    { njs_str("var qs = require('querystring');"
-              "qs.stringify([NaN, Infinity, -Infinity, 2**69, 2**70])"),
-      njs_str("0=&1=&2=&3=590295810358705700000&4=1.1805916207174113e%2B21") },
-#else
-    { njs_str("var qs = require('querystring');"
-              "qs.stringify([NaN, Infinity, -Infinity, 2**69, 2**70])"),
-      njs_str("0=&1=&2=&3=590295810358705700000&4=1.1805916207174114e%2B21") },
-#endif
-
-    { njs_str("var qs = require('querystring');"
-              "qs.stringify([[1,2,3],[4,5,6]])"),
-      njs_str("0=1&0=2&0=3&1=4&1=5&1=6") },
-
-    { njs_str("var qs = require('querystring');"
-              "qs.stringify([['a',,,],['b',,,]])"),
-      njs_str("0=a&0=&0=&1=b&1=&1=") },
-
-    { njs_str("var qs = require('querystring');"
-              "qs.stringify([[,'a','b',,]])"),
-      njs_str("0=&0=a&0=b&0=") },
-
-    { njs_str("var qs = require('querystring');"
-              "qs.escape('abcααααdef')"),
-      njs_str("abc%CE%B1%CE%B1%CE%B1%CE%B1def") },
-
-    { njs_str("var qs = require('querystring');"
-              "qs.unescape('abc%CE%B1%CE%B1%CE%B1%CE%B1def')"),
-      njs_str("abcααααdef") },
-};
-
 
 #define NJS_XML_DOC "const xml = require('xml');" \
                     "let data = `<note><to b=\"bar\" a= \"foo\" >Tove</to><from>Jani</from></note>`;" \
@@ -23918,12 +23570,6 @@ static njs_test_suite_t  njs_suites[] =
       njs_nitems(njs_crypto_module_test),
       njs_unit_test },
 
-    { njs_str("querystring module"),
-      { .repeat = 1, .unsafe = 1 },
-      njs_querystring_module_test,
-      njs_nitems(njs_querystring_module_test),
-      njs_unit_test },
-
     { njs_str("externals"),
       { .externals = 1, .repeat = 1, .unsafe = 1 },
       njs_externals_test,
diff --git a/test/harness/compareObjects.js b/test/harness/compareObjects.js
index d4a20c15..ea0dad18 100644
--- a/test/harness/compareObjects.js
+++ b/test/harness/compareObjects.js
@@ -4,6 +4,14 @@ function compareObjects(ref, obj) {
     }
 
     for (const key in ref) {
+        if (!Object.prototype.hasOwnProperty.call(ref, key)) {
+            continue;
+        }
+
+        if (!Object.prototype.hasOwnProperty.call(obj, key)) {
+            return false;
+        }
+
         if (!compareObjects(ref[key], obj[key])) {
             return false;
         }
@@ -13,5 +21,5 @@ function compareObjects(ref, obj) {
 }
 
 function isObject(object) {
-    return object != null && typeof object === 'object';
+    return object !== null && typeof object === 'object';
 }
diff --git a/test/querystring.t.mjs b/test/querystring.t.mjs
new file mode 100644
index 00000000..b382d10d
--- /dev/null
+++ b/test/querystring.t.mjs
@@ -0,0 +1,253 @@
+/*---
+includes: [runTsuite.js, compareObjects.js]
+flags: [async]
+---*/
+
+import qs from 'querystring';
+
+let escape_tsuite = {
+    name: "querystring.escape() tests",
+    T: async (params) => {
+        let r = qs.escape(params.value);
+
+        if (r !== params.expected) {
+            throw Error(`unexpected output "${r}" != "${params.expected}"`);
+        }
+
+        return 'SUCCESS';
+    },
+    prepare_args: (args, default_opts) => {
+        let params = merge({}, default_opts);
+        params = merge(params, args);
+
+        return params;
+    },
+    opts: { },
+
+    tests: [
+        { value: '', expected: '' },
+        { value: 'baz=fuz', expected: 'baz%3Dfuz' },
+        { value: 'abcαdef', expected: 'abc%CE%B1def' },
+]};
+
+let parse_tsuite = {
+    name: "querystring.parse() tests",
+    T: async (params) => {
+        let r;
+        let unescape = qs.unescape;
+
+        if (params.unescape) {
+            qs.unescape = params.unescape;
+        }
+
+        try {
+            if (params.options !== undefined) {
+                r = qs.parse(params.value, params.sep, params.eq, params.options);
+
+            } else if (params.eq !== undefined) {
+                r = qs.parse(params.value, params.sep, params.eq);
+
+            } else if (params.sep !== undefined) {
+                r = qs.parse(params.value, params.sep);
+
+            } else {
+                r = qs.parse(params.value);
+            }
+
+        } finally {
+            if (params.unescape) {
+                qs.unescape = unescape;
+            }
+        }
+
+        if (!compareObjects(r, params.expected)) {
+            throw Error(`unexpected output "${JSON.stringify(r)}" != "${JSON.stringify(params.expected)}"`);
+        }
+
+        return 'SUCCESS';
+    },
+    prepare_args: (args, default_opts) => {
+        let params = merge({}, default_opts);
+        params = merge(params, args);
+
+        return params;
+    },
+    opts: { },
+
+    tests: [
+        { value: '', expected: {} },
+        { value: 'baz=fuz', expected: { baz:'fuz' } },
+        { value: 'baz=fuz', expected: { baz:'fuz' } },
+        { value: 'baz=fuz&', expected: { baz:'fuz' } },
+        { value: '&baz=fuz', expected: { baz:'fuz' } },
+        { value: '&&baz=fuz', expected: { baz:'fuz' } },
+        { value: 'baz=fuz&muz=tax', expected: { baz:'fuz', muz:'tax' } },
+        { value: 'baz=fuz&baz=bar', expected: { baz:['fuz', 'bar'] } },
+
+        { value: qs.encode({ baz:'fuz', muz:'tax' }), expected: { baz:'fuz', muz:'tax' } },
+
+        { value: 'baz=fuz&baz=bar', sep: '&', eq: '=', expected: { baz:['fuz', 'bar'] } },
+        { value: 'baz=fuz&baz=bar&baz=zap', expected: { baz:['fuz', 'bar', 'zap'] } },
+        { value: 'baz=', expected: { baz:'' } },
+        { value: '=fuz', expected: { '':'fuz' } },
+        { value: '=fuz=', expected: { '':'fuz=' } },
+        { value: '==fu=z', expected: { '':'=fu=z' } },
+        { value: '===fu=z&baz=bar', expected: { baz:'bar', '':'==fu=z' } },
+        { value: 'freespace', expected: { freespace:'' } },
+        { value: 'name&value=12', expected: { name:'', value:'12' } },
+        { value: 'baz=fuz&muz=tax', sep: 'fuz', expected: { baz:'', '&muz':'tax' } },
+        { value: 'baz=fuz&muz=tax', sep: '', expected: { baz:'fuz', 'muz':'tax' } },
+        { value: 'baz=fuz&muz=tax', sep: null, expected: { baz:'fuz', 'muz':'tax' } },
+        { value: 'baz=fuz123muz=tax', sep: 123, expected: { baz:'fuz', 'muz':'tax' } },
+        { value: 'baz=fuzαααmuz=tax', sep: 'ααα', expected: { baz:'fuz', 'muz':'tax' } },
+        { value: 'baz=fuz&muz=tax', sep: '=', expected: { baz:'', 'fuz&muz':'', 'tax':'' } },
+
+        { value: 'baz=fuz&muz=tax', sep: '', eq: '', expected: { baz:'fuz', muz:'tax' } },
+        { value: 'baz=fuz&muz=tax', sep: null, eq: 'fuz', expected: { 'baz=':'','muz=tax':'' } },
+        { value: 'baz123fuz&muz123tax', sep: null, eq: '123', expected: { baz:'fuz', 'muz':'tax' } },
+        { value: 'bazαααfuz&muzαααtax', sep: null, eq: 'ααα', expected: { baz:'fuz', 'muz':'tax' } },
+
+        { value: 'baz=fuz&muz=tax', sep: null, eq: null, options: { maxKeys: 1 }, expected: { baz:'fuz' } },
+        { value: 'baz=fuz&muz=tax', sep: null, eq: null, options: { maxKeys: -1 },
+          expected: { baz:'fuz', muz:'tax' } },
+        { value: 'baz=fuz&muz=tax', sep: null, eq: null, options: { maxKeys: { valueOf: () => { throw 'Oops'; } }},
+          exception: 'Oops' },
+        { value: 'baz=fuz&muz=tax', sep: null, eq: null,
+          options: { decodeURIComponent: (s) => `|${s}|` }, expected: { '|baz|':'|fuz|', '|muz|':'|tax|' } },
+        { value: 'baz=fuz&muz=tax', sep: null, eq: null,
+          options: { decodeURIComponent: 123 },
+          exception: 'TypeError: option decodeURIComponent is not a function' },
+        { value: 'baz=fuz&muz=tax', unescape: (s) => `|${s}|`, expected: { '|baz|':'|fuz|', '|muz|':'|tax|' } },
+        { value: 'baz=fuz&muz=tax', unescape: 123,
+          exception: 'TypeError: QueryString.unescape is not a function' },
+
+        { value: 'ba%32z=f%32uz', expected: { ba2z:'f2uz' } },
+        { value: 'ba%F0%9F%92%A9z=f%F0%9F%92%A9uz', expected: { 'ba💩z':'f💩uz' } },
+        { value: '==', expected: { '':'=' } },
+        { value: 'baz=%F0%9F%A9', expected: { baz:'�' } },
+        { value: 'baz=α%00%01%02α', expected: { baz:'α' + String.fromCharCode(0, 1, 2) + 'α' } },
+        { value: 'baz=%F6', expected: { baz:'�' } },
+        { value: 'baz=%FG', expected: { baz:'%FG' } },
+        { value: 'baz=%F', expected: { baz:'%F' } },
+        { value: 'baz=%', expected: { baz:'%' } },
+        { value: 'ba+z=f+uz', expected: { 'ba z':'f uz' } },
+        { value: 'X=' + 'α'.repeat(33), expected: { X:'α'.repeat(33) } },
+        { value: 'X=' + 'α1'.repeat(33), expected: { X:'α1'.repeat(33) } },
+
+        { value: {toString: () => { throw 'Oops'; }}, sep: "&", eq: "=",
+          exception: 'TypeError: Cannot convert object to primitive value' },
+]};
+
+let stringify_tsuite = {
+    name: "querystring.stringify() tests",
+    T: async (params) => {
+        let r;
+        let escape = qs.escape;
+
+        if (params.escape) {
+            qs.escape = params.escape;
+        }
+
+        try {
+            if (params.options !== undefined) {
+                r = qs.stringify(params.obj, params.sep, params.eq, params.options);
+
+            } else if (params.eq !== undefined) {
+                r = qs.stringify(params.obj, params.sep, params.eq);
+
+            } else if (params.sep !== undefined) {
+                r = qs.stringify(params.obj, params.sep);
+
+            } else {
+                r = qs.stringify(params.obj);
+            }
+
+        } finally {
+            if (params.escape) {
+                qs.escape = escape;
+            }
+        }
+
+        if (r !== params.expected) {
+            throw Error(`unexpected output "${r}" != "${params.expected}"`);
+        }
+
+        return 'SUCCESS';
+    },
+    prepare_args: (args, default_opts) => {
+        let params = merge({}, default_opts);
+        params = merge(params, args);
+
+        return params;
+    },
+    opts: { },
+
+    tests: [
+        { obj: {}, expected: '' },
+        { obj: { baz:'fuz', muz:'tax' }, expected: 'baz=fuz&muz=tax' },
+        { obj: { baz:['fuz', 'tax'] }, expected: 'baz=fuz&baz=tax' },
+        { obj: {'baαz': 'fαuz', 'muαz': 'tαax' }, expected: 'ba%CE%B1z=f%CE%B1uz&mu%CE%B1z=t%CE%B1ax' },
+        { obj: {A:'α', 'δ': 'D' }, expected: 'A=%CE%B1&%CE%B4=D' },
+        { obj: { baz:'fuz', muz:'tax' }, sep: '*', expected: 'baz=fuz*muz=tax' },
+        { obj: { baz:'fuz', muz:'tax' }, sep: null, eq: '^', expected: 'baz^fuz&muz^tax' },
+        { obj: { baz:'fuz', muz:'tax' }, sep: '', eq: '', expected: 'baz=fuz&muz=tax' },
+        { obj: { baz:'fuz', muz:'tax' }, sep: '?', eq: '/', expected: 'baz/fuz?muz/tax' },
+        { obj: { baz:'fuz', muz:'tax' }, sep: null, eq: null, options: { encodeURIComponent: (key) => `|${key}|` },
+          expected: '|baz|=|fuz|&|muz|=|tax|' },
+        { obj: { baz:'fuz', muz:'tax' }, sep: null, eq: null, options: { encodeURIComponent: 123 },
+          exception: 'TypeError: option encodeURIComponent is not a function' },
+        { obj: { baz:'fuz', muz:'tax' }, escape: (key) => `|${key}|`, expected: '|baz|=|fuz|&|muz|=|tax|' },
+        { obj: { '':'' }, escape: (s) => s.length == 0 ? '#' : s, expected: '#=#' },
+        { obj: { baz:'fuz', muz:'tax' }, escape: 123,
+          exception: 'TypeError: QueryString.escape is not a function' },
+
+        { obj: qs.decode('baz=fuz&muz=tax'), expected: 'baz=fuz&muz=tax' },
+
+        { obj: '123', expected: '' },
+        { obj: 123, expected: '' },
+        { obj: { baz:'fuz' }, expected: 'baz=fuz' },
+        { obj: { baz:undefined }, expected: 'baz=' },
+        { obj: Object.create({ baz:'fuz' }), expected: '' },
+        { obj: [], expected: '' },
+        { obj: ['a'], expected: '0=a' },
+        { obj: ['a', 'b'], expected: '0=a&1=b' },
+        { obj: ['', ''], expected: '0=&1=' },
+        { obj: [undefined, null, Symbol(), Object(0), Object('test'), Object(false),,,],
+          expected: '0=&1=&2=&3=&4=&5=' },
+        { obj: [['a', 'b'], ['c', 'd']], expected: '0=a&0=b&1=c&1=d' },
+        { obj: [['a',,,], ['b',,,]], expected: '0=a&0=&0=&1=b&1=&1=' },
+        { obj: [[,'a','b',,]], expected: '0=&0=a&0=b&0=' },
+]};
+
+let unescape_tsuite = {
+    name: "querystring.unescape() tests",
+    T: async (params) => {
+        let r = qs.unescape(params.value);
+
+        if (r !== params.expected) {
+            throw Error(`unexpected output "${r}" != "${params.expected}"`);
+        }
+
+        return 'SUCCESS';
+    },
+    prepare_args: (args, default_opts) => {
+        let params = merge({}, default_opts);
+        params = merge(params, args);
+
+        return params;
+    },
+    opts: { },
+
+    tests: [
+        { value: '', expected: '' },
+        { value: 'baz%3Dfuz', expected: 'baz=fuz' },
+        { value: 'abc%CE%B1def', expected: 'abcαdef' },
+]};
+
+run([
+    escape_tsuite,
+    parse_tsuite,
+    stringify_tsuite,
+    unescape_tsuite,
+])
+.then($DONE, $DONE);


More information about the nginx-devel mailing list