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 ca85f0a3f..585a21661 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 ); ``` @@ -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` @@ -776,7 +777,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. @@ -802,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 @@ -810,9 +814,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 leaves them untouched. ### `zxc_create_dctx` diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index 6ca084761..890573d4e 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 a level or block-size override needs +only that field; `checksum_enabled` and `seekable` are taken as given (0 = off). ```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 4e1a4e038..3c772851a 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 @@ -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. @@ -535,10 +536,13 @@ 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(). 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 + * 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 @@ -709,10 +713,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 + * 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. * diff --git a/src/lib/zxc_dispatch.c b/src/lib/zxc_dispatch.c index f3a2b1190..e2b8c1e48 100644 --- a/src/lib/zxc_dispatch.c +++ b/src/lib/zxc_dispatch.c @@ -1095,6 +1095,14 @@ int zxc_get_last_frame_info(const void* src, const size_t src_size, zxc_frame_in // --- Compression --------------------------------------------------------- +/** @brief Compression options with every default applied. */ +typedef struct { + int level; + int checksum; + int seekable; + size_t block_size; +} zxc_cctx_opts_t; + /** * @brief Opaque reusable compression context (public handle @ref zxc_cctx). * @@ -1103,18 +1111,16 @@ int zxc_get_last_frame_info(const void* src, const size_t src_size, zxc_frame_in * changes. */ 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 */ + 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 */ }; @@ -1142,26 +1148,42 @@ static int zxc_ctx_sync_dict_huf(zxc_cctx_t* RESTRICT inner, uint8_t* RESTRICT c return ZXC_OK; } +/** @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; + 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)}; +} + 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; + const zxc_cctx_opts_t o = zxc_cctx_base(opts); + cctx->base_level = o.level; + cctx->base_block_size = o.block_size; + 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,68 +1207,56 @@ void zxc_free_cctx(zxc_cctx* cctx) { } /** - * @brief Compresses data using a reusable context. + * @brief Frame compression with resolved options @p o; @p opts supplies the + * dictionary only. * - * 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_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; - 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; + 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; - // 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); + 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; @@ -1254,8 +1264,8 @@ int64_t zxc_compress_cctx(zxc_cctx* cctx, const void* RESTRICT src, const size_t 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; @@ -1273,8 +1283,8 @@ int64_t zxc_compress_cctx(zxc_cctx* cctx, const void* RESTRICT src, const size_t 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; @@ -1282,7 +1292,7 @@ int64_t zxc_compress_cctx(zxc_cctx* cctx, const void* RESTRICT src, const size_t 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; @@ -1295,7 +1305,7 @@ int64_t zxc_compress_cctx(zxc_cctx* cctx, const void* RESTRICT src, const size_t } 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; @@ -1311,21 +1321,39 @@ int64_t zxc_compress_cctx(zxc_cctx* cctx, const void* RESTRICT src, const size_t 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; 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, o, opts); + if (LIKELY(n > 0)) cctx->sticky = o; + return n; +} + // --- Decompression ------------------------------------------------------- /** @@ -1654,48 +1682,27 @@ 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 with resolved options @p o; @p opts supplies the + * dictionary only. * - * 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; - - 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. +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_cctx_opts_t o, + const zxc_compress_opts_t* opts, + 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 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; + 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 @@ -1705,18 +1712,14 @@ int64_t zxc_compress_block_at(zxc_cctx* cctx, const void* RESTRICT src, const si 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; - 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). - if (UNLIKELY(!cctx->initialized || cctx->last_block_size != effective_block_size || - (level >= ZXC_LEVEL_DENSITY && !cctx->inner.opt_scratch) || + // 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) { // LCOV_EXCL_START @@ -1725,7 +1728,7 @@ int64_t zxc_compress_block_at(zxc_cctx* cctx, const void* RESTRICT src, const si // 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 @@ -1733,8 +1736,8 @@ int64_t zxc_compress_block_at(zxc_cctx* cctx, const void* RESTRICT src, const si 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; @@ -1758,6 +1761,42 @@ 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); + // No seek table in a block: seekable stays as the frame API set it. + o.seekable = cctx->sticky.seekable; + 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, carve, + opts, 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 @@ -1982,14 +2021,12 @@ 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); + if (UNLIKELY(!zxc_validate_block_size(o.block_size))) return NULL; - 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); + 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; @@ -1997,17 +2034,16 @@ 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->stored_level = level; - cctx->stored_block_size = block_size; - cctx->stored_checksum = checksum_enabled; - cctx->stored_seekable = opts->seekable; + 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_common.h b/tests/test_common.h index 1526df008..6e2679041 100644 --- a/tests/test_common.h +++ b/tests/test_common.h @@ -141,6 +141,8 @@ 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_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 b2e39385a..d524fc2b8 100644 --- a/tests/test_context_api.c +++ b/tests/test_context_api.c @@ -187,6 +187,191 @@ 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, .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); + 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; +} + +/* 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, BIG = 600000 }; + const size_t cap = (size_t)zxc_compress_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 (!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 declares 4 KiB, not 64. */ + const zxc_compress_opts_t lvl3 = {.level = 3}; + n = zxc_compress_block(c, src, SRC, blk, bcap, &lvl3); + if (n > 0) n = zxc_compress_cctx(c, src, SRC, arc, cap, NULL); + if (frame_block_size(arc, n) != BS) { + printf(" [FAIL] block call then NULL frame: %lld, declares %zu, want %d\n", (long long)n, + frame_block_size(arc, n), (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 whether the source fits it or not; sticky survives. */ + const zxc_compress_opts_t bad = {.block_size = 10000}; + 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 (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; carve kept\n"); + printf("PASS\n\n"); + +done: + zxc_free_cctx(c); + zxc_free_cctx(fresh); + free(big); + 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 3372f096b..226d4bad4 100644 --- a/tests/test_main.c +++ b/tests/test_main.c @@ -75,6 +75,8 @@ 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_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 5257f428e..07ac81063 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,6 +245,27 @@ int test_static_ctx_block_size_locked(void) { return 0; } + /* 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); + if (rc3 <= 0 || rc3 != rc4 || memcmp(dst, ref, (size_t)rc3) != 0) { + 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; + } + zxc_free_cctx(cctx); test_aligned_free(ws); printf(" [PASS] mismatched block_size rejected; matching one accepted\n");