
/*
 * Copyright (C) Dmitry Volyntsev
 * Copyright (C) F5, Inc.
 */

#include <qjs.h>

#define INT24_MAX  0x7FFFFF
#define INT24_MIN  (-0x800000)
#define INT40_MAX  0x7FFFFFFFFFLL
#define INT40_MIN  (-0x8000000000LL)
#define INT48_MAX  0x7FFFFFFFFFFFLL
#define INT48_MIN  (-0x800000000000LL)
#define UINT24_MAX 0xFFFFFFLL
#define UINT40_MAX 0xFFFFFFFFFFLL
#define UINT48_MAX 0xFFFFFFFFFFFFLL

#define qjs_buffer_magic(size, sign, little)                                 \
      ((size << 2) | (sign << 1) | little)

static JSValue qjs_buffer(JSContext *ctx, JSValueConst this_val, int argc,
    JSValueConst *argv);
static JSValue qjs_buffer_ctor(JSContext *ctx, JSValueConst this_val, int argc,
    JSValueConst *argv);
static JSValue qjs_bufferobj_alloc(JSContext *ctx, JSValueConst this_val,
    int argc, JSValueConst *argv, int ignored);
static JSValue qjs_buffer_byte_length(JSContext *ctx, JSValueConst this_val,
    int argc, JSValueConst *argv);
static JSValue qjs_buffer_compare(JSContext *ctx, JSValueConst this_val,
    int argc, JSValueConst *argv);
static JSValue qjs_buffer_concat(JSContext *ctx, JSValueConst this_val,
    int argc, JSValueConst *argv);
static JSValue qjs_buffer_fill(JSContext *ctx, JSValueConst buffer,
    JSValueConst fill, JSValueConst encode, uint64_t offset, uint64_t end);
static JSValue qjs_buffer_from(JSContext *ctx, JSValueConst this_val, int argc,
    JSValueConst *argv);
static JSValue qjs_buffer_is_buffer(JSContext *ctx, JSValueConst this_val,
    int argc, JSValueConst *argv);
static JSValue qjs_buffer_is_encoding(JSContext *ctx, JSValueConst this_val,
    int argc, JSValueConst *argv);
static JSValue qjs_buffer_prototype_compare(JSContext *ctx,
    JSValueConst this_val, int argc, JSValueConst *argv);
static JSValue qjs_buffer_prototype_copy(JSContext *ctx,
    JSValueConst this_val, int argc, JSValueConst *argv);
static JSValue qjs_buffer_prototype_equals(JSContext *ctx,
    JSValueConst this_val, int argc, JSValueConst *argv);
static JSValue qjs_buffer_prototype_fill(JSContext *ctx,
    JSValueConst this_val, int argc, JSValueConst *argv);
static JSValue qjs_buffer_prototype_includes(JSContext *ctx,
    JSValueConst this_val, int argc, JSValueConst *argv);
static JSValue qjs_buffer_prototype_index_of(JSContext *ctx,
    JSValueConst this_val, int argc, JSValueConst *argv, int last);
static JSValue qjs_buffer_prototype_read_float(JSContext *ctx,
    JSValueConst this_val, int argc, JSValueConst *argv, int magic);
static JSValue qjs_buffer_prototype_read_int(JSContext *ctx,
    JSValueConst this_val, int argc, JSValueConst *argv, int magic);
static JSValue qjs_buffer_prototype_swap(JSContext *ctx, JSValueConst this_val,
    int argc, JSValueConst *argv, int size);
static JSValue qjs_buffer_prototype_to_json(JSContext *ctx,
    JSValueConst this_val, int argc, JSValueConst *argv);
static JSValue qjs_buffer_prototype_to_string(JSContext *ctx,
    JSValueConst this_val, int argc, JSValueConst *argv);
static JSValue qjs_buffer_prototype_write(JSContext *ctx, JSValueConst this_val,
    int argc, JSValueConst *argv);
static JSValue qjs_buffer_prototype_write_int(JSContext *ctx,
    JSValueConst this_val, int argc, JSValueConst *argv, int magic);
static JSValue qjs_buffer_prototype_write_float(JSContext *ctx,
    JSValueConst this_val, int argc, JSValueConst *argv, int magic);
static JSValue qjs_buffer_from_string(JSContext *ctx, JSValueConst str,
    JSValueConst encoding);
static JSValue qjs_buffer_from_typed_array(JSContext *ctx, JSValueConst obj,
    size_t offset, size_t size, size_t bytes, int float32);
static JSValue qjs_buffer_from_object(JSContext *ctx, JSValueConst obj);
static JSValue qjs_buffer_compare_array(JSContext *ctx, JSValue val1,
    JSValue val2, JSValueConst target_start, JSValueConst target_end,
    JSValueConst source_start, JSValueConst source_end);
static JSModuleDef *qjs_buffer_init(JSContext *ctx, const char *name);


static qjs_buffer_encoding_t  qjs_buffer_encodings[] =
{
    {
        njs_str("utf-8"),
        NULL,
        NULL,
        NULL,
        NULL,
    },

    {
        njs_str("utf8"),
        NULL,
        NULL,
        NULL,
        NULL,
    },

    {
        njs_str("base64"),
        qjs_base64_encode,
        qjs_base64_encode_length,
        qjs_base64_decode,
        qjs_base64_decode_length,
    },

    {
        njs_str("base64url"),
        qjs_base64url_encode,
        qjs_base64_encode_length,
        qjs_base64url_decode,
        qjs_base64url_decode_length,
    },

    {
        njs_str("hex"),
        qjs_hex_encode,
        qjs_hex_encode_length,
        qjs_hex_decode,
        qjs_hex_decode_length,
    },

    { njs_null_str, 0, 0, 0, 0 }
};


static const JSCFunctionListEntry qjs_buffer_constants[] = {
    JS_PROP_INT32_DEF("MAX_LENGTH", INT32_MAX, JS_PROP_ENUMERABLE),
    JS_PROP_INT32_DEF("MAX_STRING_LENGTH", 0x3fffffff, JS_PROP_ENUMERABLE),
};


static const JSCFunctionListEntry qjs_buffer_export[] = {
    JS_OBJECT_DEF("constants",
                  qjs_buffer_constants,
                  njs_nitems(qjs_buffer_constants),
                  JS_PROP_CONFIGURABLE),
};


static const JSCFunctionListEntry qjs_buffer_props[] = {
    JS_CFUNC_MAGIC_DEF("alloc", 3, qjs_bufferobj_alloc, 0),
    JS_CFUNC_MAGIC_DEF("allocUnsafe", 3, qjs_bufferobj_alloc, 1),
    JS_CFUNC_DEF("byteLength", 2, qjs_buffer_byte_length),
    JS_CFUNC_DEF("compare", 6, qjs_buffer_compare),
    JS_CFUNC_DEF("concat", 2, qjs_buffer_concat),
    JS_CFUNC_DEF("from", 3, qjs_buffer_from),
    JS_CFUNC_DEF("isBuffer", 1, qjs_buffer_is_buffer),
    JS_CFUNC_DEF("isEncoding", 1, qjs_buffer_is_encoding),
};


static const JSCFunctionListEntry qjs_buffer_proto[] = {
    JS_CFUNC_DEF("compare", 5, qjs_buffer_prototype_compare),
    JS_CFUNC_DEF("copy", 5, qjs_buffer_prototype_copy),
    JS_CFUNC_DEF("equals", 1, qjs_buffer_prototype_equals),
    JS_CFUNC_DEF("fill", 4, qjs_buffer_prototype_fill),
    JS_CFUNC_DEF("includes", 3, qjs_buffer_prototype_includes),
    JS_CFUNC_MAGIC_DEF("indexOf", 3, qjs_buffer_prototype_index_of, 0),
    JS_CFUNC_MAGIC_DEF("lastIndexOf", 3, qjs_buffer_prototype_index_of, 1),
    JS_CFUNC_MAGIC_DEF("readFloatLE", 1, qjs_buffer_prototype_read_float,
                       qjs_buffer_magic(4, 1, 1)),
    JS_CFUNC_MAGIC_DEF("readFloatBE", 1, qjs_buffer_prototype_read_float,
                       qjs_buffer_magic(4, 1, 0)),
    JS_CFUNC_MAGIC_DEF("readDoubleLE", 1, qjs_buffer_prototype_read_float,
                       qjs_buffer_magic(8, 1, 1)),
    JS_CFUNC_MAGIC_DEF("readDoubleBE", 1, qjs_buffer_prototype_read_float,
                       qjs_buffer_magic(8, 1, 0)),
    JS_CFUNC_MAGIC_DEF("readInt8", 1, qjs_buffer_prototype_read_int,
                       qjs_buffer_magic(1, 1, 1)),
    JS_CFUNC_MAGIC_DEF("readUInt8", 1, qjs_buffer_prototype_read_int,
                       qjs_buffer_magic(1, 0, 1)),
    JS_CFUNC_MAGIC_DEF("readInt16LE", 1, qjs_buffer_prototype_read_int,
                       qjs_buffer_magic(2, 1, 1)),
    JS_CFUNC_MAGIC_DEF("readUInt16LE", 1, qjs_buffer_prototype_read_int,
                       qjs_buffer_magic(2, 0, 1)),
    JS_CFUNC_MAGIC_DEF("readInt16BE", 1, qjs_buffer_prototype_read_int,
                       qjs_buffer_magic(2, 1, 0)),
    JS_CFUNC_MAGIC_DEF("readUInt16BE", 1, qjs_buffer_prototype_read_int,
                       qjs_buffer_magic(2, 0, 0)),
    JS_CFUNC_MAGIC_DEF("readInt32LE", 1, qjs_buffer_prototype_read_int,
                       qjs_buffer_magic(4, 1, 1)),
    JS_CFUNC_MAGIC_DEF("readUInt32LE", 1, qjs_buffer_prototype_read_int,
                       qjs_buffer_magic(4, 0, 1)),
    JS_CFUNC_MAGIC_DEF("readInt32BE", 1, qjs_buffer_prototype_read_int,
                       qjs_buffer_magic(4, 1, 0)),
    JS_CFUNC_MAGIC_DEF("readUInt32BE", 1, qjs_buffer_prototype_read_int,
                       qjs_buffer_magic(4, 0, 0)),
    JS_CFUNC_MAGIC_DEF("readIntLE", 2, qjs_buffer_prototype_read_int,
                       qjs_buffer_magic(0, 1, 1)),
    JS_CFUNC_MAGIC_DEF("readUIntLE", 2, qjs_buffer_prototype_read_int,
                       qjs_buffer_magic(0, 0, 1)),
    JS_CFUNC_MAGIC_DEF("readIntBE", 2, qjs_buffer_prototype_read_int,
                       qjs_buffer_magic(0, 1, 0)),
    JS_CFUNC_MAGIC_DEF("readUIntBE", 2, qjs_buffer_prototype_read_int,
                       qjs_buffer_magic(0, 0, 0)),
    JS_CFUNC_MAGIC_DEF("swap16", 0, qjs_buffer_prototype_swap, 2),
    JS_CFUNC_MAGIC_DEF("swap32", 0, qjs_buffer_prototype_swap, 4),
    JS_CFUNC_MAGIC_DEF("swap64", 0, qjs_buffer_prototype_swap, 8),
    JS_CFUNC_DEF("toJSON", 0, qjs_buffer_prototype_to_json),
    JS_CFUNC_DEF("toString", 1, qjs_buffer_prototype_to_string),
    JS_CFUNC_DEF("write", 4, qjs_buffer_prototype_write),
    JS_CFUNC_MAGIC_DEF("writeInt8", 1, qjs_buffer_prototype_write_int,
                       qjs_buffer_magic(1, 1, 1)),
    JS_CFUNC_MAGIC_DEF("writeUInt8", 1, qjs_buffer_prototype_write_int,
                       qjs_buffer_magic(1, 0, 1)),
    JS_CFUNC_MAGIC_DEF("writeInt16LE", 1, qjs_buffer_prototype_write_int,
                       qjs_buffer_magic(2, 1, 1)),
    JS_CFUNC_MAGIC_DEF("writeUInt16LE", 1, qjs_buffer_prototype_write_int,
                       qjs_buffer_magic(2, 0, 1)),
    JS_CFUNC_MAGIC_DEF("writeInt16BE", 1, qjs_buffer_prototype_write_int,
                       qjs_buffer_magic(2, 1, 0)),
    JS_CFUNC_MAGIC_DEF("writeUInt16BE", 1, qjs_buffer_prototype_write_int,
                       qjs_buffer_magic(2, 0, 0)),
    JS_CFUNC_MAGIC_DEF("writeInt32LE", 1, qjs_buffer_prototype_write_int,
                       qjs_buffer_magic(4, 1, 1)),
    JS_CFUNC_MAGIC_DEF("writeUInt32LE", 1, qjs_buffer_prototype_write_int,
                       qjs_buffer_magic(4, 0, 1)),
    JS_CFUNC_MAGIC_DEF("writeInt32BE", 1, qjs_buffer_prototype_write_int,
                       qjs_buffer_magic(4, 1, 0)),
    JS_CFUNC_MAGIC_DEF("writeUInt32BE", 1, qjs_buffer_prototype_write_int,
                       qjs_buffer_magic(4, 0, 0)),
    JS_CFUNC_MAGIC_DEF("writeIntLE", 2, qjs_buffer_prototype_write_int,
                       qjs_buffer_magic(0, 1, 1)),
    JS_CFUNC_MAGIC_DEF("writeUIntLE", 2, qjs_buffer_prototype_write_int,
                       qjs_buffer_magic(0, 0, 1)),
    JS_CFUNC_MAGIC_DEF("writeIntBE", 2, qjs_buffer_prototype_write_int,
                       qjs_buffer_magic(0, 1, 0)),
    JS_CFUNC_MAGIC_DEF("writeUIntBE", 2, qjs_buffer_prototype_write_int,
                       qjs_buffer_magic(0, 0, 0)),
    JS_CFUNC_MAGIC_DEF("writeFloatLE", 2, qjs_buffer_prototype_write_float,
                       qjs_buffer_magic(4, 1, 1)),
    JS_CFUNC_MAGIC_DEF("writeFloatBE", 2, qjs_buffer_prototype_write_float,
                       qjs_buffer_magic(4, 1, 0)),
    JS_CFUNC_MAGIC_DEF("writeDoubleLE", 2, qjs_buffer_prototype_write_float,
                       qjs_buffer_magic(8, 1, 1)),
    JS_CFUNC_MAGIC_DEF("writeDoubleBE", 2, qjs_buffer_prototype_write_float,
                       qjs_buffer_magic(8, 1, 0)),
};


static JSClassDef qjs_buffer_class = {
    "Buffer",
    .finalizer = NULL,
};


#ifndef NJS_HAVE_QUICKJS_NEW_TYPED_ARRAY
static JSClassDef qjs_uint8_array_ctor_class = {
    "Uint8ArrayConstructor",
    .finalizer = NULL,
};

#endif


qjs_module_t  qjs_buffer_module = {
    .name = "buffer",
    .init = qjs_buffer_init,
};


static u_char   qjs_basis64[] = {
    77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77,
    77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77,
    77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 62, 77, 77, 77, 63,
    52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 77, 77, 77, 77, 77, 77,
    77,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
    15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 77, 77, 77, 77, 77,
    77, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
    41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 77, 77, 77, 77, 77,

    77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77,
    77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77,
    77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77,
    77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77,
    77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77,
    77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77,
    77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77,
    77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77
};


static u_char   qjs_basis64url[] = {
    77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77,
    77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77,
    77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 62, 77, 77,
    52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 77, 77, 77, 77, 77, 77,
    77,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
    15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 77, 77, 77, 77, 63,
    77, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
    41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 77, 77, 77, 77, 77,

    77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77,
    77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77,
    77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77,
    77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77,
    77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77,
    77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77,
    77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77,
    77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77, 77
};


static u_char   qjs_basis64_enc[] =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
static u_char   qjs_basis64url_enc[] =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";


#define qjs_base64_encoded_length(len)       (((len + 2) / 3) * 4)
#define qjs_base64_decoded_length(len, pad)  (((len / 4) * 3) - pad)


static JSValue
qjs_buffer(JSContext *ctx, JSValueConst this_val, int argc,
    JSValueConst *argv)
{
    JS_ThrowTypeError(ctx, "Buffer() is deprecated. Use the Buffer.alloc() "
                      "or Buffer.from() methods instead.");

    return JS_EXCEPTION;
}


static JSValue
qjs_buffer_ctor(JSContext *ctx, JSValueConst this_val, int argc,
    JSValueConst *argv)
{
    JSValue  ret, proto;

    ret = qjs_new_uint8_array(ctx, argc, argv);
    if (JS_IsException(ret)) {
        return ret;
    }

    proto = JS_GetClassProto(ctx, QJS_CORE_CLASS_ID_BUFFER);
    JS_SetPrototype(ctx, ret, proto);
    JS_FreeValue(ctx, proto);

    return ret;
}


static JSValue
qjs_bufferobj_alloc(JSContext *ctx, JSValueConst this_val, int argc,
    JSValueConst *argv, int ignored)
{
    JSValue   buffer, ret;
    uint32_t  size;

    if (!JS_IsNumber(argv[0])) {
        return JS_ThrowTypeError(ctx, "The \"size\" argument must be of type"
                                 " number");
    }

    if (JS_ToUint32(ctx, &size, argv[0])) {
        return JS_EXCEPTION;
    }

    buffer = qjs_buffer_alloc(ctx, size);
    if (JS_IsException(buffer)) {
        return buffer;
    }

    if (!JS_IsUndefined(argv[1])) {
        ret = qjs_buffer_fill(ctx, buffer, argv[1], argv[2], 0, size);
        if (JS_IsException(ret)) {
            JS_FreeValue(ctx, buffer);
            return ret;
        }
    }

    return buffer;
}


static JSValue
qjs_buffer_byte_length(JSContext *ctx, JSValueConst this_val, int argc,
    JSValueConst *argv)
{
    size_t                       size;
    JSValue                      ret;
    njs_str_t                    src;
    const qjs_buffer_encoding_t  *encoding;

    if (JS_GetArrayBuffer(ctx, &size, argv[0]) != NULL) {
        return JS_NewInt32(ctx, size);
    }

    ret = JS_GetTypedArrayBuffer(ctx, argv[0], NULL, &size, NULL);
    if (!JS_IsException(ret)) {
        JS_FreeValue(ctx, ret);
        return JS_NewInt32(ctx, size);
    }

    if (!JS_IsString(argv[0])) {
        return JS_ThrowTypeError(ctx, "first argument is not a string "
                                 "or Buffer-like object");
    }

    encoding = qjs_buffer_encoding(ctx, argv[1], 1);
    if (encoding == NULL) {
        return JS_EXCEPTION;
    }

    src.start = (u_char *) JS_ToCStringLen(ctx, &src.length, argv[0]);

    if (encoding->decode_length != NULL) {
        size = encoding->decode_length(ctx, &src);

    } else {
        size = src.length;
    }

    JS_FreeCString(ctx, (char *) src.start);

    return JS_NewInt32(ctx, size);
}


static JSValue
qjs_buffer_compare(JSContext *ctx, JSValueConst this_val, int argc,
    JSValueConst *argv)
{
    return qjs_buffer_compare_array(ctx, argv[0], argv[1], argv[2], argv[3],
                                   argv[4], argv[5]);
}


static JSValue
qjs_buffer_concat(JSContext *ctx, JSValueConst this_val, int argc,
    JSValueConst *argv)
{
    u_char     *p;
    size_t     n;
    JSValue    list, length, val, ret, buffer;
    uint32_t   i, len, list_len;
    njs_str_t  buf, dst;

    list = argv[0];

    if (!qjs_is_array(ctx, list)) {
        return JS_ThrowTypeError(ctx,
                            "\"list\" argument must be an instance of Array");
    }

    length = JS_GetPropertyStr(ctx, list, "length");
    if (JS_IsException(length)) {
        return JS_EXCEPTION;
    }

    len = 0;
    if (JS_ToUint32(ctx, &list_len, length)) {
        JS_FreeValue(ctx, length);
        return JS_EXCEPTION;
    }

    JS_FreeValue(ctx, length);

    if (JS_IsUndefined(argv[1])) {
        for (i = 0; i < list_len; i++) {
            val = JS_GetPropertyUint32(ctx, list, i);
            if (JS_IsException(val)) {
                return JS_EXCEPTION;
            }

            ret = qjs_typed_array_data(ctx, val, &buf);
            JS_FreeValue(ctx, val);
            if (JS_IsException(ret)) {
                return JS_ThrowTypeError(ctx, "\"list[%d]\" argument must be an"
                                        " instance of Buffer or Uint8Array", i);
            }

            if ((SIZE_MAX - len) < buf.length) {
                return JS_ThrowTypeError(ctx,
                                         "Total size of buffers is too large");
            }

            len += buf.length;
        }

    } else {
        if (JS_ToUint32(ctx, &len, argv[1])) {
            return JS_EXCEPTION;
        }
    }

    buffer = qjs_buffer_alloc(ctx, len);
    if (JS_IsException(buffer)) {
        return JS_EXCEPTION;
    }

    ret = qjs_typed_array_data(ctx, buffer, &dst);
    if (JS_IsException(ret)) {
        JS_FreeValue(ctx, buffer);
        return JS_EXCEPTION;
    }

    p = dst.start;

    for (i = 0; len != 0 && i < list_len; i++) {
        val = JS_GetPropertyUint32(ctx, list, i);
        if (JS_IsException(val)) {
            JS_FreeValue(ctx, buffer);
            return JS_EXCEPTION;
        }

        ret = qjs_typed_array_data(ctx, val, &buf);
        if (JS_IsException(ret)) {
            JS_FreeValue(ctx, buffer);
            JS_FreeValue(ctx, val);
            return JS_EXCEPTION;
        }

        JS_FreeValue(ctx, val);

        n = njs_min((size_t) len, buf.length);
        p = njs_cpymem(p, buf.start, n);

        len -= n;
    }

    if (len != 0) {
        njs_memzero(p, len);
    }

    return buffer;
}


static JSValue
qjs_buffer_fill(JSContext *ctx, JSValueConst buffer, JSValueConst fill,
    JSValueConst encode, uint64_t offset, uint64_t end)
{
    JSValue    ret, fill_buf;
    uint32_t   n;
    njs_str_t  dst, src;

    ret = qjs_typed_array_data(ctx, buffer, &dst);
    if (JS_IsException(ret)) {
        return ret;
    }

    if (end > dst.length) {
        return JS_ThrowRangeError(ctx, "\"end\" is out of range");
    }

    if (offset >= end) {
        return buffer;
    }

    if (JS_IsNumber(fill)) {
        if (JS_ToUint32(ctx, &n, fill)) {
            return JS_EXCEPTION;
        }

        memset(dst.start + offset, n & 0xff, end - offset);
        return buffer;
    }

    fill_buf = JS_UNDEFINED;

    if (JS_IsString(fill)) {
        fill_buf = qjs_buffer_from_string(ctx, fill, encode);
        if (JS_IsException(fill_buf)) {
            return fill_buf;
        }

        fill = fill_buf;
    }

    ret = qjs_typed_array_data(ctx, fill, &src);
    if (JS_IsException(ret)) {
        JS_FreeValue(ctx, fill_buf);
        return ret;
    }

    if (src.length == 0) {
        memset(dst.start + offset, 0, end - offset);
        JS_FreeValue(ctx, fill_buf);
        return buffer;
    }

    if (src.start >= (dst.start + dst.length)
        || dst.start >= (dst.start + dst.length))
    {
        while (offset < end) {
            n = njs_min(src.length, end - offset);
            memcpy(dst.start + offset, src.start, n);
            offset += n;
        }

    } else {
        while (offset < end) {
            n = njs_min(src.length, end - offset);
            memmove(dst.start + offset, src.start, n);
            offset += n;
        }
    }

    JS_FreeValue(ctx, fill_buf);

    return buffer;
}


static JSValue
qjs_buffer_from(JSContext *ctx, JSValueConst this_val, int argc,
    JSValueConst *argv)
{
    int         float32;
    size_t      off, size, bytes;
    u_char      *buf;
    JSValue     ret, ctor, name, obj, valueOf;
    const char  *str;

    if (JS_IsString(argv[0])) {
        return qjs_buffer_from_string(ctx, argv[0], argv[1]);

    } else if (ret = JS_GetTypedArrayBuffer(ctx, argv[0], &off, &size, &bytes),
               !JS_IsException(ret))
    {
        float32 = 0;

        if (bytes == 4) {
            /*
             * API workaround for JS_GetTypedArrayBuffer()
             * that does not distinguish between Float32Array and Uint32Array.
             */
            ctor = JS_GetPropertyStr(ctx, argv[0], "constructor");
            if (JS_IsException(ctor)) {
                JS_FreeValue(ctx, ret);
                return ctor;
            }

            name = JS_GetPropertyStr(ctx, ctor, "name");
            if (JS_IsException(name)) {
                JS_FreeValue(ctx, ret);
                return name;
            }

            JS_FreeValue(ctx, ctor);
            str = JS_ToCString(ctx, name);

            if (strncmp(str, "Float32Array", 12) == 0) {
                float32 = 1;
            }

            JS_FreeCString(ctx, str);
            JS_FreeValue(ctx, name);
        }

        return qjs_buffer_from_typed_array(ctx, ret, off, size, bytes, float32);

    } else if ((buf = JS_GetArrayBuffer(ctx, &size, argv[0])) != NULL) {
        return qjs_buffer_ctor(ctx, JS_UNDEFINED, argc, argv);

    } else if (JS_IsObject(argv[0])) {
        obj = argv[0];
        valueOf = JS_GetPropertyStr(ctx, obj, "valueOf");
        if (JS_IsException(valueOf)) {
            return valueOf;
        }

        if (JS_IsFunction(ctx, valueOf)) {
            ret = JS_Call(ctx, valueOf, obj, 0, NULL);
            JS_FreeValue(ctx, valueOf);
            if (JS_IsException(ret)) {
                return ret;
            }

            if (JS_IsString(ret)) {
                obj = ret;
                ret = qjs_buffer_from_string(ctx, obj, argv[1]);
                JS_FreeValue(ctx, obj);
                return ret;
            }

            if (JS_IsObject(ret)
                && JS_VALUE_GET_PTR(ret) != JS_VALUE_GET_PTR(obj))
            {
                obj = ret;
                ret = qjs_buffer_from_object(ctx, obj);
                JS_FreeValue(ctx, obj);
                return ret;
            }

            JS_FreeValue(ctx, ret);
        }

        return qjs_buffer_from_object(ctx, obj);
    }

    JS_ThrowTypeError(ctx, "first argument is not a string "
                      "or Buffer-like object");
    return JS_EXCEPTION;
}


static JSValue
qjs_buffer_is_buffer(JSContext *ctx, JSValueConst this_val,
    int argc, JSValueConst *argv)
{
    JSValue proto, buffer_proto, ret;

    proto = JS_GetPrototype(ctx, argv[0]);
    buffer_proto = JS_GetClassProto(ctx, QJS_CORE_CLASS_ID_BUFFER);

    ret = JS_NewBool(ctx, JS_IsObject(argv[0])
                          && qjs_is_same_value(ctx, proto, buffer_proto));

    JS_FreeValue(ctx, buffer_proto);
    JS_FreeValue(ctx, proto);

    return ret;
}


static JSValue
qjs_buffer_is_encoding(JSContext *ctx, JSValueConst this_val,
    int argc, JSValueConst *argv)
{
    return JS_NewBool(ctx, qjs_buffer_encoding(ctx, argv[0], 0) != NULL);
}


static JSValue
qjs_buffer_array_range(JSContext *ctx, njs_str_t *array, JSValueConst start,
    JSValueConst end, const char *name)
{
    int64_t  num_start, num_end;

    num_start = 0;

    if (!JS_IsUndefined(start)) {
        if (JS_ToInt64(ctx, &num_start, start)) {
            return JS_EXCEPTION;
        }
    }

    if (num_start < 0 || (size_t) num_start > array->length) {
        return JS_ThrowRangeError(ctx, "\"%sStart\" is out of range: %" PRId64,
                                  name, num_start);
    }

    num_end = array->length;

    if (!JS_IsUndefined(end)) {
        if (JS_ToInt64(ctx, &num_end, end)) {
            return JS_EXCEPTION;
        }
    }

    if (num_end < 0 || (size_t) num_end > array->length) {
        return JS_ThrowRangeError(ctx, "\"%sEnd\" is out of range: %" PRId64,
                                  name, num_end);
    }

    if (num_start > num_end) {
        num_end = num_start;
    }

    array->start += num_start;
    array->length = num_end - num_start;

    return JS_UNDEFINED;
}


static JSValue
qjs_buffer_compare_array(JSContext *ctx, JSValue val1, JSValue val2,
    JSValueConst target_start, JSValueConst target_end,
    JSValueConst source_start, JSValueConst source_end)
{
    int        rc;
    size_t     size;
    JSValue    ret;
    njs_str_t  src, target;

    ret = qjs_typed_array_data(ctx, val1, &src);
    if (JS_IsException(ret)) {
        return ret;
    }

    ret = qjs_typed_array_data(ctx, val2, &target);
    if (JS_IsException(ret)) {
        return ret;
    }

    ret = qjs_buffer_array_range(ctx, &src, source_start, source_end, "source");
    if (JS_IsException(ret)) {
        return ret;
    }

    ret = qjs_buffer_array_range(ctx, &target, target_start, target_end,
                                 "target");
    if (JS_IsException(ret)) {
        return ret;
    }

    size = njs_min(src.length, target.length);

    rc = memcmp(src.start, target.start, size);

    if (rc != 0) {
        return JS_NewInt32(ctx, (rc < 0) ? -1 : 1);
    }

    if (target.length > src.length) {
        rc = -1;

    } else if (target.length < src.length) {
        rc = 1;
    }

    return JS_NewInt32(ctx, rc);
}


static JSValue
qjs_buffer_prototype_compare(JSContext *ctx, JSValueConst this_val, int argc,
    JSValueConst *argv)
{
    return qjs_buffer_compare_array(ctx, this_val, argv[0], argv[1], argv[2],
                                   argv[3], argv[4]);
}


static JSValue
qjs_buffer_prototype_copy(JSContext *ctx, JSValueConst this_val, int argc,
    JSValueConst *argv)
{
    size_t     size;
    JSValue    ret;
    njs_str_t  src, target;

    ret = qjs_typed_array_data(ctx, this_val, &src);
    if (JS_IsException(ret)) {
        return ret;
    }

    ret = qjs_typed_array_data(ctx, argv[0], &target);
    if (JS_IsException(ret)) {
        return ret;
    }

    ret = qjs_buffer_array_range(ctx, &target, argv[1], JS_UNDEFINED, "target");
    if (JS_IsException(ret)) {
        return ret;
    }

    ret = qjs_buffer_array_range(ctx, &src, argv[2], argv[3], "source");
    if (JS_IsException(ret)) {
        return ret;
    }

    size = njs_min(src.length, target.length);

    if (src.start >= (target.start + size)
        || target.start >= (src.start + size))
    {
        memcpy(target.start, src.start, size);

    } else {
        memmove(target.start, src.start, size);
    }

    return JS_NewInt32(ctx, size);
}


static JSValue
qjs_buffer_prototype_equals(JSContext *ctx, JSValueConst this_val, int argc,
    JSValueConst *argv)
{
    JSValue  ret;

    ret = qjs_buffer_compare_array(ctx, this_val, argv[0], JS_UNDEFINED,
                                   JS_UNDEFINED, JS_UNDEFINED, JS_UNDEFINED);
    if (JS_IsException(ret)) {
        return ret;
    }

    return JS_NewBool(ctx, JS_VALUE_GET_INT(ret) == 0);
}


static JSValue
qjs_buffer_prototype_fill(JSContext *ctx, JSValueConst this_val, int argc,
    JSValueConst *argv)
{
    JSValue    ret, encode;
    uint64_t   offset, end;
    njs_str_t  dst;

    offset = 0;
    encode = argv[3];

    ret = qjs_typed_array_data(ctx, this_val, &dst);
    if (JS_IsException(ret)) {
        return ret;
    }

    end = dst.length;

    if (!JS_IsUndefined(argv[1])) {
        if (JS_IsString(argv[0]) && JS_IsString(argv[1])) {
            encode = argv[1];
            goto fill;
        }

        if (JS_ToIndex(ctx, &offset, argv[1])) {
            return JS_EXCEPTION;
        }
    }

    if (!JS_IsUndefined(argv[2])) {
        if (JS_IsString(argv[0]) && JS_IsString(argv[2])) {
            encode = argv[2];
            goto fill;
        }

        if (JS_ToIndex(ctx, &end, argv[2])) {
            return JS_EXCEPTION;
        }
    }

fill:

    ret = qjs_buffer_fill(ctx, this_val, argv[0], encode, offset, end);
    if (JS_IsException(ret)) {
        return ret;
    }

    JS_DupValue(ctx, ret);

    return ret;
}


static JSValue
qjs_buffer_prototype_includes(JSContext *ctx, JSValueConst this_val, int argc,
    JSValueConst *argv)
{
    JSValue    ret;

    ret = qjs_buffer_prototype_index_of(ctx, this_val, argc, argv, 0);
    if (JS_IsException(ret)) {
        return ret;
    }

    return JS_NewBool(ctx, JS_VALUE_GET_INT(ret) != -1);
}


static JSValue
qjs_buffer_prototype_index_of(JSContext *ctx, JSValueConst this_val, int argc,
    JSValueConst *argv, int last)
{
    JSValue                      ret, buffer, encode, value;
    int64_t                      from, to, increment, length, i;
    uint32_t                     byte;
    njs_str_t                    self, str;
    const qjs_buffer_encoding_t  *encoding;

    ret = qjs_typed_array_data(ctx, this_val, &self);
    if (JS_IsException(ret)) {
        return ret;
    }

    length = self.length;

    if (last) {
        from = length - 1;
        to = -1;
        increment = -1;

    } else {
        from = 0;
        to = length;
        increment = 1;
    }

    encode = argv[2];

    if (!JS_IsUndefined(argv[1])) {
        if (JS_IsString(argv[0]) && JS_IsString(argv[1])) {
            encode = argv[1];
            goto encoding;
        }

        if (JS_ToInt64(ctx, &from, argv[1])) {
            return JS_EXCEPTION;
        }

        if (from >= 0) {
            from = njs_min(from, length);

        } else {
            from = njs_max(0, length + from);
        }
    }

    if (JS_IsNumber(argv[0])) {
        if (JS_ToUint32(ctx, &byte, argv[0])) {
            return JS_EXCEPTION;
        }

        if (last) {
            from = njs_min(from, length - 1);
        }

        for (i = from; i != to; i += increment) {
            if (self.start[i] == (uint8_t) byte) {
                return JS_NewInt32(ctx, i);
            }
        }

        return JS_NewInt32(ctx, -1);
    }

encoding:

    buffer = JS_UNDEFINED;
    value = argv[0];

    if (JS_IsString(value)) {
        encoding = qjs_buffer_encoding(ctx, encode, 1);
        if (encoding == NULL) {
            return JS_EXCEPTION;
        }

        buffer = qjs_buffer_from_string(ctx, value, encode);
        if (JS_IsException(buffer)) {
            return buffer;
        }

        value = buffer;
    }

    ret = qjs_typed_array_data(ctx, value, &str);
    if (JS_IsException(ret)) {
        JS_FreeValue(ctx, buffer);
        return JS_ThrowTypeError(ctx, "\"value\" argument is not a string "
                                "or Buffer-like object");
    }

    if (last) {
        from = njs_min(from, length - (int64_t) str.length);

        if (to > from) {
            goto done;
        }

    } else {
        to -= (int64_t) str.length - 1;

        if (from > to) {
            goto done;
        }
    }

    if (from == to && str.length == 0) {
        JS_FreeValue(ctx, buffer);
        return JS_NewInt32(ctx, 0);
    }

    for (i = from; i != to; i += increment) {
        if (memcmp(&self.start[i], str.start, str.length) == 0) {
            JS_FreeValue(ctx, buffer);
            return JS_NewInt32(ctx, i);
        }
    }

done:

    JS_FreeValue(ctx, buffer);
    return JS_NewInt32(ctx, -1);
}


static JSValue
qjs_buffer_prototype_read_float(JSContext *ctx, JSValueConst this_val,
    int argc, JSValueConst *argv, int magic)
{
    double          v;
    JSValue         ret;
    uint32_t        u32;
    uint64_t        u64, index, size;
    njs_str_t       self;
    njs_bool_t      little, swap;
    njs_conv_f32_t  conv_f32;
    njs_conv_f64_t  conv_f64;

    ret = qjs_typed_array_data(ctx, this_val, &self);
    if (JS_IsException(ret)) {
        return ret;
    }

    if (JS_ToIndex(ctx, &index, argv[0])) {
        return JS_EXCEPTION;
    }

    size = magic >> 2;

    if (size + index > self.length) {
        return JS_ThrowRangeError(ctx, "index %" PRIu64 " is outside the bound"
                                  " of the buffer", index);
    }

    little = magic & 1;
    swap = little;

#if NJS_HAVE_LITTLE_ENDIAN
    swap = !swap;
#endif

    switch (size) {
    case 4:
        u32 = *((uint32_t *) &self.start[index]);

        if (swap) {
            u32 = njs_bswap_u32(u32);
        }

        conv_f32.u = u32;
        v = conv_f32.f;
        break;

    case 8:
    default:
        u64 = *((uint64_t *) &self.start[index]);

        if (swap) {
            u64 = njs_bswap_u64(u64);
        }

        conv_f64.u = u64;
        v = conv_f64.f;
        break;
    }

    return JS_NewFloat64(ctx, v);
}


static JSValue
qjs_buffer_prototype_read_int(JSContext *ctx, JSValueConst this_val,
    int argc, JSValueConst *argv, int magic)
{
    JSValue     ret;
    uint32_t    u32;
    uint64_t    u64, index, size;
    njs_str_t   self;
    njs_bool_t  little, swap, sign;

    ret = qjs_typed_array_data(ctx, this_val, &self);
    if (JS_IsException(ret)) {
        return ret;
    }

    if (JS_ToIndex(ctx, &index, argv[0])) {
        return JS_EXCEPTION;
    }

    size = magic >> 2;

    if (!size) {
        if (!JS_IsNumber(argv[1])) {
            return JS_ThrowTypeError(ctx, "\"byteLength\" is not a number");
        }

        if (JS_ToIndex(ctx, &size, argv[1])) {
            return JS_EXCEPTION;
        }

        if (size > 6) {
            return JS_ThrowRangeError(ctx, "\"byteLength\" must be <= 6");
        }
    }

    if (size + index > self.length) {
        return JS_ThrowRangeError(ctx, "index %" PRIu64 " is outside the bound"
                                  " of the buffer", index);
    }

    sign = (magic >> 1) & 1;
    little = magic & 1;
    swap = little;

#if NJS_HAVE_LITTLE_ENDIAN
    swap = !swap;
#endif

    switch (size) {
    case 1:
        if (sign) {
            return JS_NewInt32(ctx, (int8_t) self.start[index]);
        }

        return JS_NewUint32(ctx, self.start[index]);

    case 2:
        u32 = njs_get_u16(&self.start[index]);

        if (swap) {
            u32 = njs_bswap_u16(u32);
        }

        if (sign) {
            /* Sign extension. */
            u32 |= (u32 & (INT16_MAX + 1ULL)) * UINT32_MAX;

            return JS_NewInt32(ctx, (int16_t) u32);
        }

        return JS_NewUint32(ctx, u32);

    case 3:

        if (little) {
            u32 = (self.start[index + 2] << 16)
                  | (self.start[index + 1] << 8)
                  | self.start[index];

        } else {
            u32 = (self.start[index] << 16)
                  | (self.start[index + 1] << 8)
                  | self.start[index + 2];
        }

        if (sign) {
            /* Sign extension. */
            u32 |= (u32 & (INT24_MAX + 1ULL)) * UINT32_MAX;

            return JS_NewInt32(ctx, (int32_t) u32);
        }

        return JS_NewUint32(ctx, u32);

    case 4:
        u32 = njs_get_u32(&self.start[index]);

        if (swap) {
            u32 = njs_bswap_u32(u32);
        }

        if (sign) {
            /* Sign extension. */
            u32 |= (u32 & (INT32_MAX + 1ULL)) * UINT32_MAX;

            return JS_NewInt32(ctx, (int32_t) u32);
        }

        return JS_NewUint32(ctx, u32);

    case 5:
        if (little) {
            u64 = ((uint64_t) self.start[index + 4] << 32)
                  | ((uint64_t) self.start[index + 3] << 24)
                  | (self.start[index + 2] << 16)
                  | (self.start[index + 1] << 8)
                  | self.start[index];

        } else {
            u64 = ((uint64_t) self.start[index] << 32)
                  | ((uint64_t) self.start[index + 1] << 24)
                  | (self.start[index + 2] << 16)
                  | (self.start[index + 3] << 8)
                  | self.start[index + 4];
        }

        if (sign) {
            /* Sign extension. */
            u64 |= (u64 & (INT40_MAX + 1ULL)) * UINT64_MAX;

            return JS_NewFloat64(ctx, (int64_t) u64);
        }

        return JS_NewFloat64(ctx, u64);

    case 6:
    default:
        if (little) {
            u64 = ((uint64_t) self.start[index + 5] << 40)
                  | ((uint64_t) self.start[index + 4] << 32)
                  | ((uint64_t) self.start[index + 3] << 24)
                  | (self.start[index + 2] << 16)
                  | (self.start[index + 1] << 8)
                  | self.start[index];

        } else {
            u64 = ((uint64_t) self.start[index] << 40)
                  | ((uint64_t) self.start[index + 1] << 32)
                  | ((uint64_t) self.start[index + 2] << 24)
                  | (self.start[index + 3] << 16)
                  | (self.start[index + 4] << 8)
                  | self.start[index + 5];
        }

        if (sign) {
            /* Sign extension. */
            u64 |= (u64 & (INT48_MAX + 1ULL)) * UINT64_MAX;

            return JS_NewFloat64(ctx, (int64_t) u64);
        }

        return JS_NewFloat64(ctx, u64);
    }
}


static JSValue
qjs_buffer_prototype_to_json(JSContext *ctx, JSValueConst this_val, int argc,
    JSValueConst *argv)
{
    int         rc;
    JSValue     obj, data, ret;
    njs_str_t   src;
    njs_uint_t  i;

    ret = qjs_typed_array_data(ctx, this_val, &src);
    if (JS_IsException(ret)) {
        return ret;
    }

    obj = JS_NewObject(ctx);
    if (JS_IsException(obj)) {
        return obj;
    }

    data = JS_NewArray(ctx);
    if (JS_IsException(data)) {
        JS_FreeValue(ctx, obj);
        return data;
    }

    rc = JS_DefinePropertyValueStr(ctx, obj, "type",
                                   JS_NewString(ctx, "Buffer"),
                                   JS_PROP_ENUMERABLE);
    if (rc == -1) {
        JS_FreeValue(ctx, obj);
        JS_FreeValue(ctx, data);
        return ret;
    }

    rc = JS_DefinePropertyValueStr(ctx, obj, "data", data, JS_PROP_ENUMERABLE);
    if (rc == -1) {
        JS_FreeValue(ctx, obj);
        JS_FreeValue(ctx, data);
        return ret;
    }

    for (i = 0; i < src.length; i++) {
        rc = JS_SetPropertyUint32(ctx, data, i, JS_NewInt32(ctx, src.start[i]));
        if (rc == -1) {
            JS_FreeValue(ctx, obj);
            JS_FreeValue(ctx, data);
            return ret;
        }
    }

    return obj;
}


static JSValue
qjs_buffer_prototype_swap(JSContext *ctx, JSValueConst this_val,
    int argc, JSValueConst *argv, int size)
{
    uint8_t    *p, *end;
    JSValue    ret;
    njs_str_t  self;

    ret = qjs_typed_array_data(ctx, this_val, &self);
    if (JS_IsException(ret)) {
        return ret;
    }

    if ((self.length % size) != 0) {
        return JS_ThrowRangeError(ctx, "Buffer size must be a multiple "
                                  "of %d-bits", (int) (size << 3));
    }

    p = self.start;
    end = p + self.length;

    switch (size) {
    case 2:
        for (; p < end; p += 2) {
            njs_set_u16(p, njs_bswap_u16(njs_get_u16(p)));
        }

        break;

    case 4:
        for (; p < end; p += 4) {
            njs_set_u32(p, njs_bswap_u32(njs_get_u32(p)));
        }

        break;

    case 8:
    default:
        for (; p < end; p += 8) {
            njs_set_u64(p, njs_bswap_u64(njs_get_u64(p)));
        }
    }

    JS_DupValue(ctx, this_val);

    return this_val;
}


static JSValue
qjs_buffer_prototype_to_string(JSContext *ctx, JSValueConst this_val,
    int argc, JSValueConst *argv)
{
    JSValue                      ret;
    njs_str_t                    src, data;
    const qjs_buffer_encoding_t  *encoding;

    ret = qjs_typed_array_data(ctx, this_val, &src);
    if (JS_IsException(ret)) {
        return JS_ThrowTypeError(ctx, "method toString() called on incompatible"
                                 " object");
    }

    if (JS_IsUndefined(argv[0]) || src.length == 0) {
        return JS_NewStringLen(ctx, (char *) src.start, src.length);
    }

    encoding = qjs_buffer_encoding(ctx, argv[0], 1);
    if (njs_slow_path(encoding == NULL)) {
        return JS_EXCEPTION;
    }

    if (encoding->encode_length == NULL) {
        return JS_NewStringLen(ctx, (char *) src.start, src.length);
    }

    data.length = encoding->encode_length(ctx, &src);
    data.start = js_malloc(ctx, data.length);
    if (njs_slow_path(data.start == NULL)) {
        JS_ThrowOutOfMemory(ctx);
        return JS_EXCEPTION;
    }

    if (encoding->encode(ctx, &src, &data) != 0) {
        js_free(ctx, data.start);
        JS_ThrowTypeError(ctx, "failed to encode buffer");
        return JS_EXCEPTION;
    }

    ret = JS_NewStringLen(ctx, (char *) data.start, data.length);

    js_free(ctx, data.start);

    return ret;
}


static JSValue
qjs_buffer_prototype_write(JSContext *ctx, JSValueConst this_val,
    int argc, JSValueConst *argv)
{
    JSValue                      ret, buffer, encode;
    uint64_t                     offset, max_length;
    njs_str_t                    self, src;
    const uint8_t                *p, *end, *prev;
    const qjs_buffer_encoding_t  *encoding;

    ret = qjs_typed_array_data(ctx, this_val, &self);
    if (JS_IsException(ret)) {
        return ret;
    }

    offset = 0;
    max_length = self.length;
    encode = argv[3];

    if (!JS_IsUndefined(argv[1])) {
        if (JS_IsString(argv[0]) && JS_IsString(argv[1])) {
            encode = argv[1];
            goto write;
        }

        if (JS_ToIndex(ctx, &offset, argv[1])) {
            return JS_EXCEPTION;
        }

        max_length = self.length - offset;
    }

    if (!JS_IsUndefined(argv[2])) {
        if (JS_IsString(argv[0]) && JS_IsString(argv[2])) {
            encode = argv[2];
            goto write;
        }

        if (JS_ToIndex(ctx, &max_length, argv[2])) {
            return JS_EXCEPTION;
        }
    }

write:

    encoding = qjs_buffer_encoding(ctx, encode, 1);
    if (encoding == NULL) {
        return JS_EXCEPTION;
    }

    buffer = qjs_buffer_from_string(ctx, argv[0], encode);
    if (JS_IsException(buffer)) {
        return buffer;
    }

    (void) qjs_typed_array_data(ctx, buffer, &src);

    if (offset > self.length) {
        JS_FreeValue(ctx, buffer);
        return JS_ThrowRangeError(ctx, "\"offset\" is out of range");
    }

    if (src.length == 0) {
        JS_FreeValue(ctx, buffer);
        return JS_NewInt32(ctx, 0);
    }

    if (max_length > self.length - offset) {
        JS_FreeValue(ctx, buffer);
        return JS_ThrowRangeError(ctx, "\"length\" is out of range");
    }

    max_length = njs_min(max_length, src.length);

    if (encoding->decode == NULL) {
        /* Avoid writing incomplete UTF-8 characters. */
        p = prev = src.start;
        end = p + max_length;

        while (p < end) {
            p = njs_utf8_next(p, src.start + src.length);
            if (p <= end) {
                prev = p;
            }
        }

        max_length = prev - src.start;
    }

    memcpy(&self.start[offset], src.start, max_length);

    JS_FreeValue(ctx, buffer);

    return JS_NewInt32(ctx, max_length);
}


static JSValue
qjs_buffer_prototype_write_int(JSContext *ctx, JSValueConst this_val,
    int argc, JSValueConst *argv, int magic)
{
    JSValue     ret;
    int64_t     i64;
    uint32_t    u32;
    uint64_t    index, size;
    njs_str_t   self;
    njs_bool_t  little, swap, sign;

    ret = qjs_typed_array_data(ctx, this_val, &self);
    if (JS_IsException(ret)) {
        return ret;
    }

    if (JS_ToIndex(ctx, &index, argv[1])) {
        return JS_EXCEPTION;
    }

    size = magic >> 2;

    if (!size) {
        if (JS_ToIndex(ctx, &size, argv[2])) {
            return JS_EXCEPTION;
        }

        if (size > 6) {
            return JS_ThrowRangeError(ctx, "\"byteLength\" must be <= 6");
        }
    }

    if (size + index > self.length) {
        return JS_ThrowRangeError(ctx, "index %" PRIu64 " is outside the bound"
                                  " of the buffer", index);
    }

    little = magic & 1;
    sign = (magic >> 1) & 1;
    swap = little;

#if NJS_HAVE_LITTLE_ENDIAN
    swap = !swap;
#endif

    if (JS_ToInt64(ctx, &i64, argv[0])) {
        return JS_EXCEPTION;
    }

    switch (size) {
    case 1:
        if (sign) {
            if (i64 < INT8_MIN || i64 > INT8_MAX) {
                return JS_ThrowRangeError(ctx, "value is outside the range of"
                                          " representable values");
            }

        } else {
            if (i64 < 0 || i64 > UINT8_MAX) {
                return JS_ThrowRangeError(ctx, "value is outside the range of"
                                          " representable values");
            }
        }

        self.start[index] = (uint8_t) i64;
        break;

    case 2:
        u32 = (uint16_t) i64;

        if (sign) {
            if (i64 < INT16_MIN || i64 > INT16_MAX) {
                return JS_ThrowRangeError(ctx, "value is outside the range of"
                                          " representable values");
            }

        } else {
            if (i64 < 0 || i64 > UINT16_MAX) {
                return JS_ThrowRangeError(ctx, "value is outside the range of"
                                          " representable values");
            }
        }

        if (swap) {
            u32 = njs_bswap_u16(u32);
        }

        njs_set_u16(&self.start[index], u32);
        break;

    case 3:
        if (sign) {
            if (i64 < INT24_MIN || i64 > INT24_MAX) {
                return JS_ThrowRangeError(ctx, "value is outside the range of"
                                          " representable values");
            }

        } else {
            if (i64 < 0 || i64 > UINT24_MAX) {
                return JS_ThrowRangeError(ctx, "value is outside the range of"
                                          " representable values");
            }
        }

        if (little) {
            self.start[index] = i64; i64 >>= 8;
            self.start[index + 1] = i64; i64 >>= 8;
            self.start[index + 2] = i64;

        } else {
            self.start[index + 2] = i64; i64 >>= 8;
            self.start[index + 1] = i64; i64 >>= 8;
            self.start[index] = i64;
        }

        break;

    case 4:
        u32 = i64;

        if (sign) {
            if (i64 < INT32_MIN || i64 > INT32_MAX) {
                return JS_ThrowRangeError(ctx, "value is outside the range of"
                                          " representable values");
            }

        } else {
            if (i64 < 0 || i64 > UINT32_MAX) {
                return JS_ThrowRangeError(ctx, "value is outside the range of"
                                          " representable values");
            }
        }

        if (swap) {
            u32 = njs_bswap_u32(u32);
        }

        njs_set_u32(&self.start[index], u32);
        break;

    case 5:
        if (sign) {
            if (i64 < INT40_MIN || i64 > INT40_MAX) {
                return JS_ThrowRangeError(ctx, "value is outside the range of"
                                          " representable values");
            }

        } else {
            if (i64 < 0 || i64 > UINT40_MAX) {
                return JS_ThrowRangeError(ctx, "value is outside the range of"
                                          " representable values");
            }
        }

        if (little) {
            self.start[index] = i64; i64 >>= 8;
            self.start[index + 1] = i64; i64 >>= 8;
            self.start[index + 2] = i64; i64 >>= 8;
            self.start[index + 3] = i64; i64 >>= 8;
            self.start[index + 4] = i64;

        } else {
            self.start[index + 4] = i64; i64 >>= 8;
            self.start[index + 3] = i64; i64 >>= 8;
            self.start[index + 2] = i64; i64 >>= 8;
            self.start[index + 1] = i64; i64 >>= 8;
            self.start[index] = i64;
        }

        break;

    case 6:
    default:
        if (sign) {
            if (i64 < INT48_MIN || i64 > INT48_MAX) {
                return JS_ThrowRangeError(ctx, "value is outside the range of"
                                          " representable values");
            }

        } else {
            if (i64 < 0 || i64 > UINT48_MAX) {
                return JS_ThrowRangeError(ctx, "value is outside the range of"
                                          " representable values");
            }
        }

        if (little) {
            self.start[index] = i64; i64 >>= 8;
            self.start[index + 1] = i64; i64 >>= 8;
            self.start[index + 2] = i64; i64 >>= 8;
            self.start[index + 3] = i64; i64 >>= 8;
            self.start[index + 4] = i64; i64 >>= 8;
            self.start[index + 5] = i64;

        } else {
            self.start[index + 5] = i64; i64 >>= 8;
            self.start[index + 4] = i64; i64 >>= 8;
            self.start[index + 3] = i64; i64 >>= 8;
            self.start[index + 2] = i64; i64 >>= 8;
            self.start[index + 1] = i64; i64 >>= 8;
            self.start[index] = i64;
        }

        break;
    }

    return JS_NewInt32(ctx, size + index);
}


static JSValue
qjs_buffer_prototype_write_float(JSContext *ctx, JSValueConst this_val,
    int argc, JSValueConst *argv, int magic)
{
    double          v;
    JSValue         ret;
    uint32_t        u32;
    uint64_t        u64, index, size;
    njs_str_t       self;
    njs_bool_t      little, swap;
    njs_conv_f32_t  conv_f32;
    njs_conv_f64_t  conv_f64;

    ret = qjs_typed_array_data(ctx, this_val, &self);
    if (JS_IsException(ret)) {
        return ret;
    }

    if (JS_ToFloat64(ctx, &v, argv[0])) {
        return JS_EXCEPTION;
    }

    if (JS_ToIndex(ctx, &index, argv[1])) {
        return JS_EXCEPTION;
    }

    size = magic >> 2;

    if (size + index > self.length) {
        return JS_ThrowRangeError(ctx, "index %" PRIu64 " is outside the bound"
                                  " of the buffer", index);
    }

    little = magic & 1;
    swap = little;

#if NJS_HAVE_LITTLE_ENDIAN
    swap = !swap;
#endif

    switch (size) {
    case 4:
        conv_f32.f = (float) v;

        if (swap) {
            conv_f32.u = njs_bswap_u32(conv_f32.u);
        }

        u32 = conv_f32.u;
        memcpy(&self.start[index], &u32, size);
        break;

    case 8:
    default:
        conv_f64.f = v;

        if (swap) {
            conv_f64.u = njs_bswap_u64(conv_f64.u);
        }

        u64 = conv_f64.u;
        memcpy(&self.start[index], &u64, size);
        break;
    }

    return JS_NewInt32(ctx, size + index);
}


static JSValue
qjs_buffer_from_string(JSContext *ctx, JSValueConst str,
    JSValueConst enc)
{
    size_t                       size;
    JSValue                      buffer, ret;
    njs_str_t                    src, dst;
    const qjs_buffer_encoding_t  *encoding;

    if (!JS_IsString(str)) {
        JS_ThrowTypeError(ctx, "first argument is not a string");
        return JS_EXCEPTION;
    }

    encoding = qjs_buffer_encoding(ctx, enc, 1);
    if (njs_slow_path(encoding == NULL)) {
        return JS_EXCEPTION;
    }

    src.start = (u_char *) JS_ToCStringLen(ctx, &src.length, str);

    if (encoding->decode_length != NULL) {
        size = encoding->decode_length(ctx, &src);

    } else {
        size = src.length;
    }

    buffer = qjs_buffer_alloc(ctx, size);
    if (JS_IsException(buffer)) {
        JS_FreeCString(ctx, (char *) src.start);
        return buffer;
    }

    ret = qjs_typed_array_data(ctx, buffer, &dst);
    if (JS_IsException(ret)) {
        JS_FreeCString(ctx, (char *) src.start);
        return ret;
    }

    if (encoding->decode != NULL) {
        if (encoding->decode(ctx, &src, &dst) != 0) {
            JS_FreeCString(ctx, (char *) src.start);
            JS_ThrowTypeError(ctx, "failed to decode string");
            return JS_EXCEPTION;
        }

    } else {
        memcpy(dst.start, src.start, src.length);
    }

    JS_FreeCString(ctx, (char *) src.start);

    return buffer;
}


static JSValue
qjs_buffer_from_typed_array(JSContext *ctx, JSValueConst arr_buf,
    size_t offset, size_t size, size_t bytes, int float32)
{
    float      *f32;
    u_char     *p, *u8;
    size_t     i;
    double     *f64;
    JSValue    buffer, ret;
    uint16_t   *u16;
    uint32_t   *u32;
    njs_str_t  src, dst;

    size = size / bytes;
    buffer = qjs_buffer_alloc(ctx, size);
    if (JS_IsException(buffer)) {
        JS_FreeValue(ctx, arr_buf);
        return buffer;
    }

    ret = qjs_typed_array_data(ctx, buffer, &dst);
    if (JS_IsException(ret)) {
        JS_FreeValue(ctx, arr_buf);
        JS_FreeValue(ctx, buffer);
        return ret;
    }

    src.start = JS_GetArrayBuffer(ctx, &src.length, arr_buf);
    if (src.start == NULL) {
        JS_FreeValue(ctx, arr_buf);
        JS_FreeValue(ctx, buffer);
        return JS_EXCEPTION;
    }

    p = dst.start;

    switch (bytes) {
    case 1:
        u8 = src.start;
        memcpy(p, u8 + offset, size);
        break;

    case 2:
        u16 = (uint16_t *) src.start;

        for (i = 0; i < size; i++) {
            *p++ = u16[offset + i];
        }

        break;

    case 4:
        if (float32) {
            f32 = (float *) src.start;

            for (i = 0; i < size; i++) {
                *p++ = f32[offset + i];
            }

            break;
        }

        u32 = (uint32_t *) src.start;

        for (i = 0; i < size; i++) {
            *p++ = u32[offset + i];
        }

        break;

    case 8:
        f64 = (double *) src.start;

        for (i = 0; i < size; i++) {
            *p++ = f64[offset + i];
        }

        break;
    }

    JS_FreeValue(ctx, arr_buf);

    return buffer;
}


static JSValue
qjs_buffer_from_object(JSContext *ctx, JSValueConst obj)
{
    int         v;
    u_char      *p;
    int64_t     i, len;
    JSValue     buffer, ret;
    njs_str_t   dst;
    const char  *str;

    ret = JS_GetPropertyStr(ctx, obj, "length");
    if (JS_IsException(ret)) {
        return ret;
    }

    if (JS_IsUndefined(ret)) {
        ret = JS_GetPropertyStr(ctx, obj, "type");
        if (JS_IsException(ret)) {
            return ret;
        }

        if (JS_IsString(ret)) {
            str = JS_ToCString(ctx, ret);
            JS_FreeValue(ctx, ret);
            if (str != NULL) {
                if (strcmp(str, "Buffer") != 0) {
                    JS_FreeCString(ctx, str);
                    goto reject;
                }

                JS_FreeCString(ctx, str);

                ret = JS_GetPropertyStr(ctx, obj, "data");
                if (JS_IsException(ret)) {
                    return ret;
                }

                if (JS_IsObject(ret)) {
                    obj = ret;
                    ret = qjs_buffer_from_object(ctx, obj);
                    JS_FreeValue(ctx, obj);
                    return ret;
                }
            }
        }
    }

    if (!JS_IsNumber(ret)) {
        JS_FreeValue(ctx, ret);
reject:
        JS_ThrowTypeError(ctx, "first argument is not a string "
                          "or Buffer-like object");
        return JS_EXCEPTION;
    }

    len = JS_VALUE_GET_INT(ret);

    buffer = qjs_buffer_alloc(ctx, len);
    if (JS_IsException(buffer)) {
        return buffer;
    }

    ret = qjs_typed_array_data(ctx, buffer, &dst);
    if (JS_IsException(ret)) {
        return ret;
    }

    p = dst.start;

    for (i = 0; i < len; i++) {
        ret = JS_GetPropertyUint32(ctx, obj, i);
        if (njs_slow_path(JS_IsException(ret))) {
            return ret;
        }

        if (njs_slow_path(JS_ToInt32(ctx, &v, ret))) {
            return JS_EXCEPTION;
        }

        JS_FreeValue(ctx, ret);

        *p++ = v;
    }

    return buffer;
}


const qjs_buffer_encoding_t *
qjs_buffer_encoding(JSContext *ctx, JSValueConst value, JS_BOOL thrw)
{
    njs_str_t              name;
    qjs_buffer_encoding_t  *encoding;

    if (!JS_IsString(value)){
        if (!JS_IsUndefined(value)) {
            JS_ThrowTypeError(ctx, "encoding must be a string");
            return NULL;
        }

        return &qjs_buffer_encodings[0];
    }

    name.start = (u_char *) JS_ToCStringLen(ctx, &name.length, value);

    for (encoding = &qjs_buffer_encodings[0];
         encoding->name.length != 0;
         encoding++)
    {
        if (njs_strstr_eq(&name, &encoding->name)) {
            JS_FreeCString(ctx, (char *) name.start);
            return encoding;
        }
    }

    JS_FreeCString(ctx, (char *) name.start);

    if (thrw) {
        JS_ThrowTypeError(ctx, "\"%.*s\" encoding is not supported",
                          (int) name.length, name.start);
    }

    return NULL;
}


static void
qjs_base64_encode_core(njs_str_t *dst, const njs_str_t *src,
    const u_char *basis, njs_bool_t padding)
{
   u_char  *d, *s, c0, c1, c2;
   size_t  len;

    len = src->length;
    s = src->start;
    d = dst->start;

    while (len > 2) {
        c0 = s[0];
        c1 = s[1];
        c2 = s[2];

        *d++ = basis[c0 >> 2];
        *d++ = basis[((c0 & 0x03) << 4) | (c1 >> 4)];
        *d++ = basis[((c1 & 0x0f) << 2) | (c2 >> 6)];
        *d++ = basis[c2 & 0x3f];

        s += 3;
        len -= 3;
    }

    if (len > 0) {
        c0 = s[0];
        *d++ = basis[c0 >> 2];

        if (len == 1) {
            *d++ = basis[(c0 & 0x03) << 4];
            if (padding) {
                *d++ = '=';
                *d++ = '=';
            }

        } else {
            c1 = s[1];

            *d++ = basis[((c0 & 0x03) << 4) | (c1 >> 4)];
            *d++ = basis[(c1 & 0x0f) << 2];

            if (padding) {
                *d++ = '=';
            }
        }

    }

    dst->length = d - dst->start;
}


int
qjs_base64_encode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst)
{
    qjs_base64_encode_core(dst, src, qjs_basis64_enc, 1);

    return 0;
}


size_t
qjs_base64_encode_length(JSContext *ctx, const njs_str_t *src)
{
    return qjs_base64_encoded_length(src->length);
}


static void
qjs_base64_decode_core(njs_str_t *dst, const njs_str_t *src,
    const u_char *basis)
{
    size_t  len;
    u_char  *d, *s;

    s = src->start;
    d = dst->start;

    len = dst->length;

    while (len >= 3) {
        *d++ = (u_char) (basis[s[0]] << 2 | basis[s[1]] >> 4);
        *d++ = (u_char) (basis[s[1]] << 4 | basis[s[2]] >> 2);
        *d++ = (u_char) (basis[s[2]] << 6 | basis[s[3]]);

        s += 4;
        len -= 3;
    }

    if (len >= 1) {
        *d++ = (u_char) (basis[s[0]] << 2 | basis[s[1]] >> 4);
    }

    if (len >= 2) {
        *d++ = (u_char) (basis[s[1]] << 4 | basis[s[2]] >> 2);
    }
}


static size_t
qjs_base64_decode_length_core(const njs_str_t *src, const u_char *basis)
{
    uint    pad;
    size_t  len;

    for (len = 0; len < src->length; len++) {
        if (basis[src->start[len]] == 77) {
            break;
        }
    }

    pad = 0;

    if (len % 4 != 0) {
        pad = 4 - (len % 4);
        len += pad;
    }

    return qjs_base64_decoded_length(len, pad);
}


int
qjs_base64_decode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst)
{
    qjs_base64_decode_core(dst, src, qjs_basis64);

    return 0;
}


size_t
qjs_base64_decode_length(JSContext *ctx, const njs_str_t *src)
{
    return qjs_base64_decode_length_core(src, qjs_basis64);
}


int
qjs_base64url_encode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst)
{
    qjs_base64_encode_core(dst, src, qjs_basis64url_enc, 0);

    return 0;
}


int
qjs_base64url_decode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst)
{
    qjs_base64_decode_core(dst, src, qjs_basis64url);

    return 0;
}


size_t
qjs_base64url_decode_length(JSContext *ctx, const njs_str_t *src)
{
    return qjs_base64_decode_length_core(src, qjs_basis64url);
}


njs_inline njs_int_t
qjs_char_to_hex(u_char c)
{
    c |= 0x20;

    /* Values less than '0' become >= 208. */
    c = c - '0';

    if (c > 9) {
        /* Values less than 'a' become >= 159. */
        c = c - ('a' - '0');

        if (njs_slow_path(c > 5)) {
            return -1;
        }

        c += 10;
    }

    return c;
}


int
qjs_hex_decode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst)
{
    u_char        *p;
    size_t        len;
    njs_int_t     c;
    njs_uint_t    i, n;
    const u_char  *start;

    n = 0;
    p = dst->start;

    start = src->start;
    len = src->length;

    for (i = 0; i < len; i++) {
        c = qjs_char_to_hex(start[i]);
        if (njs_slow_path(c < 0)) {
            break;
        }

        n = n * 16 + c;

        if ((i & 1) != 0) {
            *p++ = (u_char) n;
            n = 0;
        }
    }

    dst->length -= (dst->start + dst->length) - p;

    return 0;
}


size_t
qjs_hex_decode_length(JSContext *ctx, const njs_str_t *src)
{
    const u_char  *p, *end;

    p = src->start;
    end = p + src->length;

    for (; p < end; p++) {
        if (njs_slow_path(qjs_char_to_hex(*p) < 0)) {
            break;
        }
    }

    return (p - src->start) / 2;
}


int
qjs_hex_encode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst)
{
    u_char        *p, c;
    size_t        i, len;
    const u_char  *start;

    static const u_char  hex[] = "0123456789abcdef";

    len = src->length;
    start = src->start;

    p = dst->start;

    for (i = 0; i < len; i++) {
        c = start[i];
        *p++ = hex[c >> 4];
        *p++ = hex[c & 0x0f];
    }

    return 0;
}


size_t
qjs_hex_encode_length(JSContext *ctx, const njs_str_t *src)
{
    return src->length * 2;
}


JSValue
qjs_buffer_alloc(JSContext *ctx, size_t size)
{
    JSValue  ret, proto, value;

    value = JS_NewInt64(ctx, size);

    ret = qjs_new_uint8_array(ctx, 1, &value);
    if (JS_IsException(ret)) {
        return ret;
    }

    proto = JS_GetClassProto(ctx, QJS_CORE_CLASS_ID_BUFFER);
    JS_SetPrototype(ctx, ret, proto);
    JS_FreeValue(ctx, proto);

    return ret;
}



JSValue
qjs_buffer_create(JSContext *ctx, u_char *start, size_t size)
{
    JSValue    buffer, ret;
    njs_str_t  dst;

    buffer = qjs_buffer_alloc(ctx, size);
    if (JS_IsException(buffer)) {
        return buffer;
    }

    ret = qjs_typed_array_data(ctx, buffer, &dst);
    if (JS_IsException(ret)) {
        return ret;
    }

    memcpy(dst.start, start, size);

    return buffer;
}


JSValue
qjs_buffer_chb_alloc(JSContext *ctx, njs_chb_t *chain)
{
    ssize_t      size;
    JSValue      ret;
    qjs_bytes_t  bytes;

    size = njs_chb_size(chain);
    if (njs_slow_path(size < 0)) {
        JS_ThrowOutOfMemory(ctx);
        return JS_EXCEPTION;
    }

    ret = qjs_buffer_alloc(ctx, size);
    if (JS_IsException(ret)) {
        return ret;
    }

    (void) qjs_to_bytes(ctx, &bytes, ret);

    njs_chb_join_to(chain, bytes.start);
    qjs_bytes_free(ctx, &bytes);

    return ret;
}


JSValue
qjs_new_uint8_array(JSContext *ctx, int argc, JSValueConst *argv)
{
    JSValue  ret;

#ifdef NJS_HAVE_QUICKJS_NEW_TYPED_ARRAY
    ret = JS_NewTypedArray(ctx, argc, argv, JS_TYPED_ARRAY_UINT8);
#else
    JSValue ctor;

    ctor = JS_GetClassProto(ctx, QJS_CORE_CLASS_ID_UINT8_ARRAY_CTOR);
    ret = JS_CallConstructor(ctx, ctor, argc, argv);
    JS_FreeValue(ctx, ctor);
#endif

    return ret;
}


static int
qjs_buffer_builtin_init(JSContext *ctx)
{
    int        rc;
    JSAtom     species_atom;
    JSValue    global_obj, buffer, proto, ctor, ta, ta_proto, symbol, species;
    JSClassID  u8_ta_class_id;

    JS_NewClass(JS_GetRuntime(ctx), QJS_CORE_CLASS_ID_BUFFER,
                &qjs_buffer_class);

    global_obj = JS_GetGlobalObject(ctx);

    proto = JS_NewObject(ctx);
    JS_SetPropertyFunctionList(ctx, proto, qjs_buffer_proto,
                               njs_nitems(qjs_buffer_proto));

    ctor = JS_GetPropertyStr(ctx, global_obj, "Uint8Array");

#ifndef NJS_HAVE_QUICKJS_NEW_TYPED_ARRAY
    /*
     * Workaround for absence of JS_NewTypedArray() in QuickJS.
     * We use JS_SetClassProto()/JS_GetClassProto() as a key-value store
     * for fast value query by class ID without querying the global object.
     */
    JS_NewClass(JS_GetRuntime(ctx), QJS_CORE_CLASS_ID_UINT8_ARRAY_CTOR,
                &qjs_uint8_array_ctor_class);
    JS_SetClassProto(ctx, QJS_CORE_CLASS_ID_UINT8_ARRAY_CTOR,
                     JS_DupValue(ctx, ctor));
#endif

    ta = JS_CallConstructor(ctx, ctor, 0, NULL);
    u8_ta_class_id = JS_GetClassID(ta);
    JS_FreeValue(ctx, ta);
    JS_FreeValue(ctx, ctor);

    ta_proto = JS_GetClassProto(ctx, u8_ta_class_id);
    JS_SetPrototype(ctx, proto, ta_proto);
    JS_FreeValue(ctx, ta_proto);

    JS_SetClassProto(ctx, QJS_CORE_CLASS_ID_BUFFER, proto);

    buffer = JS_NewCFunction2(ctx, qjs_buffer, "Buffer", 3,
                              JS_CFUNC_constructor, 0);
    if (JS_IsException(buffer)) {
        return -1;
    }

    JS_SetConstructor(ctx, buffer, proto);

    JS_SetPropertyFunctionList(ctx, buffer, qjs_buffer_props,
                               njs_nitems(qjs_buffer_props));

    symbol = JS_GetPropertyStr(ctx, global_obj, "Symbol");
    species = JS_GetPropertyStr(ctx, symbol, "species");
    JS_FreeValue(ctx, symbol);
    species_atom = JS_ValueToAtom(ctx, species);
    JS_FreeValue(ctx, species);

    ctor = JS_NewCFunction2(ctx, qjs_buffer_ctor, "Buffer species ctor", 3,
                            JS_CFUNC_constructor, 0);

    JS_SetProperty(ctx, buffer, species_atom, ctor);
    JS_FreeAtom(ctx, species_atom);

    rc = JS_SetPropertyStr(ctx, global_obj, "Buffer", buffer);
    if (rc == -1) {
        return -1;
    }

    JS_FreeValue(ctx, global_obj);

    return 0;
}


static int
qjs_buffer_module_init(JSContext *ctx, JSModuleDef *m)
{
    int      rc;
    JSValue  proto;

    proto = JS_NewObject(ctx);
    if (JS_IsException(proto)) {
        return -1;
    }

    JS_SetPropertyFunctionList(ctx, proto, qjs_buffer_export,
                               njs_nitems(qjs_buffer_export));

    rc = JS_SetModuleExport(ctx, m, "default", proto);
    if (rc != 0) {
        return -1;
    }

    return JS_SetModuleExportList(ctx, m, qjs_buffer_export,
                                  njs_nitems(qjs_buffer_export));
}


static JSModuleDef *
qjs_buffer_init(JSContext *ctx, const char *name)
{
    int          rc;
    JSModuleDef  *m;

    qjs_buffer_builtin_init(ctx);

    m = JS_NewCModule(ctx, name, qjs_buffer_module_init);
    if (m == NULL) {
        return NULL;
    }

    if (JS_AddModuleExport(ctx, m, "default") < 0) {
        return NULL;
    }

    rc = JS_AddModuleExportList(ctx, m, qjs_buffer_export,
                                njs_nitems(qjs_buffer_export));
    if (rc != 0) {
        return NULL;
    }

    return m;
}
