From bb3dcf21318391417ec43bde7195a768329c1240 Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Wed, 1 May 2024 17:31:01 -0700 Subject: [PATCH] QuickJS: added zlib module. --- auto/init | 1 + auto/make | 42 +- auto/qjs_module | 6 + auto/qjs_modules | 20 + auto/quickjs | 21 + auto/sources | 4 + configure | 3 +- external/njs_shell.c | 15 +- external/qjs_zlib_module.c | 512 ++++++++++++++++ src/qjs.c | 109 ++++ src/qjs.h | 79 +++ src/qjs_buffer.c | 1174 ++++++++++++++++++++++++++++++++++++ src/test/njs_unit_test.c | 301 --------- test/buffer.t.js | 236 ++++++++ test/harness/runTsuite.js | 10 +- test/setup | 2 +- test/zlib.t.mjs | 109 ++++ 17 files changed, 2322 insertions(+), 322 deletions(-) create mode 100644 auto/qjs_module create mode 100644 auto/qjs_modules create mode 100644 external/qjs_zlib_module.c create mode 100644 src/qjs.c create mode 100644 src/qjs.h create mode 100644 src/qjs_buffer.c create mode 100644 test/buffer.t.js create mode 100644 test/zlib.t.mjs diff --git a/auto/init b/auto/init index 3c9285915..019c62cbe 100644 --- a/auto/init +++ b/auto/init @@ -16,6 +16,7 @@ NJS_CFLAGS=${NJS_CFLAGS=} NJS_BUILD_DIR=${NJS_BUILD_DIR:-build} NJS_LIB_MODULES= +QJS_LIB_MODULES= NJS_LIBRT= diff --git a/auto/make b/auto/make index 879cabec3..4a77335b8 100644 --- a/auto/make +++ b/auto/make @@ -15,11 +15,6 @@ njs_modules_c=$NJS_BUILD_DIR/njs_modules.c NJS_LIB_SRCS="$NJS_LIB_SRCS $njs_modules_c" -njs_incs=`echo $NJS_LIB_INCS \ - | sed -e "s# *\([^ ]*\)#$njs_regex_cont-I\1#g"` -njs_objs=`echo $NJS_LIB_SRCS \ - | sed -e "s# *\([^ ]*\.\)c#$NJS_BUILD_DIR/\1o$njs_regex_cont#g"` - cat << END > $njs_modules_c #include @@ -45,6 +40,43 @@ cat << END >> $njs_modules_c END +if [ $NJS_HAVE_QUICKJS = YES ]; then + +qjs_modules_c=$NJS_BUILD_DIR/qjs_modules.c + +NJS_LIB_SRCS="$NJS_LIB_SRCS $qjs_modules_c" + +cat << END > $qjs_modules_c + +#include + +END + +for mod in $QJS_LIB_MODULES +do + echo "extern qjs_module_t $mod;" >> $qjs_modules_c +done + +echo >> $qjs_modules_c +echo 'qjs_module_t *qjs_modules[] = {' >> $qjs_modules_c + +for mod in $QJS_LIB_MODULES +do + echo " &$mod," >> $qjs_modules_c +done + +cat << END >> $qjs_modules_c + NULL +}; + +END +fi + +njs_incs=`echo $NJS_LIB_INCS \ + | sed -e "s# *\([^ ]*\)#$njs_regex_cont-I\1#g"` +njs_objs=`echo $NJS_LIB_SRCS \ + | sed -e "s# *\([^ ]*\.\)c#$NJS_BUILD_DIR/\1o$njs_regex_cont#g"` + cat << END > $NJS_MAKEFILE # This file is auto-generated by configure diff --git a/auto/qjs_module b/auto/qjs_module new file mode 100644 index 000000000..382704817 --- /dev/null +++ b/auto/qjs_module @@ -0,0 +1,6 @@ +# Copyright (C) Dmitry Volyntsev +# Copyright (C) F5, Inc + +QJS_LIB_MODULES="$QJS_LIB_MODULES $njs_module_name" +NJS_LIB_SRCS="$NJS_LIB_SRCS $njs_module_srcs" +NJS_LIB_INCS="$NJS_LIB_INCS $njs_module_incs" diff --git a/auto/qjs_modules b/auto/qjs_modules new file mode 100644 index 000000000..9dde2efd2 --- /dev/null +++ b/auto/qjs_modules @@ -0,0 +1,20 @@ +# Copyright (C) Dmitry Volyntsev +# Copyright (C) F5, Inc + +if [ $NJS_HAVE_QUICKJS = YES ]; then + + njs_module_name=qjs_buffer_module + njs_module_incs= + njs_module_srcs=src/qjs_buffer.c + + . auto/qjs_module + + if [ $NJS_ZLIB = YES -a $NJS_HAVE_ZLIB = YES ]; then + njs_module_name=qjs_zlib_module + njs_module_incs= + njs_module_srcs=external/qjs_zlib_module.c + + . auto/qjs_module + fi + +fi diff --git a/auto/quickjs b/auto/quickjs index f910aff1e..4491fc5c5 100644 --- a/auto/quickjs +++ b/auto/quickjs @@ -25,6 +25,7 @@ if [ $NJS_TRY_QUICKJS = YES ]; then JSRuntime *rt; rt = JS_NewRuntime(); + (void) JS_GetClassID; JS_FreeRuntime(rt); return 0; }" @@ -54,6 +55,26 @@ if [ $NJS_TRY_QUICKJS = YES ]; then fi if [ $njs_found = yes ]; then + + njs_feature="QuickJS JS_NewTypedArray()" + njs_feature_test="#if defined(__GNUC__) && (__GNUC__ >= 8) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored \"-Wcast-function-type\" + #endif + + #include + + int main() { + (void) JS_NewTypedArray; + return 0; + }" + + . auto/feature + + if [ $njs_found = yes ]; then + njs_define=NJS_HAVE_QUICKJS_NEW_TYPED_ARRAY . auto/define + fi + NJS_HAVE_QUICKJS=YES NJS_QUICKJS_LIB="$njs_feature_libs" NJS_LIB_INCS="$NJS_LIB_INCS $njs_feature_incs" diff --git a/auto/sources b/auto/sources index d5637a521..ae2082933 100644 --- a/auto/sources +++ b/auto/sources @@ -72,6 +72,10 @@ if [ "$NJS_HAVE_LIBBFD" = "YES" -a "$NJS_HAVE_DL_ITERATE_PHDR" = "YES" ]; then NJS_LIB_SRCS="$NJS_LIB_SRCS src/njs_addr2line.c" fi +if [ "$NJS_HAVE_QUICKJS" = "YES" ]; then + NJS_LIB_SRCS="$NJS_LIB_SRCS src/qjs.c" +fi + NJS_TS_SRCS=$(find ts/ -name "*.d.ts" -o -name "*.json") NJS_TEST_TS_SRCS=$(find test/ts/ -name "*.ts" -o -name "*.json") diff --git a/configure b/configure index 1a80d86de..57730b057 100755 --- a/configure +++ b/configure @@ -21,7 +21,7 @@ NJS_AUTOCONF_ERR=$NJS_BUILD_DIR/autoconf.err NJS_AUTO_CONFIG_H=$NJS_BUILD_DIR/njs_auto_config.h NJS_MAKEFILE=$NJS_BUILD_DIR/Makefile -NJS_LIB_INCS="src $NJS_BUILD_DIR" +NJS_LIB_INCS="src external $NJS_BUILD_DIR" test -d $NJS_BUILD_DIR || mkdir $NJS_BUILD_DIR @@ -59,6 +59,7 @@ NJS_LIB_AUX_LIBS= . auto/sources . auto/modules +. auto/qjs_modules . auto/make . auto/expect . auto/summary diff --git a/external/njs_shell.c b/external/njs_shell.c index 2b59f0a04..cc4bf8e48 100644 --- a/external/njs_shell.c +++ b/external/njs_shell.c @@ -12,18 +12,7 @@ #include #if (NJS_HAVE_QUICKJS) -#if defined(__GNUC__) && (__GNUC__ >= 8) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wcast-function-type" -#endif - -#include - -#if defined(__GNUC__) && (__GNUC__ >= 8) -#pragma GCC diagnostic pop -#endif -#define NJS_QUICKJS_VERSION "Unknown version" -#include +#include #endif #if (!defined NJS_FUZZER_TARGET && defined NJS_HAVE_READLINE) @@ -2822,7 +2811,7 @@ njs_engine_qjs_init(njs_engine_t *engine, njs_opts_t *opts) return NJS_ERROR; } - engine->u.qjs.ctx = JS_NewContext(engine->u.qjs.rt); + engine->u.qjs.ctx = qjs_new_context(engine->u.qjs.rt); if (engine->u.qjs.ctx == NULL) { njs_stderror("JS_NewContext() failed\n"); return NJS_ERROR; diff --git a/external/qjs_zlib_module.c b/external/qjs_zlib_module.c new file mode 100644 index 000000000..2d3b2d92e --- /dev/null +++ b/external/qjs_zlib_module.c @@ -0,0 +1,512 @@ + +/* + * Copyright (C) Dmitry Volyntsev + * Copyright (C) F5, Inc. + */ + +#include +#include + +#define NJS_ZLIB_CHUNK_SIZE 1024 + +static JSValue qjs_zlib_ext_deflate(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv, int raw); +static JSValue qjs_zlib_ext_inflate(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv, int raw); + +static JSModuleDef *qjs_zlib_init(JSContext *ctx, const char *name); +static void *qjs_zlib_alloc(void *opaque, u_int items, u_int size); +static void qjs_zlib_free(void *opaque, void *address); + + +static const JSCFunctionListEntry qjs_zlib_constants[] = { + JS_PROP_INT32_DEF("Z_NO_COMPRESSION", + Z_NO_COMPRESSION, + JS_PROP_ENUMERABLE), + JS_PROP_INT32_DEF("Z_BEST_SPEED", + Z_BEST_SPEED, + JS_PROP_ENUMERABLE), + JS_PROP_INT32_DEF("Z_BEST_COMPRESSION", + Z_BEST_COMPRESSION, + JS_PROP_ENUMERABLE), + JS_PROP_INT32_DEF("Z_FILTERED", + Z_FILTERED, + JS_PROP_ENUMERABLE), + JS_PROP_INT32_DEF("Z_HUFFMAN_ONLY", + Z_HUFFMAN_ONLY, + JS_PROP_ENUMERABLE), + JS_PROP_INT32_DEF("Z_RLE", + Z_RLE, + JS_PROP_ENUMERABLE), + JS_PROP_INT32_DEF("Z_FIXED", + Z_FIXED, + JS_PROP_ENUMERABLE), + JS_PROP_INT32_DEF("Z_DEFAULT_STRATEGY", + Z_DEFAULT_STRATEGY, + JS_PROP_ENUMERABLE), +}; + +static const JSCFunctionListEntry qjs_zlib_export[] = { + JS_CFUNC_MAGIC_DEF("deflateRawSync", 2, qjs_zlib_ext_deflate, 1), + JS_CFUNC_MAGIC_DEF("deflateSync", 2, qjs_zlib_ext_deflate, 0), + JS_CFUNC_MAGIC_DEF("inflateRawSync", 2, qjs_zlib_ext_inflate, 1), + JS_CFUNC_MAGIC_DEF("inflateSync", 2, qjs_zlib_ext_inflate, 0), + JS_OBJECT_DEF("constants", + qjs_zlib_constants, + njs_nitems(qjs_zlib_constants), + JS_PROP_CONFIGURABLE), +}; + + +qjs_module_t qjs_zlib_module = { + .name = "zlib", + .init = qjs_zlib_init, +}; + + +static JSValue +qjs_zlib_ext_deflate(JSContext *ctx, JSValueConst this_val, int argc, + JSValueConst *argv, int raw) +{ + int rc, chunk_size, level, mem_level, strategy, window_bits; + JSValue ret, options; + z_stream stream; + njs_chb_t chain; + qjs_bytes_t bytes, dictionary; + + chunk_size = NJS_ZLIB_CHUNK_SIZE; + mem_level = 8; + level = Z_DEFAULT_COMPRESSION; + strategy = Z_DEFAULT_STRATEGY; + window_bits = raw ? -MAX_WBITS : MAX_WBITS; + + NJS_CHB_CTX_INIT(&chain, ctx); + dictionary.start = NULL; + dictionary.length = 0; + stream.opaque = NULL; + + options = argv[1]; + + if (JS_IsObject(options)) { + ret = JS_GetPropertyStr(ctx, options, "chunkSize"); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + + if (!JS_IsUndefined(ret)) { + rc = JS_ToInt32(ctx, &chunk_size, ret); + JS_FreeValue(ctx, ret); + if (rc != 0) { + return JS_EXCEPTION; + } + + if (chunk_size < 64) { + JS_ThrowRangeError(ctx, "chunkSize must be >= 64"); + return JS_EXCEPTION; + } + } + + ret = JS_GetPropertyStr(ctx, options, "level"); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + + if (!JS_IsUndefined(ret)) { + rc = JS_ToInt32(ctx, &level, ret); + JS_FreeValue(ctx, ret); + if (rc != 0) { + return JS_EXCEPTION; + } + + if (level < Z_DEFAULT_COMPRESSION || level > Z_BEST_COMPRESSION) { + JS_ThrowRangeError(ctx, "level must be in the range %d..%d", + Z_DEFAULT_COMPRESSION, Z_BEST_COMPRESSION); + return JS_EXCEPTION; + } + } + + ret = JS_GetPropertyStr(ctx, options, "windowBits"); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + + if (!JS_IsUndefined(ret)) { + rc = JS_ToInt32(ctx, &window_bits, ret); + JS_FreeValue(ctx, ret); + if (rc != 0) { + return JS_EXCEPTION; + } + + if (raw) { + if (window_bits < -15 || window_bits > -9) { + JS_ThrowRangeError(ctx, "windowBits must be in the range " + "-15..-9"); + return JS_EXCEPTION; + } + + } else { + if (window_bits < 9 || window_bits > 15) { + JS_ThrowRangeError(ctx, "windowBits must be in the range " + "9..15"); + return JS_EXCEPTION; + } + } + } + + ret = JS_GetPropertyStr(ctx, options, "memLevel"); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + + if (!JS_IsUndefined(ret)) { + rc = JS_ToInt32(ctx, &mem_level, ret); + JS_FreeValue(ctx, ret); + if (rc != 0) { + return JS_EXCEPTION; + } + + if (mem_level < 1 || mem_level > 9) { + JS_ThrowRangeError(ctx, "memLevel must be in the range 1..9"); + return JS_EXCEPTION; + } + } + + ret = JS_GetPropertyStr(ctx, options, "strategy"); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + + if (!JS_IsUndefined(ret)) { + rc = JS_ToInt32(ctx, &strategy, ret); + JS_FreeValue(ctx, ret); + if (rc != 0) { + return JS_EXCEPTION; + } + + switch (strategy) { + case Z_FILTERED: + case Z_HUFFMAN_ONLY: + case Z_RLE: + case Z_FIXED: + case Z_DEFAULT_STRATEGY: + break; + + default: + JS_ThrowRangeError(ctx, "unknown strategy: %d", strategy); + return JS_EXCEPTION; + } + } + + ret = JS_GetPropertyStr(ctx, options, "dictionary"); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + + if (!JS_IsUndefined(ret)) { + rc = qjs_to_bytes(ctx, &dictionary, ret); + JS_FreeValue(ctx, ret); + if (rc != 0) { + return JS_EXCEPTION; + } + } + } + + rc = qjs_to_bytes(ctx, &bytes, argv[0]); + if (rc != 0) { + return JS_EXCEPTION; + } + + stream.next_in = bytes.start; + stream.avail_in = bytes.length; + + stream.zalloc = qjs_zlib_alloc; + stream.zfree = qjs_zlib_free; + stream.opaque = ctx; + + rc = deflateInit2(&stream, level, Z_DEFLATED, window_bits, mem_level, + strategy); + if (njs_slow_path(rc != Z_OK)) { + JS_ThrowInternalError(ctx, "deflateInit2() failed"); + goto fail; + } + + if (dictionary.start != NULL) { + rc = deflateSetDictionary(&stream, dictionary.start, dictionary.length); + if (rc != Z_OK) { + JS_ThrowInternalError(ctx, "deflateSetDictionary() failed"); + goto fail; + } + } + + do { + stream.next_out = njs_chb_reserve(&chain, chunk_size); + if (njs_slow_path(stream.next_out == NULL)) { + JS_ThrowOutOfMemory(ctx); + goto fail; + } + + stream.avail_out = chunk_size; + + rc = deflate(&stream, Z_FINISH); + if (njs_slow_path(rc < 0)) { + JS_ThrowInternalError(ctx, "failed to deflate the data: %s", + stream.msg); + goto fail; + } + + njs_chb_written(&chain, chunk_size - stream.avail_out); + + } while (stream.avail_out == 0); + + deflateEnd(&stream); + + qjs_bytes_free(ctx, &bytes); + + if (dictionary.start != NULL) { + qjs_bytes_free(ctx, &dictionary); + } + + ret = qjs_buffer_chb_alloc(ctx, &chain); + + njs_chb_destroy(&chain); + + return ret; + +fail: + + qjs_bytes_free(ctx, &bytes); + + if (dictionary.start != NULL) { + qjs_bytes_free(ctx, &dictionary); + } + + if (stream.opaque != NULL) { + deflateEnd(&stream); + } + + if (chain.pool != NULL) { + njs_chb_destroy(&chain); + } + + return JS_EXCEPTION; +} + + +static JSValue +qjs_zlib_ext_inflate(JSContext *ctx, JSValueConst this_val, int argc, + JSValueConst *argv, int raw) +{ + int rc, chunk_size, window_bits; + JSValue ret, options; + z_stream stream; + njs_chb_t chain; + qjs_bytes_t bytes, dictionary; + + chunk_size = NJS_ZLIB_CHUNK_SIZE; + window_bits = raw ? -MAX_WBITS : MAX_WBITS; + + NJS_CHB_CTX_INIT(&chain, ctx); + dictionary.start = NULL; + dictionary.length = 0; + stream.opaque = NULL; + + options = argv[1]; + + if (JS_IsObject(options)) { + ret = JS_GetPropertyStr(ctx, options, "chunkSize"); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + + if (!JS_IsUndefined(ret)) { + rc = JS_ToInt32(ctx, &chunk_size, ret); + JS_FreeValue(ctx, ret); + if (rc != 0) { + return JS_EXCEPTION; + } + + if (chunk_size < 64) { + JS_ThrowRangeError(ctx, "chunkSize must be >= 64"); + return JS_EXCEPTION; + } + } + + ret = JS_GetPropertyStr(ctx, options, "windowBits"); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + + if (!JS_IsUndefined(ret)) { + rc = JS_ToInt32(ctx, &window_bits, ret); + JS_FreeValue(ctx, ret); + if (rc != 0) { + return JS_EXCEPTION; + } + + if (raw) { + if (window_bits < -15 || window_bits > -8) { + JS_ThrowRangeError(ctx, "windowBits must be in the range " + "-15..-8"); + return JS_EXCEPTION; + } + + } else { + if (window_bits < 8 || window_bits > 15) { + JS_ThrowRangeError(ctx, "windowBits must be in the range " + "8..15"); + return JS_EXCEPTION; + } + } + } + + ret = JS_GetPropertyStr(ctx, options, "dictionary"); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + + if (!JS_IsUndefined(ret)) { + rc = qjs_to_bytes(ctx, &dictionary, ret); + JS_FreeValue(ctx, ret); + if (rc != 0) { + return JS_EXCEPTION; + } + } + } + + rc = qjs_to_bytes(ctx, &bytes, argv[0]); + if (rc != 0) { + return JS_EXCEPTION; + } + + stream.next_in = bytes.start; + stream.avail_in = bytes.length; + + stream.zalloc = qjs_zlib_alloc; + stream.zfree = qjs_zlib_free; + stream.opaque = ctx; + + rc = inflateInit2(&stream, window_bits); + if (njs_slow_path(rc != Z_OK)) { + JS_ThrowInternalError(ctx, "inflateInit2() failed"); + goto fail; + } + + if (dictionary.start != NULL) { + rc = inflateSetDictionary(&stream, dictionary.start, dictionary.length); + if (rc != Z_OK) { + JS_ThrowInternalError(ctx, "inflateSetDictionary() failed"); + goto fail; + } + } + + while (rc != Z_STREAM_END) { + stream.next_out = njs_chb_reserve(&chain, chunk_size); + if (njs_slow_path(stream.next_out == NULL)) { + JS_ThrowOutOfMemory(ctx); + goto fail; + } + + stream.avail_out = chunk_size; + + rc = inflate(&stream, Z_NO_FLUSH); + if (njs_slow_path(rc < 0)) { + JS_ThrowInternalError(ctx, "failed to inflate the data: %s", + stream.msg); + goto fail; + } + + njs_chb_written(&chain, chunk_size - stream.avail_out); + } + + rc = inflateEnd(&stream); + if (njs_slow_path(rc != Z_OK)) { + JS_ThrowInternalError(ctx, "inflateEnd() failed"); + goto fail; + } + + qjs_bytes_free(ctx, &bytes); + + if (dictionary.start != NULL) { + qjs_bytes_free(ctx, &dictionary); + } + + ret = qjs_buffer_chb_alloc(ctx, &chain); + + njs_chb_destroy(&chain); + + return ret; + +fail: + + qjs_bytes_free(ctx, &bytes); + + if (dictionary.start != NULL) { + qjs_bytes_free(ctx, &dictionary); + } + + if (stream.opaque != NULL) { + inflateEnd(&stream); + } + + if (chain.pool != NULL) { + njs_chb_destroy(&chain); + } + + return JS_EXCEPTION; +} + + +static int +qjs_zlib_module_init(JSContext *ctx, JSModuleDef *m) +{ + int rc; + JSValue proto; + + proto = JS_NewObject(ctx); + JS_SetPropertyFunctionList(ctx, proto, qjs_zlib_export, + njs_nitems(qjs_zlib_export)); + + rc = JS_SetModuleExport(ctx, m, "default", proto); + if (rc != 0) { + return -1; + } + + return JS_SetModuleExportList(ctx, m, qjs_zlib_export, + njs_nitems(qjs_zlib_export)); +} + + +static JSModuleDef * +qjs_zlib_init(JSContext *ctx, const char *name) +{ + int rc; + JSModuleDef *m; + + m = JS_NewCModule(ctx, name, qjs_zlib_module_init); + if (m == NULL) { + return NULL; + } + + JS_AddModuleExport(ctx, m, "default"); + rc = JS_AddModuleExportList(ctx, m, qjs_zlib_export, + njs_nitems(qjs_zlib_export)); + if (rc != 0) { + return NULL; + } + + return m; +} + + +static void * +qjs_zlib_alloc(void *opaque, u_int items, u_int size) +{ + return js_malloc(opaque, items * size); +} + + +static void +qjs_zlib_free(void *opaque, void *address) +{ + js_free(opaque, address); +} diff --git a/src/qjs.c b/src/qjs.c new file mode 100644 index 000000000..83b43ad71 --- /dev/null +++ b/src/qjs.c @@ -0,0 +1,109 @@ + +/* + * Copyright (C) Dmitry Volyntsev + * Copyright (C) F5, Inc. + */ + +#include + + +JSContext * +qjs_new_context(JSRuntime *rt) +{ + JSContext *ctx; + qjs_module_t **module; + + ctx = JS_NewContext(rt); + if (ctx == NULL) { + return NULL; + } + + for (module = qjs_modules; *module != NULL; module++) { + if ((*module)->init(ctx, (*module)->name) == NULL) { + return NULL; + } + } + + return ctx; +} + + +int +qjs_to_bytes(JSContext *ctx, qjs_bytes_t *bytes, JSValueConst value) +{ + size_t byte_offset, byte_length; + JSValue val; + + val = JS_GetTypedArrayBuffer(ctx, value, &byte_offset, &byte_length, NULL); + if (!JS_IsException(val)) { + bytes->start = JS_GetArrayBuffer(ctx, &bytes->length, val); + + JS_FreeValue(ctx, val); + + if (bytes->start != NULL) { + bytes->tag = JS_TAG_OBJECT; + bytes->start += byte_offset; + bytes->length = byte_length; + return 0; + } + } + + bytes->start = JS_GetArrayBuffer(ctx, &bytes->length, value); + if (bytes->start != NULL) { + bytes->tag = JS_TAG_OBJECT; + return 0; + } + + bytes->tag = JS_TAG_STRING; + + if (!JS_IsString(value)) { + val = JS_ToString(ctx, value); + + bytes->start = (u_char *) JS_ToCStringLen(ctx, &bytes->length, val); + + JS_FreeValue(ctx, val); + + if (bytes->start == NULL) { + return -1; + } + } + + bytes->start = (u_char *) JS_ToCStringLen(ctx, &bytes->length, value); + + return (bytes->start != NULL) ? 0 : -1; +} + + +void +qjs_bytes_free(JSContext *ctx, qjs_bytes_t *bytes) +{ + if (bytes->tag == JS_TAG_STRING) { + JS_FreeCString(ctx, (char *) bytes->start); + } +} + + +JSValue +qjs_typed_array_data(JSContext *ctx, JSValueConst value, njs_str_t *data) +{ + size_t byte_offset, byte_length; + + value = JS_GetTypedArrayBuffer(ctx, value, &byte_offset, &byte_length, + NULL); + if (JS_IsException(value)) { + return value; + } + + data->start = JS_GetArrayBuffer(ctx, &data->length, value); + + JS_FreeValue(ctx, value); + + if (data->start == NULL) { + return JS_EXCEPTION; + } + + data->start += byte_offset; + data->length = byte_length; + + return JS_UNDEFINED; +} diff --git a/src/qjs.h b/src/qjs.h new file mode 100644 index 000000000..2307d4d9d --- /dev/null +++ b/src/qjs.h @@ -0,0 +1,79 @@ + +/* + * Copyright (C) Dmitry Volyntsev + * Copyright (C) F5, Inc. + */ + +#ifndef _QJS_H_INCLUDED_ +#define _QJS_H_INCLUDED_ + +#include + +#include +#include +#include +#include +#include +#include +#include + +#if defined(__GNUC__) && (__GNUC__ >= 8) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-function-type" +#endif + +#include + +#if defined(__GNUC__) && (__GNUC__ >= 8) +#pragma GCC diagnostic pop +#endif +#define NJS_QUICKJS_VERSION "Unknown version" +#include + + +typedef JSModuleDef *(*qjs_addon_init_pt)(JSContext *ctx, const char *name); + +typedef struct { + const char *name; + qjs_addon_init_pt init; +} qjs_module_t; + + +JSContext *qjs_new_context(JSRuntime *rt); + + +JSValue qjs_buffer_alloc(JSContext *ctx, size_t size); +JSValue qjs_buffer_chb_alloc(JSContext *ctx, njs_chb_t *chain); + +typedef int (*qjs_buffer_encode_t)(JSContext *ctx, const njs_str_t *src, + njs_str_t *dst); +typedef size_t (*qjs_buffer_encode_length_t)(JSContext *ctx, + const njs_str_t *src); + +typedef struct { + njs_str_t name; + qjs_buffer_encode_t encode; + qjs_buffer_encode_length_t encode_length; + qjs_buffer_encode_t decode; + qjs_buffer_encode_length_t decode_length; +} qjs_buffer_encoding_t; + +const qjs_buffer_encoding_t *qjs_buffer_encoding(JSContext *ctx, + JSValueConst value, JS_BOOL thrw); + + +typedef struct { + int tag; + size_t length; + u_char *start; +} qjs_bytes_t; + +int qjs_to_bytes(JSContext *ctx, qjs_bytes_t *data, JSValueConst value); +void qjs_bytes_free(JSContext *ctx, qjs_bytes_t *data); +JSValue qjs_typed_array_data(JSContext *ctx, JSValueConst value, + njs_str_t *data); + + +extern qjs_module_t *qjs_modules[]; + +#endif /* _QJS_H_INCLUDED_ */ diff --git a/src/qjs_buffer.c b/src/qjs_buffer.c new file mode 100644 index 000000000..166eb970e --- /dev/null +++ b/src/qjs_buffer.c @@ -0,0 +1,1174 @@ + +/* + * Copyright (C) Dmitry Volyntsev + * Copyright (C) F5, Inc. + */ + +#include + +static JSValue qjs_buffer(JSContext *ctx, JSValueConst this_val, int argc, + JSValueConst *argv); +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_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_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_array_buffer(JSContext *ctx, u_char *buf, + size_t size, JSValueConst offset, JSValueConst length); +static JSValue qjs_buffer_from_object(JSContext *ctx, JSValueConst obj); +static int qjs_base64_encode(JSContext *ctx, const njs_str_t *src, + njs_str_t *dst); +static size_t qjs_base64_encode_length(JSContext *ctx, const njs_str_t *src); +static int qjs_base64_decode(JSContext *ctx, const njs_str_t *src, + njs_str_t *dst); +static size_t qjs_base64_decode_length(JSContext *ctx, const njs_str_t *src); +static int qjs_base64url_encode(JSContext *ctx, const njs_str_t *src, + njs_str_t *dst); +static int qjs_base64url_decode(JSContext *ctx, const njs_str_t *src, + njs_str_t *dst); +static size_t qjs_base64url_decode_length(JSContext *ctx, const njs_str_t *src); +static int qjs_hex_encode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst); +static size_t qjs_hex_encode_length(JSContext *ctx, const njs_str_t *src); +static int qjs_hex_decode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst); +static size_t qjs_hex_decode_length(JSContext *ctx, const njs_str_t *src); +static JSValue qjs_new_uint8_array(JSContext *ctx, size_t size); +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_DEF("from", 3, qjs_buffer_from), + JS_CFUNC_DEF("isBuffer", 1, qjs_buffer_is_buffer), +}; + + +static const JSCFunctionListEntry qjs_buffer_proto[] = { + JS_CFUNC_DEF("toJSON", 0, qjs_buffer_prototype_to_json), + JS_CFUNC_DEF("toString", 1, qjs_buffer_prototype_to_string), +}; + + +static JSClassDef qjs_buffer_class = { + "Buffer", + .finalizer = NULL, +}; + + +static JSClassID qjs_buffer_class_id; + +#ifndef NJS_HAVE_QUICKJS_NEW_TYPED_ARRAY +static JSClassDef qjs_uint8_array_ctor_class = { + "Uint8ArrayConstructor", + .finalizer = NULL, +}; + +static JSClassID qjs_uint8_array_ctor_id; +#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_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_from_array_buffer(ctx, buf, size, argv[1], argv[2]); + + } 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_buffer_class_id); + + ret = JS_NewBool(ctx, JS_VALUE_GET_TAG(argv[0]) == JS_TAG_OBJECT && + JS_VALUE_GET_OBJ(buffer_proto) == JS_VALUE_GET_OBJ(proto)); + + JS_FreeValue(ctx, buffer_proto); + JS_FreeValue(ctx, proto); + + return ret; +} + + +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_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 ret; + } + + 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_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; + + 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_array_buffer(JSContext *ctx, u_char *buf, size_t size, + JSValueConst offset, JSValueConst length) +{ + JSValue buffer, ret; + int64_t len; + uint64_t off; + njs_str_t dst; + + if (JS_ToIndex(ctx, &off, offset)) { + return JS_EXCEPTION; + } + + if ((size_t) off > size) { + JS_ThrowRangeError(ctx, "\"offset\" is outside of buffer bounds"); + return JS_EXCEPTION; + } + + if (JS_IsUndefined(length)) { + len = size - off; + + } else { + if (JS_ToInt64(ctx, &len, length)) { + return JS_EXCEPTION; + } + + if (len < 0) { + len = 0; + } + + if ((size_t) (off + len) > size) { + JS_ThrowRangeError(ctx, "\"length\" is outside of buffer bounds"); + return JS_EXCEPTION; + } + } + + 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; + } + + memcpy(dst.start, buf + off, len); + + 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; +} + + +static 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; +} + + +static 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); +} + + +static 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; +} + + +static size_t +qjs_base64_decode_length(JSContext *ctx, const njs_str_t *src) +{ + return qjs_base64_decode_length_core(src, qjs_basis64); +} + + +static int +qjs_base64url_encode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst) +{ + qjs_base64_encode_core(dst, src, qjs_basis64url_enc, 1); + + return 0; +} + + +static 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; +} + + +static 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; +} + + +static 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; +} + + +static 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; +} + + +static 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[16] = "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; +} + + +static 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; + + ret = qjs_new_uint8_array(ctx, size); + if (JS_IsException(ret)) { + return ret; + } + + proto = JS_GetClassProto(ctx, qjs_buffer_class_id); + JS_SetPrototype(ctx, ret, proto); + JS_FreeValue(ctx, proto); + + return ret; +} + + +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; +} + + +static JSValue +qjs_new_uint8_array(JSContext *ctx, size_t size) +{ + JSValue ret, value; + + value = JS_NewInt64(ctx, size); + +#ifdef NJS_HAVE_QUICKJS_NEW_TYPED_ARRAY + ret = JS_NewTypedArray(ctx, 1, &value, JS_TYPED_ARRAY_UINT8); +#else + JSValue ctor; + + ctor = JS_GetClassProto(ctx, qjs_uint8_array_ctor_id); + ret = JS_CallConstructor(ctx, ctor, 1, &value); + JS_FreeValue(ctx, ctor); +#endif + + JS_FreeValue(ctx, value); + + return ret; +} + + +static int +qjs_buffer_builtin_init(JSContext *ctx) +{ + int rc; + JSValue global_obj, buffer, proto, ctor, ta, ta_proto; + JSClassID u8_ta_class_id; + + JS_NewClassID(&qjs_buffer_class_id); + JS_NewClass(JS_GetRuntime(ctx), qjs_buffer_class_id, &qjs_buffer_class); + + proto = JS_NewObject(ctx); + JS_SetPropertyFunctionList(ctx, proto, qjs_buffer_proto, + njs_nitems(qjs_buffer_proto)); + + global_obj = JS_GetGlobalObject(ctx); + + 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_NewClassID(&qjs_uint8_array_ctor_id); + JS_NewClass(JS_GetRuntime(ctx), qjs_uint8_array_ctor_id, + &qjs_uint8_array_ctor_class); + JS_SetClassProto(ctx, qjs_uint8_array_ctor_id, 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_buffer_class_id, proto); + + buffer = JS_NewCFunction2(ctx, qjs_buffer, "Buffer", 0, + JS_CFUNC_generic, 0); + if (JS_IsException(buffer)) { + return -1; + } + + JS_SetPropertyFunctionList(ctx, buffer, qjs_buffer_props, + njs_nitems(qjs_buffer_props)); + + 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); + 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; + } + + JS_AddModuleExport(ctx, m, "default"); + rc = JS_AddModuleExportList(ctx, m, qjs_buffer_export, + njs_nitems(qjs_buffer_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 e34a5a28d..2fa7e01ff 100644 --- a/src/test/njs_unit_test.c +++ b/src/test/njs_unit_test.c @@ -21069,217 +21069,12 @@ static njs_unit_test_t njs_buffer_module_test[] = "].every(args => Buffer.byteLength(args[0], args[1]) == args[2])"), njs_str("true") }, - { njs_str("Buffer.from({length:5, 0:'A'.charCodeAt(0), 2:'X', 3:NaN,4:0xfd}).toString('hex')"), - njs_str("41000000fd") }, - - { njs_str("Buffer.from([1, 2, 0.23, '5', 'A']).toString('hex')"), - njs_str("0102000500") }, - - { njs_str("Buffer.from([NaN, Infinity]).toString('hex')"), - njs_str("0000") }, - - { njs_str("Buffer.from(new Uint8Array([0xff,0xde,0xba])).toString('hex')"), - njs_str("ffdeba") }, - - { njs_str("Buffer.from((new Uint8Array([0xff,0xde,0xba])).buffer).toString('hex')"), - njs_str("ffdeba") }, - - { njs_str("[" - " ['', '']," - " ['aa0', 'aa']," - " ['00aabbcc', '00aabbcc']," - " [new String('00aabbcc'), '00aabbcc']," - " ['deadBEEF##', 'deadbeef']," - "].every(args => { " - " if (Buffer.from(args[0], 'hex').toString('hex') != args[1]) {" - " throw `Buffer.from(\"${args[0]}\", 'hex').toString('hex') != \"${args[1]}\"`;" - " }" - " return true;" - "})"), - njs_str("true") }, - - { njs_str("[" - " ['', '']," - " ['#', '']," - " ['Q', '']," - " ['QQ', 'A']," - " ['QQ=', 'A']," - " ['QQ==', 'A']," - " ['QUI=', 'AB']," - " ['QUI', 'AB']," - " ['QUJD', 'ABC']," - " ['QUJDRA==', 'ABCD']," - "].every(args => { " - " if (Buffer.from(args[0], 'base64') != args[1]) {" - " throw `Buffer.from(\"${args[0]}\", 'base64') != \"${args[1]}\"`;" - " }" - " return true;" - "})"), - njs_str("true") }, - - { njs_str("[" - " ['', '']," - " ['QQ', 'A']," - " ['QUI', 'AB']," - " ['QUJD', 'ABC']," - " ['QUJDRA', 'ABCD']," - " ['QUJDRA#', 'ABCD']," - "].every(args => { " - " if (Buffer.from(args[0], 'base64url') != args[1]) {" - " throw `Buffer.from(\"${args[0]}\", 'base64url') != \"${args[1]}\"`;" - " }" - " return true;" - "})"), - njs_str("true") }, - - { njs_str("Buffer.from([0x62, 0x75, 0x66, 0x66, 0x65, 0x72])"), - njs_str("buffer") }, - - { njs_str(njs_declare_sparse_array("arr", 6) - "[0x62, 0x75, 0x66, 0x66, 0x65, 0x72].map((v, i) => {arr[i] = v;});" - "Buffer.from(arr)"), - njs_str("buffer") }, - - { njs_str("Buffer.from({length:3, 0:0x62, 1:0x75, 2:0x66})"), - njs_str("buf") }, - - { njs_str("njs.dump(Buffer.from([-1,1,255,22323,-Infinity,Infinity,NaN]))"), - njs_str("Buffer [255,1,255,51,0,0,0]") }, - - { njs_str("var buf = Buffer.from([0x62, 0x75, 0x66, 0x66, 0x65, 0x72]); njs.dump(buf)"), - njs_str("Buffer [98,117,102,102,101,114]") }, - - { njs_str("var buf = Buffer.from([1,2,3]); njs.dump(Buffer.from(buf.toJSON()))"), - njs_str("Buffer [1,2,3]") }, - - { njs_str("[" - " {type: 'B'}," - " {type: undefined}," - " {type:'Buffer'}," - " {type:'Buffer', data:null}," - " {type:'Buffer', data:{}}," - "].every(v=>{ try { Buffer.from(v)} catch(e) {return e.name == 'TypeError'}})"), - njs_str("true") }, - - { njs_str("var foo = new Uint16Array(2);" - "foo[0] = 5000; foo[1] = 4000;" - "var buf = Buffer.from(foo.buffer);" - "foo[1] = 6000;" - "njs.dump(buf)"), - njs_str("Buffer [" njs_evar("136,19,112,23", "19,136,23,112") "]") }, - - { njs_str("var foo = new Uint16Array(2).fill(950);" - "var buf = Buffer.from(foo.buffer, 1); njs.dump(buf)"), - njs_str("Buffer [" njs_evar("3,182,3", "182,3,182") "]") }, - - { njs_str("var foo = new Uint16Array(2).fill(950);" - "var buf = Buffer.from(foo.buffer, -1); njs.dump(buf)"), - njs_str("RangeError: invalid index") }, - - { njs_str("var foo = new Uint16Array(2).fill(950);" - "var buf = Buffer.from(foo.buffer, 5); njs.dump(buf)"), - njs_str("RangeError: \"offset\" is outside of buffer bounds") }, - - { njs_str("var foo = new Uint16Array(2).fill(950);" - "var buf = Buffer.from(foo.buffer, 2, 1); njs.dump(buf)"), - njs_str("Buffer [" njs_evar("182", "3") "]") }, - - { njs_str("var foo = new Uint16Array(2).fill(950);" - "var buf = Buffer.from(foo.buffer, 2, -1); njs.dump(buf)"), - njs_str("Buffer []") }, - - { njs_str("var foo = new Uint16Array(2).fill(950);" - "var buf = Buffer.from(foo.buffer, 2, 3); njs.dump(buf)"), - njs_str("RangeError: \"length\" is outside of buffer bounds") }, - - { njs_str("var foo = new Uint16Array(2).fill(950);" - "var buf = Buffer.from(foo.buffer, 2, 0); njs.dump(buf)"), - njs_str("Buffer []") }, - - { njs_str("var foo = new Uint16Array(2).fill(950);" - "var buf = Buffer.from(foo.buffer, 2, 2); njs.dump(buf)"), - njs_str("Buffer [" njs_evar("182,3", "3,182") "]") }, - - { njs_str("var foo = new Uint16Array(2).fill(950);" - "var buf = Buffer.from(foo.buffer, '2', '2'); njs.dump(buf)"), - njs_str("Buffer [" njs_evar("182,3", "3,182") "]") }, - - { njs_str("var foo = new Uint32Array(1).fill(0xF1F2F3F4);" - "var buf = Buffer.from(foo); njs.dump(buf)"), - njs_str("Buffer [244]") }, - - { njs_str("var foo = new Uint32Array(2).fill(0xF1F2F3F4);" - "var buf = Buffer.from(foo); njs.dump(buf)"), - njs_str("Buffer [244,244]") }, - { njs_str("var foo = new Uint8Array(5);" "foo[0] = 1; foo[1] = 2; foo[2] = 3; foo[3] = 4; foo[4] = 5;" "foo = foo.subarray(1, 3);" "var buf = Buffer.from(foo); njs.dump(buf)"), njs_str("Buffer [2,3]") }, - { njs_str("var buf = Buffer.from(''); njs.dump(buf)"), - njs_str("Buffer []") }, - - { njs_str("var buf = Buffer.from('α'); njs.dump(buf)"), - njs_str("Buffer [206,177]") }, - - { njs_str("var arr = new Array(1,2,3); arr.valueOf = () => arr;" - "njs.dump(Buffer.from(arr))"), - njs_str("Buffer [1,2,3]") }, - - { njs_str("var obj = new Object(); obj.valueOf = () => obj;" - "Buffer.from(obj)"), - njs_str("TypeError: first argument object is not a string or Buffer-like object") }, - - { njs_str("var obj = new Object(); obj.valueOf = () => undefined;" - "njs.dump(Buffer.from(obj))"), - njs_str("TypeError: first argument undefined is not a string or Buffer-like object") }, - - { njs_str("var arr = new Array(1,2,3); arr.valueOf = () => null;" - "njs.dump(Buffer.from(arr))"), - njs_str("Buffer [1,2,3]") }, - - { njs_str("var obj = new Object(); obj.valueOf = () => new Array(1,2,3);" - "njs.dump(Buffer.from(obj))"), - njs_str("Buffer [1,2,3]") }, - - { njs_str("njs.dump(Buffer.from(new String('test')))"), - njs_str("Buffer [116,101,115,116]") }, - - { njs_str("Buffer.from({ get type() { throw new Error('test'); } })"), - njs_str("Error: test") }, - - { njs_str("Buffer.from({ type: 'Buffer', get data() { throw new Error('test'); } })"), - njs_str("Error: test") }, - - { njs_str("var a = [1,2,3,4]; a[1] = { valueOf() { a.length = 3; return 1; } };" - "njs.dump(Buffer.from(a))"), - njs_str("Buffer [1,1,3,0]") }, - - { njs_str("var a = [1,2,3,4]; a[1] = { valueOf() { a.length = 4096; a.fill(13); return 1; } };" - "njs.dump(Buffer.from(a))"), - njs_str("Buffer [1,1,13,13]") }, - - { njs_str("[" - " ['6576696c', 'hex']," - " ['ZXZpbA==', 'base64']," - " ['ZXZpbA==#', 'base64']," - " ['ZXZpbA', 'base64url']," - " ['ZXZpbA##', 'base64url']," - "].every(args => Buffer.from(args[0], args[1]) == 'evil')"), - njs_str("true") }, - - { njs_str("var buf = Buffer.from($262.byteString([0xF3])); buf"), - njs_str("�") }, - - { njs_str("Buffer.from('', 'utf-128')"), - njs_str("TypeError: \"utf-128\" encoding is not supported") }, - - { njs_str("[Buffer.from('α'), new Uint8Array(10), {}, 1]" - ".map(v=>Buffer.isBuffer(v))"), - njs_str("true,false,false,false") }, - { njs_str("['utf8', 'utf-8', 'hex', 'base64', 'base64url', 'utf-88', '1hex']" ".map(v=>Buffer.isEncoding(v))"), njs_str("true,true,true,true,true,false,false") }, @@ -21760,12 +21555,6 @@ static njs_unit_test_t njs_buffer_module_test[] = "Buffer [4,3,2,1,8,7,6,5]," "Buffer [8,7,6,5,4,3,2,1]") }, - { njs_str("njs.dump(Buffer.from('αααα').toJSON())"), - njs_str("{type:'Buffer',data:[206,177,206,177,206,177,206,177]}") }, - - { njs_str("njs.dump(Buffer.from('').toJSON())"), - njs_str("{type:'Buffer',data:[]}") }, - { njs_str("[" " [['base64'], 'ZXZpbA==']," " [['base64url'], 'ZXZpbA']," @@ -21785,9 +21574,6 @@ static njs_unit_test_t njs_buffer_module_test[] = "})"), njs_str("true") }, - { njs_str("Buffer.from('evil').toString('utf-128')"), - njs_str("TypeError: \"utf-128\" encoding is not supported") }, - { njs_str("var buf = Buffer.allocUnsafe(4);" "var len = buf.write('ZXZpbA==', 'base64'); [len, buf]"), njs_str("4,evil") }, @@ -22306,82 +22092,6 @@ static njs_unit_test_t njs_xml_test[] = }; -static njs_unit_test_t njs_zlib_test[] = -{ - { njs_str("const zlib = require('zlib');" - "['C3f0dgQA', 'O7fx3KZzmwE=']" - ".map(v => zlib.inflateRawSync(Buffer.from(v, 'base64')).toString())"), - njs_str("WAKA,αβγ") }, - - { njs_str("const zlib = require('zlib');" - "['eJwLd/R2BAAC+gEl', 'eJw7t/HcpnObAQ/sBIE=']" - ".map(v => zlib.inflateSync(Buffer.from(v, 'base64')).toString())"), - njs_str("WAKA,αβγ") }, - - { njs_str("const zlib = require('zlib');" - "const enc = ['WAKA', 'αβγ'].map(v => zlib.deflateRawSync(v).toString('base64'));" - "enc.map(v => zlib.inflateRawSync(Buffer.from(v, 'base64')).toString())"), - njs_str("WAKA,αβγ") }, - - { njs_str("const zlib = require('zlib');" - "const enc = ['WAKA', 'αβγ']" - ".map(v => zlib.deflateRawSync(v, {dictionary: Buffer.from('WAKA')}).toString('base64'));" - "enc.map(v => zlib.inflateRawSync(Buffer.from(v, 'base64'), {dictionary: Buffer.from('WAKA')}))"), - njs_str("WAKA,αβγ") }, - - { njs_str("const zlib = require('zlib');" - "['WAKA', 'αβγ']" - ".map(v => zlib.deflateRawSync(v, {level: zlib.constants.Z_NO_COMPRESSION}).toString('base64'))"), - njs_str("AQQA+/9XQUtB,AQYA+f/Osc6yzrM=") }, - - { njs_str("const zlib = require('zlib');" - "[zlib.constants.Z_FIXED, zlib.constants.Z_RLE]" - ".map(v => zlib.deflateRawSync('WAKA'.repeat(10), {strategy: v}).toString('base64'))"), - njs_str("C3f0dgwnAgMA,BcExAQAAAMKgbNwLYP8mwmQymUwmk8lkcg==") }, - - { njs_str("const zlib = require('zlib');" - "[1, 8]" - ".map(v => zlib.deflateRawSync('WAKA'.repeat(35)," - " {strategy: zlib.constants.Z_RLE, memLevel: v})" - " .toString('base64'))"), - njs_str("BMExAQAAAMKgbNwLYP8mwmQymUwmk8lkMplMJpPJZDKZTCaTyWQymUwmk+lzDHf0dgx39HYMd/R2BAA=," - "BcExAQAAAMKgbNwLYP8mwmQymUwmk8lkMplMJpPJZDKZTCaTyWQymUwmk8lkMjk=") }, - - { njs_str("const zlib = require('zlib');" - "['WAKA', 'αβγ']" - ".map(v => zlib.deflateSync(v).toString('base64'))"), - njs_str("eJwLd/R2BAAC+gEl,eJw7t/HcpnObAQ/sBIE=") }, - - { njs_str("const zlib = require('zlib');" - "const buf = 'αβγ'.repeat(56);" - "const enc = zlib.deflateRawSync(buf, {chunkSize:64}).toString('base64');" - "const dec = zlib.inflateRawSync(Buffer.from(enc, 'base64')).toString();" - "buf == dec"), - njs_str("true") }, - - { njs_str("const zlib = require('zlib');" - "['WAKA'.repeat(1024), 'αβγ'.repeat(1024)]" - ".map(v => [v, zlib.deflateRawSync(v).toString('base64')])" - ".every(pair => pair[0] == zlib.inflateRawSync(Buffer.from(pair[1], 'base64')).toString())"), - njs_str("true") }, - - { njs_str("const zlib = require('zlib');" - "['WAKA'.repeat(1024), 'αβγ'.repeat(1024)]" - ".map(v => [v, zlib.deflateRawSync(v, {chunkSize:64}).toString('base64')])" - ".every(pair => pair[0] == zlib.inflateRawSync(Buffer.from(pair[1], 'base64')," - " {chunkSize:64}).toString())"), - njs_str("true") }, - - { njs_str("const zlib = require('zlib');" - "['WAKA', 'αβγ']" - ".map(v => [v, zlib.deflateRawSync(v, {dictionary: Buffer.from('WAKA')}).toString('base64')])" - ".every(pair => pair[0] == zlib.inflateRawSync(Buffer.from(pair[1], 'base64')," - " {dictionary: Buffer.from('WAKA')}).toString())"), - njs_str("true") }, - -}; - - static njs_unit_test_t njs_module_test[] = { { njs_str("function f(){return 2}; var f; f()"), @@ -25115,17 +24825,6 @@ static njs_test_suite_t njs_suites[] = njs_nitems(njs_xml_test), njs_unit_test }, - { -#if (NJS_HAVE_ZLIB && !NJS_HAVE_MEMORY_SANITIZER) - njs_str("zlib"), -#else - njs_str(""), -#endif - { .externals = 1, .repeat = 1, .unsafe = 1 }, - njs_zlib_test, - njs_nitems(njs_zlib_test), - njs_unit_test }, - { njs_str("module"), { .repeat = 1, .module = 1, .unsafe = 1 }, njs_module_test, diff --git a/test/buffer.t.js b/test/buffer.t.js new file mode 100644 index 000000000..1d96d342d --- /dev/null +++ b/test/buffer.t.js @@ -0,0 +1,236 @@ +/*--- +includes: [compatBuffer.js, runTsuite.js] +flags: [async] +---*/ + +function p(args, default_opts) { + let params = merge({}, default_opts); + params = merge(params, args); + + return params; +} + +let from_tsuite = { + name: "Buffer.from() tests", + skip: () => (!has_buffer()), + T: async (params) => { + let buf = Buffer.from.apply(null, params.args); + + if (params.modify) { + params.modify(buf); + } + + let r = buf.toString(params.fmt); + + if (r.length !== params.expected.length) { + throw Error(`unexpected "${r}" length ${r.length} != ${params.expected.length}`); + } + + if (r !== params.expected) { + throw Error(`unexpected output "${r}" != "${params.expected}"`); + } + + return 'SUCCESS'; + }, + prepare_args: p, + opts: { fmt: 'utf-8' }, + + tests: [ + { args: [[0x62, 0x75, 0x66, 0x66, 0x65, 0x72]], expected: 'buffer' }, + { args: [{length:3, 0:0x62, 1:0x75, 2:0x66}], expected: 'buf' }, + { args: [[-1, 1, 255, 22323, -Infinity, Infinity, NaN]], fmt: "hex", expected: 'ff01ff33000000' }, + { args: [{length:5, 0:'A'.charCodeAt(0), 2:'X', 3:NaN, 4:0xfd}], fmt: "hex", expected: '41000000fd' }, + { args: [[1, 2, 0.23, '5', 'A']], fmt: "hex", expected: '0102000500' }, + { args: [new Uint8Array([0xff, 0xde, 0xba])], fmt: "hex", expected: 'ffdeba' }, + + { args: [(new Uint8Array([0xaa, 0xbb, 0xcc])).buffer], fmt: "hex", expected: 'aabbcc' }, + { args: [(new Uint8Array([0xaa, 0xbb, 0xcc])).buffer, 1], fmt: "hex", expected: 'bbcc' }, + { args: [(new Uint8Array([0xaa, 0xbb, 0xcc])).buffer, 1, 1], fmt: "hex", expected: 'bb' }, + { args: [(new Uint8Array([0xaa, 0xbb, 0xcc])).buffer, '1', '1'], fmt: "hex", expected: 'bb' }, + { args: [(new Uint8Array([0xaa, 0xbb, 0xcc])).buffer, 1, 0], fmt: "hex", expected: '' }, + { args: [(new Uint8Array([0xaa, 0xbb, 0xcc])).buffer, 1, -1], fmt: "hex", expected: '' }, + + { args: [(new Uint8Array([0xaa, 0xbb, 0xcc])).buffer], fmt: "hex", + modify: (buf) => { buf[1] = 0; }, + expected: 'aa00cc' }, + + { args: [new Uint16Array([234, 123])], fmt: "hex", expected: 'ea7b' }, + { args: [new Uint32Array([234, 123])], fmt: "hex", expected: 'ea7b' }, + { args: [new Float32Array([234.001, 123.11])], fmt: "hex", expected: 'ea7b' }, + { args: [new Uint32Array([234, 123])], fmt: "hex", expected: 'ea7b' }, + { args: [new Float64Array([234.001, 123.11])], fmt: "hex", expected: 'ea7b' }, + + { args: [(new Uint8Array(2)).buffer, -1], + exception: 'RangeError: invalid index' }, + { args: [(new Uint8Array(2)).buffer, 3], + exception: 'RangeError: \"offset\" is outside of buffer bounds' }, + { args: [(new Uint8Array(2)).buffer, 1, 2], + exception: 'RangeError: \"length\" is outside of buffer bounds' }, + + { args: [Buffer.from([0xaa, 0xbb, 0xcc]).toJSON()], fmt: "hex", expected: 'aabbcc' }, + { args: [{type: 'Buffer', data: [0xaa, 0xbb, 0xcc]}], fmt: "hex", expected: 'aabbcc' }, + { args: [new String('00aabbcc'), 'hex'], fmt: "hex", expected: '00aabbcc' }, + { args: [Buffer.from([0xaa, 0xbb, 0xcc]).toJSON()], fmt: "hex", expected: 'aabbcc' }, + + { args: [(function() {var arr = new Array(1, 2, 3); arr.valueOf = () => arr; return arr})()], + fmt: "hex", expected: '010203' }, + { args: [(function() {var obj = new Object(); obj.valueOf = () => obj; return obj})()], + exception: 'TypeError: first argument is not a string or Buffer-like object' }, + { args: [(function() {var obj = new Object(); obj.valueOf = () => undefined; return obj})()], + exception: 'TypeError: first argument is not a string or Buffer-like object' }, + { args: [(function() {var obj = new Object(); obj.valueOf = () => null; return obj})()], + exception: 'TypeError: first argument is not a string or Buffer-like object' }, + { args: [(function() {var obj = new Object(); obj.valueOf = () => new Array(1, 2, 3); return obj})()], + fmt: "hex", expected: '010203' }, + { args: [(function() {var a = [1,2,3,4]; a[1] = { valueOf() { a.length = 3; return 1; } }; return a})()], + fmt: "hex", expected: '01010300' }, + + { args: [{type: 'B'}], + exception: 'TypeError: first argument is not a string or Buffer-like object' }, + { args: [{type: undefined}], + exception: 'TypeError: first argument is not a string or Buffer-like object' }, + { args: [{type: 'Buffer'}], + exception: 'TypeError: first argument is not a string or Buffer-like object' }, + { args: [{type: 'Buffer', data: null}], + exception: 'TypeError: first argument is not a string or Buffer-like object' }, + { args: [{type: 'Buffer', data: {}}], + exception: 'TypeError: first argument is not a string or Buffer-like object' }, + + { args: ['', 'utf-128'], exception: 'TypeError: "utf-128" encoding is not supported' }, + + { args: [''], fmt: "hex", expected: '' }, + { args: ['α'], fmt: "hex", expected: 'ceb1' }, + { args: ['α', 'utf-8'], fmt: "hex", expected: 'ceb1' }, + { args: ['α', 'utf8'], fmt: "hex", expected: 'ceb1' }, + { args: ['', 'hex'], fmt: "hex", expected: '' }, + { args: ['aa0', 'hex'], fmt: "hex", expected: 'aa' }, + { args: ['00aabbcc', 'hex'], fmt: "hex", expected: '00aabbcc' }, + { args: ['deadBEEF##', 'hex'], fmt: "hex", expected: 'deadbeef' }, + { args: ['6576696c', 'hex'], expected: 'evil' }, + { args: ['f3', 'hex'], expected: '�' }, + + { args: ['', "base64"], expected: '' }, + { args: ['#', "base64"], expected: '' }, + { args: ['Q', "base64"], expected: '' }, + { args: ['QQ', "base64"], expected: 'A' }, + { args: ['QQ=', "base64"], expected: 'A' }, + { args: ['QQ==', "base64"], expected: 'A' }, + { args: ['QUI=', "base64"], expected: 'AB' }, + { args: ['QUI', "base64"], expected: 'AB' }, + { args: ['QUJD', "base64"], expected: 'ABC' }, + { args: ['QUJDRA==', "base64"], expected: 'ABCD' }, + + { args: ['', "base64url"], expected: '' }, + { args: ['QQ', "base64url"], expected: 'A' }, + { args: ['QUI', "base64url"], expected: 'AB' }, + { args: ['QUJD', "base64url"], expected: 'ABC' }, + { args: ['QUJDRA', "base64url"], expected: 'ABCD' }, + { args: ['QUJDRA#', "base64url"], expected: 'ABCD' }, +]}; + +let isBuffer_tsuite = { + name: "Buffer.isBuffer() tests", + skip: () => (!has_buffer()), + T: async (params) => { + let r = Buffer.isBuffer(params.value); + + if (r !== params.expected) { + throw Error(`unexpected output "${r}" != "${params.expected}"`); + } + + return 'SUCCESS'; + }, + prepare_args: p, + opts: {}, + + tests: [ + { value: Buffer.from('α'), expected: true }, + { value: new Uint8Array(10), expected: false }, + { value: {}, expected: false }, + { value: 1, expected: false }, +]}; + + +function compare_object(a, b) { + if (a === b) { + return true; + } + + if (typeof a !== 'object' || typeof b !== 'object') { + return false; + } + + if (Object.keys(a).length !== Object.keys(b).length) { + return false; + } + + for (let key in a) { + if (!compare_object(a[key], b[key])) { + return false; + } + + } + + return true; +} + + +let toJSON_tsuite = { + name: "Buffer.toJSON() tests", + skip: () => (!has_buffer()), + T: async (params) => { + let r = Buffer.from(params.value).toJSON(); + + if (!compare_object(r, params.expected)) { + throw Error(`unexpected output "${JSON.stringify(r)}" != "${JSON.stringify(params.expected)}"`); + } + + return 'SUCCESS'; + }, + + prepare_args: p, + opts: {}, + tests: [ + { value: '', expected: { type: 'Buffer', data: [] } }, + { value: 'αβγ', expected: { type: 'Buffer', data: [0xCE, 0xB1, 0xCE, 0xB2, 0xCE, 0xB3] } }, + { value: new Uint8Array([0xff, 0xde, 0xba]), expected: { type: 'Buffer', data: [0xFF, 0xDE, 0xBA] } }, + ], +}; + + +let toString_tsuite = { + name: "Buffer.toString() tests", + skip: () => (!has_buffer()), + T: async (params) => { + let r = Buffer.from(params.value).toString(params.fmt); + + if (r.length !== params.expected.length) { + throw Error(`unexpected "${r}" length ${r.length} != ${params.expected.length}`); + } + + if (r !== params.expected) { + throw Error(`unexpected output "${r}" != "${params.expected}"`); + } + + return 'SUCCESS'; + }, + prepare_args: p, + opts: { fmt: 'utf-8' }, + + tests: [ + { value: '💩', expected: '💩' }, + { value: String.fromCharCode(0xD83D, 0xDCA9), expected: '💩' }, + { value: String.fromCharCode(0xD83D, 0xDCA9), expected: String.fromCharCode(0xD83D, 0xDCA9) }, + { value: new Uint8Array([0xff, 0xde, 0xba]), fmt: "hex", expected: 'ffdeba' }, + { value: new Uint8Array([0xff, 0xde, 0xba]), fmt: "base64", expected: '/966' }, + { value: new Uint8Array([0xff, 0xde, 0xba]), fmt: "base64url", expected: '_966' }, + { value: '', fmt: "utf-128", exception: 'TypeError: "utf-128" encoding is not supported' }, +]}; + +run([ + from_tsuite, + isBuffer_tsuite, + toJSON_tsuite, + toString_tsuite, +]) +.then($DONE, $DONE); diff --git a/test/harness/runTsuite.js b/test/harness/runTsuite.js index 3d8268072..dc2034fd9 100644 --- a/test/harness/runTsuite.js +++ b/test/harness/runTsuite.js @@ -42,7 +42,15 @@ function merge(to, from) { r[v] = merge(r[v], from[v]); } else if (typeof from[v] == 'object') { - r[v] = Object.assign(Array.isArray(from[v]) ? [] : {}, from[v]); + if (Buffer.isBuffer(from[v])) { + r[v] = Buffer.from(from[v]); + + } else if (from[v] instanceof Uint8Array) { + r[v] = new Uint8Array(from[v]); + + } else { + r[v] = Object.assign(Array.isArray(from[v]) ? [] : {}, from[v]); + } } else { r[v] = from[v]; diff --git a/test/setup b/test/setup index ae7fcae16..c4187b530 100644 --- a/test/setup +++ b/test/setup @@ -38,7 +38,7 @@ njs_failed_list="" NJS_TESTS="" for arg in $NJS_TEST_PATHS; do if [ -d $arg ]; then - NJS_TESTS="$NJS_TESTS $(find $arg -name '*\.t\.js')" + NJS_TESTS="$NJS_TESTS $(find $arg -name '*\.t\.js' -o -name '*\.t\.mjs')" else NJS_TESTS="$NJS_TESTS $arg" fi diff --git a/test/zlib.t.mjs b/test/zlib.t.mjs new file mode 100644 index 000000000..d972f6f83 --- /dev/null +++ b/test/zlib.t.mjs @@ -0,0 +1,109 @@ +/*--- +includes: [runTsuite.js] +flags: [async] +---*/ + +import zlib from 'zlib'; + +function p(args, default_opts) { + let params = merge({}, default_opts); + params = merge(params, args); + + return params; +} + +let deflateSync_tsuite = { + name: "deflateSync()/deflateRawSync() tests", + skip: () => !zlib.deflateRawSync, + T: async (params) => { + const method = params.raw ? zlib.deflateRawSync : zlib.deflateSync; + const r = method(params.value, params.options).toString('base64'); + + if (r.length !== params.expected.length) { + throw Error(`unexpected "${r}" length ${r.length} != ${params.expected.length}`); + } + + if (r !== params.expected) { + throw Error(`unexpected output "${r}" != "${params.expected}"`); + } + + return 'SUCCESS'; + }, + prepare_args: p, + opts: { raw: true }, + + tests: [ + { value: 'WAKA', expected: 'C3f0dgQA' }, + { value: 'αβγ', expected: 'O7fx3KZzmwE=' }, + { value: new Uint8Array([0x57, 0x41, 0x4b, 0x41]), expected: 'C3f0dgQA' }, + { value: Buffer.from([0x57, 0x41, 0x4b, 0x41]), expected: 'C3f0dgQA' }, + { value: 'WAKA', options: {level: zlib.constants.Z_NO_COMPRESSION}, expected: 'AQQA+/9XQUtB' }, + { value: 'αβγ', options: {level: zlib.constants.Z_NO_COMPRESSION}, expected: 'AQYA+f/Osc6yzrM=' }, + { value: 'WAKA'.repeat(10), options: {strategy: zlib.constants.Z_FIXED}, expected: 'C3f0dgwnAgMA' }, + { value: 'WAKA'.repeat(10), options: {strategy: zlib.constants.Z_RLE}, + expected: 'BcExAQAAAMKgbNwLYP8mwmQymUwmk8lkcg==' }, + { value: 'WAKA'.repeat(35), options: {strategy: zlib.constants.Z_RLE, memLevel: 1}, + expected: 'BMExAQAAAMKgbNwLYP8mwmQymUwmk8lkMplMJpPJZDKZTCaTyWQymUwmk+lzDHf0dgx39HYMd/R2BAA=' }, + { value: 'WAKA'.repeat(35), options: {strategy: zlib.constants.Z_RLE, memLevel: 8}, + expected: 'BcExAQAAAMKgbNwLYP8mwmQymUwmk8lkMplMJpPJZDKZTCaTyWQymUwmk8lkMjk=' }, + { value: 'WAKA', raw: false, expected: 'eJwLd/R2BAAC+gEl' }, + { value: 'αβγ', raw: false, expected: 'eJw7t/HcpnObAQ/sBIE=' }, + + { value: 'WAKA', options: {level: 10}, exception: 'RangeError: level must be in the range -1..9' }, + { value: 'WAKA', options: {strategy: 10}, exception: 'RangeError: unknown strategy: 10' }, + { value: 'WAKA', options: {memLevel: 10}, exception: 'RangeError: memLevel must be in the range 1..9' }, + { value: 'WAKA', options: {windowBits: 99}, exception: 'RangeError: windowBits must be in the range -15..-9' }, +]}; + +let inflateSync_tsuite = { + name: "inflateSync()/inflateRawSync() tests", + skip: () => !zlib.inflateRawSync || !zlib.deflateRawSync, + T: async (params) => { + const method = params.raw ? zlib.inflateRawSync : zlib.inflateSync; + const r = method(params.value, params.options).toString(); + + if (r.length !== params.expected.length) { + throw Error(`unexpected "${r}" length ${r.length} != ${params.expected.length}`); + } + + if (r !== params.expected) { + throw Error(`unexpected output "${r}" != "${params.expected}"`); + } + + return 'SUCCESS'; + }, + prepare_args: p, + opts: { raw: true }, + + tests: [ + { value: Buffer.from('C3f0dgQA', 'base64'), expected: 'WAKA' }, + { value: Buffer.from('C3f0dgQA', 'base64'), expected: 'WAKA' }, + { value: new Uint8Array([0x0b, 0x77, 0xf4, 0x76, 0x04, 0x00]), expected: 'WAKA' }, + { value: Buffer.from('eJwLd/R2BAAC+gEl', 'base64'), raw: false, expected: 'WAKA' }, + { value: Buffer.from('eJw7t/HcpnObAQ/sBIE=', 'base64'), raw: false, expected: 'αβγ' }, + { value: zlib.deflateRawSync('WAKA'), expected: 'WAKA' }, + { value: zlib.deflateRawSync('αβγ'), expected: 'αβγ' }, + { value: zlib.deflateRawSync('WAKA', {dictionary: Buffer.from('WAKA')}), options: {dictionary: Buffer.from('WAKA')}, + expected: 'WAKA' }, + { value: zlib.deflateRawSync('αβγ', {dictionary: Buffer.from('αβγ')}), options: {dictionary: Buffer.from('αβγ')}, + expected: 'αβγ' }, + { value: zlib.deflateRawSync('αβγ'.repeat(56), {chunkSize: 64}), expected: 'αβγ'.repeat(56) }, + { value: zlib.deflateRawSync('WAKA'.repeat(1024)), expected: 'WAKA'.repeat(1024) }, + { value: zlib.deflateRawSync('αβγ'.repeat(1024)), expected: 'αβγ'.repeat(1024) }, + { value: zlib.deflateRawSync('WAKA'.repeat(1024)), options: {chunkSize: 64}, expected: 'WAKA'.repeat(1024) }, + { value: zlib.deflateRawSync('αβγ'.repeat(1024)), options: {chunkSize: 64}, expected: 'αβγ'.repeat(1024) }, + + { value: Buffer.from('C3f0dgQA', 'base64'), options: {chunkSize: 0}, + exception: 'RangeError: chunkSize must be >= 64' }, + { value: Buffer.from('C3f0dgQA', 'base64'), options: {windowBits: 0}, + exception: 'RangeError: windowBits must be in the range -15..-8' }, + + { value: zlib.deflateRawSync('WAKA', {dictionary: Buffer.from('WAKA')}), + exception: 'InternalError: failed to inflate the compressed data: invalid distance too far back' }, +]}; + +run([ + deflateSync_tsuite, + inflateSync_tsuite, +]) +.then($DONE, $DONE);