From ad8da6b9e8bb6a8e31336e88315e2c44acc683c4 Mon Sep 17 00:00:00 2001 From: hellobertrand <5901952+hellobertrand@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:41:49 +0200 Subject: [PATCH 1/3] fix(cctx): resolve sticky options handling and default values Ensure that zero-valued options are correctly interpreted as defaults and that options are only cached upon successful compression calls. This prevents failed operations from polluting the sticky state and allows zeroed fields to properly trigger default behavior (such as using a pinned block size in static contexts). Signed-off-by: Bertrand Lebonnois <5901952+hellobertrand@users.noreply.github.com> --- include/zxc_buffer.h | 22 +++-- src/lib/zxc_dispatch.c | 203 +++++++++++++++++++++++---------------- tests/test_common.h | 1 + tests/test_context_api.c | 84 ++++++++++++++++ tests/test_main.c | 1 + tests/test_static_ctx.c | 13 +++ 6 files changed, 231 insertions(+), 93 deletions(-) diff --git a/include/zxc_buffer.h b/include/zxc_buffer.h index 4e1a4e038..e057c9ce2 100644 --- a/include/zxc_buffer.h +++ b/include/zxc_buffer.h @@ -373,7 +373,7 @@ ZXC_EXPORT uint64_t zxc_decompress_block_bound(const size_t uncompressed_size); * @param[in] src_size Source size in bytes, in [1, @ref ZXC_BLOCK_SIZE_MAX]. * @param[out] dst Destination buffer. * @param[in] dst_capacity Capacity of @p dst (see zxc_compress_block_bound()). - * @param[in] opts Compression options, or NULL for defaults. + * @param[in] opts Options, or NULL to reuse the sticky settings. * @c level, @c block_size, @c checksum_enabled and * the dictionary fields are used; the shared table * is rebuilt only when it changes. @c seekable is @@ -535,10 +535,12 @@ ZXC_EXPORT void zxc_free_cctx(zxc_cctx* cctx); * raise returns @ref ZXC_ERROR_BAD_LEVEL instead, since the workspace cannot * grow. * - * Options are **sticky**: each entry point remembers the options it uses for - * later calls that pass NULL, starting from zxc_create_cctx(): level, - * block_size, checksum_enabled and seekable here, the first three for - * zxc_compress_block(). Levels above @ref ZXC_LEVEL_ULTRA are silently clamped. + * Options are **sticky**: NULL @p opts reuses the last successful call's level, + * block_size, checksum_enabled and seekable (the first three for + * zxc_compress_block()), starting from zxc_create_cctx(). A zero field in a + * non-NULL @p opts means its default, never a remembered value; a static + * context defaults block_size to its pinned size. Levels above + * @ref ZXC_LEVEL_ULTRA are silently clamped. * Dictionary options are the exception: honoured as in zxc_compress() but * never remembered, so pass them on every call; the shared table is rebuilt * only when it changes. A static context returns @@ -709,11 +711,11 @@ ZXC_EXPORT size_t zxc_static_cctx_workspace_size(const size_t block_size, const * @par Locked parameters * @c block_size, @c level and @c checksum_enabled are pinned at init time. A * later @ref zxc_compress_cctx call asking for a different @c block_size - * returns @ref ZXC_ERROR_BAD_BLOCK_SIZE without re-initialising. A different - * @c level / @c checksum_enabled is honoured per call without re-partitioning, - * except a raise into @ref ZXC_LEVEL_DENSITY on a workspace carved below it: - * the optimal-parser scratch is absent, so the call returns - * @ref ZXC_ERROR_BAD_LEVEL. Dictionaries are rejected too: + * returns @ref ZXC_ERROR_BAD_BLOCK_SIZE without re-initialising; a zero one + * means the pinned size. A different @c level / @c checksum_enabled is honoured + * per call without re-partitioning, except a raise into @ref ZXC_LEVEL_DENSITY + * on a workspace carved below it: the optimal-parser scratch is absent, so the + * call returns @ref ZXC_ERROR_BAD_LEVEL. Dictionaries are rejected too: * @ref ZXC_ERROR_DICT_UNSUPPORTED. * * @param[in,out] workspace Caller-allocated buffer, cache-line aligned. diff --git a/src/lib/zxc_dispatch.c b/src/lib/zxc_dispatch.c index f3a2b1190..6fdc93070 100644 --- a/src/lib/zxc_dispatch.c +++ b/src/lib/zxc_dispatch.c @@ -1102,18 +1102,22 @@ int zxc_get_last_frame_info(const void* src, const size_t src_size, zxc_frame_in * needed to reuse buffers across calls and re-init only when the block size * changes. */ +/** @brief Compression options with every default applied. */ +typedef struct { + int level; + int checksum; + int seekable; + size_t block_size; +} zxc_cctx_opts_t; + struct zxc_cctx_s { - zxc_cctx_t inner; /* existing internal context */ - int initialized; /* 1 if inner has live allocations */ - int owns_workspace; /* 0 = library-allocated (free in zxc_free_cctx), - 1 = caller-supplied static workspace (no-op free, - block_size pinned at init) */ - size_t last_block_size; /* block size used for last init */ - // Sticky options (remembered from create or last compress call). - int stored_level; - int stored_checksum; - int stored_seekable; - size_t stored_block_size; + zxc_cctx_t inner; /* existing internal context */ + int initialized; /* 1 if inner has live allocations */ + int owns_workspace; /* 0 = library-allocated (free in zxc_free_cctx), + 1 = caller-supplied static workspace (no-op free, + block_size pinned at init) */ + size_t last_block_size; /* block size used for last init */ + zxc_cctx_opts_t sticky; /* from create or the last successful compress call */ int huf_cached; /* inner carries the table below */ uint8_t huf_cache[ZXC_HUF_TABLE_SIZE]; /* last table attached */ }; @@ -1142,26 +1146,39 @@ static int zxc_ctx_sync_dict_huf(zxc_cctx_t* RESTRICT inner, uint8_t* RESTRICT c return ZXC_OK; } +/** + * @brief Resolves a call's options: NULL reuses the sticky ones, a zero field + * means its default. A static context's default block_size is its + * pinned one, the only size its workspace serves. + */ +static zxc_cctx_opts_t zxc_cctx_resolve(const zxc_cctx* cctx, const zxc_compress_opts_t* opts) { + if (!opts) return cctx->sticky; + const size_t bs = cctx->owns_workspace ? cctx->last_block_size : ZXC_BLOCK_SIZE_DEFAULT; + return (zxc_cctx_opts_t){.level = ZXC_OPTS_LEVEL(opts, ZXC_LEVEL_DEFAULT), + .checksum = opts->checksum_enabled, + .seekable = opts->seekable, + .block_size = ZXC_OPTS_BLOCK_SIZE(opts, bs)}; +} + zxc_cctx* zxc_create_cctx(const zxc_compress_opts_t* opts) { zxc_cctx* const cctx = (zxc_cctx*)ZXC_CALLOC(1, sizeof(zxc_cctx)); if (UNLIKELY(!cctx)) return NULL; // LCOV_EXCL_LINE - // Resolve and store sticky defaults. - cctx->stored_level = ZXC_OPTS_LEVEL(opts, ZXC_LEVEL_DEFAULT); - cctx->stored_block_size = ZXC_OPTS_BLOCK_SIZE(opts, ZXC_BLOCK_SIZE_DEFAULT); - cctx->stored_checksum = opts ? opts->checksum_enabled : 0; - cctx->stored_seekable = opts ? opts->seekable : 0; + // Nothing sticky yet: NULL opts resolves as all-defaults. + static const zxc_compress_opts_t none = {0}; + const zxc_cctx_opts_t o = zxc_cctx_resolve(cctx, opts ? opts : &none); + cctx->sticky = o; if (opts) { // LCOV_EXCL_START - if (UNLIKELY(!zxc_validate_block_size(cctx->stored_block_size) || - zxc_cctx_init(&cctx->inner, cctx->stored_block_size, 1, cctx->stored_level, - cctx->stored_checksum, 0) != ZXC_OK)) { + if (UNLIKELY(!zxc_validate_block_size(o.block_size) || + zxc_cctx_init(&cctx->inner, o.block_size, 1, o.level, o.checksum, 0) != + ZXC_OK)) { ZXC_FREE(cctx); return NULL; } // LCOV_EXCL_STOP - cctx->last_block_size = cctx->stored_block_size; + cctx->last_block_size = o.block_size; cctx->initialized = 1; } @@ -1185,23 +1202,21 @@ void zxc_free_cctx(zxc_cctx* cctx) { } /** - * @brief Compresses data using a reusable context. + * @brief Frame compression on a reusable context with resolved options @p o. * - * Public API; full contract in @c zxc_buffer.h. Same frame walk and dictionary - * binding as zxc_compress(), empty source included; buffers re-carved only when - * [dict | block] or the parser tier changes, shared table rebuilt only when it - * changes. + * Same frame walk and dictionary binding as zxc_compress(), empty source + * included; buffers re-carved only when [dict | block] or the parser tier + * changes, shared table rebuilt only when it changes. */ -int64_t zxc_compress_cctx(zxc_cctx* cctx, const void* RESTRICT src, const size_t src_size, - void* RESTRICT dst, const size_t dst_capacity, - const zxc_compress_opts_t* opts) { - if (UNLIKELY(!cctx)) return ZXC_ERROR_NULL_INPUT; - if (UNLIKELY(!dst || dst_capacity == 0 || (src_size > 0 && !src))) return ZXC_ERROR_NULL_INPUT; - - const int checksum_enabled = opts ? opts->checksum_enabled : cctx->stored_checksum; - const int seekable = opts ? opts->seekable : cctx->stored_seekable; - const int level = ZXC_OPTS_LEVEL(opts, cctx->stored_level); - const size_t block_size = ZXC_OPTS_BLOCK_SIZE(opts, cctx->stored_block_size); +static int64_t zxc_compress_cctx_resolved(zxc_cctx* cctx, const void* RESTRICT src, + const size_t src_size, void* RESTRICT dst, + const size_t dst_capacity, + const zxc_compress_opts_t* opts, + const zxc_cctx_opts_t* o) { + const int checksum_enabled = o->checksum; + const int seekable = o->seekable; + const int level = o->level; + const size_t block_size = o->block_size; // Dictionary options are never sticky: a remembered pointer would dangle. const uint8_t* dict = opts ? (const uint8_t*)opts->dict : NULL; const size_t dict_size = ZXC_OPTS_DICT_SIZE(opts); @@ -1219,18 +1234,12 @@ int64_t zxc_compress_cctx(zxc_cctx* cctx, const void* RESTRICT src, const size_t if (UNLIKELY(cctx->owns_workspace && level >= ZXC_LEVEL_DENSITY && !cctx->inner.opt_scratch)) return ZXC_ERROR_BAD_LEVEL; - cctx->stored_level = level; - cctx->stored_block_size = block_size; - cctx->stored_checksum = checksum_enabled; - cctx->stored_seekable = seekable; - // The encoder sees [dict | block], so the carved chunk covers both. const size_t eff_chunk = dict_size > 0 ? zxc_block_size_ceil(dict_size + block_size) : block_size; const uint32_t did = (dict && dict_size > 0) ? zxc_dict_id(dict, dict_size, dict_huf) : 0; - // The sticky settings above are already stored, so an empty source is done: - // same writer as the one-shot, and the workspace is left alone. + // Empty source: same writer as the one-shot, workspace left alone. if (UNLIKELY(src_size == 0)) return zxc_write_empty_frame((uint8_t*)dst, dst_capacity, block_size, checksum_enabled, did, seekable); @@ -1326,6 +1335,24 @@ int64_t zxc_compress_cctx(zxc_cctx* cctx, const void* RESTRICT src, const size_t return (int64_t)(op - op_start); } +/** + * @brief Compresses data using a reusable context. + * + * Public API; full contract in @c zxc_buffer.h. Resolves the options, runs the + * frame walk and remembers the options on success only. + */ +int64_t zxc_compress_cctx(zxc_cctx* cctx, const void* RESTRICT src, const size_t src_size, + void* RESTRICT dst, const size_t dst_capacity, + const zxc_compress_opts_t* opts) { + if (UNLIKELY(!cctx || !dst || dst_capacity == 0 || (src_size > 0 && !src))) + return ZXC_ERROR_NULL_INPUT; + + const zxc_cctx_opts_t o = zxc_cctx_resolve(cctx, opts); + const int64_t n = zxc_compress_cctx_resolved(cctx, src, src_size, dst, dst_capacity, opts, &o); + if (LIKELY(n > 0)) cctx->sticky = o; + return n; +} + // --- Decompression ------------------------------------------------------- /** @@ -1654,46 +1681,28 @@ static int64_t zxc_decompress_oneshot(const uint8_t* src, const size_t src_size, // ========================================================================= /** - * @brief Compresses a single block (no file framing), reusing @p cctx. + * @brief Block compression on a reusable context with resolved options @p o. * - * Public API; full contract in @c zxc_buffer.h. Produces one format-conformant - * block with no header / EOF / footer, so @p src_size must not exceed - * @c ZXC_BLOCK_SIZE_MAX (use the frame or streaming APIs for larger inputs). - * With a dictionary in @p opts, [dict | block] is assembled in the cctx-owned - * bounce buffer before encoding. Inner buffers are re-initialised when the - * effective block size changes, or when a per-call level raise into the - * optimal-parser tier requires the opt_scratch region; static contexts - * reject both cases instead (the workspace cannot grow). + * Produces one format-conformant block with no header / EOF / footer. With a + * dictionary in @p opts, [dict | block] is assembled in the cctx-owned bounce + * buffer before encoding. Inner buffers are re-initialised when the effective + * block size changes, or when a per-call level raise into the optimal-parser + * tier requires the opt_scratch region; static contexts reject both cases + * instead (the workspace cannot grow). */ -int64_t zxc_compress_block(zxc_cctx* cctx, const void* RESTRICT src, const size_t src_size, - void* RESTRICT dst, const size_t dst_capacity, - const zxc_compress_opts_t* opts) { - return zxc_compress_block_at(cctx, src, src_size, dst, dst_capacity, opts, 0); -} - -int64_t zxc_compress_block_at(zxc_cctx* cctx, const void* RESTRICT src, const size_t src_size, - void* RESTRICT dst, const size_t dst_capacity, - const zxc_compress_opts_t* opts, const uint64_t block_index) { - if (UNLIKELY(!cctx || !src || !dst || src_size == 0 || dst_capacity == 0)) - return ZXC_ERROR_NULL_INPUT; - - // Block API processes a single format-conformant block: src_size must not - // exceed ZXC_BLOCK_SIZE_MAX. Callers with larger inputs should use the - // frame or streaming APIs which chunk transparently. - if (UNLIKELY(src_size > ZXC_BLOCK_SIZE_MAX)) return ZXC_ERROR_BAD_BLOCK_SIZE; +static int64_t zxc_compress_block_resolved(zxc_cctx* cctx, const void* RESTRICT src, + const size_t src_size, void* RESTRICT dst, + const size_t dst_capacity, + const zxc_compress_opts_t* opts, + const zxc_cctx_opts_t* o, const uint64_t block_index) { + const int checksum_enabled = o->checksum; + const int level = o->level; + const size_t base_block_size = o->block_size; - const int checksum_enabled = opts ? opts->checksum_enabled : cctx->stored_checksum; - const int level = ZXC_OPTS_LEVEL(opts, cctx->stored_level); - // For block API, block_size == src_size (the caller compresses one block at a time). - const size_t block_size = ZXC_OPTS_BLOCK_SIZE(opts, cctx->stored_block_size); - const size_t min_bs = zxc_block_size_ceil(src_size); - - // Always ensure internal buffers can hold src_size. // When a dictionary is active, offset_bits must accommodate dict + block. const uint8_t* b_dict = opts ? (const uint8_t*)opts->dict : NULL; const size_t b_dict_size = ZXC_OPTS_DICT_SIZE(opts); if (UNLIKELY(b_dict_size > ZXC_DICT_SIZE_MAX)) return ZXC_ERROR_DICT_TOO_LARGE; - const size_t base_block_size = (block_size > min_bs) ? block_size : min_bs; const size_t effective_block_size = b_dict_size > 0 ? zxc_block_size_ceil(b_dict_size + base_block_size) : base_block_size; @@ -1708,10 +1717,6 @@ int64_t zxc_compress_block_at(zxc_cctx* cctx, const void* RESTRICT src, const si if (UNLIKELY(cctx->owns_workspace && level >= ZXC_LEVEL_DENSITY && !cctx->inner.opt_scratch)) return ZXC_ERROR_BAD_LEVEL; - cctx->stored_level = level; - cctx->stored_block_size = base_block_size; - cctx->stored_checksum = checksum_enabled; - // Re-init when block_size changed, a level raise needs the optimal-parser // scratch, or a dictionary arrives on a context carved without its prefix // (a block_size switch can round [dict | block] back to the same size). @@ -1758,6 +1763,38 @@ int64_t zxc_compress_block_at(zxc_cctx* cctx, const void* RESTRICT src, const si return (int64_t)res; } +/** + * @brief Compresses a single block (no file framing), reusing @p cctx. + * + * Public API; full contract in @c zxc_buffer.h. @p src_size must not exceed + * @c ZXC_BLOCK_SIZE_MAX (use the frame or streaming APIs for larger inputs). + * Resolves the options, grows block_size to hold the source, encodes and + * remembers the options on success only. + */ +int64_t zxc_compress_block(zxc_cctx* cctx, const void* RESTRICT src, const size_t src_size, + void* RESTRICT dst, const size_t dst_capacity, + const zxc_compress_opts_t* opts) { + return zxc_compress_block_at(cctx, src, src_size, dst, dst_capacity, opts, 0); +} + +int64_t zxc_compress_block_at(zxc_cctx* cctx, const void* RESTRICT src, const size_t src_size, + void* RESTRICT dst, const size_t dst_capacity, + const zxc_compress_opts_t* opts, const uint64_t block_index) { + if (UNLIKELY(!cctx || !src || !dst || src_size == 0 || dst_capacity == 0)) + return ZXC_ERROR_NULL_INPUT; + if (UNLIKELY(src_size > ZXC_BLOCK_SIZE_MAX)) return ZXC_ERROR_BAD_BLOCK_SIZE; + + zxc_cctx_opts_t o = zxc_cctx_resolve(cctx, opts); + o.seekable = cctx->sticky.seekable; /* no seek table here: not an option */ + const size_t min_bs = zxc_block_size_ceil(src_size); + if (o.block_size < min_bs) o.block_size = min_bs; + + const int64_t n = + zxc_compress_block_resolved(cctx, src, src_size, dst, dst_capacity, opts, &o, block_index); + if (LIKELY(n > 0)) cctx->sticky = o; + return n; +} + /** * @brief Block decode through work_buf, for a @p dst too tight for the * speculative writes, then copied out. @p carved bounds the decoded size @@ -2004,10 +2041,10 @@ zxc_cctx* zxc_init_static_cctx(void* RESTRICT workspace, const size_t workspace_ cctx->owns_workspace = 1; cctx->initialized = 1; cctx->last_block_size = block_size; - cctx->stored_level = level; - cctx->stored_block_size = block_size; - cctx->stored_checksum = checksum_enabled; - cctx->stored_seekable = opts->seekable; + cctx->sticky = (zxc_cctx_opts_t){.level = level, + .checksum = checksum_enabled, + .seekable = opts->seekable, + .block_size = block_size}; return cctx; } diff --git a/tests/test_common.h b/tests/test_common.h index 1526df008..187ff3142 100644 --- a/tests/test_common.h +++ b/tests/test_common.h @@ -141,6 +141,7 @@ int test_context_api_empty_input(void); int test_context_api_seekable_frame(void); int test_context_api_seekable_compress(void); int test_cctx_level_raise_reinit(void); +int test_cctx_zero_opts_defaults(void); int test_estimate_cctx_size(void); /* Static Context API */ diff --git a/tests/test_context_api.c b/tests/test_context_api.c index b2e39385a..4234ee4aa 100644 --- a/tests/test_context_api.c +++ b/tests/test_context_api.c @@ -187,6 +187,90 @@ int test_cctx_level_raise_reinit(void) { return 0; } +/* Frame or block API compression on @p c. */ +static int64_t cctx_compress(zxc_cctx* c, const int block_api, const uint8_t* src, const size_t n, + uint8_t* dst, const size_t cap, const zxc_compress_opts_t* co) { + return block_api ? zxc_compress_block(c, src, n, dst, cap, co) + : zxc_compress_cctx(c, src, n, dst, cap, co); +} + +/* #532: zero opts fields mean defaults, and a failed call is not remembered. + * Second call on a reused context vs. a fresh context. */ +int test_cctx_zero_opts_defaults(void) { + printf("=== TEST: Opaque Context API - zero opts fields, failed calls (#532) ===\n"); + + static const struct { + const char* name; + int fail_first; /* first call fails: too-small destination */ + int null_second; /* second call: NULL opts, not zeroed ones */ + int want_custom; /* expect the custom output, not the defaults */ + } cases[] = { + {"zero opts after a successful custom call", 0, 0, 0}, + {"zero opts after a failed custom call", 1, 0, 0}, + {"NULL opts after a failed custom call", 1, 1, 0}, + {"NULL opts after a successful custom call (sticky)", 0, 1, 1}, + }; + const zxc_compress_opts_t zero = {0}; + const zxc_compress_opts_t custom = {.level = 1, .block_size = 4096}; + + const size_t src_sz = 300000; + const uint64_t fb = zxc_compress_bound(src_sz), bb = zxc_compress_block_bound(src_sz); + const size_t cap = (size_t)(fb > bb ? fb : bb); + uint8_t* const src = malloc(src_sz); + uint8_t* const ref[2] = {malloc(cap), malloc(cap)}; /* defaults, custom */ + uint8_t* const out = malloc(cap); + uint8_t tiny[16]; + int ok = 0; + if (!src || !ref[0] || !ref[1] || !out) goto done; + gen_lz_data(src, src_sz); + + for (int block_api = 0; block_api <= 1; block_api++) { + const char* const api = block_api ? "block" : "frame"; + int64_t n_ref[2]; + for (int k = 0; k < 2; k++) { + zxc_cctx* const c = zxc_create_cctx(NULL); + n_ref[k] = + c ? cctx_compress(c, block_api, src, src_sz, ref[k], cap, k ? &custom : &zero) : -1; + zxc_free_cctx(c); + } + if (n_ref[0] <= 0 || n_ref[1] <= 0 || + (n_ref[0] == n_ref[1] && memcmp(ref[0], ref[1], (size_t)n_ref[0]) == 0)) { + printf(" [FAIL] %s: custom opts must change the output (%lld vs %lld)\n", api, + (long long)n_ref[0], (long long)n_ref[1]); + goto done; + } + + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + zxc_cctx* const c = zxc_create_cctx(NULL); + if (!c) goto done; + const int64_t first = + cases[i].fail_first + ? cctx_compress(c, block_api, src, src_sz, tiny, sizeof(tiny), &custom) + : cctx_compress(c, block_api, src, src_sz, out, cap, &custom); + const int64_t n = cctx_compress(c, block_api, src, src_sz, out, cap, + cases[i].null_second ? NULL : &zero); + zxc_free_cctx(c); + const int want = cases[i].want_custom; + if ((cases[i].fail_first ? first >= 0 : first <= 0) || n != n_ref[want] || + memcmp(out, ref[want], (size_t)n) != 0) { + printf(" [FAIL] %s, %s: first %lld, second %lld, want %lld\n", api, cases[i].name, + (long long)first, (long long)n, (long long)n_ref[want]); + goto done; + } + } + printf(" [PASS] %s API: zero fields mean defaults; failures not remembered\n", api); + } + ok = 1; + printf("PASS\n\n"); + +done: + free(src); + free(ref[0]); + free(ref[1]); + free(out); + return ok; +} + int test_estimate_cctx_size(void) { printf("=== TEST: Unit - zxc_estimate_cctx_size ===\n"); diff --git a/tests/test_main.c b/tests/test_main.c index 3372f096b..56347f4f4 100644 --- a/tests/test_main.c +++ b/tests/test_main.c @@ -75,6 +75,7 @@ static const test_entry_t g_tests[] = { TEST_CASE(test_context_api_seekable_frame), TEST_CASE(test_context_api_seekable_compress), TEST_CASE(test_cctx_level_raise_reinit), + TEST_CASE(test_cctx_zero_opts_defaults), TEST_CASE(test_estimate_cctx_size), /* --- Static Context API --- */ diff --git a/tests/test_static_ctx.c b/tests/test_static_ctx.c index 5257f428e..0072ccf14 100644 --- a/tests/test_static_ctx.c +++ b/tests/test_static_ctx.c @@ -245,6 +245,19 @@ int test_static_ctx_block_size_locked(void) { return 0; } + /* Zero block_size means the pinned one (#532). */ + uint8_t ref[1024]; + const zxc_compress_opts_t zero_bs = {.level = ZXC_LEVEL_DEFAULT}; + const int64_t rc3 = zxc_compress_cctx(cctx, src, sizeof(src), dst, sizeof(dst), &zero_bs); + const int64_t rc4 = zxc_compress(src, sizeof(src), ref, sizeof(ref), &opts); + if (rc3 <= 0 || rc3 != rc4 || memcmp(dst, ref, (size_t)rc3) != 0) { + printf(" [FAIL] zero block_size on a pinned context: %lld, one-shot %lld\n", + (long long)rc3, (long long)rc4); + zxc_free_cctx(cctx); + test_aligned_free(ws); + return 0; + } + zxc_free_cctx(cctx); test_aligned_free(ws); printf(" [PASS] mismatched block_size rejected; matching one accepted\n"); From f0a659f09b7c4be8907de205de31d6f73297e017 Mon Sep 17 00:00:00 2001 From: hellobertrand <5901952+hellobertrand@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:17:35 +0200 Subject: [PATCH 2/3] fix(cctx): resolve ambiguity in sticky option defaults Clarify the behavior of `zxc_compress_opts_t` by ensuring that zero-valued `level` or `block_size` fields fall back to the context creation values rather than library-wide defaults. This change ensures that sticky options correctly persist across successful calls while maintaining consistent behavior for both standard and static compression contexts. Failed calls now correctly leave the sticky state unchanged. Signed-off-by: Bertrand Lebonnois <5901952+hellobertrand@users.noreply.github.com> --- docs/API.md | 14 ++-- docs/EXAMPLES.md | 12 ++-- include/zxc_buffer.h | 19 +++--- src/lib/zxc_dispatch.c | 137 +++++++++++++++++++-------------------- tests/test_common.h | 1 + tests/test_context_api.c | 64 +++++++++++++++++- tests/test_main.c | 1 + tests/test_static_ctx.c | 14 ++-- 8 files changed, 166 insertions(+), 96 deletions(-) diff --git a/docs/API.md b/docs/API.md index ca85f0a3f..b876c0f02 100644 --- a/docs/API.md +++ b/docs/API.md @@ -653,7 +653,7 @@ ZXC_EXPORT int64_t zxc_compress_block( size_t src_size, void* dst, size_t dst_capacity, - const zxc_compress_opts_t* opts // NULL = defaults + const zxc_compress_opts_t* opts // NULL = sticky settings ); ``` @@ -776,7 +776,9 @@ ZXC_EXPORT zxc_cctx* zxc_create_cctx(const zxc_compress_opts_t* opts); Creates a heap-allocated compression context. When `opts` is non-NULL, internal buffers are pre-allocated (eager init). -When `opts` is NULL, allocation is deferred to first use (lazy init). +When `opts` is NULL, allocation is deferred to first use (lazy init). +The creation options are also what a zero `level` or `block_size` in a later +call falls back to (the library defaults for NULL). **Returns**: context pointer, or `NULL` on allocation failure. @@ -810,9 +812,11 @@ context returns `ZXC_ERROR_DICT_UNSUPPORTED` for any dictionary. `seekable` appends the seek table as `zxc_compress()` does, without allocating, static contexts included. -Each entry point remembers the options it uses: `level`, `block_size`, -`checksum_enabled` and `seekable` here, the first three for -`zxc_compress_block()`. +Options are **sticky**: `NULL` reuses the last successful call's `level`, +`block_size`, `checksum_enabled` and `seekable` (the first three for +`zxc_compress_block()`), starting from the creation options. A zero `level` or +`block_size` in a non-NULL `opts` means the value the context was created +with, never an earlier call's. A failed call changes nothing. ### `zxc_create_dctx` diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index 6ca084761..c4ee0b36a 100644 --- a/docs/EXAMPLES.md +++ b/docs/EXAMPLES.md @@ -230,9 +230,11 @@ ZXC provides opaque reusable contexts. Internal buffers are only reallocated when the **block size changes**, so the common case (same block size, different data) is completely allocation-free. -Options are **sticky**: settings passed via `opts` to `zxc_create_cctx()` or -`zxc_compress_cctx()` are remembered and reused on subsequent calls where -`opts` is `NULL`. +Options are **sticky**: `zxc_compress_cctx()` remembers the options of its +last successful call, starting from those given to `zxc_create_cctx()`, and +reuses them when `opts` is `NULL`. In a non-NULL `opts`, a zero `level` or +`block_size` means the creation value, so an override only needs the fields it +changes. ```c #include "zxc.h" @@ -283,8 +285,8 @@ int main(void) { } } - // Override sticky settings for a single call (e.g. switch to level 5). - // This also updates the sticky settings for future NULL calls. + // Switch to level 5. block_size 0 keeps the one from create_opts, so no + // re-allocation; later NULL calls reuse level 5. zxc_compress_opts_t high_opts = { .level = 5 }; int64_t csz = zxc_compress_cctx(cctx, src, BLOCK_SIZE, comp, comp_cap, &high_opts); printf("Level-5 compressed: %lld bytes\n", (long long)csz); diff --git a/include/zxc_buffer.h b/include/zxc_buffer.h index e057c9ce2..06298573d 100644 --- a/include/zxc_buffer.h +++ b/include/zxc_buffer.h @@ -537,10 +537,10 @@ ZXC_EXPORT void zxc_free_cctx(zxc_cctx* cctx); * * Options are **sticky**: NULL @p opts reuses the last successful call's level, * block_size, checksum_enabled and seekable (the first three for - * zxc_compress_block()), starting from zxc_create_cctx(). A zero field in a - * non-NULL @p opts means its default, never a remembered value; a static - * context defaults block_size to its pinned size. Levels above - * @ref ZXC_LEVEL_ULTRA are silently clamped. + * zxc_compress_block()), starting from zxc_create_cctx(). In a non-NULL + * @p opts, a zero level or block_size means the creation value (the library + * default for a NULL zxc_create_cctx()), never an earlier call's; a failed call + * changes nothing. Levels above @ref ZXC_LEVEL_ULTRA are silently clamped. * Dictionary options are the exception: honoured as in zxc_compress() but * never remembered, so pass them on every call; the shared table is rebuilt * only when it changes. A static context returns @@ -711,11 +711,12 @@ ZXC_EXPORT size_t zxc_static_cctx_workspace_size(const size_t block_size, const * @par Locked parameters * @c block_size, @c level and @c checksum_enabled are pinned at init time. A * later @ref zxc_compress_cctx call asking for a different @c block_size - * returns @ref ZXC_ERROR_BAD_BLOCK_SIZE without re-initialising; a zero one - * means the pinned size. A different @c level / @c checksum_enabled is honoured - * per call without re-partitioning, except a raise into @ref ZXC_LEVEL_DENSITY - * on a workspace carved below it: the optimal-parser scratch is absent, so the - * call returns @ref ZXC_ERROR_BAD_LEVEL. Dictionaries are rejected too: + * returns @ref ZXC_ERROR_BAD_BLOCK_SIZE without re-initialising; a zero + * @c block_size or @c level means the pinned one. A different @c level / + * @c checksum_enabled is honoured per call without re-partitioning, except a + * raise into @ref ZXC_LEVEL_DENSITY on a workspace carved below it: the + * optimal-parser scratch is absent, so the call returns + * @ref ZXC_ERROR_BAD_LEVEL. Dictionaries are rejected too: * @ref ZXC_ERROR_DICT_UNSUPPORTED. * * @param[in,out] workspace Caller-allocated buffer, cache-line aligned. diff --git a/src/lib/zxc_dispatch.c b/src/lib/zxc_dispatch.c index 6fdc93070..440cc49af 100644 --- a/src/lib/zxc_dispatch.c +++ b/src/lib/zxc_dispatch.c @@ -1095,13 +1095,6 @@ int zxc_get_last_frame_info(const void* src, const size_t src_size, zxc_frame_in // --- Compression --------------------------------------------------------- -/** - * @brief Opaque reusable compression context (public handle @ref zxc_cctx). - * - * Wraps one internal @ref zxc_cctx_t plus the sticky options and bookkeeping - * needed to reuse buffers across calls and re-init only when the block size - * changes. - */ /** @brief Compression options with every default applied. */ typedef struct { int level; @@ -1110,6 +1103,13 @@ typedef struct { size_t block_size; } zxc_cctx_opts_t; +/** + * @brief Opaque reusable compression context (public handle @ref zxc_cctx). + * + * Wraps one internal @ref zxc_cctx_t plus the sticky options and bookkeeping + * needed to reuse buffers across calls and re-init only when the block size + * changes. + */ struct zxc_cctx_s { zxc_cctx_t inner; /* existing internal context */ int initialized; /* 1 if inner has live allocations */ @@ -1117,7 +1117,8 @@ struct zxc_cctx_s { 1 = caller-supplied static workspace (no-op free, block_size pinned at init) */ size_t last_block_size; /* block size used for last init */ - zxc_cctx_opts_t sticky; /* from create or the last successful compress call */ + zxc_cctx_opts_t base; /* creation options; zero fields fall back to it */ + zxc_cctx_opts_t sticky; /* last successful call; NULL opts reuses it */ int huf_cached; /* inner carries the table below */ uint8_t huf_cache[ZXC_HUF_TABLE_SIZE]; /* last table attached */ }; @@ -1146,27 +1147,29 @@ static int zxc_ctx_sync_dict_huf(zxc_cctx_t* RESTRICT inner, uint8_t* RESTRICT c return ZXC_OK; } -/** - * @brief Resolves a call's options: NULL reuses the sticky ones, a zero field - * means its default. A static context's default block_size is its - * pinned one, the only size its workspace serves. - */ +/** @brief Creation options, library defaults for NULL or zero fields. */ +static zxc_cctx_opts_t zxc_cctx_base(const zxc_compress_opts_t* opts) { + return (zxc_cctx_opts_t){.level = ZXC_OPTS_LEVEL(opts, ZXC_LEVEL_DEFAULT), + .checksum = opts ? opts->checksum_enabled : 0, + .seekable = opts ? opts->seekable : 0, + .block_size = ZXC_OPTS_BLOCK_SIZE(opts, ZXC_BLOCK_SIZE_DEFAULT)}; +} + +/** @brief NULL reuses the sticky options; a zero field means the creation value. */ static zxc_cctx_opts_t zxc_cctx_resolve(const zxc_cctx* cctx, const zxc_compress_opts_t* opts) { if (!opts) return cctx->sticky; - const size_t bs = cctx->owns_workspace ? cctx->last_block_size : ZXC_BLOCK_SIZE_DEFAULT; - return (zxc_cctx_opts_t){.level = ZXC_OPTS_LEVEL(opts, ZXC_LEVEL_DEFAULT), + return (zxc_cctx_opts_t){.level = ZXC_OPTS_LEVEL(opts, cctx->base.level), .checksum = opts->checksum_enabled, .seekable = opts->seekable, - .block_size = ZXC_OPTS_BLOCK_SIZE(opts, bs)}; + .block_size = ZXC_OPTS_BLOCK_SIZE(opts, cctx->base.block_size)}; } zxc_cctx* zxc_create_cctx(const zxc_compress_opts_t* opts) { zxc_cctx* const cctx = (zxc_cctx*)ZXC_CALLOC(1, sizeof(zxc_cctx)); if (UNLIKELY(!cctx)) return NULL; // LCOV_EXCL_LINE - // Nothing sticky yet: NULL opts resolves as all-defaults. - static const zxc_compress_opts_t none = {0}; - const zxc_cctx_opts_t o = zxc_cctx_resolve(cctx, opts ? opts : &none); + const zxc_cctx_opts_t o = zxc_cctx_base(opts); + cctx->base = o; cctx->sticky = o; if (opts) { @@ -1202,7 +1205,8 @@ void zxc_free_cctx(zxc_cctx* cctx) { } /** - * @brief Frame compression on a reusable context with resolved options @p o. + * @brief Frame compression with resolved options @p o; @p opts supplies the + * dictionary only. * * Same frame walk and dictionary binding as zxc_compress(), empty source * included; buffers re-carved only when [dict | block] or the parser tier @@ -1210,52 +1214,47 @@ void zxc_free_cctx(zxc_cctx* cctx) { */ static int64_t zxc_compress_cctx_resolved(zxc_cctx* cctx, const void* RESTRICT src, const size_t src_size, void* RESTRICT dst, - const size_t dst_capacity, - const zxc_compress_opts_t* opts, - const zxc_cctx_opts_t* o) { - const int checksum_enabled = o->checksum; - const int seekable = o->seekable; - const int level = o->level; - const size_t block_size = o->block_size; + const size_t dst_capacity, const zxc_cctx_opts_t o, + const zxc_compress_opts_t* opts) { // Dictionary options are never sticky: a remembered pointer would dangle. const uint8_t* dict = opts ? (const uint8_t*)opts->dict : NULL; const size_t dict_size = ZXC_OPTS_DICT_SIZE(opts); const uint8_t* dict_huf = ZXC_OPTS_DICT_HUF(opts); if (UNLIKELY(dict_size > ZXC_DICT_SIZE_MAX)) return ZXC_ERROR_DICT_TOO_LARGE; - if (UNLIKELY(!zxc_validate_block_size(block_size))) return ZXC_ERROR_BAD_BLOCK_SIZE; + if (UNLIKELY(!zxc_validate_block_size(o.block_size))) return ZXC_ERROR_BAD_BLOCK_SIZE; // Static cctx: no room for a dictionary prefix, and the workspace cannot // grow, so reject a block_size change or a level raise into the // optimal-parser tier it was carved without. if (UNLIKELY(cctx->owns_workspace && dict_size > 0)) return ZXC_ERROR_DICT_UNSUPPORTED; - if (UNLIKELY(cctx->owns_workspace && block_size != cctx->last_block_size)) + if (UNLIKELY(cctx->owns_workspace && o.block_size != cctx->last_block_size)) return ZXC_ERROR_BAD_BLOCK_SIZE; - if (UNLIKELY(cctx->owns_workspace && level >= ZXC_LEVEL_DENSITY && !cctx->inner.opt_scratch)) + if (UNLIKELY(cctx->owns_workspace && o.level >= ZXC_LEVEL_DENSITY && !cctx->inner.opt_scratch)) return ZXC_ERROR_BAD_LEVEL; // The encoder sees [dict | block], so the carved chunk covers both. const size_t eff_chunk = - dict_size > 0 ? zxc_block_size_ceil(dict_size + block_size) : block_size; + dict_size > 0 ? zxc_block_size_ceil(dict_size + o.block_size) : o.block_size; const uint32_t did = (dict && dict_size > 0) ? zxc_dict_id(dict, dict_size, dict_huf) : 0; // Empty source: same writer as the one-shot, workspace left alone. if (UNLIKELY(src_size == 0)) - return zxc_write_empty_frame((uint8_t*)dst, dst_capacity, block_size, checksum_enabled, did, - seekable); + return zxc_write_empty_frame((uint8_t*)dst, dst_capacity, o.block_size, o.checksum, did, + o.seekable); // Re-init when the chunk changed, a level raise needs the optimal-parser // scratch, or a dictionary arrives on a context carved without its prefix. if (UNLIKELY(!cctx->initialized || cctx->last_block_size != eff_chunk || - (level >= ZXC_LEVEL_DENSITY && !cctx->inner.opt_scratch) || + (o.level >= ZXC_LEVEL_DENSITY && !cctx->inner.opt_scratch) || (dict_size > 0 && !cctx->inner.dict_buffer))) { if (cctx->initialized) { zxc_cctx_free(&cctx->inner); cctx->initialized = 0; } // LCOV_EXCL_START - if (UNLIKELY(zxc_cctx_init(&cctx->inner, eff_chunk, 1, level, checksum_enabled, - dict_size) != ZXC_OK)) + if (UNLIKELY(zxc_cctx_init(&cctx->inner, eff_chunk, 1, o.level, o.checksum, dict_size) != + ZXC_OK)) return ZXC_ERROR_MEMORY; // LCOV_EXCL_STOP cctx->last_block_size = eff_chunk; @@ -1263,8 +1262,8 @@ static int64_t zxc_compress_cctx_resolved(zxc_cctx* cctx, const void* RESTRICT s cctx->huf_cached = 0; } else { // Same chunk: update level + checksum without realloc. - cctx->inner.compression_level = level; - cctx->inner.checksum_enabled = checksum_enabled; + cctx->inner.compression_level = o.level; + cctx->inner.checksum_enabled = o.checksum; } zxc_cctx_t* const ctx = &cctx->inner; @@ -1282,8 +1281,8 @@ static int64_t zxc_compress_cctx_resolved(zxc_cctx* cctx, const void* RESTRICT s const uint8_t* const op_end = op + dst_capacity; const uint8_t* const ip = (const uint8_t*)src; - const int h_val = zxc_write_file_header(op, (size_t)(op_end - op), block_size, checksum_enabled, - did, seekable); + const int h_val = + zxc_write_file_header(op, (size_t)(op_end - op), o.block_size, o.checksum, did, o.seekable); if (UNLIKELY(h_val < 0)) return h_val; // LCOV_EXCL_LINE op += h_val; @@ -1291,7 +1290,7 @@ static int64_t zxc_compress_cctx_resolved(zxc_cctx* cctx, const void* RESTRICT s size_t pos = 0; uint64_t bi = 0; for (; pos < src_size; bi++) { - const size_t chunk_len = (src_size - pos > block_size) ? block_size : (src_size - pos); + const size_t chunk_len = (src_size - pos > o.block_size) ? o.block_size : (src_size - pos); const size_t rem_cap = (size_t)(op_end - op); int res; @@ -1304,7 +1303,7 @@ static int64_t zxc_compress_cctx_resolved(zxc_cctx* cctx, const void* RESTRICT s } if (UNLIKELY(res < 0)) return res; - if (checksum_enabled && LIKELY(res >= ZXC_BLOCK_CHECKSUM_SIZE)) + if (o.checksum && LIKELY(res >= ZXC_BLOCK_CHECKSUM_SIZE)) digest = zxc_digest_combine(digest, zxc_le32(op + res - ZXC_BLOCK_CHECKSUM_SIZE)); op += res; @@ -1320,15 +1319,15 @@ static int64_t zxc_compress_cctx_resolved(zxc_cctx* cctx, const void* RESTRICT s op += eof_val; // Seek table between the EOF block and the footer. - if (seekable) { + if (o.seekable) { const int64_t st_val = zxc_write_seek_table_from_blocks( - op, (size_t)(op_end - op), op_start + h_val, eof_at, bi, checksum_enabled); + op, (size_t)(op_end - op), op_start + h_val, eof_at, bi, o.checksum); if (UNLIKELY(st_val < 0)) return st_val; op += st_val; } const int footer_val = zxc_write_file_footer( - op, (size_t)(op_end - op), (uint64_t)(op - op_start), src_size, digest, checksum_enabled); + op, (size_t)(op_end - op), (uint64_t)(op - op_start), src_size, digest, o.checksum); if (UNLIKELY(footer_val < 0)) return footer_val; op += footer_val; @@ -1348,7 +1347,7 @@ int64_t zxc_compress_cctx(zxc_cctx* cctx, const void* RESTRICT src, const size_t return ZXC_ERROR_NULL_INPUT; const zxc_cctx_opts_t o = zxc_cctx_resolve(cctx, opts); - const int64_t n = zxc_compress_cctx_resolved(cctx, src, src_size, dst, dst_capacity, opts, &o); + const int64_t n = zxc_compress_cctx_resolved(cctx, src, src_size, dst, dst_capacity, o, opts); if (LIKELY(n > 0)) cctx->sticky = o; return n; } @@ -1681,7 +1680,8 @@ static int64_t zxc_decompress_oneshot(const uint8_t* src, const size_t src_size, // ========================================================================= /** - * @brief Block compression on a reusable context with resolved options @p o. + * @brief Block compression with resolved options @p o; @p opts supplies the + * dictionary only. * * Produces one format-conformant block with no header / EOF / footer. With a * dictionary in @p opts, [dict | block] is assembled in the cctx-owned bounce @@ -1692,19 +1692,15 @@ static int64_t zxc_decompress_oneshot(const uint8_t* src, const size_t src_size, */ static int64_t zxc_compress_block_resolved(zxc_cctx* cctx, const void* RESTRICT src, const size_t src_size, void* RESTRICT dst, - const size_t dst_capacity, + const size_t dst_capacity, const zxc_cctx_opts_t o, const zxc_compress_opts_t* opts, - const zxc_cctx_opts_t* o, const uint64_t block_index) { - const int checksum_enabled = o->checksum; - const int level = o->level; - const size_t base_block_size = o->block_size; - + const uint64_t block_index) { // When a dictionary is active, offset_bits must accommodate dict + block. const uint8_t* b_dict = opts ? (const uint8_t*)opts->dict : NULL; const size_t b_dict_size = ZXC_OPTS_DICT_SIZE(opts); if (UNLIKELY(b_dict_size > ZXC_DICT_SIZE_MAX)) return ZXC_ERROR_DICT_TOO_LARGE; const size_t effective_block_size = - b_dict_size > 0 ? zxc_block_size_ceil(b_dict_size + base_block_size) : base_block_size; + b_dict_size > 0 ? zxc_block_size_ceil(b_dict_size + o.block_size) : o.block_size; // Static cctx: the workspace cannot grow, so reject anything forcing a // re-partition - another block size, or a level raise into the @@ -1714,14 +1710,14 @@ static int64_t zxc_compress_block_resolved(zxc_cctx* cctx, const void* RESTRICT if (UNLIKELY(cctx->owns_workspace && b_dict_size > 0)) return ZXC_ERROR_DICT_UNSUPPORTED; if (UNLIKELY(cctx->owns_workspace && effective_block_size != cctx->last_block_size)) return ZXC_ERROR_BAD_BLOCK_SIZE; // LCOV_EXCL_LINE - if (UNLIKELY(cctx->owns_workspace && level >= ZXC_LEVEL_DENSITY && !cctx->inner.opt_scratch)) + if (UNLIKELY(cctx->owns_workspace && o.level >= ZXC_LEVEL_DENSITY && !cctx->inner.opt_scratch)) return ZXC_ERROR_BAD_LEVEL; // Re-init when block_size changed, a level raise needs the optimal-parser // scratch, or a dictionary arrives on a context carved without its prefix // (a block_size switch can round [dict | block] back to the same size). if (UNLIKELY(!cctx->initialized || cctx->last_block_size != effective_block_size || - (level >= ZXC_LEVEL_DENSITY && !cctx->inner.opt_scratch) || + (o.level >= ZXC_LEVEL_DENSITY && !cctx->inner.opt_scratch) || (b_dict_size > 0 && !cctx->inner.dict_buffer))) { if (cctx->initialized) { // LCOV_EXCL_START @@ -1730,7 +1726,7 @@ static int64_t zxc_compress_block_resolved(zxc_cctx* cctx, const void* RESTRICT // LCOV_EXCL_STOP } // LCOV_EXCL_START - if (UNLIKELY(zxc_cctx_init(&cctx->inner, effective_block_size, 1, level, checksum_enabled, + if (UNLIKELY(zxc_cctx_init(&cctx->inner, effective_block_size, 1, o.level, o.checksum, b_dict_size) != ZXC_OK)) return ZXC_ERROR_MEMORY; // LCOV_EXCL_STOP @@ -1738,8 +1734,8 @@ static int64_t zxc_compress_block_resolved(zxc_cctx* cctx, const void* RESTRICT cctx->initialized = 1; cctx->huf_cached = 0; } else { - cctx->inner.compression_level = level; - cctx->inner.checksum_enabled = checksum_enabled; + cctx->inner.compression_level = o.level; + cctx->inner.checksum_enabled = o.checksum; } cctx->inner.dict_size = b_dict_size; @@ -1785,12 +1781,15 @@ int64_t zxc_compress_block_at(zxc_cctx* cctx, const void* RESTRICT src, const si if (UNLIKELY(src_size > ZXC_BLOCK_SIZE_MAX)) return ZXC_ERROR_BAD_BLOCK_SIZE; zxc_cctx_opts_t o = zxc_cctx_resolve(cctx, opts); - o.seekable = cctx->sticky.seekable; /* no seek table here: not an option */ + // No seek table in a block: seekable stays as the frame API set it. + o.seekable = cctx->sticky.seekable; + // Grown to hold the source, then checked: nothing invalid gets remembered. const size_t min_bs = zxc_block_size_ceil(src_size); if (o.block_size < min_bs) o.block_size = min_bs; + if (UNLIKELY(!zxc_validate_block_size(o.block_size))) return ZXC_ERROR_BAD_BLOCK_SIZE; const int64_t n = - zxc_compress_block_resolved(cctx, src, src_size, dst, dst_capacity, opts, &o, block_index); + zxc_compress_block_resolved(cctx, src, src_size, dst, dst_capacity, o, opts, block_index); if (LIKELY(n > 0)) cctx->sticky = o; return n; } @@ -2019,12 +2018,14 @@ zxc_cctx* zxc_init_static_cctx(void* RESTRICT workspace, const size_t workspace_ const zxc_compress_opts_t* RESTRICT opts) { if (UNLIKELY(!workspace || !opts)) return NULL; - const int level = (opts->level > 0) ? opts->level : ZXC_LEVEL_DEFAULT; - const size_t block_size = (opts->block_size > 0) ? opts->block_size : ZXC_BLOCK_SIZE_DEFAULT; - const int checksum_enabled = opts->checksum_enabled; + // Not clamped: the workspace is sized for the level as given. + if (UNLIKELY(opts->level > ZXC_LEVEL_ULTRA)) return NULL; + const zxc_cctx_opts_t o = zxc_cctx_base(opts); + const int level = o.level; + const size_t block_size = o.block_size; + const int checksum_enabled = o.checksum; if (UNLIKELY(!zxc_validate_block_size(block_size))) return NULL; - if (UNLIKELY(level < ZXC_LEVEL_FASTEST || level > ZXC_LEVEL_ULTRA)) return NULL; const size_t inner_sz = zxc_cctx_compute_workspace_size(block_size, 1, level, 0); if (UNLIKELY(inner_sz == 0)) return NULL; @@ -2041,10 +2042,8 @@ zxc_cctx* zxc_init_static_cctx(void* RESTRICT workspace, const size_t workspace_ cctx->owns_workspace = 1; cctx->initialized = 1; cctx->last_block_size = block_size; - cctx->sticky = (zxc_cctx_opts_t){.level = level, - .checksum = checksum_enabled, - .seekable = opts->seekable, - .block_size = block_size}; + cctx->base = o; + cctx->sticky = o; return cctx; } diff --git a/tests/test_common.h b/tests/test_common.h index 187ff3142..6e2679041 100644 --- a/tests/test_common.h +++ b/tests/test_common.h @@ -142,6 +142,7 @@ int test_context_api_seekable_frame(void); int test_context_api_seekable_compress(void); int test_cctx_level_raise_reinit(void); int test_cctx_zero_opts_defaults(void); +int test_cctx_zero_opts_creation_values(void); int test_estimate_cctx_size(void); /* Static Context API */ diff --git a/tests/test_context_api.c b/tests/test_context_api.c index 4234ee4aa..6bc4641c8 100644 --- a/tests/test_context_api.c +++ b/tests/test_context_api.c @@ -211,7 +211,8 @@ int test_cctx_zero_opts_defaults(void) { {"NULL opts after a successful custom call (sticky)", 0, 1, 1}, }; const zxc_compress_opts_t zero = {0}; - const zxc_compress_opts_t custom = {.level = 1, .block_size = 4096}; + const zxc_compress_opts_t custom = { + .level = 1, .block_size = 4096, .checksum_enabled = 1, .seekable = 1}; const size_t src_sz = 300000; const uint64_t fb = zxc_compress_bound(src_sz), bb = zxc_compress_block_bound(src_sz); @@ -271,6 +272,67 @@ int test_cctx_zero_opts_defaults(void) { return ok; } +/* #532: zero fields mean the creation values, on both APIs; an invalid + * block_size is never remembered. */ +int test_cctx_zero_opts_creation_values(void) { + printf("=== TEST: Opaque Context API - zero opts fields keep the creation values (#532) ===\n"); + enum { BS = 4096, SRC = 3000 }; + const size_t cap = (size_t)zxc_compress_bound(SRC), + bcap = (size_t)zxc_compress_block_bound(SRC); + uint8_t src[SRC]; + uint8_t* const arc = malloc(cap); + uint8_t* const ref = malloc(cap); + uint8_t* const blk = malloc(bcap); + const zxc_compress_opts_t create = {.level = ZXC_LEVEL_FASTEST, .block_size = BS}; + zxc_cctx* const c = zxc_create_cctx(&create); + int ok = 0; + if (!arc || !ref || !blk || !c) goto done; + gen_lz_data(src, sizeof(src)); + + /* Block call with level only: the next NULL frame still declares 4 KiB. */ + const zxc_compress_opts_t lvl3 = {.level = 3}; + zxc_frame_info_t info = {0}; + int64_t n = zxc_compress_block(c, src, SRC, blk, bcap, &lvl3); + if (n > 0) n = zxc_compress_cctx(c, src, SRC, arc, cap, NULL); + if (n <= 0 || zxc_get_frame_info(arc, (size_t)n, &info, sizeof(info)) != ZXC_OK || + info.block_size != BS) { + printf(" [FAIL] block call then NULL frame: %lld, declares %zu, want %d\n", (long long)n, + info.block_size, (int)BS); + goto done; + } + + /* Frame call with level only: byte-identical to the one-shot at {5, 4 KiB}. */ + const zxc_compress_opts_t lvl5 = {.level = 5}; + const zxc_compress_opts_t lvl5_bs = {.level = 5, .block_size = BS}; + n = zxc_compress_cctx(c, src, SRC, arc, cap, &lvl5); + const int64_t r = zxc_compress(src, SRC, ref, cap, &lvl5_bs); + if (n <= 0 || n != r || memcmp(arc, ref, (size_t)n) != 0) { + printf(" [FAIL] {.level = 5} on a 4 KiB context: %lld, one-shot %lld\n", (long long)n, + (long long)r); + goto done; + } + + /* Invalid block_size refused; the sticky settings survive it. */ + const zxc_compress_opts_t bad = {.block_size = 10000}; + const int64_t e = zxc_compress_block(c, src, SRC, blk, bcap, &bad); + n = zxc_compress_cctx(c, src, SRC, arc, cap, NULL); + if (e != ZXC_ERROR_BAD_BLOCK_SIZE || n != r || memcmp(arc, ref, (size_t)n) != 0) { + printf(" [FAIL] invalid block_size: %lld, NULL call after it %lld\n", (long long)e, + (long long)n); + goto done; + } + ok = 1; + printf(" [PASS] creation values kept; invalid block_size refused\n"); + printf("PASS\n\n"); + +done: + zxc_free_cctx(c); + free(arc); + free(ref); + free(blk); + return ok; +} + int test_estimate_cctx_size(void) { printf("=== TEST: Unit - zxc_estimate_cctx_size ===\n"); diff --git a/tests/test_main.c b/tests/test_main.c index 56347f4f4..226d4bad4 100644 --- a/tests/test_main.c +++ b/tests/test_main.c @@ -76,6 +76,7 @@ static const test_entry_t g_tests[] = { TEST_CASE(test_context_api_seekable_compress), TEST_CASE(test_cctx_level_raise_reinit), TEST_CASE(test_cctx_zero_opts_defaults), + TEST_CASE(test_cctx_zero_opts_creation_values), TEST_CASE(test_estimate_cctx_size), /* --- Static Context API --- */ diff --git a/tests/test_static_ctx.c b/tests/test_static_ctx.c index 0072ccf14..2dcc0dc49 100644 --- a/tests/test_static_ctx.c +++ b/tests/test_static_ctx.c @@ -209,14 +209,14 @@ int test_static_ctx_block_size_locked(void) { printf("=== TEST: Static Context API - block_size lock ===\n"); const size_t pinned_bs = 64 * 1024; - const size_t ws_sz = zxc_static_cctx_workspace_size(pinned_bs, ZXC_LEVEL_DEFAULT); + const size_t ws_sz = zxc_static_cctx_workspace_size(pinned_bs, ZXC_LEVEL_DENSITY); void* const ws = test_aligned_alloc(64, ws_sz); if (!ws) { printf(" [FAIL] aligned_alloc\n"); return 0; } - zxc_compress_opts_t opts = {.level = ZXC_LEVEL_DEFAULT, .block_size = pinned_bs}; + zxc_compress_opts_t opts = {.level = ZXC_LEVEL_DENSITY, .block_size = pinned_bs}; zxc_cctx* const cctx = zxc_init_static_cctx(ws, ws_sz, &opts); if (!cctx) { printf(" [FAIL] init_static_cctx\n"); @@ -245,14 +245,14 @@ int test_static_ctx_block_size_locked(void) { return 0; } - /* Zero block_size means the pinned one (#532). */ + /* Zero level and block_size mean the pinned ones (#532). */ uint8_t ref[1024]; - const zxc_compress_opts_t zero_bs = {.level = ZXC_LEVEL_DEFAULT}; - const int64_t rc3 = zxc_compress_cctx(cctx, src, sizeof(src), dst, sizeof(dst), &zero_bs); + const zxc_compress_opts_t zero = {0}; + const int64_t rc3 = zxc_compress_cctx(cctx, src, sizeof(src), dst, sizeof(dst), &zero); const int64_t rc4 = zxc_compress(src, sizeof(src), ref, sizeof(ref), &opts); if (rc3 <= 0 || rc3 != rc4 || memcmp(dst, ref, (size_t)rc3) != 0) { - printf(" [FAIL] zero block_size on a pinned context: %lld, one-shot %lld\n", - (long long)rc3, (long long)rc4); + printf(" [FAIL] zero opts on a pinned context: %lld, one-shot %lld\n", (long long)rc3, + (long long)rc4); zxc_free_cctx(cctx); test_aligned_free(ws); return 0; From 82417a2361620af96534e57c911b219a2a7f908d Mon Sep 17 00:00:00 2001 From: hellobertrand <5901952+hellobertrand@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:08:57 +0200 Subject: [PATCH 3/3] fix(cctx): isolate creation-time defaults from sticky state Ensure that zero-valued options in `zxc_compress_cctx` and block compression calls strictly revert to creation-time defaults rather than the last successful call's state. This change explicitly tracks `base_level` and `base_block_size` to decouple them from the sticky cache, preventing cross-call interference. Additionally, refine workspace re-carving logic to better handle block size and dictionary transitions, and ensure failed calls do not mutate the sticky context. Signed-off-by: Bertrand Lebonnois <5901952+hellobertrand@users.noreply.github.com> --- README.md | 5 +-- docs/API.md | 8 +++-- docs/EXAMPLES.md | 4 +-- include/zxc_buffer.h | 8 +++-- src/lib/zxc_dispatch.c | 46 ++++++++++++------------- tests/test_context_api.c | 73 ++++++++++++++++++++++++++++++---------- tests/test_static_ctx.c | 10 +++++- 7 files changed, 103 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 1bd88c2c7..9be4ca06f 100644 --- a/README.md +++ b/README.md @@ -250,8 +250,9 @@ int64_t decompressed_size = zxc_decompress(src, src_size, dst, dst_capacity, &d_ The same options structs drive the other entry points: `zxc_stream_compress()` / `zxc_stream_decompress()` for multi-threaded file streaming, reusable `zxc_cctx` / `zxc_dctx` -contexts for tight loops where per-call `malloc`/`free` overhead matters (settings are **sticky**, -so passing `NULL` reuses those given at creation), and `.seekable = 1` to append a seek table for +contexts for tight loops where per-call `malloc`/`free` overhead matters (settings are **sticky**: +passing `NULL` reuses the last successful call's, starting from those given at creation), and +`.seekable = 1` to append a seek table for O(1) random-access decompression. **[👉 See complete examples and advanced usage](docs/EXAMPLES.md)** — stream API, reusable contexts, diff --git a/docs/API.md b/docs/API.md index b876c0f02..585a21661 100644 --- a/docs/API.md +++ b/docs/API.md @@ -669,7 +669,8 @@ use the frame API (`zxc_compress`) or streaming API (`zxc_cstream_*`), which chunk transparently into format-conformant blocks. **Returns**: compressed block size (> 0) on success, or negative `zxc_error_t`. -Returns `ZXC_ERROR_BAD_BLOCK_SIZE` if `src_size > ZXC_BLOCK_SIZE_MAX`. +Returns `ZXC_ERROR_BAD_BLOCK_SIZE` if `src_size > ZXC_BLOCK_SIZE_MAX`, or if +`block_size` is not a power of two in range. ### `zxc_decompress_block` @@ -804,7 +805,8 @@ ZXC_EXPORT int64_t zxc_compress_cctx( ``` Same as `zxc_compress()` but reuses internal buffers from `cctx`. -Automatically re-initializes when `block_size` or `level` changes. A zero +The workspace is re-carved when the block size changes, or when a level raise +into `ZXC_LEVEL_DENSITY` needs the optimal-parser scratch. A zero `src_size` writes the empty archive, as `zxc_compress()` does, without carving the workspace. Dictionary options are honoured as in `zxc_compress()` but are not sticky; the shared literal table is rebuilt only when it changes. A static @@ -816,7 +818,7 @@ Options are **sticky**: `NULL` reuses the last successful call's `level`, `block_size`, `checksum_enabled` and `seekable` (the first three for `zxc_compress_block()`), starting from the creation options. A zero `level` or `block_size` in a non-NULL `opts` means the value the context was created -with, never an earlier call's. A failed call changes nothing. +with, never an earlier call's. A failed call leaves them untouched. ### `zxc_create_dctx` diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index c4ee0b36a..890573d4e 100644 --- a/docs/EXAMPLES.md +++ b/docs/EXAMPLES.md @@ -233,8 +233,8 @@ common case (same block size, different data) is completely allocation-free. Options are **sticky**: `zxc_compress_cctx()` remembers the options of its last successful call, starting from those given to `zxc_create_cctx()`, and reuses them when `opts` is `NULL`. In a non-NULL `opts`, a zero `level` or -`block_size` means the creation value, so an override only needs the fields it -changes. +`block_size` means the creation value, so a level or block-size override needs +only that field; `checksum_enabled` and `seekable` are taken as given (0 = off). ```c #include "zxc.h" diff --git a/include/zxc_buffer.h b/include/zxc_buffer.h index 06298573d..3c772851a 100644 --- a/include/zxc_buffer.h +++ b/include/zxc_buffer.h @@ -383,7 +383,8 @@ ZXC_EXPORT uint64_t zxc_decompress_block_bound(const size_t uncompressed_size); * * @return Compressed block size (> 0), or a negative @ref zxc_error_t. * @ref ZXC_ERROR_BAD_BLOCK_SIZE if @p src_size exceeds - * @ref ZXC_BLOCK_SIZE_MAX; @ref ZXC_ERROR_BAD_LEVEL on a static + * @ref ZXC_BLOCK_SIZE_MAX or @c block_size is not a power of two in + * range; @ref ZXC_ERROR_BAD_LEVEL on a static * context for a level raise its workspace cannot accommodate (levels * above @ref ZXC_LEVEL_ULTRA are otherwise silently clamped); * @ref ZXC_ERROR_DICT_UNSUPPORTED on a static context with a dictionary. @@ -539,8 +540,9 @@ ZXC_EXPORT void zxc_free_cctx(zxc_cctx* cctx); * block_size, checksum_enabled and seekable (the first three for * zxc_compress_block()), starting from zxc_create_cctx(). In a non-NULL * @p opts, a zero level or block_size means the creation value (the library - * default for a NULL zxc_create_cctx()), never an earlier call's; a failed call - * changes nothing. Levels above @ref ZXC_LEVEL_ULTRA are silently clamped. + * default for a NULL zxc_create_cctx()), never an earlier call's. A failed call + * leaves them untouched, workspace re-carving aside. Levels above + * @ref ZXC_LEVEL_ULTRA are silently clamped. * Dictionary options are the exception: honoured as in zxc_compress() but * never remembered, so pass them on every call; the shared table is rebuilt * only when it changes. A static context returns diff --git a/src/lib/zxc_dispatch.c b/src/lib/zxc_dispatch.c index 440cc49af..e2b8c1e48 100644 --- a/src/lib/zxc_dispatch.c +++ b/src/lib/zxc_dispatch.c @@ -1117,9 +1117,10 @@ struct zxc_cctx_s { 1 = caller-supplied static workspace (no-op free, block_size pinned at init) */ size_t last_block_size; /* block size used for last init */ - zxc_cctx_opts_t base; /* creation options; zero fields fall back to it */ + size_t base_block_size; /* creation block_size, used for a zero one */ zxc_cctx_opts_t sticky; /* last successful call; NULL opts reuses it */ int huf_cached; /* inner carries the table below */ + int base_level; /* creation level, used for a zero one */ uint8_t huf_cache[ZXC_HUF_TABLE_SIZE]; /* last table attached */ }; @@ -1158,10 +1159,10 @@ static zxc_cctx_opts_t zxc_cctx_base(const zxc_compress_opts_t* opts) { /** @brief NULL reuses the sticky options; a zero field means the creation value. */ static zxc_cctx_opts_t zxc_cctx_resolve(const zxc_cctx* cctx, const zxc_compress_opts_t* opts) { if (!opts) return cctx->sticky; - return (zxc_cctx_opts_t){.level = ZXC_OPTS_LEVEL(opts, cctx->base.level), + return (zxc_cctx_opts_t){.level = ZXC_OPTS_LEVEL(opts, cctx->base_level), .checksum = opts->checksum_enabled, .seekable = opts->seekable, - .block_size = ZXC_OPTS_BLOCK_SIZE(opts, cctx->base.block_size)}; + .block_size = ZXC_OPTS_BLOCK_SIZE(opts, cctx->base_block_size)}; } zxc_cctx* zxc_create_cctx(const zxc_compress_opts_t* opts) { @@ -1169,7 +1170,8 @@ zxc_cctx* zxc_create_cctx(const zxc_compress_opts_t* opts) { if (UNLIKELY(!cctx)) return NULL; // LCOV_EXCL_LINE const zxc_cctx_opts_t o = zxc_cctx_base(opts); - cctx->base = o; + cctx->base_level = o.level; + cctx->base_block_size = o.block_size; cctx->sticky = o; if (opts) { @@ -1713,10 +1715,10 @@ static int64_t zxc_compress_block_resolved(zxc_cctx* cctx, const void* RESTRICT if (UNLIKELY(cctx->owns_workspace && o.level >= ZXC_LEVEL_DENSITY && !cctx->inner.opt_scratch)) return ZXC_ERROR_BAD_LEVEL; - // Re-init when block_size changed, a level raise needs the optimal-parser - // scratch, or a dictionary arrives on a context carved without its prefix - // (a block_size switch can round [dict | block] back to the same size). - if (UNLIKELY(!cctx->initialized || cctx->last_block_size != effective_block_size || + // Re-init when [dict | block] outgrows the workspace (a larger one encodes + // the same, so a smaller block keeps it), a level raise needs the + // optimal-parser scratch, or a dictionary meets a context carved without it. + if (UNLIKELY(!cctx->initialized || cctx->last_block_size < effective_block_size || (o.level >= ZXC_LEVEL_DENSITY && !cctx->inner.opt_scratch) || (b_dict_size > 0 && !cctx->inner.dict_buffer))) { if (cctx->initialized) { @@ -1783,13 +1785,14 @@ int64_t zxc_compress_block_at(zxc_cctx* cctx, const void* RESTRICT src, const si zxc_cctx_opts_t o = zxc_cctx_resolve(cctx, opts); // No seek table in a block: seekable stays as the frame API set it. o.seekable = cctx->sticky.seekable; - // Grown to hold the source, then checked: nothing invalid gets remembered. - const size_t min_bs = zxc_block_size_ceil(src_size); - if (o.block_size < min_bs) o.block_size = min_bs; if (UNLIKELY(!zxc_validate_block_size(o.block_size))) return ZXC_ERROR_BAD_BLOCK_SIZE; + // Carve for the source if block_size is smaller; remember block_size as asked. + zxc_cctx_opts_t carve = o; + const size_t min_bs = zxc_block_size_ceil(src_size); + if (carve.block_size < min_bs) carve.block_size = min_bs; - const int64_t n = - zxc_compress_block_resolved(cctx, src, src_size, dst, dst_capacity, o, opts, block_index); + const int64_t n = zxc_compress_block_resolved(cctx, src, src_size, dst, dst_capacity, carve, + opts, block_index); if (LIKELY(n > 0)) cctx->sticky = o; return n; } @@ -2021,13 +2024,9 @@ zxc_cctx* zxc_init_static_cctx(void* RESTRICT workspace, const size_t workspace_ // Not clamped: the workspace is sized for the level as given. if (UNLIKELY(opts->level > ZXC_LEVEL_ULTRA)) return NULL; const zxc_cctx_opts_t o = zxc_cctx_base(opts); - const int level = o.level; - const size_t block_size = o.block_size; - const int checksum_enabled = o.checksum; + if (UNLIKELY(!zxc_validate_block_size(o.block_size))) return NULL; - if (UNLIKELY(!zxc_validate_block_size(block_size))) return NULL; - - const size_t inner_sz = zxc_cctx_compute_workspace_size(block_size, 1, level, 0); + const size_t inner_sz = zxc_cctx_compute_workspace_size(o.block_size, 1, o.level, 0); if (UNLIKELY(inner_sz == 0)) return NULL; if (UNLIKELY(workspace_size < ZXC_STATIC_CCTX_HDR_SIZE + inner_sz)) return NULL; @@ -2035,14 +2034,15 @@ zxc_cctx* zxc_init_static_cctx(void* RESTRICT workspace, const size_t workspace_ ZXC_MEMSET(cctx, 0, sizeof(*cctx)); uint8_t* const inner_ws = (uint8_t*)workspace + ZXC_STATIC_CCTX_HDR_SIZE; - if (UNLIKELY(zxc_cctx_init_in_workspace(&cctx->inner, inner_ws, inner_sz, block_size, 1, level, - checksum_enabled, 0, 0) != ZXC_OK)) + if (UNLIKELY(zxc_cctx_init_in_workspace(&cctx->inner, inner_ws, inner_sz, o.block_size, 1, + o.level, o.checksum, 0, 0) != ZXC_OK)) return NULL; cctx->owns_workspace = 1; cctx->initialized = 1; - cctx->last_block_size = block_size; - cctx->base = o; + cctx->last_block_size = o.block_size; + cctx->base_level = o.level; + cctx->base_block_size = o.block_size; cctx->sticky = o; return cctx; } diff --git a/tests/test_context_api.c b/tests/test_context_api.c index 6bc4641c8..d524fc2b8 100644 --- a/tests/test_context_api.c +++ b/tests/test_context_api.c @@ -272,32 +272,49 @@ int test_cctx_zero_opts_defaults(void) { return ok; } -/* #532: zero fields mean the creation values, on both APIs; an invalid - * block_size is never remembered. */ +/* Block size declared by archive @p arc (@p n bytes); 0 if unreadable. */ +static size_t frame_block_size(const uint8_t* arc, const int64_t n) { + zxc_frame_info_t info = {0}; + if (n <= 0 || zxc_get_frame_info(arc, (size_t)n, &info, sizeof(info)) != ZXC_OK) return 0; + return info.block_size; +} + +/* #532: zero fields mean the creation values even after a call with others; + * an invalid block_size is refused whatever the source; the block API + * remembers the block_size asked for, not the carve, and reuses a larger carve. */ int test_cctx_zero_opts_creation_values(void) { printf("=== TEST: Opaque Context API - zero opts fields keep the creation values (#532) ===\n"); - enum { BS = 4096, SRC = 3000 }; + enum { BS = 4096, SRC = 3000, BIG = 600000 }; const size_t cap = (size_t)zxc_compress_bound(SRC), - bcap = (size_t)zxc_compress_block_bound(SRC); + bcap = (size_t)zxc_compress_block_bound(BIG); uint8_t src[SRC]; + uint8_t* const big = malloc(BIG); uint8_t* const arc = malloc(cap); uint8_t* const ref = malloc(cap); uint8_t* const blk = malloc(bcap); const zxc_compress_opts_t create = {.level = ZXC_LEVEL_FASTEST, .block_size = BS}; zxc_cctx* const c = zxc_create_cctx(&create); + zxc_cctx* const fresh = zxc_create_cctx(NULL); int ok = 0; - if (!arc || !ref || !blk || !c) goto done; + if (!big || !arc || !ref || !blk || !c || !fresh) goto done; gen_lz_data(src, sizeof(src)); + gen_lz_data(big, BIG); + + /* Other values first, so creation and last call differ from here on. */ + const zxc_compress_opts_t other = {.level = 3, .block_size = 65536}; + int64_t n = zxc_compress_block(c, src, SRC, blk, bcap, &other); + if (n <= 0) { + printf(" [FAIL] setup call: %lld\n", (long long)n); + goto done; + } - /* Block call with level only: the next NULL frame still declares 4 KiB. */ + /* Block call with level only: the next NULL frame declares 4 KiB, not 64. */ const zxc_compress_opts_t lvl3 = {.level = 3}; - zxc_frame_info_t info = {0}; - int64_t n = zxc_compress_block(c, src, SRC, blk, bcap, &lvl3); + n = zxc_compress_block(c, src, SRC, blk, bcap, &lvl3); if (n > 0) n = zxc_compress_cctx(c, src, SRC, arc, cap, NULL); - if (n <= 0 || zxc_get_frame_info(arc, (size_t)n, &info, sizeof(info)) != ZXC_OK || - info.block_size != BS) { + if (frame_block_size(arc, n) != BS) { printf(" [FAIL] block call then NULL frame: %lld, declares %zu, want %d\n", (long long)n, - info.block_size, (int)BS); + frame_block_size(arc, n), (int)BS); goto done; } @@ -312,21 +329,43 @@ int test_cctx_zero_opts_creation_values(void) { goto done; } - /* Invalid block_size refused; the sticky settings survive it. */ + /* Invalid block_size refused whether the source fits it or not; sticky survives. */ const zxc_compress_opts_t bad = {.block_size = 10000}; - const int64_t e = zxc_compress_block(c, src, SRC, blk, bcap, &bad); + const int64_t e1 = zxc_compress_block(c, src, SRC, blk, bcap, &bad); + const int64_t e2 = zxc_compress_block(c, big, BIG, blk, bcap, &bad); n = zxc_compress_cctx(c, src, SRC, arc, cap, NULL); - if (e != ZXC_ERROR_BAD_BLOCK_SIZE || n != r || memcmp(arc, ref, (size_t)n) != 0) { - printf(" [FAIL] invalid block_size: %lld, NULL call after it %lld\n", (long long)e, - (long long)n); + if (e1 != ZXC_ERROR_BAD_BLOCK_SIZE || e2 != ZXC_ERROR_BAD_BLOCK_SIZE || n != r || + memcmp(arc, ref, (size_t)n) != 0) { + printf(" [FAIL] invalid block_size: %lld / %lld, NULL call after it %lld\n", (long long)e1, + (long long)e2, (long long)n); + goto done; + } + + /* 600 KB block on NULL: carved at 1 MiB, remembered block_size still 4 KiB; + * a small block then encodes in that carve as on a fresh context. */ + n = zxc_compress_block(c, big, BIG, blk, bcap, NULL); + if (n > 0) n = zxc_compress_cctx(c, src, SRC, arc, cap, NULL); + if (frame_block_size(arc, n) != BS) { + printf(" [FAIL] 600 KB block then NULL frame: %lld, declares %zu, want %d\n", (long long)n, + frame_block_size(arc, n), (int)BS); + goto done; + } + uint8_t* const blk2 = ref; /* cap >= a 3000-byte block's bound */ + n = zxc_compress_block(c, src, SRC, blk, bcap, &lvl3); + const int64_t f = zxc_compress_block(fresh, src, SRC, blk2, cap, &lvl3); + if (n <= 0 || n != f || memcmp(blk, blk2, (size_t)n) != 0) { + printf(" [FAIL] block in a 1 MiB carve: %lld, fresh context %lld\n", (long long)n, + (long long)f); goto done; } ok = 1; - printf(" [PASS] creation values kept; invalid block_size refused\n"); + printf(" [PASS] creation values kept; invalid block_size refused; carve kept\n"); printf("PASS\n\n"); done: zxc_free_cctx(c); + zxc_free_cctx(fresh); + free(big); free(arc); free(ref); free(blk); diff --git a/tests/test_static_ctx.c b/tests/test_static_ctx.c index 2dcc0dc49..07ac81063 100644 --- a/tests/test_static_ctx.c +++ b/tests/test_static_ctx.c @@ -245,8 +245,16 @@ int test_static_ctx_block_size_locked(void) { return 0; } - /* Zero level and block_size mean the pinned ones (#532). */ + /* Zero fields mean the pinned values, not the last call's (#532): level 3 + * first, so the two differ. */ uint8_t ref[1024]; + const zxc_compress_opts_t lvl3 = {.level = 3, .block_size = pinned_bs}; + if (zxc_compress_cctx(cctx, src, sizeof(src), dst, sizeof(dst), &lvl3) <= 0) { + printf(" [FAIL] level-3 call on the pinned context\n"); + zxc_free_cctx(cctx); + test_aligned_free(ws); + return 0; + } const zxc_compress_opts_t zero = {0}; const int64_t rc3 = zxc_compress_cctx(cctx, src, sizeof(src), dst, sizeof(dst), &zero); const int64_t rc4 = zxc_compress(src, sizeof(src), ref, sizeof(ref), &opts);