From 1ab3e43326defc57c19eb2388831f5108c88d022 Mon Sep 17 00:00:00 2001 From: Nitjsefnie Date: Wed, 22 Jul 2026 11:21:22 +0000 Subject: [PATCH] Add DEFLATE inflate/deflate builtins via system zlib (#684) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four codecs as thin wrappers over the system zlib (-lz), two dual pairs in the #677 sense: - inflate / deflate: raw DEFLATE (windowBits -15) — the ZIP member format, so .xlsx/.ods entries are readable from script. - zlib_inflate / zlib_deflate: zlib-wrapped; inflate uses windowBits 15+32, auto-detecting zlib AND gzip headers — that is what makes plain .gz files readable. Byte representation mirrors read_bytes/write_bytes exactly: a list of ints 0-255 (mod 256) or a buffer in, a fresh list of ints 0-255 out. Corrupt/truncated input raises a catchable `value` error, never a crash; decompressed output is capped at 256 MiB (`limit`, zip-bomb bound, matching the sandbox_run default budget). Gating preserves the zero-dependency minimal build via the existing EIGENSCRIPT_EXT_* mechanism (same pattern as `make http`): default OFF, `make zlib` opts in. Compiled without zlib the four names stay registered — so scripts feature-detect with try/catch and the sandbox fail-closed allowlist can name real builtins — but every call raises "compiled without zlib support". All four are pure bytes<->bytes and join SANDBOX_ALLOW. Tests: tests/test_inflate.eigs (22 checks) — round-trip identity for both pairs incl. empty and 8 KB inputs, fixed known-vectors generated by python's zlib (inflate pinned independently of our deflate), gzip auto-detect, buffer input, corrupt/truncated catchable errors, and the wrong-type error shape. run_all_tests.sh [123] probe-gates the section like [44] HTTP: the real suite runs only under `make zlib`; on minimal builds it instead asserts the stub raises the documented error. Suites: release minimal 3131/3131, release zlib 3152/3152, asan minimal 3135/3135, asan+zlib 3156/3156 (leaks on, all zero-fail). Co-Authored-By: Kimi K3 --- CLAUDE.md | 1 + Makefile | 16 ++- docs/BUILTINS.md | 22 ++++ src/builtins.c | 230 ++++++++++++++++++++++++++++++++++++++++ src/eigenscript.h | 7 ++ tests/run_all_tests.sh | 41 +++++++ tests/test_inflate.eigs | 133 +++++++++++++++++++++++ 7 files changed, 449 insertions(+), 1 deletion(-) create mode 100644 tests/test_inflate.eigs diff --git a/CLAUDE.md b/CLAUDE.md index 13abd76a..42462d13 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,6 +35,7 @@ make # release build -> src/eigenscript (HTTP/MODEL/DB off) make test # build + full suite (tests/run_all_tests.sh) make asan # ASan+UBSan build (same binary path!) make http # http+model variant — run tests/test_http_server.sh +make zlib # DEFLATE codecs (inflate/deflate builtins) via system zlib (-lz) make jit-smoke # standalone emitter tests (jit_smoke.c stubs all helpers) make freestanding-check # 2-stage symbol gate for the EigenOS profile (docs/FREESTANDING.md) make freestanding-libc-diff # mini-libc/libm vs glibc oracle (src/freestanding/) diff --git a/Makefile b/Makefile index 15ae861e..d2bf6a05 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ PREFIX := $(HOME)/.local LSP_SOURCES := $(SRC_DIR)/eigenlsp.c $(filter-out $(CLI_ONLY),$(SOURCES)) LSP_BINARY := $(SRC_DIR)/eigenlsp -.PHONY: all build full http gfx lib amalgamation tsan test install install-gfx clean coverage coverage-clean fuzz fuzz-run lsp jit-smoke embed-smoke asan valgrind pgo freestanding-check freestanding-libc-diff print-% +.PHONY: all build full http gfx zlib lib amalgamation tsan test install install-gfx clean coverage coverage-clean fuzz fuzz-run lsp jit-smoke embed-smoke asan valgrind pgo freestanding-check freestanding-libc-diff print-% # Introspection helper: `make print-SOURCES` echoes a variable's value. # tests/test_leak_guard.sh derives its ASan build source list from the @@ -79,6 +79,20 @@ http: $(LDFLAGS) @echo "EigenScript $(VERSION) (http+model, no db) built. Binary: $$(du -sh $(BINARY) | cut -f1)" +# Build with the DEFLATE codecs (inflate/deflate builtins, #684) linked +# against the system zlib. Same opt-in pattern as `make http`: the +# default build stays zero-dependency and the four builtins raise +# "compiled without zlib support" there. +zlib: + $(CC) $(CFLAGS) -o $(BINARY) $(SOURCES) \ + -DEIGENSCRIPT_EXT_HTTP=0 \ + -DEIGENSCRIPT_EXT_MODEL=0 \ + -DEIGENSCRIPT_EXT_DB=0 \ + -DEIGENSCRIPT_EXT_ZLIB=1 \ + -DEIGENSCRIPT_VERSION='"$(VERSION)"' \ + $(LDFLAGS) -lz + @echo "EigenScript $(VERSION) (zlib) built. Binary: $$(du -sh $(BINARY) | cut -f1)" + gfx: $(CC) $(CFLAGS) -o $(BINARY) $(SOURCES) $(SRC_DIR)/ext_gfx.c \ -DEIGENSCRIPT_EXT_HTTP=0 \ diff --git a/docs/BUILTINS.md b/docs/BUILTINS.md index 17d6b98d..4c9f4a81 100644 --- a/docs/BUILTINS.md +++ b/docs/BUILTINS.md @@ -186,6 +186,28 @@ For serialization: reconstruct strings/floats from raw bytes (the inverse of an Buffers also support direct indexing (`buf[i]`, `buf[i] is val`) and compound assignment (`buf[i] += val`). +### Compression + +DEFLATE codecs (#684), thin wrappers over the system zlib. Byte +representation mirrors `read_bytes`/`write_bytes`: input is a list of +ints 0–255 (values taken mod 256) or a buffer; output is always a fresh +list of ints 0–255. Corrupt or truncated input raises a catchable +`value` error; decompressed output is capped at 256 MiB (`limit`, +zip-bomb bound). + +Requires the `zlib` build (`make zlib`, `-DEIGENSCRIPT_EXT_ZLIB=1 +-lz`) — the minimal build stays zero-dependency: the four names are +still registered there (so `type of inflate` is `builtin` and the +sandbox allowlist can name them) but every call raises `value`: +"compiled without zlib support". Feature-detect with try/catch. + +| Name | Signature | Description | +|------|-----------|-------------| +| `inflate` | `inflate of ` | Raw DEFLATE decompression (windowBits −15) — the ZIP member format, so `.xlsx`/`.ods` entries are readable. Dual of `deflate`. | +| `deflate` | `deflate of ` | Raw DEFLATE compression (windowBits −15, default level). Dual of `inflate`. | +| `zlib_inflate` | `zlib_inflate of ` | Wrapped decompression with windowBits 15+32: auto-detects **zlib AND gzip** headers — this is what makes plain `.gz` files readable (`read_bytes of path` then `zlib_inflate`). Dual of `zlib_deflate`. | +| `zlib_deflate` | `zlib_deflate of ` | zlib-wrapped compression (RFC 1950 header, default level). Dual of `zlib_inflate`. | + ### JSON | Name | Signature | Description | diff --git a/src/builtins.c b/src/builtins.c index 2fab4697..a5ce247c 100644 --- a/src/builtins.c +++ b/src/builtins.c @@ -29,6 +29,10 @@ #include "model_internal.h" #endif +#if EIGENSCRIPT_EXT_ZLIB +#include +#endif + /* How many bindings the runtime itself installs. * * register_builtins fills the global env from slot 0 upward and nothing in the @@ -4238,6 +4242,8 @@ static const char *SANDBOX_ALLOW[] = { "text_builder_to_string", "text_builder_part_count", /* json (string <-> value, pure) */ "json_build", "json_decode", "json_encode", "json_path", "json_raw", + /* DEFLATE codecs (pure bytes <-> bytes; #684) */ + "deflate", "inflate", "zlib_deflate", "zlib_inflate", /* path string manipulation (no fs access) */ "path_base", "path_dir", "path_ext", "path_join", /* type / value utilities */ @@ -5993,6 +5999,224 @@ Value* builtin_f64_from_bytes(Value *arg) { return make_num(d); } +/* ---- DEFLATE codecs (inflate/deflate, #684) ---- + * Thin wrappers over the system zlib (-lz), gated behind + * EIGENSCRIPT_EXT_ZLIB — the same EIGENSCRIPT_EXT_* mechanism the http + * variant uses. Default OFF so the minimal build stays zero-dependency; + * compiled without zlib the four names stay registered but raise a + * catchable runtime error, so a script can feature-detect with + * try/catch instead of dying on "undefined variable". + * + * Byte representation mirrors read_bytes/write_bytes exactly: input is + * a list of ints 0-255 (values taken mod 256, non-numbers read as 0) + * or a VAL_BUFFER; output is always a fresh list of ints 0-255. + * + * inflate/deflate are the RAW DEFLATE pair (windowBits -15) — the ZIP + * member format, so .xlsx/.ods entries are readable. zlib_inflate/ + * zlib_deflate are the zlib-wrapped pair; zlib_inflate uses windowBits + * 15+32, which auto-detects zlib AND gzip headers — that is what makes + * plain .gz files readable. + */ +#if EIGENSCRIPT_EXT_ZLIB + +/* Inflate is an amplifier: a few KB of DEFLATE can expand without bound + * (zip bomb). Cap the decompressed size like the other size caps + * (read_bytes 10 MB, read_bytes_buf 512 MB): over the cap is a loud, + * catchable `limit` error, never silent truncation. 256 MiB matches the + * sandbox_run default allocation budget. */ +#define EIGS_INFLATE_MAX_OUT ((unsigned long)256 * 1024 * 1024) + +/* Shared argument extraction for the four codecs: accept the byte + * representations write_bytes accepts and copy them into a malloc'd + * byte array. Returns 1 on success; on a wrong-shape argument raises + * `type` and returns 0. */ +static int zlib_bytes_arg(Value *arg, const char *who, + unsigned char **out, size_t *out_n) { + *out = NULL; + *out_n = 0; + int n = 0; + Value **items = NULL; + double *bufd = NULL; + if (arg && arg->type == VAL_LIST) { + n = arg->data.list.count; + items = arg->data.list.items; + } else if (arg && arg->type == VAL_BUFFER) { + n = arg->data.buffer.count; + bufd = arg->data.buffer.data; + } else { + rt_error(EK_TYPE, 0, + "%s requires a list of byte values (0-255) or a buffer, got %s", + who, val_type_name(arg ? arg->type : VAL_NULL)); + return 0; + } + unsigned char *b = xmalloc((size_t)(n > 0 ? n : 1)); + for (int i = 0; i < n; i++) { + double dv = items ? (items[i] && items[i]->type == VAL_NUM ? items[i]->data.num : 0.0) + : bufd[i]; + b[i] = (unsigned char)((int)dv & 0xFF); + } + *out = b; + *out_n = (size_t)n; + return 1; +} + +/* Wrap a finished byte buffer as the list-of-ints result value (the + * read_bytes shape). Takes ownership of nothing; caller still frees. */ +static Value *zlib_bytes_result(const unsigned char *buf, unsigned long n) { + Value *result = make_list((int)n); + for (unsigned long i = 0; i < n; i++) + list_append_owned(result, make_num((double)buf[i])); + return result; +} + +/* Shared inflate core. window_bits selects the wrapper (-15 raw, + * 15+32 zlib/gzip auto-detect). A corrupt or truncated stream raises a + * catchable `value` error; output over EIGS_INFLATE_MAX_OUT raises + * `limit` (the zip-bomb bound). */ +static Value *zlib_inflate_impl(const char *who, int window_bits, Value *arg) { + unsigned char *src; + size_t src_n; + if (!zlib_bytes_arg(arg, who, &src, &src_n)) return make_null(); + + z_stream zs; + memset(&zs, 0, sizeof(zs)); + if (inflateInit2(&zs, window_bits) != Z_OK) { + free(src); + rt_error(EK_INTERNAL, 0, "%s: inflateInit2 failed", who); + return make_null(); + } + size_t cap = src_n * 3 + 64; + if (cap > EIGS_INFLATE_MAX_OUT) cap = EIGS_INFLATE_MAX_OUT; + unsigned char *out = xmalloc(cap); + int zrc = Z_OK; + for (;;) { + if (zs.avail_in == 0 && zs.total_in < src_n) { + /* uInt is 32-bit: feed a >4 GiB input in chunks. */ + zs.next_in = src + zs.total_in; + unsigned long rem = src_n - zs.total_in; + zs.avail_in = (uInt)(rem > UINT_MAX ? UINT_MAX : rem); + } + if (zs.total_out == cap) { + if (cap >= EIGS_INFLATE_MAX_OUT) break; /* limit raise below */ + size_t ncap = cap * 2; + if (ncap > EIGS_INFLATE_MAX_OUT) ncap = EIGS_INFLATE_MAX_OUT; + out = xrealloc(out, ncap); + cap = ncap; + } + zs.next_out = out + zs.total_out; + zs.avail_out = (uInt)(cap - zs.total_out); + zrc = inflate(&zs, Z_NO_FLUSH); + if (zrc == Z_STREAM_END) break; + if (zrc != Z_OK) break; + if (zs.avail_out != 0 && zs.total_in == src_n) { + /* Output not full yet zlib made no progress: input ran out + * mid-stream — truncated. */ + zrc = Z_BUF_ERROR; + break; + } + } + if (zrc != Z_STREAM_END) { + if (zrc == Z_OK && zs.total_out >= EIGS_INFLATE_MAX_OUT) { + inflateEnd(&zs); + free(out); + free(src); + rt_error(EK_LIMIT, 0, + "%s: decompressed output exceeds the %lu-byte cap", + who, EIGS_INFLATE_MAX_OUT); + return make_null(); + } + const char *msg = zs.msg; + inflateEnd(&zs); + free(out); + free(src); + rt_error(EK_VALUE, 0, "%s: invalid or truncated compressed stream (%s)", + who, msg ? msg : "unexpected end of input"); + return make_null(); + } + unsigned long n = zs.total_out; + inflateEnd(&zs); + free(src); + Value *result = zlib_bytes_result(out, n); + free(out); + return result; +} + +/* Shared deflate core (dual of zlib_inflate_impl). The output buffer is + * deflateBound-sized up front, so a single Z_FINISH pass always fits. */ +static Value *zlib_deflate_impl(const char *who, int window_bits, Value *arg) { + unsigned char *src; + size_t src_n; + if (!zlib_bytes_arg(arg, who, &src, &src_n)) return make_null(); + + z_stream zs; + memset(&zs, 0, sizeof(zs)); + if (deflateInit2(&zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, window_bits, + 8, Z_DEFAULT_STRATEGY) != Z_OK) { + free(src); + rt_error(EK_INTERNAL, 0, "%s: deflateInit2 failed", who); + return make_null(); + } + uLong bound = deflateBound(&zs, (uLong)src_n); + unsigned char *out = xmalloc(bound > 0 ? bound : 1); + size_t pos = 0; + int zrc = Z_OK; + for (;;) { + if (zs.avail_in == 0 && pos < src_n) { + unsigned long rem = src_n - pos; + uInt chunk = (uInt)(rem > UINT_MAX ? UINT_MAX : rem); + zs.next_in = src + pos; + zs.avail_in = chunk; + pos += chunk; + } + int flush = (pos == src_n && zs.avail_in == 0) ? Z_FINISH : Z_NO_FLUSH; + zs.next_out = out + zs.total_out; + zs.avail_out = (uInt)(bound - zs.total_out); + zrc = deflate(&zs, flush); + if (zrc == Z_STREAM_END) break; + if (zrc != Z_OK && zrc != Z_BUF_ERROR) break; + if (flush == Z_FINISH) break; /* cannot happen with bound space */ + } + if (zrc != Z_STREAM_END) { + const char *msg = zs.msg; + deflateEnd(&zs); + free(out); + free(src); + rt_error(EK_INTERNAL, 0, "%s: deflate failed (%s)", + who, msg ? msg : "unknown zlib error"); + return make_null(); + } + unsigned long n = zs.total_out; + deflateEnd(&zs); + free(src); + Value *result = zlib_bytes_result(out, n); + free(out); + return result; +} + +Value* builtin_inflate(Value *arg) { return zlib_inflate_impl("inflate", -15, arg); } +Value* builtin_zlib_inflate(Value *arg) { return zlib_inflate_impl("zlib_inflate", 15 + 32, arg); } +Value* builtin_deflate(Value *arg) { return zlib_deflate_impl("deflate", -15, arg); } +Value* builtin_zlib_deflate(Value *arg) { return zlib_deflate_impl("zlib_deflate", 15, arg); } + +#else /* !EIGENSCRIPT_EXT_ZLIB */ + +/* Minimal build: the names exist so scripts can feature-detect (and the + * sandbox allowlist can name real builtins), but every call raises a + * clear catchable error pointing at the zlib build. */ +static Value *zlib_unavailable(const char *who) { + rt_error(EK_VALUE, 0, + "%s: compiled without zlib support (rebuild with `make zlib`)", + who); + return make_null(); +} + +Value* builtin_inflate(Value *arg) { (void)arg; return zlib_unavailable("inflate"); } +Value* builtin_zlib_inflate(Value *arg) { (void)arg; return zlib_unavailable("zlib_inflate"); } +Value* builtin_deflate(Value *arg) { (void)arg; return zlib_unavailable("deflate"); } +Value* builtin_zlib_deflate(Value *arg) { (void)arg; return zlib_unavailable("zlib_deflate"); } + +#endif /* EIGENSCRIPT_EXT_ZLIB */ + /* write_bytes of [path, data, append?] — write raw bytes to a file. * `data` is a list of byte ints or a buffer (values taken mod 256). `append` * (optional, default 0): 0 truncates the file, nonzero appends. Returns the @@ -6963,6 +7187,12 @@ void register_builtins(Env *env) { env_set_local_owned(env, "str_from_bytes", make_builtin(builtin_str_from_bytes)); env_set_local_owned(env, "f64_to_bytes", make_builtin(builtin_f64_to_bytes)); env_set_local_owned(env, "f64_from_bytes", make_builtin(builtin_f64_from_bytes)); + /* DEFLATE codecs (#684) — registered unconditionally; the bodies + * raise "compiled without zlib support" when EIGENSCRIPT_EXT_ZLIB=0. */ + env_set_local_owned(env, "inflate", make_builtin(builtin_inflate)); + env_set_local_owned(env, "zlib_inflate", make_builtin(builtin_zlib_inflate)); + env_set_local_owned(env, "deflate", make_builtin(builtin_deflate)); + env_set_local_owned(env, "zlib_deflate", make_builtin(builtin_zlib_deflate)); env_set_local_owned(env, "buf_copy", make_builtin(builtin_buf_copy)); env_set_local_owned(env, "buf_mix", make_builtin(builtin_buf_mix)); env_set_local_owned(env, "buf_scale_range", make_builtin(builtin_buf_scale_range)); diff --git a/src/eigenscript.h b/src/eigenscript.h index 39540935..3bef72cf 100644 --- a/src/eigenscript.h +++ b/src/eigenscript.h @@ -24,6 +24,13 @@ #ifndef EIGENSCRIPT_EXT_GFX #define EIGENSCRIPT_EXT_GFX 0 #endif +/* DEFLATE codecs (inflate/deflate via the system zlib, -lz). Default OFF + * like GFX: the minimal build stays zero-dependency — the four builtins + * stay registered but raise "compiled without zlib support" until + * `make zlib` opts in (same EIGENSCRIPT_EXT_* gating mechanism as http). */ +#ifndef EIGENSCRIPT_EXT_ZLIB +#define EIGENSCRIPT_EXT_ZLIB 0 +#endif /* Freestanding profile (docs/FREESTANDING.md) — the no-libc/EigenOS * carve-out. Compiles out everything that needs a host OS beyond the diff --git a/tests/run_all_tests.sh b/tests/run_all_tests.sh index 528446de..6945addd 100755 --- a/tests/run_all_tests.sh +++ b/tests/run_all_tests.sh @@ -2424,6 +2424,47 @@ else fi echo "" +# [123] DEFLATE codecs (inflate/deflate + zlib-wrapped duals, #684) — +# probe-gated like [44] HTTP: the minimal build keeps the names as +# "compiled without zlib support" stubs (zero-dependency posture), so the +# real-codec suite only runs under the `make zlib` binary. +ZLIB_PROBE_FILE=$(mktemp /tmp/eigs_zlib_probe_XXXXXX.eigs) +cat > "$ZLIB_PROBE_FILE" <<'PROBE' +d is deflate of [0] +print of d +PROBE +ZLIB_PROBE_OUT=$(./eigenscript "$ZLIB_PROBE_FILE" 2>&1) +rm -f "$ZLIB_PROBE_FILE" + +if ! echo "$ZLIB_PROBE_OUT" | grep -q "compiled without zlib support"; then + echo "[123] DEFLATE Codecs (#684, 22 checks)" + INF_OUTPUT=$(./eigenscript ../tests/test_inflate.eigs 2>&1); INF_OUTPUT_RC=$? + if rc_ok "$INF_OUTPUT_RC" "$INF_OUTPUT" && echo "$INF_OUTPUT" | grep -q "DEFLATE_ALL_PASS"; then + TOTAL=$((TOTAL + 22)) + PASS=$((PASS + 22)) + echo " PASS: all 22 inflate/deflate checks" + else + TOTAL=$((TOTAL + 22)) + FAIL=$((FAIL + 22)) + echo " FAIL: inflate/deflate tests" + echo "$INF_OUTPUT" | grep -iE "assert|error|FAIL" | head -5 + fi + echo "" +else + # Minimal build: the four names stay registered but must raise the + # documented catchable error (the zero-dependency gating contract). + echo "[123] DEFLATE Codecs (#684) — minimal build, stub check (1 check)" + TOTAL=$((TOTAL + 1)) + if echo "$ZLIB_PROBE_OUT" | grep -q "deflate: compiled without zlib support"; then + PASS=$((PASS + 1)) + echo " PASS: zlib-gated stub raises 'compiled without zlib support'" + else + FAIL=$((FAIL + 1)) + echo " FAIL: zlib stub missing or mis-phrased (probe: '$ZLIB_PROBE_OUT')" + fi + echo "" +fi + # [65] sort_by builtin echo "[65] Sort By (9 checks)" SBY_OUTPUT=$(./eigenscript ../tests/test_sort_by.eigs 2>&1); SBY_OUTPUT_RC=$? diff --git a/tests/test_inflate.eigs b/tests/test_inflate.eigs new file mode 100644 index 00000000..2e36c34f --- /dev/null +++ b/tests/test_inflate.eigs @@ -0,0 +1,133 @@ +# DEFLATE codecs (inflate / zlib_inflate / deflate / zlib_deflate, #684) — +# thin wrappers over the system zlib, gated behind EIGENSCRIPT_EXT_ZLIB +# (`make zlib`). This suite exercises the REAL codecs, so it only runs +# under the zlib build: run_all_tests.sh probe-gates the section and +# skips it entirely on the minimal build (where the builtins raise +# "compiled without zlib support" — that stub behavior is probe-checked +# there, not here). +# +# Covers: round-trip identity for both pairs (incl. empty and multi-KB +# inputs), FIXED known vectors generated by python's zlib (so inflate is +# pinned independently of our own deflate), gzip auto-detection +# (windowBits 15+32 — what makes plain .gz readable), buffer input, +# corrupt/truncated-stream catchable errors, and the wrong-type error +# shape matching neighboring builtins. +# +# Argument-shape note (issue #153): a literal list arg spreads into +# positional args and a one-element literal collapses to the scalar, so +# every vector below is held in a VARIABLE — same convention as the +# dispatch tests in test_builtin_errors.eigs. + +fails is 0 +define check(tag, got, want) as: + if got == want: + print of f" PASS: {tag}" + if got != want: + print of f" FAIL: {tag} (got {str of got}, want {str of want})" + fails is fails + 1 + +print of "=== DEFLATE codecs (#684) ===" + +payload is [69, 105, 103, 101, 110, 83, 99, 114, 105, 112, 116, 32, 35, 54, 56, 52, 58, 32, 100, 101, 102, 108, 97, 116, 101, 47, 105, 110, 102, 108, 97, 116, 101, 32, 118, 105, 97, 32, 122, 108, 105, 98] + +# Fixed vectors: python zlib (level 6) over the payload above. Pinned so +# inflate is verified against an INDEPENDENT compressor, not our deflate. +rawvec is [115, 205, 76, 79, 205, 11, 78, 46, 202, 44, 40, 81, 80, 54, 179, 48, 177, 82, 72, 73, 77, 203, 73, 44, 73, 213, 207, 204, 3, 211, 10, 101, 153, 137, 10, 85, 57, 153, 73, 0] +zlibvec is [120, 156, 115, 205, 76, 79, 205, 11, 78, 46, 202, 44, 40, 81, 80, 54, 179, 48, 177, 82, 72, 73, 77, 203, 73, 44, 73, 213, 207, 204, 3, 211, 10, 101, 153, 137, 10, 85, 57, 153, 73, 0, 54, 67, 14, 181] +gzipvec is [31, 139, 8, 0, 0, 0, 0, 0, 0, 255, 115, 205, 76, 79, 205, 11, 78, 46, 202, 44, 40, 81, 80, 54, 179, 48, 177, 82, 72, 73, 77, 203, 73, 44, 73, 213, 207, 204, 3, 211, 10, 101, 153, 137, 10, 85, 57, 153, 73, 0, 33, 152, 113, 23, 42, 0, 0, 0] + +# ---- round-trip identity: raw pair ---- +check of ["D1 raw round-trip", inflate of (deflate of payload), payload] +empty_in is [] +check of ["D2 raw round-trip empty", inflate of (deflate of empty_in), empty_in] + +# ---- round-trip identity: zlib pair ---- +check of ["D3 zlib round-trip", zlib_inflate of (zlib_deflate of payload), payload] +check of ["D4 zlib round-trip empty", zlib_inflate of (zlib_deflate of empty_in), empty_in] + +# ---- multi-KB round-trip (8000 LCG bytes, both pairs) ---- +big is [] +seed is 7 +i is 0 +loop while i < 8000: + seed is (seed * 1103515245 + 12345) % 2147483648 + append of [big, bit_and of [bit_shr of [seed, 16], 255]] + i is i + 1 +check of ["D5 raw round-trip 8KB", inflate of (deflate of big), big] +check of ["D6 zlib round-trip 8KB", zlib_inflate of (zlib_deflate of big), big] + +# ---- FIXED known vectors (python zlib) ---- +check of ["D7 known vector: raw inflate", inflate of rawvec, payload] +check of ["D8 known vector: zlib inflate", zlib_inflate of zlibvec, payload] +# windowBits 15+32 auto-detects the gzip header — the .gz read path +check of ["D9 known vector: gzip auto-detect", zlib_inflate of gzipvec, payload] + +# ---- buffer input (the read_bytes_buf shape) ---- +check of ["D10 inflate of buffer", inflate of (buf_from_list of rawvec), payload] +check of ["D11 deflate of buffer round-trips", inflate of (deflate of (buf_from_list of payload)), payload] + +# ---- zlib wrapper pin: RFC 1950 header for default level/window is +# ---- 0x78 0x9C — fixed regardless of zlib version ---- +zc is zlib_deflate of payload +check of ["D12 zlib_deflate emits 0x78 0x9C header", [zc[0], zc[1]], [120, 156]] + +# ---- corrupt stream: catchable `value` error, never a crash ---- +c13 is 0 +m13 is "" +badvec is [1, 2, 3, 4, 5] +try: + inflate of badvec +catch e: + c13 is 1 + m13 is e +check of ["D13 corrupt stream caught", c13, 1] +check of ["D14 corrupt kind=value", m13.kind, "value"] +check of ["D15 corrupt message", contains of [m13.message, "invalid or truncated"], 1] + +# ---- truncated stream: valid stream minus its last byte ---- +c16 is 0 +truncvec is list_slice of [rawvec, 0, (len of rawvec) - 1] +try: + inflate of truncvec +catch e2: + c16 is 1 +check of ["D16 truncated stream caught", c16, 1] + +# ---- wrong-type error shape (matches neighboring builtins: fill/dispatch) ---- +c17 is 0 +m17 is "" +try: + inflate of "not bytes" +catch e3: + c17 is 1 + m17 is e3 +check of ["D17 string arg caught", c17, 1] +check of ["D18 wrong-type kind", m17.kind, "type_mismatch"] +check of ["D19 wrong-type message", contains of [m17.message, "requires a list of byte values"], 1] + +c20 is 0 +try: + deflate of 42 +catch e4: + c20 is 1 +check of ["D20 numeric arg caught", c20, 1] + +c21 is 0 +try: + zlib_inflate of null +catch e5: + c21 is 1 +check of ["D21 null arg caught", c21, 1] + +c22 is 0 +try: + zlib_deflate of null +catch e6: + c22 is 1 +check of ["D22 zlib_deflate null arg caught", c22, 1] + +if fails == 0: + print of "DEFLATE_ALL_PASS" +if fails > 0: + print of f"{str of fails} FAILURES" + throw of "deflate/inflate tests failed"