summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt14
-rw-r--r--lib/zlib.c493
2 files changed, 507 insertions, 0 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index adc04ec..4564818 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -28,6 +28,7 @@ find_library(libuci NAMES uci)
find_library(libubox NAMES ubox)
find_library(libubus NAMES ubus)
find_library(libblobmsg_json NAMES blobmsg_json)
+find_package(ZLIB)
if(LINUX)
find_library(libnl_tiny NAMES nl-tiny)
@@ -49,6 +50,10 @@ if(libubox)
set(DEFAULT_ULOOP_SUPPORT ON)
endif()
+if(ZLIB_FOUND)
+ set(DEFAULT_ZLIB_SUPPORT ON)
+endif()
+
option(DEBUG_SUPPORT "Debug plugin support" ON)
option(FS_SUPPORT "Filesystem plugin support" ON)
option(MATH_SUPPORT "Math plugin support" ON)
@@ -61,6 +66,7 @@ option(STRUCT_SUPPORT "Struct plugin support" ON)
option(ULOOP_SUPPORT "Uloop plugin support" ${DEFAULT_ULOOP_SUPPORT})
option(LOG_SUPPORT "Log plugin support" ON)
option(SOCKET_SUPPORT "Socket plugin support" ON)
+option(ZLIB_SUPPORT "Zlib plugin support" ${DEFAULT_ZLIB_SUPPORT})
set(LIB_SEARCH_PATH "${CMAKE_INSTALL_PREFIX}/lib/ucode/*.so:${CMAKE_INSTALL_PREFIX}/share/ucode/*.uc:./*.so:./*.uc" CACHE STRING "Default library search path")
string(REPLACE ":" "\", \"" LIB_SEARCH_DEFINE "${LIB_SEARCH_PATH}")
@@ -276,6 +282,14 @@ if(SOCKET_SUPPORT)
target_link_options(socket_lib PRIVATE ${UCODE_MODULE_LINK_OPTIONS})
endif()
+if(ZLIB_SUPPORT)
+ set(LIBRARIES ${LIBRARIES} zlib_lib)
+ add_library(zlib_lib MODULE lib/zlib.c)
+ set_target_properties(zlib_lib PROPERTIES OUTPUT_NAME zlib PREFIX "")
+ target_link_options(zlib_lib PRIVATE ${UCODE_MODULE_LINK_OPTIONS})
+ target_link_libraries(zlib_lib ZLIB::ZLIB)
+endif()
+
if(UNIT_TESTING)
enable_testing()
add_definitions(-DUNIT_TESTING)
diff --git a/lib/zlib.c b/lib/zlib.c
new file mode 100644
index 0000000..8190251
--- /dev/null
+++ b/lib/zlib.c
@@ -0,0 +1,493 @@
+/*
+ * Copyright (C) 2024 Thibaut VARĂˆNE <hacks@slashdirt.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/**
+ * # Zlib bindings
+ *
+ * The `zlib` module provides single-call-oriented functions for interacting with zlib data.
+ *
+ * @module zlib
+ */
+
+#include <stdio.h>
+#include <string.h>
+#include <assert.h>
+#include <errno.h>
+#include <zlib.h>
+
+#include "ucode/module.h"
+#include "ucode/platform.h"
+
+// https://zlib.net/zlib_how.html
+
+/*
+ * CHUNK is simply the buffer size for feeding data to and pulling data from
+ * the zlib routines. Larger buffer sizes would be more efficient, especially
+ * for inflate(). If the memory is available, buffers sizes on the order of
+ * 128K or 256K bytes should be used.
+ */
+#define CHUNK 16384
+
+
+/* zlib init error message */
+static const char * ziniterr(int ret)
+{
+ const char * msg;
+
+ switch (ret) {
+ case Z_ERRNO:
+ msg = strerror(errno);
+ break;
+ case Z_STREAM_ERROR: // can only happen for deflateInit2() by construction
+ msg = "invalid compression level";
+ break;
+ case Z_MEM_ERROR:
+ msg = "out of memory";
+ break;
+ case Z_VERSION_ERROR:
+ msg = "zlib version mismatch!";
+ break;
+ default:
+ msg = "unknown error";
+ break;
+ }
+
+ return msg;
+}
+
+static uc_stringbuf_t *
+uc_zlib_def_object(uc_vm_t *vm, uc_value_t *obj, z_stream *strm)
+{
+ int ret;
+ bool eof = false;
+ uc_value_t *rfn, *rbuf;
+ uc_stringbuf_t *buf = NULL;
+
+ rfn = ucv_property_get(obj, "read");
+
+ if (!ucv_is_callable(rfn)) {
+ uc_vm_raise_exception(vm, EXCEPTION_TYPE,
+ "Input object does not implement read() method");
+ return NULL;
+ }
+
+ buf = ucv_stringbuf_new();
+
+ do {
+ rbuf = NULL;
+ uc_vm_stack_push(vm, ucv_get(obj));
+ uc_vm_stack_push(vm, ucv_get(rfn));
+ uc_vm_stack_push(vm, ucv_int64_new(CHUNK));
+
+ if (uc_vm_call(vm, true, 1) != EXCEPTION_NONE)
+ goto fail;
+
+ rbuf = uc_vm_stack_pop(vm); // read output chunk
+
+ /* we only accept strings */
+ if (rbuf != NULL && ucv_type(rbuf) != UC_STRING) {
+ uc_vm_raise_exception(vm, EXCEPTION_TYPE,
+ "Input object read() method returned non-string value");
+ goto fail;
+ }
+
+ /* check EOF */
+ eof = (rbuf == NULL || ucv_string_length(rbuf) == 0);
+
+ strm->next_in = (unsigned char *)ucv_string_get(rbuf);
+ strm->avail_in = ucv_string_length(rbuf);
+
+ /* run deflate() on input until output buffer not full */
+ do {
+ // enlarge buf by CHUNK amount as needed
+ printbuf_memset(buf, printbuf_length(buf) + CHUNK - 1, 0, 1);
+ buf->bpos -= CHUNK;
+
+ strm->avail_out = CHUNK;
+ strm->next_out = (unsigned char *)(buf->buf + buf->bpos);;
+
+ ret = deflate(strm, eof ? Z_FINISH : Z_NO_FLUSH); // no bad return value here
+ assert(ret != Z_STREAM_ERROR); // state never clobbered (would be mem corruption)
+ (void)ret; // XXX make annoying compiler that ignores assert() happy
+
+ // update bpos past data written by deflate()
+ buf->bpos += CHUNK - strm->avail_out;
+ } while (strm->avail_out == 0);
+ assert(strm->avail_in == 0); // all input will be used
+
+ ucv_put(rbuf); // release rbuf
+ } while (!eof); // finish compression if all of source has been read in
+ assert(ret == Z_STREAM_END); // stream will be complete
+
+ return buf;
+
+fail:
+ ucv_put(rbuf);
+ printbuf_free(buf);
+ return NULL;
+}
+
+static uc_stringbuf_t *
+uc_zlib_def_string(uc_vm_t *vm, uc_value_t *str, z_stream *strm)
+{
+ int ret;
+ uc_stringbuf_t *buf = NULL;
+
+ buf = ucv_stringbuf_new();
+
+ strm->next_in = (unsigned char *)ucv_string_get(str);
+ strm->avail_in = ucv_string_length(str);
+
+ do {
+ printbuf_memset(buf, printbuf_length(buf) + CHUNK - 1, 0, 1);
+ buf->bpos -= CHUNK;
+
+ strm->avail_out = CHUNK;
+ strm->next_out = (unsigned char *)(buf->buf + buf->bpos);
+
+ ret = deflate(strm, Z_FINISH);
+ assert(ret != Z_STREAM_ERROR);
+
+ buf->bpos += CHUNK - strm->avail_out;
+ } while (ret != Z_STREAM_END);
+ assert(strm->avail_in == 0);
+
+ return buf;
+}
+
+/**
+ * Compresses data in Zlib or gzip format.
+ *
+ * If the input argument is a plain string, it is directly compressed.
+ *
+ * If an array, object or resource value is given, this function will attempt to
+ * invoke a `read()` method on it to read chunks of input text to incrementally
+ * compress. Reading will stop if the object's `read()` method returns
+ * either `null` or an empty string.
+ *
+ * Throws an exception on errors.
+ *
+ * Returns the compressed data.
+ *
+ * @function module:zlib#deflate
+ *
+ * @param {string} str_or_resource
+ * The string or resource object to be parsed as JSON.
+ *
+ * @param {?boolean} [gzip=false]
+ * Add a gzip header if true (creates a gzip-compliant output, otherwise defaults to Zlib)
+ *
+ * @param {?number} [level=Z_DEFAULT_COMPRESSION]
+ * The compression level (0-9).
+ *
+ * @returns {?string}
+ *
+ * @example
+ * // deflate content using default compression
+ * const deflated = deflate(content);
+ *
+ * // deflate content using fastest compression
+ * const deflated = deflate(content, Z_BEST_SPEED);
+ */
+static uc_value_t *
+uc_zlib_deflate(uc_vm_t *vm, size_t nargs)
+{
+ uc_value_t *rv = NULL;
+ uc_value_t *src = uc_fn_arg(0);
+ uc_value_t *gzip = uc_fn_arg(1);
+ uc_value_t *level = uc_fn_arg(2);
+ uc_stringbuf_t *buf = NULL;
+ int ret, lvl = Z_DEFAULT_COMPRESSION;
+ bool gz = false;
+ z_stream strm = {
+ .zalloc = Z_NULL,
+ .zfree = Z_NULL,
+ .opaque = Z_NULL,
+ };
+
+ if (gzip) {
+ if (ucv_type(gzip) != UC_BOOLEAN) {
+ uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Passed gzip flag is not a boolean");
+ goto out;
+ }
+
+ gz = (int)ucv_boolean_get(gzip);
+ }
+
+ if (level) {
+ if (ucv_type(level) != UC_INTEGER) {
+ uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Passed level is not a number");
+ goto out;
+ }
+
+ lvl = (int)ucv_int64_get(level);
+ }
+
+ ret = deflateInit2(&strm, lvl,
+ Z_DEFLATED, // only allowed method
+ gz ? 15+16 : 15, // 15 Zlib default, +16 for gzip
+ 8, // default value
+ Z_DEFAULT_STRATEGY); // default value
+ if (ret != Z_OK) {
+ uc_vm_raise_exception(vm, EXCEPTION_RUNTIME, "Zlib error: %s", ziniterr(ret));
+ goto out;
+ }
+
+ switch (ucv_type(src)) {
+ case UC_STRING:
+ buf = uc_zlib_def_string(vm, src, &strm);
+ break;
+
+ case UC_RESOURCE:
+ case UC_OBJECT:
+ case UC_ARRAY:
+ buf = uc_zlib_def_object(vm, src, &strm);
+ break;
+
+ default:
+ uc_vm_raise_exception(vm, EXCEPTION_TYPE,
+ "Passed value is neither a string nor an object");
+ goto out;
+ }
+
+ if (!buf) {
+ if (vm->exception.type == EXCEPTION_NONE) // do not clobber previous exception
+ uc_vm_raise_exception(vm, EXCEPTION_RUNTIME, "Zlib error: %s", strm.msg);
+ goto out;
+ }
+
+ rv = ucv_stringbuf_finish(buf);
+
+out:
+ (void)deflateEnd(&strm);
+ return rv;
+}
+
+static uc_stringbuf_t *
+uc_zlib_inf_object(uc_vm_t *vm, uc_value_t *obj, z_stream *strm)
+{
+ int ret = Z_STREAM_ERROR; // error out if EOF on first loop
+ bool eof = false;
+ uc_value_t *rfn, *rbuf;
+ uc_stringbuf_t *buf = NULL;
+
+ rfn = ucv_property_get(obj, "read");
+
+ if (!ucv_is_callable(rfn)) {
+ uc_vm_raise_exception(vm, EXCEPTION_TYPE,
+ "Input object does not implement read() method");
+ return NULL;
+ }
+
+ buf = ucv_stringbuf_new();
+
+ do {
+ rbuf = NULL;
+ uc_vm_stack_push(vm, ucv_get(obj));
+ uc_vm_stack_push(vm, ucv_get(rfn));
+ uc_vm_stack_push(vm, ucv_int64_new(CHUNK));
+
+ if (uc_vm_call(vm, true, 1) != EXCEPTION_NONE)
+ goto fail;
+
+ rbuf = uc_vm_stack_pop(vm); // read output chunk
+
+ /* we only accept strings */
+ if (rbuf != NULL && ucv_type(rbuf) != UC_STRING) {
+ uc_vm_raise_exception(vm, EXCEPTION_TYPE,
+ "Input object read() method returned non-string value");
+ goto fail;
+ }
+
+ /* check EOF */
+ eof = (rbuf == NULL || ucv_string_length(rbuf) == 0);
+ if (eof)
+ break;
+
+ strm->next_in = (unsigned char *)ucv_string_get(rbuf);
+ strm->avail_in = ucv_string_length(rbuf);
+
+ /* run deflate() on input until output buffer not full */
+ do {
+ // enlarge buf by CHUNK amount as needed
+ printbuf_memset(buf, printbuf_length(buf) + CHUNK - 1, 0, 1);
+ buf->bpos -= CHUNK;
+
+ strm->avail_out = CHUNK;
+ strm->next_out = (unsigned char *)(buf->buf + buf->bpos);;
+
+ ret = inflate(strm, Z_NO_FLUSH);
+ assert(ret != Z_STREAM_ERROR); // state never clobbered (would be mem corruption)
+ switch (ret) {
+ case Z_NEED_DICT:
+ case Z_DATA_ERROR:
+ case Z_MEM_ERROR:
+ goto fail;
+ }
+
+ // update bpos past data written by deflate()
+ buf->bpos += CHUNK - strm->avail_out;
+ } while (strm->avail_out == 0);
+
+ ucv_put(rbuf); // release rbuf
+ } while (ret != Z_STREAM_END); // done when inflate() says it's done
+
+ if (ret != Z_STREAM_END) { // data error
+ printbuf_free(buf);
+ buf = NULL;
+ }
+
+ return buf;
+
+fail:
+ ucv_put(rbuf);
+ printbuf_free(buf);
+ return NULL;
+}
+
+static uc_stringbuf_t *
+uc_zlib_inf_string(uc_vm_t *vm, uc_value_t *str, z_stream *strm)
+{
+ int ret;
+ uc_stringbuf_t *buf = NULL;
+
+ buf = ucv_stringbuf_new();
+
+ strm->next_in = (unsigned char *)ucv_string_get(str);
+ strm->avail_in = ucv_string_length(str);
+
+ do {
+ printbuf_memset(buf, printbuf_length(buf) + CHUNK - 1, 0, 1);
+ buf->bpos -= CHUNK;
+
+ strm->avail_out = CHUNK;
+ strm->next_out = (unsigned char *)(buf->buf + buf->bpos);
+
+ ret = inflate(strm, Z_FINISH);
+ assert(ret != Z_STREAM_ERROR);
+ switch (ret) {
+ case Z_NEED_DICT:
+ case Z_DATA_ERROR:
+ case Z_MEM_ERROR:
+ printbuf_free(buf);
+ return NULL;
+ }
+
+ buf->bpos += CHUNK - strm->avail_out;
+ } while (ret != Z_STREAM_END);
+ assert(strm->avail_in == 0);
+
+ return buf;
+}
+
+/**
+ * Decompresses data in Zlib or gzip format.
+ *
+ * If the input argument is a plain string, it is directly decompressed.
+ *
+ * If an array, object or resource value is given, this function will attempt to
+ * invoke a `read()` method on it to read chunks of input text to incrementally
+ * decompress. Reading will stop if the object's `read()` method returns
+ * either `null` or an empty string.
+ *
+ * Throws an exception on errors.
+ *
+ * Returns the decompressed data.
+ *
+ * @function module:zlib#inflate
+ *
+ * @param {string} str_or_resource
+ * The string or resource object to be parsed as JSON.
+ *
+ * @returns {?string}
+ */
+static uc_value_t *
+uc_zlib_inflate(uc_vm_t *vm, size_t nargs)
+{
+ uc_value_t *rv = NULL;
+ uc_value_t *src = uc_fn_arg(0);
+ uc_stringbuf_t *buf = NULL;
+ int ret;
+ z_stream strm = {
+ .zalloc = Z_NULL,
+ .zfree = Z_NULL,
+ .opaque = Z_NULL,
+ .avail_in = 0, // must be initialized before call to inflateInit
+ .next_in = Z_NULL, // must be initialized before call to inflateInit
+ };
+
+ /* tell inflateInit2 to perform either zlib or gzip decompression: 15+32 */
+ ret = inflateInit2(&strm, 15+32);
+ if (ret != Z_OK) {
+ uc_vm_raise_exception(vm, EXCEPTION_RUNTIME, "Zlib error: %s", ziniterr(ret));
+ goto out;
+ }
+
+ switch (ucv_type(src)) {
+ case UC_STRING:
+ buf = uc_zlib_inf_string(vm, src, &strm);
+ break;
+
+ case UC_RESOURCE:
+ case UC_OBJECT:
+ case UC_ARRAY:
+ buf = uc_zlib_inf_object(vm, src, &strm);
+ break;
+
+ default:
+ uc_vm_raise_exception(vm, EXCEPTION_TYPE,
+ "Passed value is neither a string nor an object");
+ goto out;
+ }
+
+ if (!buf) {
+ if (vm->exception.type == EXCEPTION_NONE) // do not clobber previous exception
+ uc_vm_raise_exception(vm, EXCEPTION_RUNTIME, "Zlib error: %s", strm.msg);
+ goto out;
+ }
+
+ rv = ucv_stringbuf_finish(buf);
+
+out:
+ (void)inflateEnd(&strm);
+ return rv;
+}
+
+static const uc_function_list_t global_fns[] = {
+ { "deflate", uc_zlib_deflate },
+ { "inflate", uc_zlib_inflate },
+};
+
+void uc_module_init(uc_vm_t *vm, uc_value_t *scope)
+{
+ uc_function_list_register(scope, global_fns);
+
+#define ADD_CONST(x) ucv_object_add(scope, #x, ucv_int64_new(x))
+
+ /**
+ * @typedef
+ * @name Compression levels
+ * @description Constants representing predefined compression levels.
+ * @property {number} Z_NO_COMPRESSION.
+ * @property {number} Z_BEST_SPEED.
+ * @property {number} Z_BEST_COMPRESSION.
+ * @property {number} Z_DEFAULT_COMPRESSION - default compromise between speed and compression (currently equivalent to level 6).
+ */
+ ADD_CONST(Z_NO_COMPRESSION);
+ ADD_CONST(Z_BEST_SPEED);
+ ADD_CONST(Z_BEST_COMPRESSION);
+ ADD_CONST(Z_DEFAULT_COMPRESSION);
+}