diff --git a/examples/spec/zlib.js b/examples/spec/zlib.js new file mode 100644 index 0000000..0d274ba --- /dev/null +++ b/examples/spec/zlib.js @@ -0,0 +1,196 @@ +import { test, testThrows, summary } from './helpers.js'; +import zlib from 'node:zlib'; + +console.log('node:zlib Tests\n'); + +test('zlib is object', typeof zlib, 'object'); +test('zlib toStringTag', Object.prototype.toString.call(zlib), '[object zlib]'); + +test('constants exists', typeof zlib.constants, 'object'); +test('Z_OK is 0', zlib.constants.Z_OK, 0); +test('Z_NO_FLUSH is 0', zlib.constants.Z_NO_FLUSH, 0); +test('Z_FINISH is 4', zlib.constants.Z_FINISH, 4); +test('Z_DEFAULT_COMPRESSION is -1', zlib.constants.Z_DEFAULT_COMPRESSION, -1); +test('Z_BEST_SPEED is 1', zlib.constants.Z_BEST_SPEED, 1); +test('Z_BEST_COMPRESSION is 9', zlib.constants.Z_BEST_COMPRESSION, 9); + +test('createGzip is function', typeof zlib.createGzip, 'function'); +test('createGunzip is function', typeof zlib.createGunzip, 'function'); +test('createDeflate is function', typeof zlib.createDeflate, 'function'); +test('createInflate is function', typeof zlib.createInflate, 'function'); +test('createDeflateRaw is function', typeof zlib.createDeflateRaw, 'function'); +test('createInflateRaw is function', typeof zlib.createInflateRaw, 'function'); +test('createUnzip is function', typeof zlib.createUnzip, 'function'); +test('createBrotliCompress is function', typeof zlib.createBrotliCompress, 'function'); +test('createBrotliDecompress is function', typeof zlib.createBrotliDecompress, 'function'); + +test('gzipSync is function', typeof zlib.gzipSync, 'function'); +test('gunzipSync is function', typeof zlib.gunzipSync, 'function'); +test('deflateSync is function', typeof zlib.deflateSync, 'function'); +test('inflateSync is function', typeof zlib.inflateSync, 'function'); +test('deflateRawSync is function', typeof zlib.deflateRawSync, 'function'); +test('inflateRawSync is function', typeof zlib.inflateRawSync, 'function'); +test('unzipSync is function', typeof zlib.unzipSync, 'function'); +test('brotliCompressSync is function', typeof zlib.brotliCompressSync, 'function'); +test('brotliDecompressSync is function', typeof zlib.brotliDecompressSync, 'function'); + +test('gzip is function', typeof zlib.gzip, 'function'); +test('gunzip is function', typeof zlib.gunzip, 'function'); +test('deflate is function', typeof zlib.deflate, 'function'); +test('inflate is function', typeof zlib.inflate, 'function'); +test('crc32 is function', typeof zlib.crc32, 'function'); + +const gz = zlib.createGzip(); +test('createGzip returns object', typeof gz, 'object'); +test('createGzip has write', typeof gz.write, 'function'); +test('createGzip has end', typeof gz.end, 'function'); +test('createGzip has on', typeof gz.on, 'function'); +test('createGzip has pipe', typeof gz.pipe, 'function'); +test('createGzip has destroy', typeof gz.destroy, 'function'); +test('createGzip readable=true', gz.readable, true); +test('createGzip writable=true', gz.writable, true); +test('createGzip bytesWritten starts 0', gz.bytesWritten, 0); + +const gun = zlib.createGunzip(); +test('createGunzip has write', typeof gun.write, 'function'); +test('createGunzip readable=true', gun.readable, true); + +const inf = zlib.createInflate(); +test('createInflate has write', typeof inf.write, 'function'); + +const br = zlib.createBrotliDecompress(); +test('createBrotliDecompress returns object', typeof br, 'object'); +test('createBrotliDecompress has write', typeof br.write, 'function'); + +const input = Buffer.from('Hello, zlib world!'); + +const gzipped = zlib.gzipSync(input); +test('gzipSync returns Buffer', gzipped instanceof Uint8Array, true); +test('gzipSync has gzip magic bytes', gzipped[0], 0x1f); +test('gzipSync magic byte 2', gzipped[1], 0x8b); +test('gzipSync result is smaller for repetitive input', gzipped.length > 0, true); + +const gunzipped = zlib.gunzipSync(gzipped); +test('gunzipSync roundtrip length', gunzipped.length, input.length); +test('gunzipSync roundtrip value', Buffer.from(gunzipped).toString(), 'Hello, zlib world!'); + +const deflated = zlib.deflateSync(input); +test('deflateSync returns Buffer', deflated instanceof Uint8Array, true); +test('deflateSync result non-empty', deflated.length > 0, true); + +const inflated = zlib.inflateSync(deflated); +test('inflateSync roundtrip', Buffer.from(inflated).toString(), 'Hello, zlib world!'); + +const deflatedRaw = zlib.deflateRawSync(input); +test('deflateRawSync result non-empty', deflatedRaw.length > 0, true); + +const inflatedRaw = zlib.inflateRawSync(deflatedRaw); +test('inflateRawSync roundtrip', Buffer.from(inflatedRaw).toString(), 'Hello, zlib world!'); + +const brotliCompressed = zlib.brotliCompressSync(input); +test('brotliCompressSync result non-empty', brotliCompressed.length > 0, true); + +const brotliDecompressed = zlib.brotliDecompressSync(brotliCompressed); +test('brotliDecompressSync roundtrip', Buffer.from(brotliDecompressed).toString(), 'Hello, zlib world!'); + +const unzipped = zlib.unzipSync(gzipped); +test('unzipSync gzip roundtrip', Buffer.from(unzipped).toString(), 'Hello, zlib world!'); + +const fromString = zlib.gzipSync('hello string'); +test('gzipSync accepts string', fromString instanceof Uint8Array, true); +const fromStringBack = zlib.gunzipSync(fromString); +test('gunzipSync string roundtrip', Buffer.from(fromStringBack).toString(), 'hello string'); + +const repetitive = Buffer.from('a'.repeat(1000)); +const compressedRep = zlib.gzipSync(repetitive); +test('gzip compresses repetitive data', compressedRep.length < repetitive.length, true); + +const crc0 = zlib.crc32('hello'); +test('crc32 returns number', typeof crc0, 'number'); +test('crc32 hello is consistent', crc0, zlib.crc32('hello')); +test('crc32 chaining', zlib.crc32('world', zlib.crc32('hello')) !== zlib.crc32('hello'), true); +test('crc32 different inputs differ', zlib.crc32('hello') !== zlib.crc32('world'), true); + +function testAsyncGzip() { + return new Promise((resolve) => { + zlib.gzip(input, (err, result) => { + test('async gzip no error', err, null); + test('async gzip returns Buffer', result instanceof Uint8Array, true); + test('async gzip magic byte', result[0], 0x1f); + + zlib.gunzip(result, (err2, result2) => { + test('async gunzip no error', err2, null); + test('async gunzip roundtrip', Buffer.from(result2).toString(), 'Hello, zlib world!'); + resolve(); + }); + }); + }); +} + +function testAsyncDeflate() { + return new Promise((resolve) => { + zlib.deflate(input, (err, result) => { + test('async deflate no error', err, null); + test('async deflate result non-empty', result.length > 0, true); + + zlib.inflate(result, (err2, result2) => { + test('async inflate no error', err2, null); + test('async inflate roundtrip', Buffer.from(result2).toString(), 'Hello, zlib world!'); + resolve(); + }); + }); + }); +} + +function testAsyncBrotli() { + return new Promise((resolve) => { + zlib.brotliCompress(input, (err, result) => { + test('async brotliCompress no error', err, null); + test('async brotliCompress result non-empty', result.length > 0, true); + + zlib.brotliDecompress(result, (err2, result2) => { + test('async brotliDecompress no error', err2, null); + test('async brotliDecompress roundtrip', Buffer.from(result2).toString(), 'Hello, zlib world!'); + resolve(); + }); + }); + }); +} + +function testTransformStream() { + return new Promise((resolve) => { + const chunks = []; + const stream = zlib.createGunzip(); + + stream.on('data', (chunk) => chunks.push(chunk)); + stream.on('end', () => { + let total = 0; + for (const c of chunks) total += c.length; + const out = new Uint8Array(total); + let off = 0; + for (const c of chunks) { out.set(c, off); off += c.length; } + test('transform stream roundtrip', Buffer.from(out).toString(), 'Hello, zlib world!'); + resolve(); + }); + + const compressed = zlib.gzipSync(input); + stream.write(compressed); + stream.end(); + }); +} + +function testBytesWritten() { + const stream = zlib.createDeflate(); + const data = Buffer.from('track bytes written'); + stream.write(data); + test('bytesWritten after write', stream.bytesWritten, data.length); +} + +testBytesWritten(); + +await testAsyncGzip(); +await testAsyncDeflate(); +await testAsyncBrotli(); +await testTransformStream(); + +summary(); diff --git a/include/gc/modules.h b/include/gc/modules.h index 88ffc91..3d206db 100644 --- a/include/gc/modules.h +++ b/include/gc/modules.h @@ -28,5 +28,6 @@ void gc_mark_writable_streams(ant_t *js, gc_mark_fn mark); void gc_mark_transform_streams(ant_t *js, gc_mark_fn mark); void gc_mark_codec_streams(ant_t *js, gc_mark_fn mark); void gc_mark_compression_streams(ant_t *js, gc_mark_fn mark); +void gc_mark_zlib(ant_t *js, gc_mark_fn mark); #endif diff --git a/include/modules/zlib.h b/include/modules/zlib.h new file mode 100644 index 0000000..27cdd56 --- /dev/null +++ b/include/modules/zlib.h @@ -0,0 +1,9 @@ +#ifndef ANT_ZLIB_MODULE_H +#define ANT_ZLIB_MODULE_H + +#include "types.h" + +ant_value_t zlib_library(ant_t *js); +void gc_mark_zlib(ant_t *js, void (*mark)(ant_t *, ant_value_t)); + +#endif diff --git a/src/gc/objects.c b/src/gc/objects.c index a15652d..97e8ad2 100644 --- a/src/gc/objects.c +++ b/src/gc/objects.c @@ -461,6 +461,7 @@ static void gc_mark_roots(ant_t *js) { gc_mark_transform_streams(js, gc_mark_value); gc_mark_codec_streams(js, gc_mark_value); gc_mark_compression_streams(js, gc_mark_value); + gc_mark_zlib(js, gc_mark_value); for ( ant_object_t *obj = g_pending_promises; diff --git a/src/main.c b/src/main.c index 0e75e4c..e4117e2 100644 --- a/src/main.c +++ b/src/main.c @@ -83,6 +83,7 @@ #include "modules/headers.h" #include "modules/blob.h" #include "modules/formdata.h" +#include "modules/zlib.h" #include "streams/queuing.h" #include "streams/readable.h" #include "streams/writable.h" @@ -638,6 +639,7 @@ int main(int argc, char *argv[]) { ant_standard_library("worker_threads", worker_threads_library); ant_standard_library("async_hooks", async_hooks_library); ant_standard_library("v8", v8_library); + ant_standard_library("zlib", zlib_library); ant_value_t snapshot_result = ant_load_snapshot(js); if (vtype(snapshot_result) == T_ERR) { diff --git a/src/modules/zlib.c b/src/modules/zlib.c new file mode 100644 index 0000000..6cc4527 --- /dev/null +++ b/src/modules/zlib.c @@ -0,0 +1,703 @@ +#include + +#include +#include +#include + +#include "ant.h" +#include "ptr.h" +#include "errors.h" +#include "internal.h" + +#include "silver/engine.h" +#include "streams/brotli.h" +#include "modules/buffer.h" +#include "modules/events.h" +#include "modules/symbol.h" +#include "modules/zlib.h" + +#define ZLIB_CHUNK 16384 +#define ZLIB_STREAM_TAG 0x5A4C4942u + +typedef enum { + ZLIB_KIND_GZIP, + ZLIB_KIND_GUNZIP, + ZLIB_KIND_DEFLATE, + ZLIB_KIND_INFLATE, + ZLIB_KIND_DEFLATE_RAW, + ZLIB_KIND_INFLATE_RAW, + ZLIB_KIND_UNZIP, + ZLIB_KIND_BROTLI_COMPRESS, + ZLIB_KIND_BROTLI_DECOMPRESS, +} zlib_kind_t; + +typedef struct zlib_stream_s { + z_stream strm; + brotli_stream_state_t *brotli; + ant_value_t obj; + ant_t *js; + zlib_kind_t kind; + uint32_t bytes_written; + bool initialized; + bool ended; + bool destroyed; + struct zlib_stream_s *next_active; +} zlib_stream_t; + +static ant_value_t g_transform_proto = 0; +static zlib_stream_t *g_active_streams = NULL; + +static bool zlib_kind_is_compress(zlib_kind_t k) { + return + k == ZLIB_KIND_GZIP + || k == ZLIB_KIND_DEFLATE + || k == ZLIB_KIND_DEFLATE_RAW + || k == ZLIB_KIND_BROTLI_COMPRESS; +} + +static int zlib_window_bits(zlib_kind_t k) { + switch (k) { + case ZLIB_KIND_GZIP: return 15 + 16; + case ZLIB_KIND_GUNZIP: return 15 + 32; + case ZLIB_KIND_UNZIP: return 15 + 32; + case ZLIB_KIND_DEFLATE: return 15; + case ZLIB_KIND_INFLATE: return 15; + case ZLIB_KIND_DEFLATE_RAW: return -15; + case ZLIB_KIND_INFLATE_RAW: return -15; + default: return 15; + } +} + +static ant_value_t pick_callback(ant_value_t *args, int nargs) { + if (nargs > 2 && is_callable(args[2])) return args[2]; + if (nargs > 1 && is_callable(args[1])) return args[1]; + return js_mkundef(); +} + +static zlib_stream_t *zlib_stream_ptr(ant_value_t obj) { + if (!js_check_native_tag(obj, ZLIB_STREAM_TAG)) return NULL; + return (zlib_stream_t *)js_get_native_ptr(obj); +} + +static void zlib_add_active(zlib_stream_t *st) { + st->next_active = g_active_streams; + g_active_streams = st; +} + +static void zlib_remove_active(zlib_stream_t *st) { + zlib_stream_t **it; + for (it = &g_active_streams; *it; it = &(*it)->next_active) { + if (*it == st) { + *it = st->next_active; + st->next_active = NULL; + return; + }} +} + +static void zlib_stream_release(zlib_stream_t *st) { + if (!st) return; + if (st->brotli) { + brotli_stream_state_destroy(st->brotli); + st->brotli = NULL; + } else if (st->initialized) { + if (zlib_kind_is_compress(st->kind)) deflateEnd(&st->strm); + else inflateEnd(&st->strm); + st->initialized = false; + } +} + +static ant_value_t zlib_make_buffer(ant_t *js, const uint8_t *data, size_t len) { + ArrayBufferData *ab = create_array_buffer_data(len); + if (!ab) return js_mkerr(js, "out of memory"); + if (data && len > 0) memcpy(ab->data, data, len); + return create_typed_array(js, TYPED_ARRAY_UINT8, ab, 0, len, "Buffer"); +} + +static void zlib_emit_data(ant_t *js, ant_value_t obj, const uint8_t *data, size_t len) { + ant_value_t buf = zlib_make_buffer(js, data, len); + if (!is_err(buf)) eventemitter_emit_args(js, obj, "data", &buf, 1); +} + +typedef struct { ant_t *js; ant_value_t obj; } brotli_emit_ctx_t; + +static int brotli_emit_cb(void *ctx, const uint8_t *chunk, size_t len) { + brotli_emit_ctx_t *ec = (brotli_emit_ctx_t *)ctx; + zlib_emit_data(ec->js, ec->obj, chunk, len); + return 0; +} + +static ant_value_t zlib_do_process( + ant_t *js, zlib_stream_t *st, + const uint8_t *input, size_t input_len, + bool finish +) { + if (st->destroyed || st->ended) return js_mkundef(); + + if (st->brotli) { + brotli_emit_ctx_t ctx = { js, st->obj }; + int rc; + if (finish) { + rc = brotli_stream_finish(st->brotli, brotli_emit_cb, &ctx); + } else { + rc = brotli_stream_process(st->brotli, input, input_len, brotli_emit_cb, &ctx); + } + if (rc < 0) return js_mkerr(js, "brotli operation failed"); + return js_mkundef(); + } + + bool compress = zlib_kind_is_compress(st->kind); + int flush = finish ? Z_FINISH : Z_NO_FLUSH; + uint8_t out[ZLIB_CHUNK]; + + if (!finish) { + st->strm.next_in = (Bytef *)input; + st->strm.avail_in = (uInt)input_len; + } else { + st->strm.next_in = NULL; + st->strm.avail_in = 0; + } + + int ret; + do { + st->strm.next_out = out; + st->strm.avail_out = ZLIB_CHUNK; + + if (compress) { + ret = deflate(&st->strm, flush); + if (ret == Z_STREAM_ERROR) return js_mkerr(js, "zlib deflate error"); + } else { + ret = inflate(&st->strm, flush == Z_FINISH ? Z_SYNC_FLUSH : flush); + if (ret == Z_NEED_DICT || ret == Z_DATA_ERROR || ret == Z_MEM_ERROR) + return js_mkerr(js, "zlib inflate error"); + } + + size_t have = ZLIB_CHUNK - st->strm.avail_out; + if (have > 0) zlib_emit_data(js, st->obj, out, have); + if (ret == Z_STREAM_END) break; + } while (st->strm.avail_out == 0); + + return js_mkundef(); +} + +static ant_value_t js_zlib_write(ant_t *js, ant_value_t *args, int nargs) { + zlib_stream_t *st = zlib_stream_ptr(js_getthis(js)); + if (!st) return js_mkerr_typed(js, JS_ERR_TYPE, "Invalid zlib stream"); + if (st->destroyed) return js_false; + if (st->ended) return js_false; + + if (nargs < 1 || vtype(args[0]) == T_UNDEF || vtype(args[0]) == T_NULL) + return js_true; + + const uint8_t *bytes = NULL; + size_t len = 0; + + if (is_object_type(args[0])) { + buffer_source_get_bytes(js, args[0], &bytes, &len); + } + + if (!bytes) { + size_t slen = 0; + char *s = js_getstr(js, args[0], &slen); + if (s) { bytes = (const uint8_t *)s; len = slen; } + } + + if (!bytes || len == 0) return js_true; + st->bytes_written += (uint32_t)len; + + ant_value_t r = zlib_do_process(js, st, bytes, len, false); + if (is_err(r)) { + eventemitter_emit_args(js, st->obj, "error", &r, 1); + return js_false; + } + + ant_value_t cb = pick_callback(args, nargs); + if (is_callable(cb)) { + ant_value_t null_val = js_mknull(); + sv_vm_call(js->vm, js, cb, js_mkundef(), &null_val, 1, NULL, false); + } + + return js_true; +} + +static ant_value_t js_zlib_end(ant_t *js, ant_value_t *args, int nargs) { + zlib_stream_t *st = zlib_stream_ptr(js_getthis(js)); + ant_value_t self = js_getthis(js); + + if (!st) return js_mkerr_typed(js, JS_ERR_TYPE, "Invalid zlib stream"); + if (st->destroyed || st->ended) return self; + + if (nargs > 0 && vtype(args[0]) != T_UNDEF && vtype(args[0]) != T_NULL) { + ant_value_t write_args[1] = { args[0] }; + js_zlib_write(js, write_args, 1); + } + + st->ended = true; + ant_value_t r = zlib_do_process(js, st, NULL, 0, true); + + if (is_err(r)) { + eventemitter_emit_args(js, st->obj, "error", &r, 1); + return self; + } + + eventemitter_emit_args(js, st->obj, "end", NULL, 0); + eventemitter_emit_args(js, st->obj, "finish", NULL, 0); + eventemitter_emit_args(js, st->obj, "close", NULL, 0); + + ant_value_t cb = pick_callback(args, nargs); + if (is_callable(cb)) + sv_vm_call(js->vm, js, cb, js_mkundef(), NULL, 0, NULL, false); + + return self; +} + +static ant_value_t js_zlib_destroy(ant_t *js, ant_value_t *args, int nargs) { + zlib_stream_t *st = zlib_stream_ptr(js_getthis(js)); + ant_value_t self = js_getthis(js); + + if (!st || st->destroyed) return self; + st->destroyed = true; + zlib_stream_release(st); + + if (nargs > 0 && !js_truthy(js, args[0])) { + ant_value_t null_val = js_mknull(); + eventemitter_emit_args(js, st->obj, "error", &null_val, 1); + } + + eventemitter_emit_args(js, st->obj, "close", NULL, 0); + return self; +} + +static ant_value_t js_zlib_pause(ant_t *js, ant_value_t *args, int nargs) { + return js_getthis(js); +} + +static ant_value_t js_zlib_resume(ant_t *js, ant_value_t *args, int nargs) { + return js_getthis(js); +} + +static ant_value_t js_zlib_unpipe(ant_t *js, ant_value_t *args, int nargs) { + return js_getthis(js); +} + +static ant_value_t pipe_on_data(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t fn = js_getcurrentfunc(js); + ant_value_t dest = js_get_slot(fn, SLOT_DATA); + ant_value_t write_fn = js_get(js, dest, "write"); + + if (is_callable(write_fn) && nargs > 0) + sv_vm_call(js->vm, js, write_fn, dest, args, 1, NULL, false); + + return js_mkundef(); +} + +static ant_value_t pipe_on_end(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t fn = js_getcurrentfunc(js); + ant_value_t dest = js_get_slot(fn, SLOT_DATA); + ant_value_t end_fn = js_get(js, dest, "end"); + + if (is_callable(end_fn)) + sv_vm_call(js->vm, js, end_fn, dest, NULL, 0, NULL, false); + + return js_mkundef(); +} + +static ant_value_t js_zlib_pipe(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1 || !is_object_type(args[0])) return js_mkundef(); + ant_value_t self = js_getthis(js); + ant_value_t dest = args[0]; + + ant_value_t data_handler = js_heavy_mkfun(js, pipe_on_data, dest); + ant_value_t end_handler = js_heavy_mkfun(js, pipe_on_end, dest); + + eventemitter_add_listener(js, self, "data", data_handler, false); + eventemitter_add_listener(js, self, "end", end_handler, true); + + return dest; +} + +static ant_value_t js_zlib_get_bytes_written(ant_t *js, ant_value_t *args, int nargs) { + zlib_stream_t *st = zlib_stream_ptr(js_getthis(js)); + if (!st) return js_mknum(0); + return js_mknum((double)st->bytes_written); +} + +static ant_value_t zlib_create_stream(ant_t *js, zlib_kind_t kind) { + bool compress = zlib_kind_is_compress(kind); + + zlib_stream_t *st = calloc(1, sizeof(zlib_stream_t)); + if (!st) return js_mkerr(js, "out of memory"); + + st->kind = kind; + st->js = js; + + if (kind == ZLIB_KIND_BROTLI_COMPRESS || kind == ZLIB_KIND_BROTLI_DECOMPRESS) { + st->brotli = brotli_stream_state_new(kind == ZLIB_KIND_BROTLI_DECOMPRESS); + if (!st->brotli) { free(st); return js_mkerr(js, "brotli init failed"); } + st->initialized = true; + } else { + int wbits = zlib_window_bits(kind); + int ret; + if (compress) { + ret = deflateInit2(&st->strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, wbits, 8, Z_DEFAULT_STRATEGY); + } else ret = inflateInit2(&st->strm, wbits); + + if (ret != Z_OK) { free(st); return js_mkerr(js, "zlib init failed"); } + st->initialized = true; + } + + ant_value_t obj = js_mkobj(js); + if (is_object_type(g_transform_proto)) js_set_proto_init(obj, g_transform_proto); + + js_set_native_ptr(obj, st); + js_set_native_tag(obj, ZLIB_STREAM_TAG); + + js_set(js, obj, "readable", js_true); + js_set(js, obj, "writable", js_true); + + st->obj = obj; + zlib_add_active(st); + return obj; +} + +static ant_value_t js_create_gzip(ant_t *js, ant_value_t *args, int nargs) { + return zlib_create_stream(js, ZLIB_KIND_GZIP); +} + +static ant_value_t js_create_gunzip(ant_t *js, ant_value_t *args, int nargs) { + return zlib_create_stream(js, ZLIB_KIND_GUNZIP); +} + +static ant_value_t js_create_deflate(ant_t *js, ant_value_t *args, int nargs) { + return zlib_create_stream(js, ZLIB_KIND_DEFLATE); +} + +static ant_value_t js_create_inflate(ant_t *js, ant_value_t *args, int nargs) { + return zlib_create_stream(js, ZLIB_KIND_INFLATE); +} + +static ant_value_t js_create_deflate_raw(ant_t *js, ant_value_t *args, int nargs) { + return zlib_create_stream(js, ZLIB_KIND_DEFLATE_RAW); +} + +static ant_value_t js_create_inflate_raw(ant_t *js, ant_value_t *args, int nargs) { + return zlib_create_stream(js, ZLIB_KIND_INFLATE_RAW); +} + +static ant_value_t js_create_unzip(ant_t *js, ant_value_t *args, int nargs) { + return zlib_create_stream(js, ZLIB_KIND_UNZIP); +} + +static ant_value_t js_create_brotli_compress(ant_t *js, ant_value_t *args, int nargs) { + return zlib_create_stream(js, ZLIB_KIND_BROTLI_COMPRESS); +} + +static ant_value_t js_create_brotli_decompress(ant_t *js, ant_value_t *args, int nargs) { + return zlib_create_stream(js, ZLIB_KIND_BROTLI_DECOMPRESS); +} + +typedef struct { + uint8_t *data; + size_t len; + size_t cap; + int error; +} zbuf_t; + +static int zbuf_append(zbuf_t *b, const uint8_t *chunk, size_t n) { + if (b->len + n > b->cap) { + size_t newcap = b->cap ? b->cap * 2 : 4096; + while (newcap < b->len + n) newcap *= 2; + uint8_t *p = realloc(b->data, newcap); + if (!p) { b->error = 1; return -1; } + b->data = p; + b->cap = newcap; + } + memcpy(b->data + b->len, chunk, n); + b->len += n; + return 0; +} + +static int zbuf_brotli_cb(void *ctx, const uint8_t *chunk, size_t n) { + return zbuf_append((zbuf_t *)ctx, chunk, n); +} + +static ant_value_t zlib_sync_op( + ant_t *js, zlib_kind_t kind, + const uint8_t *input, size_t input_len +) { + zbuf_t out = {0}; + uint8_t tmp[ZLIB_CHUNK]; + bool compress = zlib_kind_is_compress(kind); + + if (kind == ZLIB_KIND_BROTLI_COMPRESS || kind == ZLIB_KIND_BROTLI_DECOMPRESS) { + brotli_stream_state_t *bs = brotli_stream_state_new(kind == ZLIB_KIND_BROTLI_DECOMPRESS); + if (!bs) return js_mkerr(js, "brotli init failed"); + + int rc = brotli_stream_process(bs, input, input_len, zbuf_brotli_cb, &out); + if (rc >= 0) rc = brotli_stream_finish(bs, zbuf_brotli_cb, &out); + + brotli_stream_state_destroy(bs); + if (rc < 0 || out.error) { free(out.data); return js_mkerr(js, "brotli operation failed"); } + ant_value_t buf = zlib_make_buffer(js, out.data, out.len); + free(out.data); + + return buf; + } + + z_stream strm = {0}; + int wbits = zlib_window_bits(kind); + int ret; + + if (compress) ret = deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, wbits, 8, Z_DEFAULT_STRATEGY); + else ret = inflateInit2(&strm, wbits); + if (ret != Z_OK) return js_mkerr(js, "zlib init failed"); + + strm.next_in = (Bytef *)input; + strm.avail_in = (uInt)input_len; + + do { + strm.next_out = tmp; + strm.avail_out = ZLIB_CHUNK; + if (compress) ret = deflate(&strm, Z_FINISH); + else ret = inflate(&strm, Z_SYNC_FLUSH); + if (ret == Z_STREAM_ERROR || ret == Z_DATA_ERROR || ret == Z_MEM_ERROR) { + if (compress) deflateEnd(&strm); + else inflateEnd(&strm); + free(out.data); + return js_mkerr(js, "zlib operation failed"); + } + size_t have = ZLIB_CHUNK - strm.avail_out; + if (have > 0 && zbuf_append(&out, tmp, have) < 0) { + if (compress) deflateEnd(&strm); + else inflateEnd(&strm); + free(out.data); + return js_mkerr(js, "out of memory"); + } + if (ret == Z_STREAM_END) break; + } while (strm.avail_out == 0 || strm.avail_in > 0); + + if (compress) deflateEnd(&strm); + else inflateEnd(&strm); + + ant_value_t buf = zlib_make_buffer(js, out.data, out.len); + free(out.data); + return buf; +} + +static bool get_input_bytes(ant_t *js, ant_value_t val, const uint8_t **out_bytes, size_t *out_len) { + if (is_object_type(val) && buffer_source_get_bytes(js, val, out_bytes, out_len)) return true; + size_t slen = 0; + char *s = js_getstr(js, val, &slen); + if (s) { *out_bytes = (const uint8_t *)s; *out_len = slen; return true; } + return false; +} + +static ant_value_t zlib_sync_fn(ant_t *js, ant_value_t *args, int nargs, zlib_kind_t kind) { + if (nargs < 1) return js_mkerr(js, "argument required"); + const uint8_t *bytes = NULL; + size_t len = 0; + if (!get_input_bytes(js, args[0], &bytes, &len)) + return js_mkerr_typed(js, JS_ERR_TYPE, "argument must be a string or Buffer"); + return zlib_sync_op(js, kind, bytes, len); +} + +static ant_value_t zlib_async_fn(ant_t *js, ant_value_t *args, int nargs, zlib_kind_t kind) { + if (nargs < 1) return js_mkerr(js, "argument required"); + + const uint8_t *bytes = NULL; + size_t len = 0; + ant_value_t cb = pick_callback(args, nargs); + + if (!get_input_bytes(js, args[0], &bytes, &len)) + return js_mkerr_typed(js, JS_ERR_TYPE, "argument must be a string or Buffer"); + + ant_value_t result = zlib_sync_op(js, kind, bytes, len); + + if (is_callable(cb)) { + if (is_err(result)) { + ant_value_t argv[1] = { result }; + sv_vm_call(js->vm, js, cb, js_mkundef(), argv, 1, NULL, false); + } else { + ant_value_t null_val = js_mknull(); + ant_value_t argv[2] = { null_val, result }; + sv_vm_call(js->vm, js, cb, js_mkundef(), argv, 2, NULL, false); + } + return js_mkundef(); + } + + return result; +} + +#define ZLIB_SYNC_FN(name, kind) \ + static ant_value_t js_##name##Sync(ant_t *js, ant_value_t *a, int n) { return zlib_sync_fn(js, a, n, kind); } + +#define ZLIB_ASYNC_FN(name, kind) \ + static ant_value_t js_##name(ant_t *js, ant_value_t *a, int n) { return zlib_async_fn(js, a, n, kind); } + +ZLIB_SYNC_FN(gzip, ZLIB_KIND_GZIP) +ZLIB_SYNC_FN(gunzip, ZLIB_KIND_GUNZIP) +ZLIB_SYNC_FN(deflate, ZLIB_KIND_DEFLATE) +ZLIB_SYNC_FN(inflate, ZLIB_KIND_INFLATE) +ZLIB_SYNC_FN(deflateRaw, ZLIB_KIND_DEFLATE_RAW) +ZLIB_SYNC_FN(inflateRaw, ZLIB_KIND_INFLATE_RAW) +ZLIB_SYNC_FN(unzip, ZLIB_KIND_UNZIP) +ZLIB_SYNC_FN(brotliCompress, ZLIB_KIND_BROTLI_COMPRESS) +ZLIB_SYNC_FN(brotliDecompress, ZLIB_KIND_BROTLI_DECOMPRESS) + +ZLIB_ASYNC_FN(gzip, ZLIB_KIND_GZIP) +ZLIB_ASYNC_FN(gunzip, ZLIB_KIND_GUNZIP) +ZLIB_ASYNC_FN(deflate, ZLIB_KIND_DEFLATE) +ZLIB_ASYNC_FN(inflate, ZLIB_KIND_INFLATE) +ZLIB_ASYNC_FN(deflateRaw, ZLIB_KIND_DEFLATE_RAW) +ZLIB_ASYNC_FN(inflateRaw, ZLIB_KIND_INFLATE_RAW) +ZLIB_ASYNC_FN(unzip, ZLIB_KIND_UNZIP) +ZLIB_ASYNC_FN(brotliCompress, ZLIB_KIND_BROTLI_COMPRESS) +ZLIB_ASYNC_FN(brotliDecompress, ZLIB_KIND_BROTLI_DECOMPRESS) + +static ant_value_t js_zlib_crc32(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_mkerr(js, "argument required"); + + const uint8_t *bytes = NULL; + size_t len = 0; + uLong init_val = 0; + + if (nargs >= 2 && vtype(args[1]) == T_NUM) + init_val = (uLong)js_getnum(args[1]); + + if (!get_input_bytes(js, args[0], &bytes, &len)) + return js_mkerr_typed(js, JS_ERR_TYPE, "argument must be a string or Buffer"); + + uLong crc = crc32(init_val, bytes, (uInt)len); + return js_mknum((double)(uint32_t)crc); +} + +static ant_value_t make_constants(ant_t *js) { + ant_value_t c = js_mkobj(js); + + js_set(js, c, "Z_NO_FLUSH", js_mknum(Z_NO_FLUSH)); + js_set(js, c, "Z_PARTIAL_FLUSH", js_mknum(Z_PARTIAL_FLUSH)); + js_set(js, c, "Z_SYNC_FLUSH", js_mknum(Z_SYNC_FLUSH)); + js_set(js, c, "Z_FULL_FLUSH", js_mknum(Z_FULL_FLUSH)); + js_set(js, c, "Z_FINISH", js_mknum(Z_FINISH)); + js_set(js, c, "Z_BLOCK", js_mknum(Z_BLOCK)); + + js_set(js, c, "Z_OK", js_mknum(Z_OK)); + js_set(js, c, "Z_STREAM_END", js_mknum(Z_STREAM_END)); + js_set(js, c, "Z_NEED_DICT", js_mknum(Z_NEED_DICT)); + js_set(js, c, "Z_ERRNO", js_mknum(Z_ERRNO)); + js_set(js, c, "Z_STREAM_ERROR", js_mknum(Z_STREAM_ERROR)); + js_set(js, c, "Z_DATA_ERROR", js_mknum(Z_DATA_ERROR)); + js_set(js, c, "Z_MEM_ERROR", js_mknum(Z_MEM_ERROR)); + js_set(js, c, "Z_BUF_ERROR", js_mknum(Z_BUF_ERROR)); + js_set(js, c, "Z_VERSION_ERROR", js_mknum(Z_VERSION_ERROR)); + + js_set(js, c, "Z_NO_COMPRESSION", js_mknum(Z_NO_COMPRESSION)); + js_set(js, c, "Z_BEST_SPEED", js_mknum(Z_BEST_SPEED)); + js_set(js, c, "Z_BEST_COMPRESSION", js_mknum(Z_BEST_COMPRESSION)); + js_set(js, c, "Z_DEFAULT_COMPRESSION", js_mknum(Z_DEFAULT_COMPRESSION)); + + js_set(js, c, "Z_FILTERED", js_mknum(Z_FILTERED)); + js_set(js, c, "Z_HUFFMAN_ONLY", js_mknum(Z_HUFFMAN_ONLY)); + js_set(js, c, "Z_RLE", js_mknum(Z_RLE)); + js_set(js, c, "Z_FIXED", js_mknum(Z_FIXED)); + js_set(js, c, "Z_DEFAULT_STRATEGY", js_mknum(Z_DEFAULT_STRATEGY)); + + js_set(js, c, "BROTLI_OPERATION_PROCESS", js_mknum(0)); + js_set(js, c, "BROTLI_OPERATION_FLUSH", js_mknum(1)); + js_set(js, c, "BROTLI_OPERATION_FINISH", js_mknum(2)); + js_set(js, c, "BROTLI_OPERATION_EMIT_METADATA", js_mknum(3)); + + js_set(js, c, "BROTLI_PARAM_MODE", js_mknum(0)); + js_set(js, c, "BROTLI_MODE_GENERIC", js_mknum(0)); + js_set(js, c, "BROTLI_MODE_TEXT", js_mknum(1)); + js_set(js, c, "BROTLI_MODE_FONT", js_mknum(2)); + js_set(js, c, "BROTLI_PARAM_QUALITY", js_mknum(1)); + js_set(js, c, "BROTLI_MIN_QUALITY", js_mknum(0)); + js_set(js, c, "BROTLI_MAX_QUALITY", js_mknum(11)); + js_set(js, c, "BROTLI_DEFAULT_QUALITY", js_mknum(11)); + js_set(js, c, "BROTLI_PARAM_LGWIN", js_mknum(2)); + js_set(js, c, "BROTLI_MIN_WINDOW_BITS", js_mknum(10)); + js_set(js, c, "BROTLI_MAX_WINDOW_BITS", js_mknum(24)); + js_set(js, c, "BROTLI_DEFAULT_WINDOW", js_mknum(22)); + js_set(js, c, "BROTLI_PARAM_SIZE_HINT", js_mknum(3)); + js_set(js, c, "BROTLI_PARAM_LARGE_WINDOW", js_mknum(4)); + js_set(js, c, "BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION", js_mknum(0)); + js_set(js, c, "BROTLI_DECODER_PARAM_LARGE_WINDOW", js_mknum(1)); + + js_set(js, c, "ZSTD_e_continue", js_mknum(0)); + js_set(js, c, "ZSTD_e_flush", js_mknum(1)); + js_set(js, c, "ZSTD_e_end", js_mknum(2)); + + return c; +} + +static void zlib_init_proto(ant_t *js) { + if (g_transform_proto) return; + + ant_value_t events = events_library(js); + ant_value_t ee_ctor = js_get(js, events, "EventEmitter"); + ant_value_t ee_proto = js_get(js, ee_ctor, "prototype"); + + g_transform_proto = js_mkobj(js); + js_set_proto_init(g_transform_proto, ee_proto); + + js_set(js, g_transform_proto, "write", js_mkfun(js_zlib_write)); + js_set(js, g_transform_proto, "end", js_mkfun(js_zlib_end)); + js_set(js, g_transform_proto, "destroy", js_mkfun(js_zlib_destroy)); + js_set(js, g_transform_proto, "pause", js_mkfun(js_zlib_pause)); + js_set(js, g_transform_proto, "resume", js_mkfun(js_zlib_resume)); + js_set(js, g_transform_proto, "unpipe", js_mkfun(js_zlib_unpipe)); + js_set(js, g_transform_proto, "pipe", js_mkfun(js_zlib_pipe)); + + js_set_getter_desc(js, g_transform_proto, "bytesWritten", 12, + js_mkfun(js_zlib_get_bytes_written), JS_DESC_C); +} + +ant_value_t zlib_library(ant_t *js) { + zlib_init_proto(js); + + ant_value_t lib = js_mkobj(js); + ant_value_t consts = make_constants(js); + + js_set(js, lib, "constants", consts); + + js_set(js, lib, "createGzip", js_mkfun(js_create_gzip)); + js_set(js, lib, "createGunzip", js_mkfun(js_create_gunzip)); + js_set(js, lib, "createDeflate", js_mkfun(js_create_deflate)); + js_set(js, lib, "createInflate", js_mkfun(js_create_inflate)); + js_set(js, lib, "createDeflateRaw", js_mkfun(js_create_deflate_raw)); + js_set(js, lib, "createInflateRaw", js_mkfun(js_create_inflate_raw)); + js_set(js, lib, "createUnzip", js_mkfun(js_create_unzip)); + js_set(js, lib, "createBrotliCompress", js_mkfun(js_create_brotli_compress)); + js_set(js, lib, "createBrotliDecompress", js_mkfun(js_create_brotli_decompress)); + + js_set(js, lib, "gzip", js_mkfun(js_gzip)); + js_set(js, lib, "gunzip", js_mkfun(js_gunzip)); + js_set(js, lib, "deflate", js_mkfun(js_deflate)); + js_set(js, lib, "inflate", js_mkfun(js_inflate)); + js_set(js, lib, "deflateRaw", js_mkfun(js_deflateRaw)); + js_set(js, lib, "inflateRaw", js_mkfun(js_inflateRaw)); + js_set(js, lib, "unzip", js_mkfun(js_unzip)); + js_set(js, lib, "brotliCompress", js_mkfun(js_brotliCompress)); + js_set(js, lib, "brotliDecompress", js_mkfun(js_brotliDecompress)); + + js_set(js, lib, "gzipSync", js_mkfun(js_gzipSync)); + js_set(js, lib, "gunzipSync", js_mkfun(js_gunzipSync)); + js_set(js, lib, "deflateSync", js_mkfun(js_deflateSync)); + js_set(js, lib, "inflateSync", js_mkfun(js_inflateSync)); + js_set(js, lib, "deflateRawSync", js_mkfun(js_deflateRawSync)); + js_set(js, lib, "inflateRawSync", js_mkfun(js_inflateRawSync)); + js_set(js, lib, "unzipSync", js_mkfun(js_unzipSync)); + js_set(js, lib, "brotliCompressSync", js_mkfun(js_brotliCompressSync)); + js_set(js, lib, "brotliDecompressSync", js_mkfun(js_brotliDecompressSync)); + + js_set(js, lib, "crc32", js_mkfun(js_zlib_crc32)); + js_set(js, lib, "default", lib); + + js_set_sym(js, lib, get_toStringTag_sym(), js_mkstr(js, "zlib", 4)); + return lib; +} + +void gc_mark_zlib(ant_t *js, void (*mark)(ant_t *, ant_value_t)) { + if (g_transform_proto) mark(js, g_transform_proto); + for (zlib_stream_t *st = g_active_streams; st; st = st->next_active) mark(js, st->obj); +}