Coverage Report

Created: 2026-08-14 06:27

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/zxc/src/lib/zxc_dispatch.c
Line
Count
Source
1
/*
2
 * ZXC - High-performance lossless compression
3
 *
4
 * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors.
5
 * SPDX-License-Identifier: BSD-3-Clause
6
 */
7
8
/**
9
 * @file zxc_dispatch.c
10
 * @brief Runtime CPU feature detection and SIMD dispatch layer.
11
 *
12
 * Detects AVX2/AVX512 (x86-64) and NEON (32-bit ARM) at runtime and routes
13
 * compress/decompress calls to the best available implementation via
14
 * lazy-initialised function pointers. SSE2 on x86-64 and NEON on AArch64 are
15
 * baseline ISA guarantees, so the _default variant already covers those tiers.
16
 * Also contains the public one-shot buffer API (@ref zxc_compress,
17
 * @ref zxc_decompress, @ref zxc_get_decompressed_size).
18
 */
19
20
#include "../../include/zxc_dict.h"
21
#include "../../include/zxc_error.h"
22
#include "../../include/zxc_seekable.h"
23
#include "zxc_internal.h"
24
25
/*
26
 * ZXC_DISABLE_SIMD => force ZXC_ONLY_DEFAULT so the dispatcher never selects
27
 * an AVX2/AVX512/NEON variant.
28
 */
29
#if defined(ZXC_DISABLE_SIMD) && !defined(ZXC_ONLY_DEFAULT)
30
#define ZXC_ONLY_DEFAULT
31
#endif
32
33
#if defined(_MSC_VER)
34
#include <intrin.h>
35
#if defined(_M_X64)
36
#include <immintrin.h>  // _xgetbv (x86-specific header; x64 AVX state check)
37
#endif
38
#endif
39
40
#if (defined(__x86_64__) || defined(_M_X64)) && !defined(_MSC_VER) && !defined(ZXC_ONLY_DEFAULT)
41
#include <cpuid.h>  // __cpuid_count: CPUID probes in zxc_detect_cpu_features
42
#endif
43
44
#if defined(__linux__) && (defined(__arm__) || defined(_M_ARM))
45
#include <asm/hwcap.h>
46
#include <sys/auxv.h>
47
#endif
48
49
/*
50
 * ============================================================================
51
 * PROTOTYPES FOR MULTI-VERSIONED VARIANTS
52
 * ============================================================================
53
 * These are compiled in separate translation units with different flags.
54
 */
55
56
// Decompression Prototypes
57
int zxc_decompress_chunk_wrapper_default(const zxc_cctx_t* RESTRICT ctx,
58
                                         const uint8_t* RESTRICT src, const size_t src_sz,
59
                                         uint8_t* RESTRICT dst, const size_t dst_cap);
60
int zxc_decompress_chunk_wrapper_dict_default(const zxc_cctx_t* RESTRICT ctx,
61
                                              const uint8_t* RESTRICT src, const size_t src_sz,
62
                                              uint8_t* RESTRICT dst, const size_t dst_cap);
63
int zxc_decompress_chunk_wrapper_safe_default(const zxc_cctx_t* RESTRICT ctx,
64
                                              const uint8_t* RESTRICT src, const size_t src_sz,
65
                                              uint8_t* RESTRICT dst, const size_t dst_cap);
66
67
#ifndef ZXC_ONLY_DEFAULT
68
#if defined(__x86_64__) || defined(_M_X64)
69
int zxc_decompress_chunk_wrapper_avx2(const zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src,
70
                                      const size_t src_sz, uint8_t* RESTRICT dst,
71
                                      const size_t dst_cap);
72
int zxc_decompress_chunk_wrapper_dict_avx2(const zxc_cctx_t* RESTRICT ctx,
73
                                           const uint8_t* RESTRICT src, const size_t src_sz,
74
                                           uint8_t* RESTRICT dst, const size_t dst_cap);
75
int zxc_decompress_chunk_wrapper_avx512(const zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src,
76
                                        const size_t src_sz, uint8_t* RESTRICT dst,
77
                                        const size_t dst_cap);
78
int zxc_decompress_chunk_wrapper_dict_avx512(const zxc_cctx_t* RESTRICT ctx,
79
                                             const uint8_t* RESTRICT src, const size_t src_sz,
80
                                             uint8_t* RESTRICT dst, const size_t dst_cap);
81
int zxc_decompress_chunk_wrapper_safe_avx2(const zxc_cctx_t* RESTRICT ctx,
82
                                           const uint8_t* RESTRICT src, const size_t src_sz,
83
                                           uint8_t* RESTRICT dst, const size_t dst_cap);
84
int zxc_decompress_chunk_wrapper_safe_avx512(const zxc_cctx_t* RESTRICT ctx,
85
                                             const uint8_t* RESTRICT src, const size_t src_sz,
86
                                             uint8_t* RESTRICT dst, const size_t dst_cap);
87
#elif defined(__arm__) || defined(_M_ARM)
88
int zxc_decompress_chunk_wrapper_neon32(const zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src,
89
                                        const size_t src_sz, uint8_t* RESTRICT dst,
90
                                        const size_t dst_cap);
91
int zxc_decompress_chunk_wrapper_dict_neon32(const zxc_cctx_t* RESTRICT ctx,
92
                                             const uint8_t* RESTRICT src, const size_t src_sz,
93
                                             uint8_t* RESTRICT dst, const size_t dst_cap);
94
int zxc_decompress_chunk_wrapper_safe_neon32(const zxc_cctx_t* RESTRICT ctx,
95
                                             const uint8_t* RESTRICT src, const size_t src_sz,
96
                                             uint8_t* RESTRICT dst, const size_t dst_cap);
97
#endif
98
#endif
99
100
// Compression Prototypes
101
int zxc_compress_chunk_wrapper_default(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src,
102
                                       const size_t src_sz, uint8_t* RESTRICT dst,
103
                                       const size_t dst_cap);
104
105
// Huffman prototypes (variant TUs of zxc_huffman.c). Compressor and decompressor
106
// variants bind to the matching suffixed symbol at compile time, so the hot path
107
// pays no dispatch; the wrappers below expose un-suffixed names to callers.
108
int zxc_huf_build_code_lengths_default(const uint32_t* RESTRICT freq, uint8_t* RESTRICT code_len,
109
                                       void* RESTRICT scratch, int max_code_len);
110
size_t zxc_huf_calc_size_default(const uint32_t* RESTRICT freq, const uint8_t* RESTRICT code_len,
111
                                 int with_header);
112
int zxc_huf_encode_section_default(const uint8_t* RESTRICT literals, size_t n_literals,
113
                                   const uint32_t* RESTRICT freq, const uint8_t* RESTRICT code_len,
114
                                   uint8_t* RESTRICT dst, size_t dst_cap);
115
int zxc_huf_decode_section_default(const uint8_t* RESTRICT payload, size_t payload_size,
116
                                   uint8_t* RESTRICT dst, size_t n, uint8_t* RESTRICT scratch);
117
int zxc_huf_encode_section_dict_default(const uint8_t* RESTRICT literals, size_t n_literals,
118
                                        const uint32_t* RESTRICT freq,
119
                                        const uint8_t* RESTRICT code_len,
120
                                        const zxc_pivco_tree_t* RESTRICT tree,
121
                                        const uint32_t* RESTRICT codes, uint8_t* RESTRICT dst,
122
                                        size_t dst_cap);
123
int zxc_huf_decode_section_dict_default(const uint8_t* RESTRICT payload, size_t payload_size,
124
                                        uint8_t* RESTRICT dst, size_t n,
125
                                        const zxc_pivco_tree_t* RESTRICT tree,
126
                                        const zxc_pivco_decode_aux_t* RESTRICT aux,
127
                                        uint8_t* RESTRICT scratch);
128
size_t zxc_huf_calc_size_dict_default(const uint32_t* RESTRICT freq,
129
                                      const uint8_t* RESTRICT code_len,
130
                                      const zxc_pivco_tree_t* RESTRICT tree);
131
void zxc_huf_pack_lengths_default(const uint8_t* RESTRICT code_len, uint8_t* RESTRICT out);
132
int zxc_huf_unpack_lengths_default(const uint8_t* RESTRICT in, uint8_t* RESTRICT code_len);
133
134
#if defined(__x86_64__) || defined(_M_X64)
135
int zxc_compress_chunk_wrapper_avx2(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src,
136
                                    const size_t src_sz, uint8_t* RESTRICT dst,
137
                                    const size_t dst_cap);
138
int zxc_compress_chunk_wrapper_avx512(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src,
139
                                      const size_t src_sz, uint8_t* RESTRICT dst,
140
                                      const size_t dst_cap);
141
#elif defined(__arm__) || defined(_M_ARM)
142
int zxc_compress_chunk_wrapper_neon32(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src,
143
                                      const size_t src_sz, uint8_t* RESTRICT dst,
144
                                      const size_t dst_cap);
145
#endif
146
147
/*
148
 * ============================================================================
149
 * CPU DETECTION LOGIC
150
 * ============================================================================
151
 */
152
153
#if (defined(__x86_64__) || defined(_M_X64)) && !defined(ZXC_ONLY_DEFAULT)
154
155
/** @brief Reads CPUID leaf @p leaf, subleaf @p sub into @p regs (EAX,EBX,ECX,EDX). */
156
static inline void zxc_cpuid(const uint32_t leaf, const uint32_t sub, uint32_t regs[4]) {
157
#if defined(_MSC_VER)
158
    int r[4];
159
    __cpuidex(r, (int)leaf, (int)sub);
160
    regs[0] = (uint32_t)r[0];
161
    regs[1] = (uint32_t)r[1];
162
    regs[2] = (uint32_t)r[2];
163
    regs[3] = (uint32_t)r[3];
164
#else
165
    __cpuid_count(leaf, sub, regs[0], regs[1], regs[2], regs[3]);
166
#endif
167
}
168
169
/** @brief Reads XCR0 (@c XGETBV with ECX=0). Callers must check OSXSAVE first. */
170
static inline uint64_t zxc_xgetbv0(void) {
171
#if defined(_MSC_VER)
172
    return _xgetbv(0);
173
#else
174
    /* Raw encoding: the xgetbv intrinsic needs -mxsave, which the baseline
175
     * translation unit is not compiled with. */
176
    uint32_t lo, hi;
177
    __asm__ volatile(".byte 0x0f, 0x01, 0xd0" : "=a"(lo), "=d"(hi) : "c"(0));
178
    return ((uint64_t)hi << 32) | lo;
179
#endif
180
}
181
#endif /* x86-64 && !ZXC_ONLY_DEFAULT */
182
183
/**
184
 * @enum zxc_cpu_feature_t
185
 * @brief Detected CPU SIMD capability level.
186
 */
187
typedef enum {
188
    ZXC_CPU_GENERIC = 0, /**< @brief Scalar-only fallback.   */
189
    ZXC_CPU_AVX2 = 1,    /**< @brief x86-64 AVX2 available.  */
190
    ZXC_CPU_AVX512 = 2,  /**< @brief x86-64 AVX-512F+BW available. */
191
    ZXC_CPU_NEON = 3,    /**< @brief ARM NEON available (dedicated variant on 32-bit ARM only;
192
                          *          AArch64 baseline, served by _default there). */
193
    ZXC_CPU_SSE2 = 4     /**< @brief x86 SSE2 available (no AVX2); x86-64 baseline,
194
                          *          served by _default (no dedicated variant). */
195
} zxc_cpu_feature_t;
196
197
/**
198
 * @brief Probes the running CPU for SIMD support.
199
 *
200
 * Uses CPUID on x86-64 (MSVC and GCC/Clang paths), `getauxval` on
201
 * 32-bit ARM Linux, and compile-time constants on AArch64.
202
 *
203
 * @return The highest @ref zxc_cpu_feature_t level supported.
204
 */
205
// LCOV_EXCL_START
206
9
static zxc_cpu_feature_t zxc_detect_cpu_features(void) {
207
9
#ifdef ZXC_ONLY_DEFAULT
208
9
    return ZXC_CPU_GENERIC;
209
#else
210
    zxc_cpu_feature_t features = ZXC_CPU_GENERIC;
211
212
#if defined(__x86_64__) || defined(_M_X64)
213
    // AVX2/AVX512 need OS-enabled YMM/ZMM state: gate on OSXSAVE + XGETBV/XCR0,
214
    // not CPUID alone (else a VEX/EVEX op faults #UD when the OS hasn't enabled it).
215
    uint32_t regs[4];
216
    int sse2 = 0;
217
    int avx2 = 0;
218
    int avx512 = 0;
219
    int bmi_lzcnt = 0;
220
221
    zxc_cpuid(0, 0, regs);
222
    const uint32_t max_leaf = regs[0];
223
224
    zxc_cpuid(1, 0, regs);
225
    if (regs[3] & (1U << 26)) sse2 = 1;             // CPUID.1:EDX[26]
226
    if ((regs[2] & (1U << 27)) && max_leaf >= 7) {  // OSXSAVE, and leaf 7 is real
227
        const uint64_t xcr0 = zxc_xgetbv0();
228
        if ((xcr0 & 0x6) == 0x6) {  // SSE+YMM enabled
229
            zxc_cpuid(7, 0, regs);
230
            const int bmi1 = (regs[1] >> 3) & 1;  // CPUID.7.0:EBX[3]
231
            const int bmi2 = (regs[1] >> 8) & 1;  // CPUID.7.0:EBX[8]
232
            if (regs[1] & (1U << 5)) avx2 = 1;
233
            // AVX512 also needs XCR0[5..7] (opmask/ZMM)
234
            if ((regs[1] & (1U << 16)) && (regs[1] & (1U << 30)) && (regs[2] & (1U << 6)) &&
235
                (xcr0 & 0xE0) == 0xE0)
236
                avx512 = 1; /* AVX512 tier = F+BW+VBMI2 (variant built with -mavx512vbmi2) */
237
            // The AVX2/AVX512 variants are compiled with BMI1/BMI2/LZCNT enabled,
238
            // so both gates must prove those bits too. LZCNT (ABM) lives in
239
            // CPUID.80000001H:ECX[5]; that leaf is architectural on x86-64.
240
            zxc_cpuid(0x80000001U, 0, regs);
241
            bmi_lzcnt = bmi1 && bmi2 && ((regs[2] >> 5) & 1);
242
        }
243
    }
244
245
    if (avx512 && bmi_lzcnt) {
246
        features = ZXC_CPU_AVX512;
247
    } else if (avx2 && bmi_lzcnt) {
248
        features = ZXC_CPU_AVX2;
249
    } else if (sse2) {
250
        features = ZXC_CPU_SSE2;
251
    }
252
253
#elif defined(__aarch64__) || defined(_M_ARM64)
254
    // ARM64 usually guarantees NEON
255
    features = ZXC_CPU_NEON;
256
257
#elif defined(__arm__) || defined(_M_ARM)
258
    // ARM32 Runtime detection for Linux
259
#if defined(__linux__)
260
    const unsigned long hwcaps = getauxval(AT_HWCAP);
261
    if (hwcaps & HWCAP_NEON) {
262
        features = ZXC_CPU_NEON;
263
    }
264
#else
265
// Fallback for non-Linux: rely on compiler flags.
266
// If compiled with -mfpu=neon, we assume target supports it.
267
// Otherwise, safe default is GENERIC.
268
#if defined(__ARM_NEON)
269
    features = ZXC_CPU_NEON;
270
#endif
271
#endif
272
#endif
273
274
    return features;
275
#endif
276
9
}
277
// LCOV_EXCL_STOP
278
279
/*
280
 * ============================================================================
281
 * DISPATCHERS
282
 * ============================================================================
283
 * We use a function pointer initialized on first use (lazy initialization).
284
 */
285
286
/** @brief Function pointer type for the chunk decompressor. */
287
typedef int (*zxc_decompress_func_t)(const zxc_cctx_t* RESTRICT, const uint8_t* RESTRICT,
288
                                     const size_t, uint8_t* RESTRICT, const size_t);
289
/** @brief Function pointer type for the chunk compressor. */
290
typedef int (*zxc_compress_func_t)(zxc_cctx_t* RESTRICT, const uint8_t* RESTRICT, const size_t,
291
                                   uint8_t* RESTRICT, const size_t);
292
293
/** @brief Lazily-resolved pointer to the best decompression variant. */
294
static ZXC_ATOMIC zxc_decompress_func_t zxc_decompress_ptr = (zxc_decompress_func_t)0;
295
/** @brief Lazily-resolved pointer to the best dict-decompression variant. */
296
static ZXC_ATOMIC zxc_decompress_func_t zxc_decompress_dict_ptr = (zxc_decompress_func_t)0;
297
/** @brief Lazily-resolved pointer to the best safe-decompression variant. */
298
static ZXC_ATOMIC zxc_decompress_func_t zxc_decompress_safe_ptr = (zxc_decompress_func_t)0;
299
/** @brief Lazily-resolved pointer to the best compression variant. */
300
static ZXC_ATOMIC zxc_compress_func_t zxc_compress_ptr = (zxc_compress_func_t)0;
301
302
/**
303
 * @brief First-call initialiser for the decompression dispatcher.
304
 *
305
 * Detects CPU features, selects the best implementation, stores the
306
 * pointer atomically, then tail-calls into it.
307
 *
308
 * @param[in]  ctx      Decompression context (its @c dict_size picks the dict variant).
309
 * @param[in]  src      Compressed input chunk.
310
 * @param[in]  src_sz   Size of @p src in bytes.
311
 * @param[out] dst      Destination buffer for decompressed data.
312
 * @param[in]  dst_cap  Capacity of @p dst in bytes.
313
 * @return Result of the selected variant: decompressed size, or negative
314
 *         @ref zxc_error_t.
315
 */
316
// LCOV_EXCL_START
317
static int zxc_decompress_dispatch_init(const zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src,
318
                                        const size_t src_sz, uint8_t* RESTRICT dst,
319
5
                                        const size_t dst_cap) {
320
5
    const zxc_cpu_feature_t cpu = zxc_detect_cpu_features();
321
5
    zxc_decompress_func_t zxc_decompress_ptr_local = NULL;
322
5
    zxc_decompress_func_t zxc_decompress_dict_ptr_local = NULL;
323
324
#ifndef ZXC_ONLY_DEFAULT
325
#if defined(__x86_64__) || defined(_M_X64)
326
    if (cpu == ZXC_CPU_AVX512) {
327
        zxc_decompress_ptr_local = zxc_decompress_chunk_wrapper_avx512;
328
        zxc_decompress_dict_ptr_local = zxc_decompress_chunk_wrapper_dict_avx512;
329
    } else if (cpu == ZXC_CPU_AVX2) {
330
        zxc_decompress_ptr_local = zxc_decompress_chunk_wrapper_avx2;
331
        zxc_decompress_dict_ptr_local = zxc_decompress_chunk_wrapper_dict_avx2;
332
    } else {
333
        zxc_decompress_ptr_local = zxc_decompress_chunk_wrapper_default;
334
        zxc_decompress_dict_ptr_local = zxc_decompress_chunk_wrapper_dict_default;
335
    }
336
#elif defined(__arm__) || defined(_M_ARM)
337
    // 32-bit ARM: the only arch with a real runtime NEON probe (getauxval).
338
    // cppcheck-suppress knownConditionTrueFalse
339
    if (cpu == ZXC_CPU_NEON) {
340
        zxc_decompress_ptr_local = zxc_decompress_chunk_wrapper_neon32;
341
        zxc_decompress_dict_ptr_local = zxc_decompress_chunk_wrapper_dict_neon32;
342
    } else {
343
        zxc_decompress_ptr_local = zxc_decompress_chunk_wrapper_default;
344
        zxc_decompress_dict_ptr_local = zxc_decompress_chunk_wrapper_dict_default;
345
    }
346
#else
347
    (void)cpu;
348
    zxc_decompress_ptr_local = zxc_decompress_chunk_wrapper_default;
349
    zxc_decompress_dict_ptr_local = zxc_decompress_chunk_wrapper_dict_default;
350
#endif
351
#else
352
5
    (void)cpu;
353
5
    zxc_decompress_ptr_local = zxc_decompress_chunk_wrapper_default;
354
5
    zxc_decompress_dict_ptr_local = zxc_decompress_chunk_wrapper_dict_default;
355
5
#endif
356
357
5
#if ZXC_USE_C11_ATOMICS
358
5
    atomic_store_explicit(&zxc_decompress_ptr, zxc_decompress_ptr_local, memory_order_release);
359
5
    atomic_store_explicit(&zxc_decompress_dict_ptr, zxc_decompress_dict_ptr_local,
360
5
                          memory_order_release);
361
#else
362
    zxc_decompress_ptr = zxc_decompress_ptr_local;
363
    zxc_decompress_dict_ptr = zxc_decompress_dict_ptr_local;
364
#endif
365
5
    return (ctx->dict_size ? zxc_decompress_dict_ptr_local : zxc_decompress_ptr_local)(
366
5
        ctx, src, src_sz, dst, dst_cap);
367
5
}
368
// LCOV_EXCL_STOP
369
370
/**
371
 * @brief First-call initialiser for the safe-decompression dispatcher.
372
 *
373
 * Mirrors @ref zxc_decompress_dispatch_init but selects the `_safe_*`
374
 * decoder variants used by @ref zxc_decompress_block_safe.
375
 *
376
 * @param[in]  ctx      Decompression context.
377
 * @param[in]  src      Compressed input chunk.
378
 * @param[in]  src_sz   Size of @p src in bytes.
379
 * @param[out] dst      Destination buffer (strict: exact uncompressed size).
380
 * @param[in]  dst_cap  Capacity of @p dst in bytes.
381
 * @return Result of the selected variant: decompressed size, or negative
382
 *         @ref zxc_error_t.
383
 */
384
// LCOV_EXCL_START
385
static int zxc_decompress_safe_dispatch_init(const zxc_cctx_t* RESTRICT ctx,
386
                                             const uint8_t* RESTRICT src, const size_t src_sz,
387
0
                                             uint8_t* RESTRICT dst, const size_t dst_cap) {
388
0
    const zxc_cpu_feature_t cpu = zxc_detect_cpu_features();
389
0
    zxc_decompress_func_t zxc_decompress_safe_ptr_local = NULL;
390
391
#ifndef ZXC_ONLY_DEFAULT
392
#if defined(__x86_64__) || defined(_M_X64)
393
    if (cpu == ZXC_CPU_AVX512)
394
        zxc_decompress_safe_ptr_local = zxc_decompress_chunk_wrapper_safe_avx512;
395
    else if (cpu == ZXC_CPU_AVX2)
396
        zxc_decompress_safe_ptr_local = zxc_decompress_chunk_wrapper_safe_avx2;
397
    else
398
        zxc_decompress_safe_ptr_local = zxc_decompress_chunk_wrapper_safe_default;
399
#elif defined(__arm__) || defined(_M_ARM)
400
    // cppcheck-suppress knownConditionTrueFalse
401
    if (cpu == ZXC_CPU_NEON)
402
        zxc_decompress_safe_ptr_local = zxc_decompress_chunk_wrapper_safe_neon32;
403
    else
404
        zxc_decompress_safe_ptr_local = zxc_decompress_chunk_wrapper_safe_default;
405
#else
406
    (void)cpu;
407
    zxc_decompress_safe_ptr_local = zxc_decompress_chunk_wrapper_safe_default;
408
#endif
409
#else
410
0
    (void)cpu;
411
0
    zxc_decompress_safe_ptr_local = zxc_decompress_chunk_wrapper_safe_default;
412
0
#endif
413
414
0
#if ZXC_USE_C11_ATOMICS
415
0
    atomic_store_explicit(&zxc_decompress_safe_ptr, zxc_decompress_safe_ptr_local,
416
0
                          memory_order_release);
417
#else
418
    zxc_decompress_safe_ptr = zxc_decompress_safe_ptr_local;
419
#endif
420
0
    return zxc_decompress_safe_ptr_local(ctx, src, src_sz, dst, dst_cap);
421
0
}
422
// LCOV_EXCL_STOP
423
424
/**
425
 * @brief First-call initialiser for the compression dispatcher.
426
 *
427
 * Detects CPU features, selects the best implementation, stores the
428
 * pointer atomically, then tail-calls into it.
429
 *
430
 * @param[in,out] ctx      Compression context.
431
 * @param[in]     src      Uncompressed input chunk.
432
 * @param[in]     src_sz   Size of @p src in bytes.
433
 * @param[out]    dst      Destination buffer for the compressed chunk.
434
 * @param[in]     dst_cap  Capacity of @p dst in bytes.
435
 * @return Result of the selected variant: compressed size, or negative
436
 *         @ref zxc_error_t.
437
 */
438
// LCOV_EXCL_START
439
static int zxc_compress_dispatch_init(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src,
440
                                      const size_t src_sz, uint8_t* RESTRICT dst,
441
4
                                      const size_t dst_cap) {
442
4
    const zxc_cpu_feature_t cpu = zxc_detect_cpu_features();
443
4
    zxc_compress_func_t zxc_compress_ptr_local = NULL;
444
445
#ifndef ZXC_ONLY_DEFAULT
446
#if defined(__x86_64__) || defined(_M_X64)
447
    if (cpu == ZXC_CPU_AVX512)
448
        zxc_compress_ptr_local = zxc_compress_chunk_wrapper_avx512;
449
    else if (cpu == ZXC_CPU_AVX2)
450
        zxc_compress_ptr_local = zxc_compress_chunk_wrapper_avx2;
451
    else
452
        zxc_compress_ptr_local = zxc_compress_chunk_wrapper_default;
453
#elif defined(__arm__) || defined(_M_ARM)
454
    // cppcheck-suppress knownConditionTrueFalse
455
    if (cpu == ZXC_CPU_NEON)
456
        zxc_compress_ptr_local = zxc_compress_chunk_wrapper_neon32;
457
    else
458
        zxc_compress_ptr_local = zxc_compress_chunk_wrapper_default;
459
#else
460
    (void)cpu;
461
    zxc_compress_ptr_local = zxc_compress_chunk_wrapper_default;
462
#endif
463
#else
464
4
    (void)cpu;
465
4
    zxc_compress_ptr_local = zxc_compress_chunk_wrapper_default;
466
4
#endif
467
468
4
#if ZXC_USE_C11_ATOMICS
469
4
    atomic_store_explicit(&zxc_compress_ptr, zxc_compress_ptr_local, memory_order_release);
470
#else
471
    zxc_compress_ptr = zxc_compress_ptr_local;
472
#endif
473
4
    return zxc_compress_ptr_local(ctx, src, src_sz, dst, dst_cap);
474
4
}
475
// LCOV_EXCL_STOP
476
477
/**
478
 * @brief Public decompression dispatcher (calls lazily-resolved implementation).
479
 *
480
 * @param[in,out] ctx    Decompression context.
481
 * @param[in]     src    Compressed input chunk (header + payload + optional checksum).
482
 * @param[in]     src_sz Size of @p src in bytes.
483
 * @param[out]    dst    Destination buffer for decompressed data.
484
 * @param[in]     dst_cap Capacity of @p dst.
485
 * @return Decompressed size in bytes, or a negative @ref zxc_error_t code.
486
 */
487
int zxc_decompress_chunk_wrapper(const zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src,
488
97.2k
                                 const size_t src_sz, uint8_t* RESTRICT dst, const size_t dst_cap) {
489
    /* dict_size is constant for a stream; this per-block branch (outside the decode
490
     * loop) routes to the dict variant only when a dictionary is active, so the
491
     * no-dict path runs the dict-free chunk wrapper (identical codegen to main). */
492
97.2k
#if ZXC_USE_C11_ATOMICS
493
97.2k
    const zxc_decompress_func_t func = atomic_load_explicit(
494
97.2k
        ctx->dict_size ? &zxc_decompress_dict_ptr : &zxc_decompress_ptr, memory_order_acquire);
495
#else
496
    const zxc_decompress_func_t func =
497
        ctx->dict_size ? zxc_decompress_dict_ptr : zxc_decompress_ptr;
498
#endif
499
97.2k
    if (UNLIKELY(!func)) return zxc_decompress_dispatch_init(ctx, src, src_sz, dst, dst_cap);
500
97.2k
    return func(ctx, src, src_sz, dst, dst_cap);
501
97.2k
}
502
503
/**
504
 * @brief Internal safe-decompression dispatcher (strict dst_capacity == uncompressed_size).
505
 *
506
 * Calls the lazily-resolved `_safe_*` variant, running first-call init if needed.
507
 *
508
 * @param[in]  ctx      Decompression context.
509
 * @param[in]  src      Compressed input chunk.
510
 * @param[in]  src_sz   Size of @p src in bytes.
511
 * @param[out] dst      Destination buffer (capacity == exact uncompressed size).
512
 * @param[in]  dst_cap  Capacity of @p dst in bytes.
513
 * @return Decompressed size in bytes, or a negative @ref zxc_error_t.
514
 */
515
static int zxc_decompress_chunk_wrapper_safe_public(const zxc_cctx_t* RESTRICT ctx,
516
                                                    const uint8_t* RESTRICT src,
517
                                                    const size_t src_sz, uint8_t* RESTRICT dst,
518
0
                                                    const size_t dst_cap) {
519
0
#if ZXC_USE_C11_ATOMICS
520
0
    const zxc_decompress_func_t func =
521
0
        atomic_load_explicit(&zxc_decompress_safe_ptr, memory_order_acquire);
522
#else
523
    const zxc_decompress_func_t func = zxc_decompress_safe_ptr;
524
#endif
525
0
    if (UNLIKELY(!func)) return zxc_decompress_safe_dispatch_init(ctx, src, src_sz, dst, dst_cap);
526
0
    return func(ctx, src, src_sz, dst, dst_cap);
527
0
}
528
529
/**
530
 * @brief Public compression dispatcher (calls lazily-resolved implementation).
531
 *
532
 * @param[in,out] ctx    Compression context.
533
 * @param[in]     src    Uncompressed input chunk.
534
 * @param[in]     src_sz Size of @p src in bytes.
535
 * @param[out]    dst    Destination buffer for compressed data.
536
 * @param[in]     dst_cap Capacity of @p dst.
537
 * @return Compressed size in bytes, or a negative @ref zxc_error_t code.
538
 */
539
int zxc_compress_chunk_wrapper(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src,
540
127k
                               const size_t src_sz, uint8_t* RESTRICT dst, const size_t dst_cap) {
541
127k
#if ZXC_USE_C11_ATOMICS
542
127k
    const zxc_compress_func_t func = atomic_load_explicit(&zxc_compress_ptr, memory_order_acquire);
543
#else
544
    const zxc_compress_func_t func = zxc_compress_ptr;
545
#endif
546
127k
    if (UNLIKELY(!func)) return zxc_compress_dispatch_init(ctx, src, src_sz, dst, dst_cap);
547
127k
    return func(ctx, src, src_sz, dst, dst_cap);
548
127k
}
549
550
/*
551
 * ============================================================================
552
 * HUFFMAN TRAMPOLINES
553
 * ============================================================================
554
 * The Huffman codec is built per-variant (default / avx2 / avx512, plus neon32
555
 * on 32-bit ARM)
556
 * alongside zxc_compress.c and zxc_decompress.c, so the LZ77 stages and the
557
 * Huffman stage in a given variant share the same ISA flags (e.g. -mbmi2 on
558
 * the AVX2/AVX512 variants). The compress/decompress variant TUs resolve
559
 * their Huffman calls to the matching suffixed symbol at compile time, so
560
 * the production hot path has zero dispatch overhead.
561
 *
562
 * These thin wrappers exist only for tests and external callers that link
563
 * against the un-suffixed names. They forward to the default (scalar) variant.
564
 */
565
/**
566
 * @brief Build length-limited per-symbol Huffman code lengths from frequencies.
567
 *
568
 * Un-suffixed entry forwarding to @ref zxc_huf_build_code_lengths_default; full
569
 * contract in @c zxc_internal.h.
570
 *
571
 * @param[in]  freq      Per-symbol frequency counts.
572
 * @param[out] code_len  Per-symbol code lengths.
573
 * @param[in]  scratch   Caller-provided build scratch buffer.
574
 * @return `ZXC_OK` on success, negative `zxc_error_t` on failure.
575
 */
576
int zxc_huf_build_code_lengths(const uint32_t* RESTRICT freq, uint8_t* RESTRICT code_len,
577
9.58k
                               void* RESTRICT scratch, const int max_code_len) {
578
9.58k
    return zxc_huf_build_code_lengths_default(freq, code_len, scratch, max_code_len);
579
9.58k
}
580
581
/** @brief Un-suffixed forwarders for the PivCo section codec (tests, tools). */
582
size_t zxc_huf_calc_size(const uint32_t* RESTRICT freq, const uint8_t* RESTRICT code_len,
583
0
                         const int with_header) {
584
0
    return zxc_huf_calc_size_default(freq, code_len, with_header);
585
0
}
586
587
int zxc_huf_encode_section(const uint8_t* RESTRICT literals, const size_t n_literals,
588
                           const uint32_t* RESTRICT freq, const uint8_t* RESTRICT code_len,
589
0
                           uint8_t* RESTRICT dst, const size_t dst_cap) {
590
0
    return zxc_huf_encode_section_default(literals, n_literals, freq, code_len, dst, dst_cap);
591
0
}
592
593
int zxc_huf_decode_section(const uint8_t* RESTRICT payload, const size_t payload_size,
594
0
                           uint8_t* RESTRICT dst, const size_t n, uint8_t* RESTRICT scratch) {
595
0
    return zxc_huf_decode_section_default(payload, payload_size, dst, n, scratch);
596
0
}
597
598
int zxc_huf_encode_section_dict(const uint8_t* RESTRICT literals, const size_t n_literals,
599
                                const uint32_t* RESTRICT freq, const uint8_t* RESTRICT code_len,
600
                                const zxc_pivco_tree_t* RESTRICT tree,
601
                                const uint32_t* RESTRICT codes, uint8_t* RESTRICT dst,
602
0
                                const size_t dst_cap) {
603
0
    return zxc_huf_encode_section_dict_default(literals, n_literals, freq, code_len, tree, codes,
604
0
                                               dst, dst_cap);
605
0
}
606
607
int zxc_huf_decode_section_dict(const uint8_t* RESTRICT payload, const size_t payload_size,
608
                                uint8_t* RESTRICT dst, const size_t n,
609
                                const zxc_pivco_tree_t* RESTRICT tree,
610
                                const zxc_pivco_decode_aux_t* RESTRICT aux,
611
0
                                uint8_t* RESTRICT scratch) {
612
0
    return zxc_huf_decode_section_dict_default(payload, payload_size, dst, n, tree, aux, scratch);
613
0
}
614
615
size_t zxc_huf_calc_size_dict(const uint32_t* RESTRICT freq, const uint8_t* RESTRICT code_len,
616
0
                              const zxc_pivco_tree_t* RESTRICT tree) {
617
0
    return zxc_huf_calc_size_dict_default(freq, code_len, tree);
618
0
}
619
620
/**
621
 * @brief Pack per-symbol code lengths into the 128-byte nibble header.
622
 *
623
 * Un-suffixed entry forwarding to @ref zxc_huf_pack_lengths_default; full
624
 * contract in @c zxc_internal.h.
625
 *
626
 * @param[in]  code_len  Per-symbol code lengths (one byte each).
627
 * @param[out] out       Destination 128-byte packed header.
628
 */
629
9.58k
void zxc_huf_pack_lengths(const uint8_t* RESTRICT code_len, uint8_t* RESTRICT out) {
630
9.58k
    zxc_huf_pack_lengths_default(code_len, out);
631
9.58k
}
632
633
/**
634
 * @brief Unpack and validate a 128-byte packed lengths header.
635
 *
636
 * Un-suffixed entry forwarding to @ref zxc_huf_unpack_lengths_default; full
637
 * contract in @c zxc_internal.h.
638
 *
639
 * @param[in]  in        128-byte packed lengths header.
640
 * @param[out] code_len  Destination per-symbol code lengths.
641
 * @return `ZXC_OK` on success, `ZXC_ERROR_CORRUPT_DATA` on invalid lengths.
642
 */
643
0
int zxc_huf_unpack_lengths(const uint8_t* RESTRICT in, uint8_t* RESTRICT code_len) {
644
0
    return zxc_huf_unpack_lengths_default(in, code_len);
645
0
}
646
647
/*
648
 * ============================================================================
649
 * PUBLIC UTILITY API
650
 * ============================================================================
651
 * These wrapper functions provide a simplified interface by managing context
652
 * allocation and looping over blocks. They call the dispatched wrappers above.
653
 */
654
655
/**
656
 * @brief Compresses an entire buffer in one call.
657
 *
658
 * Manages context allocation internally, loops over blocks, writes the
659
 * file header / EOF block / footer, and accumulates the global checksum.
660
 *
661
 * @param[in]  src              Uncompressed input data.
662
 * @param[in]  src_size         Size of @p src in bytes.
663
 * @param[out] dst              Destination buffer (use zxc_compress_bound() to size).
664
 * @param[in]  dst_capacity     Capacity of @p dst.
665
 * @param[in]  opts             Compression options (level, block size, checksum,
666
 *                              dictionary, seekable, threads), or NULL for defaults.
667
 * @return Total compressed size in bytes, or a negative @ref zxc_error_t code.
668
 */
669
// cppcheck-suppress unusedFunction
670
int64_t zxc_compress(const void* RESTRICT src, const size_t src_size, void* RESTRICT dst,
671
31.0k
                     const size_t dst_capacity, const zxc_compress_opts_t* opts) {
672
31.0k
    if (UNLIKELY(!dst || dst_capacity == 0 || (src_size > 0 && !src))) return ZXC_ERROR_NULL_INPUT;
673
674
31.0k
    const int checksum_enabled = opts ? opts->checksum_enabled : 0;
675
31.0k
    const int seekable = opts ? opts->seekable : 0;
676
31.0k
    const int level = zxc_level_clamp((opts && opts->level > 0) ? opts->level : ZXC_LEVEL_DEFAULT);
677
31.0k
    const size_t block_size =
678
31.0k
        (opts && opts->block_size > 0) ? opts->block_size : ZXC_BLOCK_SIZE_DEFAULT;
679
31.0k
    const uint8_t* dict = opts ? (const uint8_t*)opts->dict : NULL;
680
31.0k
    const size_t dict_size = (opts && opts->dict) ? opts->dict_size : 0;
681
31.0k
    const uint8_t* dict_huf = (opts && opts->dict) ? (const uint8_t*)opts->dict_huf : NULL;
682
683
31.0k
    if (UNLIKELY(dict_size > ZXC_DICT_SIZE_MAX)) return ZXC_ERROR_DICT_TOO_LARGE;
684
31.0k
    if (UNLIKELY(!zxc_validate_block_size(block_size))) return ZXC_ERROR_BAD_BLOCK_SIZE;
685
686
31.0k
    const uint32_t did = (dict && dict_size > 0) ? zxc_dict_id(dict, dict_size, dict_huf) : 0;
687
688
31.0k
    const uint8_t* ip = (const uint8_t*)src;
689
31.0k
    uint8_t* op = (uint8_t*)dst;
690
31.0k
    const uint8_t* op_start = op;
691
31.0k
    const uint8_t* op_end = op + dst_capacity;
692
31.0k
    uint32_t global_hash = 0;
693
31.0k
    zxc_cctx_t ctx;
694
695
31.0k
    const size_t eff_chunk =
696
31.0k
        dict_size > 0 ? zxc_block_size_ceil(dict_size + block_size) : block_size;
697
    // LCOV_EXCL_START
698
31.0k
    if (UNLIKELY(zxc_cctx_init(&ctx, eff_chunk, 1, level, checksum_enabled, dict_size) != ZXC_OK))
699
0
        return ZXC_ERROR_MEMORY;
700
    // LCOV_EXCL_STOP
701
31.0k
    if (UNLIKELY(zxc_cctx_attach_dict_huf(&ctx, dict_huf) != ZXC_OK)) {
702
        // LCOV_EXCL_START
703
0
        zxc_cctx_free(&ctx);
704
0
        return ZXC_ERROR_CORRUPT_DATA;
705
        // LCOV_EXCL_STOP
706
0
    }
707
708
    /* Dict input buffer: [dict_content | block_data] for the encoder, carved
709
     * into the cctx workspace (NULL when no dictionary is active). */
710
31.0k
    uint8_t* const dict_input = ctx.dict_buffer;
711
31.0k
    if (dict_input) ZXC_MEMCPY(dict_input, dict, dict_size);
712
713
31.0k
    const int h_val =
714
31.0k
        zxc_write_file_header(op, (size_t)(op_end - op), block_size, checksum_enabled, did);
715
    // LCOV_EXCL_START
716
31.0k
    if (UNLIKELY(h_val < 0)) {
717
0
        zxc_cctx_free(&ctx);
718
0
        return h_val;
719
0
    }
720
    // LCOV_EXCL_STOP
721
31.0k
    op += h_val;
722
723
    /* Seekable: dynamic array for per-block compressed sizes */
724
31.0k
    uint32_t* seek_comp = NULL;
725
31.0k
    uint32_t seek_count = 0;
726
31.0k
    uint32_t seek_cap = 0;
727
31.0k
    if (seekable) {
728
9.45k
        const size_t block_count = src_size / block_size;
729
9.45k
        if (UNLIKELY(block_count > (size_t)UINT32_MAX - 2)) {
730
            // LCOV_EXCL_START
731
0
            zxc_cctx_free(&ctx);
732
0
            return ZXC_ERROR_BAD_BLOCK_SIZE;
733
            // LCOV_EXCL_STOP
734
0
        }
735
9.45k
        seek_cap = (uint32_t)(block_count + 2);
736
9.45k
        seek_comp = (uint32_t*)ZXC_MALLOC(seek_cap * sizeof(uint32_t));
737
        // LCOV_EXCL_START
738
9.45k
        if (UNLIKELY(!seek_comp)) {
739
0
            zxc_cctx_free(&ctx);
740
0
            return ZXC_ERROR_MEMORY;
741
0
        }
742
        // LCOV_EXCL_STOP
743
9.45k
    }
744
745
31.0k
    size_t pos = 0;
746
63.8k
    while (pos < src_size) {
747
32.8k
        const size_t chunk_len = (src_size - pos > block_size) ? block_size : (src_size - pos);
748
32.8k
        const size_t rem_cap = (size_t)(op_end - op);
749
750
32.8k
        int res;
751
32.8k
        if (dict_input) {
752
9.69k
            ZXC_MEMCPY(dict_input + dict_size, ip + pos, chunk_len);
753
9.69k
            res = zxc_compress_chunk_wrapper(&ctx, dict_input, dict_size + chunk_len, op, rem_cap);
754
23.1k
        } else {
755
23.1k
            res = zxc_compress_chunk_wrapper(&ctx, ip + pos, chunk_len, op, rem_cap);
756
23.1k
        }
757
32.8k
        if (UNLIKELY(res < 0)) {
758
0
            ZXC_FREE(seek_comp);
759
0
            zxc_cctx_free(&ctx);
760
0
            return res;
761
0
        }
762
763
32.8k
        if (checksum_enabled) {
764
            // Update Global Hash (Rotation + XOR)
765
            // Block checksum is at the end of the written block data
766
9.93k
            if (LIKELY(res >= ZXC_GLOBAL_CHECKSUM_SIZE)) {
767
9.93k
                const uint32_t block_hash = zxc_le32(op + res - ZXC_GLOBAL_CHECKSUM_SIZE);
768
9.93k
                global_hash = zxc_hash_combine_rotate(global_hash, block_hash);
769
9.93k
            }
770
9.93k
        }
771
772
        /* Seekable: record compressed block size */
773
32.8k
        if (seekable) {
774
            // LCOV_EXCL_START
775
10.6k
            if (UNLIKELY(seek_count >= seek_cap)) {
776
0
                seek_cap = seek_cap * 2;
777
0
                uint32_t* nc = (uint32_t*)ZXC_REALLOC(seek_comp, seek_cap * sizeof(uint32_t));
778
0
                if (UNLIKELY(!nc)) {
779
0
                    ZXC_FREE(seek_comp);
780
0
                    zxc_cctx_free(&ctx);
781
0
                    return ZXC_ERROR_MEMORY;
782
0
                }
783
0
                seek_comp = nc;
784
0
            }
785
            // LCOV_EXCL_STOP
786
10.6k
            seek_comp[seek_count] = (uint32_t)res;
787
10.6k
            seek_count++;
788
10.6k
        }
789
790
32.8k
        op += res;
791
32.8k
        pos += chunk_len;
792
32.8k
    }
793
794
31.0k
    zxc_cctx_free(&ctx);
795
796
    // Write EOF Block
797
31.0k
    const size_t rem_cap = (size_t)(op_end - op);
798
31.0k
    const zxc_block_header_t eof_bh = {
799
31.0k
        .block_type = ZXC_BLOCK_EOF, .block_flags = 0, .reserved = 0, .comp_size = 0};
800
31.0k
    const int eof_val = zxc_write_block_header(op, rem_cap, &eof_bh);
801
    // LCOV_EXCL_START
802
31.0k
    if (UNLIKELY(eof_val < 0)) {
803
0
        ZXC_FREE(seek_comp);
804
0
        return eof_val;
805
0
    }
806
    // LCOV_EXCL_STOP
807
31.0k
    op += eof_val;
808
809
    /* Seekable: write seek table between EOF block and footer */
810
31.0k
    if (seekable && seek_count > 0) {
811
9.45k
        const size_t st_cap = (size_t)(op_end - op);
812
9.45k
        const int64_t st_val = zxc_write_seek_table(op, st_cap, seek_comp, seek_count);
813
9.45k
        ZXC_FREE(seek_comp);
814
9.45k
        if (UNLIKELY(st_val < 0)) return st_val;  // LCOV_EXCL_LINE
815
9.45k
        op += st_val;
816
21.6k
    } else {
817
21.6k
        ZXC_FREE(seek_comp);
818
21.6k
    }
819
820
31.0k
    if (UNLIKELY((size_t)(op_end - op) < ZXC_FILE_FOOTER_SIZE))
821
0
        return ZXC_ERROR_DST_TOO_SMALL;  // LCOV_EXCL_LINE
822
823
    // Write 12-byte Footer: [Source Size (8)] + [Global Hash (4)]
824
31.0k
    const int footer_val =
825
31.0k
        zxc_write_file_footer(op, (size_t)(op_end - op), src_size, global_hash, checksum_enabled);
826
31.0k
    if (UNLIKELY(footer_val < 0)) return footer_val;  // LCOV_EXCL_LINE
827
31.0k
    op += footer_val;
828
829
31.0k
    return (int64_t)(op - op_start);
830
31.0k
}
831
832
/* Shared frame decode body for zxc_decompress and zxc_decompress_inplace. No
833
 * RESTRICT between src and dst, so the overlapping case stays defined; the
834
 * in-place margin still gives each block disjoint regions, and the per-block
835
 * wrappers keep their own RESTRICT. */
836
static int64_t zxc_decompress_frame(const uint8_t* src, size_t src_size, uint8_t* dst,
837
                                    size_t dst_capacity, const zxc_decompress_opts_t* opts);
838
839
/**
840
 * @brief Decompresses an entire buffer in one call.
841
 *
842
 * Validates the file header and footer, loops over compressed blocks,
843
 * and verifies the global checksum when enabled.
844
 *
845
 * @param[in]  src              Compressed input data.
846
 * @param[in]  src_size         Size of @p src in bytes.
847
 * @param[out] dst              Destination buffer for decompressed data.
848
 * @param[in]  dst_capacity     Capacity of @p dst.
849
 * @param[in]  opts             Decompression options (checksum verification,
850
 *                              dictionary, threads), or NULL for defaults.
851
 * @return Total decompressed size in bytes, or a negative @ref zxc_error_t code.
852
 */
853
// cppcheck-suppress unusedFunction
854
int64_t zxc_decompress(const void* RESTRICT src, const size_t src_size, void* RESTRICT dst,
855
28.2k
                       const size_t dst_capacity, const zxc_decompress_opts_t* opts) {
856
28.2k
    if (UNLIKELY(!src || src_size < ZXC_FILE_HEADER_SIZE + ZXC_FILE_FOOTER_SIZE ||
857
28.2k
                 (!dst && dst_capacity != 0)))
858
9
        return ZXC_ERROR_NULL_INPUT;
859
860
28.2k
    if (UNLIKELY(!dst || dst_capacity == 0)) {
861
        /* Empty-frame case (stored size == 0). */
862
0
        if (UNLIKELY(zxc_le32(src) != ZXC_MAGIC_WORD)) return ZXC_ERROR_NULL_INPUT;
863
0
        const uint8_t* footer = (const uint8_t*)src + src_size - ZXC_FILE_FOOTER_SIZE;
864
0
        return (zxc_le64(footer) == 0) ? 0 : (int64_t)ZXC_ERROR_DST_TOO_SMALL;
865
0
    }
866
867
28.2k
    return zxc_decompress_frame((const uint8_t*)src, src_size, (uint8_t*)dst, dst_capacity, opts);
868
28.2k
}
869
870
static int64_t zxc_decompress_frame(const uint8_t* src, const size_t src_size, uint8_t* dst,
871
28.2k
                                    const size_t dst_capacity, const zxc_decompress_opts_t* opts) {
872
28.2k
    const int checksum_enabled = opts ? opts->checksum_enabled : 0;
873
28.2k
    const uint8_t* dict = opts ? (const uint8_t*)opts->dict : NULL;
874
28.2k
    const size_t dict_size = (opts && opts->dict) ? opts->dict_size : 0;
875
28.2k
    const uint8_t* dict_huf = (opts && opts->dict) ? (const uint8_t*)opts->dict_huf : NULL;
876
877
28.2k
    const uint8_t* ip = src;
878
28.2k
    const uint8_t* ip_end = ip + src_size;
879
28.2k
    uint8_t* op = dst;
880
28.2k
    const uint8_t* op_start = op;
881
28.2k
    const uint8_t* op_end = op + dst_capacity;
882
28.2k
    size_t runtime_chunk_size = 0;
883
28.2k
    zxc_cctx_t ctx;
884
885
28.2k
    int file_has_checksums = 0;
886
28.2k
    uint32_t header_dict_id = 0;
887
28.2k
    if (UNLIKELY(zxc_read_file_header(ip, src_size, &runtime_chunk_size, &file_has_checksums,
888
28.2k
                                      &header_dict_id) != ZXC_OK ||
889
28.2k
                 zxc_cctx_init(&ctx, runtime_chunk_size, 0, 0,
890
28.2k
                               file_has_checksums && checksum_enabled, dict_size) != ZXC_OK)) {
891
120
        return ZXC_ERROR_BAD_HEADER;
892
120
    }
893
894
    /* Dictionary validation */
895
28.1k
    if (header_dict_id != 0) {
896
9.71k
        if (UNLIKELY(!dict || dict_size == 0)) {
897
14
            zxc_cctx_free(&ctx);
898
14
            return ZXC_ERROR_DICT_REQUIRED;
899
14
        }
900
9.69k
        if (UNLIKELY(zxc_dict_id(dict, dict_size, dict_huf) != header_dict_id)) {
901
0
            zxc_cctx_free(&ctx);
902
0
            return ZXC_ERROR_DICT_MISMATCH;
903
0
        }
904
9.69k
    }
905
28.1k
    if (UNLIKELY(zxc_cctx_attach_dict_huf(&ctx, dict_huf) != ZXC_OK)) {
906
        // LCOV_EXCL_START
907
0
        zxc_cctx_free(&ctx);
908
0
        return ZXC_ERROR_CORRUPT_DATA;
909
        // LCOV_EXCL_STOP
910
0
    }
911
912
28.1k
    ip += ZXC_FILE_HEADER_SIZE;
913
914
28.1k
    const size_t work_sz = runtime_chunk_size + ZXC_DECOMPRESS_TAIL_PAD;
915
916
    /* Dict decode buffer: [dict_content | decode_space + PAD], carved into the
917
     * cctx workspace (NULL when no dictionary is active). */
918
28.1k
    uint8_t* const dict_dec = ctx.dict_buffer;
919
28.1k
    if (dict_dec) ZXC_MEMCPY(dict_dec, dict, dict_size);
920
921
    // Block decompression loop
922
28.1k
    uint32_t global_hash = 0;
923
924
96.7k
    while (ip < ip_end) {
925
95.3k
        const size_t rem_src = (size_t)(ip_end - ip);
926
95.3k
        zxc_block_header_t bh;
927
        // Read the block header to determine the compressed size
928
95.3k
        if (UNLIKELY(zxc_read_block_header(ip, rem_src, &bh) != ZXC_OK)) {
929
1.43k
            zxc_cctx_free(&ctx);
930
1.43k
            return ZXC_ERROR_BAD_HEADER;
931
1.43k
        }
932
933
        // Handle EOF block separately (not a real chunk to decompress)
934
93.8k
        if (UNLIKELY(bh.block_type == ZXC_BLOCK_EOF)) {
935
            // EOF carries no payload; a non-zero comp_size is a malformed header.
936
21.7k
            if (UNLIKELY(bh.comp_size != 0)) {
937
59
                zxc_cctx_free(&ctx);
938
59
                return ZXC_ERROR_BAD_HEADER;
939
59
            }
940
            // Footer is always the last ZXC_FILE_FOOTER_SIZE bytes of the source,
941
            // even when a seek table is inserted between EOF block and footer.
942
            // LCOV_EXCL_START
943
21.7k
            if (UNLIKELY(src_size < ZXC_FILE_FOOTER_SIZE)) {
944
0
                zxc_cctx_free(&ctx);
945
0
                return ZXC_ERROR_SRC_TOO_SMALL;
946
0
            }
947
            // LCOV_EXCL_STOP
948
21.7k
            const uint8_t* const footer = src + src_size - ZXC_FILE_FOOTER_SIZE;
949
950
            // Validate source size matches what we decompressed
951
21.7k
            const uint64_t stored_size = zxc_le64(footer);
952
21.7k
            if (UNLIKELY(stored_size != (uint64_t)(op - op_start))) {
953
122
                zxc_cctx_free(&ctx);
954
122
                return ZXC_ERROR_CORRUPT_DATA;
955
122
            }
956
957
            // Validate global checksum if enabled and file has checksums
958
21.6k
            if (checksum_enabled && file_has_checksums) {
959
4.55k
                const uint32_t stored_hash = zxc_le32(footer + sizeof(uint64_t));
960
4.55k
                if (UNLIKELY(stored_hash != global_hash)) {
961
0
                    zxc_cctx_free(&ctx);
962
0
                    return ZXC_ERROR_BAD_CHECKSUM;
963
0
                }
964
4.55k
            }
965
21.6k
            break;  // EOF reached, exit loop
966
21.6k
        }
967
968
72.1k
        int res;
969
72.1k
        const size_t rem_cap = (size_t)(op_end - op);
970
72.1k
        if (dict_dec) {
971
            /* Dict path: decode into bounce buffer with dict prefix so match
972
             * copies that reference dict content resolve naturally. */
973
9.69k
            res = zxc_decompress_chunk_wrapper(&ctx, ip, rem_src, dict_dec + dict_size, work_sz);
974
9.69k
            if (LIKELY(res > 0)) {
975
9.69k
                if (UNLIKELY((size_t)res > rem_cap)) {
976
                    // LCOV_EXCL_START
977
0
                    zxc_cctx_free(&ctx);
978
0
                    return ZXC_ERROR_DST_TOO_SMALL;
979
                    // LCOV_EXCL_STOP
980
0
                }
981
9.69k
                ZXC_MEMCPY(op, dict_dec + dict_size, (size_t)res);
982
9.69k
            }
983
62.4k
        } else if (LIKELY(rem_cap >= work_sz)) {
984
            // Fast path: decode directly into dst. Cap dst_cap to chunk_size + PAD
985
48.5k
            res = zxc_decompress_chunk_wrapper(&ctx, ip, rem_src, op, work_sz);
986
48.5k
        } else {
987
            // Safe path: decode into bounce buffer, then copy exact result.
988
13.8k
            res = zxc_decompress_chunk_wrapper(&ctx, ip, rem_src, ctx.work_buf, ctx.work_buf_cap);
989
13.8k
            if (LIKELY(res > 0)) {
990
                // LCOV_EXCL_START
991
13.0k
                if (UNLIKELY((size_t)res > rem_cap)) {
992
29
                    zxc_cctx_free(&ctx);
993
29
                    return ZXC_ERROR_DST_TOO_SMALL;
994
29
                }
995
                // LCOV_EXCL_STOP
996
13.0k
                ZXC_MEMCPY(op, ctx.work_buf, (size_t)res);
997
13.0k
            }
998
13.8k
        }
999
72.0k
        if (UNLIKELY(res < 0)) {
1000
3.51k
            zxc_cctx_free(&ctx);
1001
3.51k
            return res;
1002
3.51k
        }
1003
1004
        // Update global hash from block checksum
1005
68.5k
        if (checksum_enabled && file_has_checksums) {
1006
4.55k
            const uint32_t block_hash = zxc_le32(ip + ZXC_BLOCK_HEADER_SIZE + bh.comp_size);
1007
4.55k
            global_hash = zxc_hash_combine_rotate(global_hash, block_hash);
1008
4.55k
        }
1009
1010
68.5k
        ip += ZXC_BLOCK_HEADER_SIZE + bh.comp_size +
1011
68.5k
              (file_has_checksums ? ZXC_BLOCK_CHECKSUM_SIZE : 0);
1012
68.5k
        op += res;
1013
68.5k
    }
1014
1015
22.9k
    zxc_cctx_free(&ctx);
1016
22.9k
    return (int64_t)(op - op_start);
1017
28.1k
}
1018
1019
/**
1020
 * @brief Bytes an in-place decode needs on top of the decompressed size.
1021
 *
1022
 * Flush-right placement puts block 0 at `capacity - comp_size`, so the read
1023
 * cursor before block k sits at `capacity - sum_{j>=k} (c_j + H) - trailing`,
1024
 * and the no-overtake invariant `sum_{j<=k} o_j + PAD <= R_k` requires
1025
 *
1026
 *     capacity >= max_k [ sum_{j<=k} o_j + sum_{j>=k} (c_j + H) ] + PAD + trailing
1027
 *
1028
 * Incompressible input (all RAW, `c_j = o_j`) maximises the bracket at
1029
 * `dsize + chunk_size + nblocks * H`: the margin carries the whole accumulated
1030
 * per-block overhead, not just one block's.
1031
 *
1032
 * `trailing` is everything written after the last data block: EOF header, footer
1033
 * and seek table. The latter is always reserved at its worst case (4 bytes per
1034
 * block, <= 0.1% of the payload) because no header flag announces one; omitting
1035
 * it used to push the bound *below* comp_size for a seekable archive of
1036
 * incompressible data in small blocks.
1037
 *
1038
 * @param[in] dsize      Decompressed size, in bytes.
1039
 * @param[in] chunk_size Block size from the file header (0 = no blocks).
1040
 * @param[in] has_cs     Non-zero if blocks carry checksums.
1041
 * @return Bytes to reserve beyond @p dsize.
1042
 */
1043
static uint64_t zxc_inplace_margin(const uint64_t dsize, const size_t chunk_size,
1044
0
                                   const int has_cs) {
1045
0
    const uint64_t nblocks =
1046
0
        chunk_size ? (dsize + (uint64_t)chunk_size - 1) / (uint64_t)chunk_size : 0;
1047
0
    const uint64_t per_block =
1048
0
        (uint64_t)ZXC_BLOCK_HEADER_SIZE + (has_cs ? (uint64_t)ZXC_BLOCK_CHECKSUM_SIZE : 0);
1049
0
    const uint64_t trailing =
1050
0
        (uint64_t)ZXC_BLOCK_HEADER_SIZE +  // EOF block header
1051
0
        ((uint64_t)ZXC_BLOCK_HEADER_SIZE +
1052
0
         nblocks * (uint64_t)ZXC_SEEK_ENTRY_SIZE) +  // Seek table (worst case)
1053
0
        (uint64_t)ZXC_FILE_FOOTER_SIZE;
1054
0
    return (uint64_t)chunk_size + nblocks * per_block + trailing +
1055
0
           (uint64_t)ZXC_DECOMPRESS_TAIL_PAD;
1056
0
}
1057
1058
/**
1059
 * @brief Shared archive probe for the in-place entry points: validates the
1060
 *        magic + file header, then reads the footer's decompressed size and
1061
 *        derives the in-place margin.
1062
 *
1063
 * Keeping this parse in one place guarantees @ref zxc_decompress_inplace_bound
1064
 * and @ref zxc_decompress_inplace always agree on what a buffer of at least
1065
 * the bound must satisfy.
1066
 *
1067
 * @param[in]  comp      Compressed archive; only the header and footer are read.
1068
 * @param[in]  comp_size Size of the archive in bytes. The caller guarantees it
1069
 *                       covers at least the file header and footer.
1070
 * @param[out] dsize     Decompressed size read from the footer.
1071
 * @param[out] margin    In-place margin for @p dsize, from @ref zxc_inplace_margin.
1072
 * @return ZXC_OK, or a negative @ref zxc_error_t on an invalid archive.
1073
 */
1074
static int zxc_inplace_probe(const uint8_t* comp, const size_t comp_size, uint64_t* dsize,
1075
0
                             uint64_t* margin) {
1076
0
    if (UNLIKELY(zxc_le32(comp) != ZXC_MAGIC_WORD)) return ZXC_ERROR_BAD_MAGIC;
1077
0
    size_t chunk_size = 0;
1078
0
    int has_cs = 0;
1079
0
    uint32_t did = 0;
1080
0
    if (UNLIKELY(zxc_read_file_header(comp, comp_size, &chunk_size, &has_cs, &did) != ZXC_OK))
1081
0
        return ZXC_ERROR_BAD_HEADER;
1082
0
    *dsize = zxc_le64(comp + comp_size - ZXC_FILE_FOOTER_SIZE);
1083
0
    *margin = zxc_inplace_margin(*dsize, chunk_size, has_cs);
1084
0
    return ZXC_OK;
1085
0
}
1086
1087
/**
1088
 * @brief Minimum single-buffer size for a safe in-place decode of @p src.
1089
 *
1090
 * Reads the archive header (block size) and footer (decompressed size) without
1091
 * decoding, and returns `decompressed_size + zxc_inplace_margin(...)` (one
1092
 * block + accumulated per-block overhead + footer + wild-copy tail). A buffer
1093
 * of at least this size lets @ref zxc_decompress_inplace decode with the
1094
 * compressed data placed flush-right, the write cursor never overtaking the
1095
 * read cursor.
1096
 *
1097
 * @param[in] src      Compressed archive (only header + footer are read).
1098
 * @param[in] src_size Size of the archive in bytes.
1099
 * @return Required buffer size in bytes, or 0 if @p src is not a valid archive.
1100
 */
1101
// cppcheck-suppress unusedFunction
1102
0
size_t zxc_decompress_inplace_bound(const void* src, const size_t src_size) {
1103
0
    if (UNLIKELY(!src || src_size < ZXC_FILE_HEADER_SIZE + ZXC_FILE_FOOTER_SIZE)) return 0;
1104
0
    uint64_t dsize = 0;
1105
0
    uint64_t margin = 0;
1106
0
    if (UNLIKELY(zxc_inplace_probe((const uint8_t*)src, src_size, &dsize, &margin) != ZXC_OK))
1107
0
        return 0;
1108
0
    if (UNLIKELY(margin > (uint64_t)SIZE_MAX || dsize > (uint64_t)SIZE_MAX - margin)) return 0;
1109
0
    return (size_t)(dsize + margin);
1110
0
}
1111
1112
/**
1113
 * @brief Decompresses in place, inside a single caller-owned buffer.
1114
 *
1115
 * The compressed archive of @p comp_size bytes must sit **flush-right** in
1116
 * @p buffer, i.e. at `buffer + buffer_capacity - comp_size`. Decoding runs
1117
 * left-to-right into `buffer[0..]`; because ZXC never expands a block and the
1118
 * buffer carries a one-block + wild-copy margin (see
1119
 * @ref zxc_decompress_inplace_bound), the write cursor provably never overtakes
1120
 * the read cursor, so a single allocation replaces the usual input+output pair.
1121
 * Dictionary archives are supported (they decode through the context's own
1122
 * bounce buffer, which does not alias @p buffer).
1123
 *
1124
 * @param[in,out] buffer           Single work buffer holding the flush-right
1125
 *                                 archive; receives the decompressed output.
1126
 * @param[in]     buffer_capacity  Total size of @p buffer in bytes.
1127
 * @param[in]     comp_size        Size of the compressed archive in bytes.
1128
 * @param[in]     opts             Decompression options, or NULL for defaults.
1129
 * @return Decompressed size in bytes, or a negative @ref zxc_error_t code
1130
 *         (`ZXC_ERROR_DST_TOO_SMALL` if the buffer lacks the safety margin).
1131
 */
1132
// cppcheck-suppress unusedFunction
1133
int64_t zxc_decompress_inplace(void* buffer, const size_t buffer_capacity, const size_t comp_size,
1134
0
                               const zxc_decompress_opts_t* opts) {
1135
0
    if (UNLIKELY(!buffer || comp_size < ZXC_FILE_HEADER_SIZE + ZXC_FILE_FOOTER_SIZE ||
1136
0
                 comp_size > buffer_capacity))
1137
0
        return ZXC_ERROR_NULL_INPUT;
1138
0
    uint8_t* const buf = (uint8_t*)buffer;
1139
0
    const uint8_t* const comp = buf + (buffer_capacity - comp_size); /* flush-right */
1140
0
    uint64_t dsize = 0;
1141
0
    uint64_t margin = 0;
1142
0
    if (UNLIKELY(zxc_inplace_probe(comp, comp_size, &dsize, &margin) != ZXC_OK))
1143
0
        return ZXC_ERROR_BAD_HEADER;
1144
0
    if (UNLIKELY(dsize > (uint64_t)buffer_capacity || (uint64_t)buffer_capacity - dsize < margin))
1145
0
        return ZXC_ERROR_DST_TOO_SMALL;
1146
0
    return zxc_decompress_frame(comp, comp_size, buf, buffer_capacity, opts);
1147
0
}
1148
1149
/**
1150
 * @brief Reads the decompressed size from a ZXC-compressed buffer.
1151
 *
1152
 * The size is stored in the file footer (last @ref ZXC_FILE_FOOTER_SIZE bytes).
1153
 * The footer is untrusted input, so the value is checked for plausibility
1154
 * against the archive itself: every decoded block costs at least
1155
 * @ref ZXC_BLOCK_HEADER_SIZE compressed bytes and expands to at most one
1156
 * block size, which bounds the ratio an authentic archive can reach. A size
1157
 * beyond that bound (a forged footer) returns 0, so callers sizing an output
1158
 * allocation from this value inherit the check.
1159
 *
1160
 * @param[in] src      Compressed data.
1161
 * @param[in] src_size Size of @p src in bytes.
1162
 * @return Original uncompressed size, or 0 on error or an implausible footer.
1163
 */
1164
6.69k
uint64_t zxc_get_decompressed_size(const void* src, const size_t src_size) {
1165
6.69k
    if (UNLIKELY(src_size < ZXC_FILE_HEADER_SIZE + ZXC_FILE_FOOTER_SIZE)) return 0;
1166
1167
6.68k
    const uint8_t* const p = (const uint8_t*)src;
1168
6.68k
    if (UNLIKELY(zxc_le32(p) != ZXC_MAGIC_WORD)) return 0;
1169
1170
6.62k
    size_t chunk_size = 0;
1171
6.62k
    int has_cs = 0;
1172
6.62k
    uint32_t did = 0;
1173
6.62k
    if (UNLIKELY(zxc_read_file_header(p, src_size, &chunk_size, &has_cs, &did) != ZXC_OK)) return 0;
1174
1175
6.56k
    const uint8_t* const footer = p + src_size - ZXC_FILE_FOOTER_SIZE;
1176
6.56k
    const uint64_t dsize = zxc_le64(footer);
1177
1178
    /* Plausibility: at most src_size / ZXC_BLOCK_HEADER_SIZE blocks, each
1179
     * decoding to at most chunk_size. The division form keeps the compare
1180
     * overflow-free - a ceil would wrap on a forged dsize near UINT64_MAX. */
1181
6.56k
    const uint64_t blocks_needed =
1182
6.56k
        chunk_size ? dsize / (uint64_t)chunk_size + (dsize % (uint64_t)chunk_size != 0) : 0;
1183
6.56k
    if (UNLIKELY(blocks_needed > (uint64_t)(src_size / ZXC_BLOCK_HEADER_SIZE))) return 0;
1184
1185
716
    return dsize;
1186
6.56k
}
1187
1188
/**
1189
 * @brief Reads the dictionary id from a compressed archive's file header.
1190
 *
1191
 * Public API; see @c zxc_buffer.h. Validates the magic, then returns the
1192
 * header's @c dict_id field when the dictionary flag is set. Does not decompress.
1193
 *
1194
 * @param[in] src       Start of the compressed archive (>= @c ZXC_FILE_HEADER_SIZE).
1195
 * @param[in] src_size  Size of @p src in bytes.
1196
 * @return The dictionary id, or 0 if @p src is invalid or the archive uses no
1197
 *         dictionary.
1198
 */
1199
// cppcheck-suppress unusedFunction
1200
0
uint32_t zxc_get_dict_id(const void* src, const size_t src_size) {
1201
0
    if (UNLIKELY(!src || src_size < ZXC_FILE_HEADER_SIZE)) return 0;
1202
1203
0
    const uint8_t* const p = (const uint8_t*)src;
1204
0
    if (UNLIKELY(zxc_le32(p) != ZXC_MAGIC_WORD)) return 0;
1205
1206
0
    return (p[6] & ZXC_FILE_FLAG_HAS_DICTIONARY) ? zxc_le32(p + 7) : 0;
1207
0
}
1208
1209
/*
1210
 * ============================================================================
1211
 * REUSABLE CONTEXT API (Opaque)
1212
 * ============================================================================
1213
 *
1214
 * Provides heap-allocated, opaque contexts that integrators can reuse across
1215
 * multiple compress / decompress calls, eliminating per-call malloc/free
1216
 * overhead.
1217
 */
1218
1219
/* --- Compression --------------------------------------------------------- */
1220
1221
/**
1222
 * @brief Opaque reusable compression context (public handle @ref zxc_cctx).
1223
 *
1224
 * Wraps one internal @ref zxc_cctx_t plus the sticky options and bookkeeping
1225
 * needed to reuse buffers across calls and re-init only when the block size
1226
 * changes.
1227
 */
1228
struct zxc_cctx_s {
1229
    zxc_cctx_t inner;       /* existing internal context */
1230
    int initialized;        /* 1 if inner has live allocations */
1231
    int owns_workspace;     /* 0 = library-allocated (free in zxc_free_cctx),
1232
                               1 = caller-supplied static workspace (no-op free,
1233
                               block_size pinned at init) */
1234
    size_t last_block_size; /* block size used for last init */
1235
    /* Sticky options (remembered from create or last compress call). */
1236
    int stored_level;
1237
    int stored_checksum;
1238
    size_t stored_block_size;
1239
};
1240
1241
/**
1242
 * @brief Creates a reusable compression context.
1243
 *
1244
 * Public API; full contract in @c zxc_buffer.h. With non-NULL @p opts the
1245
 * internal buffers are pre-allocated for the given level / block size /
1246
 * checksum; with NULL @p opts allocation is deferred to the first
1247
 * @ref zxc_compress_cctx call. The resolved settings become sticky defaults.
1248
 *
1249
 * @param[in] opts  Initial compression options, or NULL to defer allocation.
1250
 * @return A context to release with @ref zxc_free_cctx, or NULL on allocation
1251
 *         failure or invalid @p opts.
1252
 */
1253
4.60k
zxc_cctx* zxc_create_cctx(const zxc_compress_opts_t* opts) {
1254
4.60k
    zxc_cctx* const cctx = (zxc_cctx*)ZXC_CALLOC(1, sizeof(zxc_cctx));
1255
4.60k
    if (UNLIKELY(!cctx)) return NULL;  // LCOV_EXCL_LINE
1256
1257
    /* Resolve and store sticky defaults. */
1258
4.60k
    cctx->stored_level =
1259
4.60k
        zxc_level_clamp((opts && opts->level > 0) ? opts->level : ZXC_LEVEL_DEFAULT);
1260
4.60k
    cctx->stored_block_size =
1261
4.60k
        (opts && opts->block_size > 0) ? opts->block_size : ZXC_BLOCK_SIZE_DEFAULT;
1262
4.60k
    cctx->stored_checksum = opts ? opts->checksum_enabled : 0;
1263
1264
4.60k
    if (opts) {
1265
        // LCOV_EXCL_START
1266
4.60k
        if (UNLIKELY(!zxc_validate_block_size(cctx->stored_block_size) ||
1267
4.60k
                     zxc_cctx_init(&cctx->inner, cctx->stored_block_size, 1, cctx->stored_level,
1268
4.60k
                                   cctx->stored_checksum, 0) != ZXC_OK)) {
1269
0
            ZXC_FREE(cctx);
1270
0
            return NULL;
1271
0
        }
1272
        // LCOV_EXCL_STOP
1273
4.60k
        cctx->last_block_size = cctx->stored_block_size;
1274
4.60k
        cctx->initialized = 1;
1275
4.60k
    }
1276
1277
4.60k
    return cctx;
1278
4.60k
}
1279
1280
/**
1281
 * @brief Releases a reusable compression context.
1282
 *
1283
 * Public API; see @c zxc_buffer.h. Frees the inner buffers and the handle.
1284
 * NULL-safe. For a static (caller-workspace) context this is a no-op, since the
1285
 * caller owns the workspace.
1286
 *
1287
 * @param[in] cctx  Context from @ref zxc_create_cctx (may be NULL).
1288
 */
1289
4.60k
void zxc_free_cctx(zxc_cctx* cctx) {
1290
4.60k
    if (UNLIKELY(!cctx)) return;
1291
    /* Static cctx: handle + inner buffers live inside the caller's workspace,
1292
     * which we do not own. Free is a no-op; the caller owns the workspace. */
1293
4.60k
    if (cctx->owns_workspace) return;
1294
4.60k
    if (cctx->initialized) zxc_cctx_free(&cctx->inner);
1295
4.60k
    ZXC_FREE(cctx);
1296
4.60k
}
1297
1298
/**
1299
 * @brief Compresses a whole buffer into a framed archive, reusing @p cctx.
1300
 *
1301
 * Public API; full contract in @c zxc_buffer.h. Resolves per-call options over
1302
 * the context's sticky defaults, re-initialises the inner buffers only when the
1303
 * block size changes (level / checksum update in place), then writes the file
1304
 * header, the compressed blocks, the EOF block and the footer.
1305
 *
1306
 * @param[in,out] cctx          Reusable compression context.
1307
 * @param[in]     src           Source bytes.
1308
 * @param[in]     src_size      Number of source bytes (must be > 0).
1309
 * @param[out]    dst           Destination buffer for the archive.
1310
 * @param[in]     dst_capacity  Capacity of @p dst in bytes.
1311
 * @param[in]     opts          Per-call option overrides, or NULL for the
1312
 *                              context defaults.
1313
 * @return Archive size in bytes on success, or a negative @ref zxc_error_t.
1314
 */
1315
int64_t zxc_compress_cctx(zxc_cctx* cctx, const void* RESTRICT src, const size_t src_size,
1316
                          void* RESTRICT dst, const size_t dst_capacity,
1317
0
                          const zxc_compress_opts_t* opts) {
1318
0
    if (UNLIKELY(!cctx)) return ZXC_ERROR_NULL_INPUT;
1319
0
    if (UNLIKELY(!src || !dst || src_size == 0 || dst_capacity == 0)) return ZXC_ERROR_NULL_INPUT;
1320
1321
0
    const int checksum_enabled = opts ? opts->checksum_enabled : cctx->stored_checksum;
1322
0
    const int level = zxc_level_clamp((opts && opts->level > 0) ? opts->level : cctx->stored_level);
1323
0
    const size_t block_size =
1324
0
        (opts && opts->block_size > 0) ? opts->block_size : cctx->stored_block_size;
1325
1326
0
    if (UNLIKELY(!zxc_validate_block_size(block_size))) return ZXC_ERROR_BAD_BLOCK_SIZE;
1327
1328
    /* Static cctx: block_size is locked at init and the workspace cannot grow,
1329
     * so reject any opts forcing a re-partition. level and checksum_enabled may
1330
     * still vary - except a raise into the optimal-parser tier, whose
1331
     * opt_scratch a workspace carved below ZXC_LEVEL_DENSITY does not carry. */
1332
0
    if (UNLIKELY(cctx->owns_workspace && block_size != cctx->last_block_size))
1333
0
        return ZXC_ERROR_BAD_BLOCK_SIZE;
1334
0
    if (UNLIKELY(cctx->owns_workspace && level >= ZXC_LEVEL_DENSITY && !cctx->inner.opt_scratch))
1335
0
        return ZXC_ERROR_BAD_LEVEL;
1336
1337
0
    cctx->stored_level = level;
1338
0
    cctx->stored_block_size = block_size;
1339
0
    cctx->stored_checksum = checksum_enabled;
1340
1341
    /* Re-init when block_size changed (it drives the buffer sizes), or when a
1342
     * level raise into the optimal-parser tier needs an opt_scratch that inits
1343
     * below ZXC_LEVEL_DENSITY never allocated. */
1344
0
    if (UNLIKELY(!cctx->initialized || cctx->last_block_size != block_size ||
1345
0
                 (level >= ZXC_LEVEL_DENSITY && !cctx->inner.opt_scratch))) {
1346
0
        if (cctx->initialized) {
1347
            // LCOV_EXCL_START
1348
0
            zxc_cctx_free(&cctx->inner);
1349
0
            cctx->initialized = 0;
1350
            // LCOV_EXCL_STOP
1351
0
        }
1352
        // LCOV_EXCL_START
1353
0
        if (UNLIKELY(zxc_cctx_init(&cctx->inner, block_size, 1, level, checksum_enabled, 0) !=
1354
0
                     ZXC_OK))
1355
0
            return ZXC_ERROR_MEMORY;
1356
        // LCOV_EXCL_STOP
1357
0
        cctx->last_block_size = block_size;
1358
0
        cctx->initialized = 1;
1359
0
    } else {
1360
        /* Same block_size: update level + checksum without realloc. */
1361
0
        cctx->inner.compression_level = level;
1362
0
        cctx->inner.checksum_enabled = checksum_enabled;
1363
0
    }
1364
1365
    /* Shared context: zxc_compress_block leaves its dictionary here. */
1366
0
    cctx->inner.dict_size = 0;
1367
1368
0
    zxc_cctx_t* const ctx = &cctx->inner;
1369
1370
0
    uint8_t* op = (uint8_t*)dst;
1371
0
    const uint8_t* const op_start = op;
1372
0
    const uint8_t* const op_end = op + dst_capacity;
1373
0
    const uint8_t* const ip = (const uint8_t*)src;
1374
0
    uint32_t global_hash = 0;
1375
1376
0
    const int h_val =
1377
0
        zxc_write_file_header(op, (size_t)(op_end - op), block_size, checksum_enabled, 0);
1378
0
    if (UNLIKELY(h_val < 0)) return h_val;  // LCOV_EXCL_LINE
1379
0
    op += h_val;
1380
1381
0
    size_t pos = 0;
1382
0
    while (pos < src_size) {
1383
0
        const size_t chunk_len = (src_size - pos > block_size) ? block_size : (src_size - pos);
1384
0
        const size_t rem_cap = (size_t)(op_end - op);
1385
1386
0
        const int res = zxc_compress_chunk_wrapper(ctx, ip + pos, chunk_len, op, rem_cap);
1387
0
        if (UNLIKELY(res < 0)) return res;
1388
1389
0
        if (checksum_enabled) {
1390
0
            if (LIKELY(res >= ZXC_GLOBAL_CHECKSUM_SIZE)) {
1391
0
                const uint32_t block_hash = zxc_le32(op + res - ZXC_GLOBAL_CHECKSUM_SIZE);
1392
0
                global_hash = zxc_hash_combine_rotate(global_hash, block_hash);
1393
0
            }
1394
0
        }
1395
1396
0
        op += res;
1397
0
        pos += chunk_len;
1398
0
    }
1399
1400
    /* EOF block */
1401
0
    const size_t rem_cap = (size_t)(op_end - op);
1402
0
    const zxc_block_header_t eof_bh = {
1403
0
        .block_type = ZXC_BLOCK_EOF, .block_flags = 0, .reserved = 0, .comp_size = 0};
1404
0
    const int eof_val = zxc_write_block_header(op, rem_cap, &eof_bh);
1405
0
    if (UNLIKELY(eof_val < 0)) return eof_val;  // LCOV_EXCL_LINE
1406
0
    op += eof_val;
1407
1408
0
    if (UNLIKELY(rem_cap < (size_t)eof_val + ZXC_FILE_FOOTER_SIZE))
1409
0
        return ZXC_ERROR_DST_TOO_SMALL;  // LCOV_EXCL_LINE
1410
1411
0
    const int footer_val =
1412
0
        zxc_write_file_footer(op, (size_t)(op_end - op), src_size, global_hash, checksum_enabled);
1413
0
    if (UNLIKELY(footer_val < 0)) return footer_val;  // LCOV_EXCL_LINE
1414
0
    op += footer_val;
1415
1416
0
    return (int64_t)(op - op_start);
1417
0
}
1418
1419
/* --- Decompression ------------------------------------------------------- */
1420
1421
/**
1422
 * @brief Opaque reusable decompression context (public handle @ref zxc_dctx).
1423
 *
1424
 * Reuses the internal @ref zxc_cctx_t type for decode, tracking the last block
1425
 * and dict sizes so the inner buffers are re-carved only when they change.
1426
 */
1427
struct zxc_dctx_s {
1428
    zxc_cctx_t inner;       /* reuses the same internal context type */
1429
    size_t last_block_size; /* block size from last header parse */
1430
    size_t last_dict_size;  /* dict_size the inner buffer was carved for (drives re-init) */
1431
    int initialized;        /* 1 if inner has live allocations */
1432
    int owns_workspace;     /* 0 = library-allocated (free in zxc_free_dctx),
1433
                               1 = caller-supplied static workspace (no-op free,
1434
                               block_size pinned at init) */
1435
};
1436
1437
/**
1438
 * @brief Creates a reusable decompression context.
1439
 *
1440
 * Public API; see @c zxc_buffer.h. The inner buffers are allocated lazily on
1441
 * the first decode (sized from the archive header), so this only allocates the
1442
 * handle itself.
1443
 *
1444
 * @return A context to release with @ref zxc_free_dctx, or NULL on allocation
1445
 *         failure.
1446
 */
1447
0
zxc_dctx* zxc_create_dctx(void) {
1448
0
    zxc_dctx* const dctx = (zxc_dctx*)ZXC_CALLOC(1, sizeof(zxc_dctx));
1449
0
    return dctx;
1450
0
}
1451
1452
/**
1453
 * @brief Releases a reusable decompression context.
1454
 *
1455
 * Public API; see @c zxc_buffer.h. Frees the inner buffers and the handle.
1456
 * NULL-safe; a no-op for a static (caller-workspace) context.
1457
 *
1458
 * @param[in] dctx  Context from @ref zxc_create_dctx (may be NULL).
1459
 */
1460
0
void zxc_free_dctx(zxc_dctx* dctx) {
1461
0
    if (UNLIKELY(!dctx)) return;
1462
    /* Static dctx: handle + inner buffers live inside the caller's workspace,
1463
     * which we do not own. Free is a no-op; the caller owns the workspace. */
1464
0
    if (dctx->owns_workspace) return;
1465
0
    if (dctx->initialized) zxc_cctx_free(&dctx->inner);
1466
0
    ZXC_FREE(dctx);
1467
0
}
1468
1469
/**
1470
 * @brief Decompresses a framed archive into @p dst, reusing @p dctx.
1471
 *
1472
 * Public API; full contract in @c zxc_buffer.h. Parses the file header,
1473
 * re-initialises the inner buffers only when the block size changes (or a prior
1474
 * dict call left a prefix), then decodes each block - straight into @p dst when
1475
 * the tail padding fits, otherwise through a bounce buffer - and verifies the
1476
 * footer size and optional checksum.
1477
 *
1478
 * @param[in,out] dctx          Reusable decompression context.
1479
 * @param[in]     src           Compressed archive bytes.
1480
 * @param[in]     src_size      Archive size (>= @c ZXC_FILE_HEADER_SIZE).
1481
 * @param[out]    dst           Destination for the decompressed output.
1482
 * @param[in]     dst_capacity  Capacity of @p dst in bytes.
1483
 * @param[in]     opts          Per-call options (e.g. checksum), or NULL.
1484
 * @return Decompressed size in bytes on success, or a negative @ref zxc_error_t.
1485
 */
1486
int64_t zxc_decompress_dctx(zxc_dctx* dctx, const void* RESTRICT src, const size_t src_size,
1487
                            void* RESTRICT dst, const size_t dst_capacity,
1488
0
                            const zxc_decompress_opts_t* opts) {
1489
0
    if (UNLIKELY(!dctx || !src || !dst || src_size < ZXC_FILE_HEADER_SIZE))
1490
0
        return ZXC_ERROR_NULL_INPUT;
1491
1492
0
    const int checksum_enabled = opts ? opts->checksum_enabled : 0;
1493
1494
0
    const uint8_t* ip = (const uint8_t*)src;
1495
0
    const uint8_t* const ip_end = ip + src_size;
1496
0
    uint8_t* op = (uint8_t*)dst;
1497
0
    const uint8_t* const op_start = op;
1498
0
    const uint8_t* const op_end = op + dst_capacity;
1499
0
    size_t runtime_chunk_size = 0;
1500
0
    int file_has_checksums = 0;
1501
0
    uint32_t global_hash = 0;
1502
1503
0
    if (UNLIKELY(zxc_read_file_header(ip, src_size, &runtime_chunk_size, &file_has_checksums,
1504
0
                                      NULL) != ZXC_OK))
1505
0
        return ZXC_ERROR_BAD_HEADER;
1506
1507
    /* Static dctx: block_size is locked at workspace init; reject any
1508
     * archive whose declared block_size would require a re-partition. */
1509
0
    if (UNLIKELY(dctx->owns_workspace && runtime_chunk_size != dctx->last_block_size))
1510
0
        return ZXC_ERROR_BAD_BLOCK_SIZE;
1511
1512
    /* Re-init when block size changed, or when a prior dict-using call (block
1513
     * API) left the inner context carrying a dict prefix. */
1514
0
    if (UNLIKELY(!dctx->initialized || dctx->last_block_size != runtime_chunk_size ||
1515
0
                 dctx->last_dict_size != 0)) {
1516
0
        if (dctx->initialized) {
1517
            // LCOV_EXCL_START
1518
0
            zxc_cctx_free(&dctx->inner);
1519
0
            dctx->initialized = 0;
1520
            // LCOV_EXCL_STOP
1521
0
        }
1522
        // LCOV_EXCL_START
1523
0
        if (UNLIKELY(zxc_cctx_init(&dctx->inner, runtime_chunk_size, 0, 0,
1524
0
                                   file_has_checksums && checksum_enabled, 0) != ZXC_OK))
1525
0
            return ZXC_ERROR_MEMORY;
1526
        // LCOV_EXCL_STOP
1527
0
        dctx->last_block_size = runtime_chunk_size;
1528
0
        dctx->last_dict_size = 0;
1529
0
        dctx->initialized = 1;
1530
0
    } else {
1531
0
        dctx->inner.checksum_enabled = file_has_checksums && checksum_enabled;
1532
0
    }
1533
1534
0
    zxc_cctx_t* const ctx = &dctx->inner;
1535
0
    ip += ZXC_FILE_HEADER_SIZE;
1536
1537
    /* work_buf was pre-sized to runtime_chunk_size + ZXC_DECOMPRESS_TAIL_PAD
1538
     * inside the matching zxc_cctx_init call above; the re-init guard ensures
1539
     * it stays in sync when chunk_size changes between calls. */
1540
0
    const size_t work_sz = runtime_chunk_size + ZXC_DECOMPRESS_TAIL_PAD;
1541
1542
0
    while (ip < ip_end) {
1543
0
        const size_t rem_src = (size_t)(ip_end - ip);
1544
0
        zxc_block_header_t bh;
1545
0
        if (UNLIKELY(zxc_read_block_header(ip, rem_src, &bh) != ZXC_OK))
1546
0
            return ZXC_ERROR_BAD_HEADER;
1547
1548
0
        if (UNLIKELY(bh.block_type == ZXC_BLOCK_EOF)) {
1549
0
            if (UNLIKELY(bh.comp_size != 0)) return ZXC_ERROR_BAD_HEADER;
1550
0
            if (UNLIKELY(rem_src < ZXC_BLOCK_HEADER_SIZE + ZXC_FILE_FOOTER_SIZE))
1551
0
                return ZXC_ERROR_SRC_TOO_SMALL;
1552
1553
0
            const uint8_t* const footer = ip + ZXC_BLOCK_HEADER_SIZE;
1554
0
            const uint64_t stored_size = zxc_le64(footer);
1555
0
            if (UNLIKELY(stored_size != (uint64_t)(op - op_start))) return ZXC_ERROR_CORRUPT_DATA;
1556
1557
0
            if (checksum_enabled && file_has_checksums) {
1558
0
                const uint32_t stored_hash = zxc_le32(footer + sizeof(uint64_t));
1559
0
                if (UNLIKELY(stored_hash != global_hash)) return ZXC_ERROR_BAD_CHECKSUM;
1560
0
            }
1561
0
            break;
1562
0
        }
1563
1564
0
        const size_t rem_cap = (size_t)(op_end - op);
1565
0
        int res;
1566
0
        if (LIKELY(rem_cap >= work_sz)) {
1567
            // Fast path: decode directly into dst (enough padding for wild copies).
1568
0
            res = zxc_decompress_chunk_wrapper(ctx, ip, rem_src, op, rem_cap);
1569
0
        } else {
1570
            // Safe path: decode into bounce buffer, then copy exact result.
1571
0
            res = zxc_decompress_chunk_wrapper(ctx, ip, rem_src, ctx->work_buf, ctx->work_buf_cap);
1572
0
            if (LIKELY(res > 0)) {
1573
0
                if (UNLIKELY((size_t)res > rem_cap))
1574
0
                    return ZXC_ERROR_DST_TOO_SMALL;  // LCOV_EXCL_LINE
1575
0
                ZXC_MEMCPY(op, ctx->work_buf, (size_t)res);
1576
0
            }
1577
0
        }
1578
0
        if (UNLIKELY(res < 0)) return res;
1579
1580
0
        if (checksum_enabled && file_has_checksums) {
1581
0
            const uint32_t block_hash = zxc_le32(ip + ZXC_BLOCK_HEADER_SIZE + bh.comp_size);
1582
0
            global_hash = zxc_hash_combine_rotate(global_hash, block_hash);
1583
0
        }
1584
1585
0
        ip += ZXC_BLOCK_HEADER_SIZE + bh.comp_size +
1586
0
              (file_has_checksums ? ZXC_BLOCK_CHECKSUM_SIZE : 0);
1587
0
        op += res;
1588
0
    }
1589
1590
0
    return (int64_t)(op - op_start);
1591
0
}
1592
1593
/* ========================================================================= */
1594
/*  Block-Level API (no file framing)                                        */
1595
/* ========================================================================= */
1596
1597
/**
1598
 * @brief Compresses a single block (no file framing), reusing @p cctx.
1599
 *
1600
 * Public API; full contract in @c zxc_buffer.h. Produces one format-conformant
1601
 * block with no header / EOF / footer, so @p src_size must not exceed
1602
 * @c ZXC_BLOCK_SIZE_MAX (use the frame or streaming APIs for larger inputs).
1603
 * With a dictionary in @p opts, [dict | block] is assembled in the cctx-owned
1604
 * bounce buffer before encoding. Inner buffers are re-initialised when the
1605
 * effective block size changes, or when a per-call level raise into the
1606
 * optimal-parser tier requires the opt_scratch region; static contexts
1607
 * reject both cases instead (the workspace cannot grow).
1608
 *
1609
 * @param[in,out] cctx          Reusable compression context.
1610
 * @param[in]     src           Source block bytes.
1611
 * @param[in]     src_size      Source length (0 < @p src_size <= @c ZXC_BLOCK_SIZE_MAX).
1612
 * @param[out]    dst           Destination buffer for the block payload.
1613
 * @param[in]     dst_capacity  Capacity of @p dst in bytes.
1614
 * @param[in]     opts          Per-call options (level, dict, ...), or NULL.
1615
 * @return Block payload size in bytes on success, or a negative @ref zxc_error_t.
1616
 */
1617
int64_t zxc_compress_block(zxc_cctx* cctx, const void* RESTRICT src, const size_t src_size,
1618
                           void* RESTRICT dst, const size_t dst_capacity,
1619
4.90k
                           const zxc_compress_opts_t* opts) {
1620
4.90k
    if (UNLIKELY(!cctx || !src || !dst || src_size == 0 || dst_capacity == 0))
1621
0
        return ZXC_ERROR_NULL_INPUT;
1622
1623
    /* Block API processes a single format-conformant block: src_size must not
1624
     * exceed ZXC_BLOCK_SIZE_MAX. Callers with larger inputs should use the
1625
     * frame or streaming APIs which chunk transparently. */
1626
4.90k
    if (UNLIKELY(src_size > ZXC_BLOCK_SIZE_MAX)) return ZXC_ERROR_BAD_BLOCK_SIZE;
1627
1628
4.90k
    const int checksum_enabled = opts ? opts->checksum_enabled : cctx->stored_checksum;
1629
4.90k
    const int level = zxc_level_clamp((opts && opts->level > 0) ? opts->level : cctx->stored_level);
1630
    /* For block API, block_size == src_size (the caller compresses one block at a time). */
1631
4.90k
    const size_t block_size =
1632
4.90k
        (opts && opts->block_size > 0) ? opts->block_size : cctx->stored_block_size;
1633
4.90k
    const size_t min_bs = zxc_block_size_ceil(src_size);
1634
1635
    /* Always ensure internal buffers can hold src_size.
1636
     * When a dictionary is active, offset_bits must accommodate dict + block. */
1637
4.90k
    const uint8_t* b_dict = opts ? (const uint8_t*)opts->dict : NULL;
1638
4.90k
    const size_t b_dict_size = (opts && opts->dict) ? opts->dict_size : 0;
1639
4.90k
    const size_t base_block_size = (block_size > min_bs) ? block_size : min_bs;
1640
4.90k
    const size_t effective_block_size =
1641
4.90k
        b_dict_size > 0 ? zxc_block_size_ceil(b_dict_size + base_block_size) : base_block_size;
1642
1643
    /* Static cctx: the workspace cannot grow, so reject anything forcing a
1644
     * re-partition - another block size, or a level raise into the
1645
     * optimal-parser tier it carries no opt_scratch for. Re-initing on the heap
1646
     * would break the no-allocation contract and leak: zxc_free_cctx is a no-op
1647
     * for static contexts. */
1648
4.90k
    if (UNLIKELY(cctx->owns_workspace && effective_block_size != cctx->last_block_size))
1649
0
        return ZXC_ERROR_BAD_BLOCK_SIZE;
1650
4.90k
    if (UNLIKELY(cctx->owns_workspace && level >= ZXC_LEVEL_DENSITY && !cctx->inner.opt_scratch))
1651
0
        return ZXC_ERROR_BAD_LEVEL;
1652
1653
4.90k
    cctx->stored_level = level;
1654
4.90k
    cctx->stored_block_size = effective_block_size;
1655
4.90k
    cctx->stored_checksum = checksum_enabled;
1656
1657
    /* Re-init when block_size changed, or when a per-call level raise into
1658
     * the optimal-parser tier requires the opt_scratch region that inits at
1659
     * level < ZXC_LEVEL_DENSITY do not allocate (using it NULL would crash). */
1660
4.90k
    if (UNLIKELY(!cctx->initialized || cctx->last_block_size != effective_block_size ||
1661
4.90k
                 (level >= ZXC_LEVEL_DENSITY && !cctx->inner.opt_scratch))) {
1662
0
        if (cctx->initialized) {
1663
            // LCOV_EXCL_START
1664
0
            zxc_cctx_free(&cctx->inner);
1665
0
            cctx->initialized = 0;
1666
            // LCOV_EXCL_STOP
1667
0
        }
1668
        // LCOV_EXCL_START
1669
0
        if (UNLIKELY(zxc_cctx_init(&cctx->inner, effective_block_size, 1, level, checksum_enabled,
1670
0
                                   b_dict_size) != ZXC_OK))
1671
0
            return ZXC_ERROR_MEMORY;
1672
        // LCOV_EXCL_STOP
1673
0
        cctx->last_block_size = effective_block_size;
1674
0
        cctx->initialized = 1;
1675
4.90k
    } else {
1676
4.90k
        cctx->inner.compression_level = level;
1677
4.90k
        cctx->inner.checksum_enabled = checksum_enabled;
1678
4.90k
    }
1679
1680
4.90k
    cctx->inner.dict_size = b_dict_size;
1681
1682
4.90k
    int res;
1683
4.90k
    if (b_dict && b_dict_size > 0) {
1684
        /* [dict | block] assembled in the cctx-owned dict_buffer */
1685
0
        uint8_t* const combined = cctx->inner.dict_buffer;
1686
0
        ZXC_MEMCPY(combined, b_dict, b_dict_size);
1687
0
        ZXC_MEMCPY(combined + b_dict_size, src, src_size);
1688
0
        res = zxc_compress_chunk_wrapper(&cctx->inner, combined, b_dict_size + src_size,
1689
0
                                         (uint8_t*)dst, dst_capacity);
1690
4.90k
    } else {
1691
4.90k
        res = zxc_compress_chunk_wrapper(&cctx->inner, (const uint8_t*)src, src_size, (uint8_t*)dst,
1692
4.90k
                                         dst_capacity);
1693
4.90k
    }
1694
4.90k
    if (UNLIKELY(res < 0)) return res;
1695
4.90k
    return (int64_t)res;
1696
4.90k
}
1697
1698
/**
1699
 * @brief Decompresses a single block (no file framing), reusing @p dctx.
1700
 *
1701
 * Public API; full contract in @c zxc_buffer.h. Decodes one format-conformant
1702
 * block; the decoded payload cannot exceed @c ZXC_BLOCK_SIZE_MAX, so
1703
 * @p dst_capacity is bounded by @c ZXC_BLOCK_SIZE_MAX + @c ZXC_DECOMPRESS_TAIL_PAD.
1704
 * With a dictionary in @p opts the decode runs through the [dict | decode]
1705
 * bounce buffer; otherwise it goes straight into @p dst when the tail padding
1706
 * fits, or via @c work_buf when it doesn't.
1707
 *
1708
 * @param[in,out] dctx          Reusable decompression context.
1709
 * @param[in]     src           Compressed block bytes.
1710
 * @param[in]     src_size      Source length (>= @c ZXC_BLOCK_HEADER_SIZE).
1711
 * @param[out]    dst           Destination for the decoded payload.
1712
 * @param[in]     dst_capacity  Capacity of @p dst in bytes.
1713
 * @param[in]     opts          Per-call options (dict, checksum), or NULL.
1714
 * @return Decoded payload size in bytes on success, or a negative @ref zxc_error_t.
1715
 */
1716
int64_t zxc_decompress_block(zxc_dctx* dctx, const void* RESTRICT src, const size_t src_size,
1717
                             void* RESTRICT dst, const size_t dst_capacity,
1718
0
                             const zxc_decompress_opts_t* opts) {
1719
0
    if (UNLIKELY(!dctx || !src || !dst || src_size < ZXC_BLOCK_HEADER_SIZE || dst_capacity == 0))
1720
0
        return ZXC_ERROR_NULL_INPUT;
1721
1722
    /* One format-conformant block, so the payload cannot exceed
1723
     * ZXC_BLOCK_SIZE_MAX; dst_capacity is bounded to that plus the tail-pad the
1724
     * wild copies need. Larger outputs belong to the frame or streaming APIs. */
1725
0
    if (UNLIKELY(dst_capacity > ZXC_BLOCK_SIZE_MAX + ZXC_DECOMPRESS_TAIL_PAD))
1726
0
        return ZXC_ERROR_BAD_BLOCK_SIZE;
1727
1728
0
    const int checksum_enabled = opts ? opts->checksum_enabled : 0;
1729
1730
0
    const uint8_t* dict = opts ? (const uint8_t*)opts->dict : NULL;
1731
0
    const size_t dict_size = (opts && opts->dict) ? opts->dict_size : 0;
1732
1733
    /* Derive the block_size from dst_capacity (callers know the original size) */
1734
0
    const size_t block_size = zxc_block_size_ceil(dst_capacity);
1735
0
    if (UNLIKELY(!dctx->initialized || dctx->last_block_size != block_size ||
1736
0
                 dctx->last_dict_size != dict_size)) {
1737
0
        if (dctx->initialized) {
1738
0
            zxc_cctx_free(&dctx->inner);
1739
0
            dctx->initialized = 0;
1740
0
        }
1741
        // LCOV_EXCL_START
1742
0
        if (UNLIKELY(zxc_cctx_init(&dctx->inner, block_size, 0, 0, checksum_enabled, dict_size) !=
1743
0
                     ZXC_OK))
1744
0
            return ZXC_ERROR_MEMORY;
1745
        // LCOV_EXCL_STOP
1746
0
        dctx->last_block_size = block_size;
1747
0
        dctx->last_dict_size = dict_size;
1748
0
        dctx->initialized = 1;
1749
0
    } else {
1750
0
        dctx->inner.checksum_enabled = checksum_enabled;
1751
0
    }
1752
1753
0
    zxc_cctx_t* const ctx = &dctx->inner;
1754
0
    ctx->dict_size = dict_size;
1755
1756
    /* work_buf was pre-sized to block_size + ZXC_DECOMPRESS_TAIL_PAD inside
1757
     * the matching zxc_cctx_init call above. */
1758
0
    const size_t work_sz = block_size + ZXC_DECOMPRESS_TAIL_PAD;
1759
1760
0
    int res;
1761
0
    if (dict && dict_size > 0) {
1762
        /* [dict | decode] assembled in the cctx-owned dict_buffer */
1763
0
        uint8_t* const dec_buf = ctx->dict_buffer;
1764
0
        ZXC_MEMCPY(dec_buf, dict, dict_size);
1765
0
        res = zxc_decompress_chunk_wrapper(ctx, (const uint8_t*)src, src_size, dec_buf + dict_size,
1766
0
                                           work_sz);
1767
0
        if (LIKELY(res > 0)) {
1768
0
            if (UNLIKELY((size_t)res > dst_capacity)) return ZXC_ERROR_DST_TOO_SMALL;
1769
0
            ZXC_MEMCPY(dst, dec_buf + dict_size, (size_t)res);
1770
0
        }
1771
0
    } else if (LIKELY(dst_capacity >= work_sz)) {
1772
0
        res = zxc_decompress_chunk_wrapper(ctx, (const uint8_t*)src, src_size, (uint8_t*)dst,
1773
0
                                           dst_capacity);
1774
0
    } else {
1775
        /* Bounce through work_buf when output can't absorb wild copies. */
1776
0
        res = zxc_decompress_chunk_wrapper(ctx, (const uint8_t*)src, src_size, ctx->work_buf,
1777
0
                                           ctx->work_buf_cap);
1778
0
        if (LIKELY(res > 0)) {
1779
0
            if (UNLIKELY((size_t)res > dst_capacity)) return ZXC_ERROR_DST_TOO_SMALL;
1780
0
            ZXC_MEMCPY(dst, ctx->work_buf, (size_t)res);
1781
0
        }
1782
0
    }
1783
0
    if (UNLIKELY(res < 0)) return res;
1784
0
    return (int64_t)res;
1785
0
}
1786
1787
/**
1788
 * @brief Safe-variant block decompressor: accepts dst_capacity == uncompressed_size.
1789
 *
1790
 * Dict inputs and RAW blocks route to @ref zxc_decompress_block; plain GLO/GHI
1791
 * use the strict safe decoder (no bounce buffer, no +ZXC_DECOMPRESS_TAIL_PAD).
1792
 *
1793
 * Public API; full contract in @c zxc_buffer.h.
1794
 *
1795
 * @param[in,out] dctx          Reusable decompression context.
1796
 * @param[in]     src           Compressed block bytes.
1797
 * @param[in]     src_size      Source length (>= @c ZXC_BLOCK_HEADER_SIZE).
1798
 * @param[out]    dst           Destination for the decoded payload.
1799
 * @param[in]     dst_capacity  Exact uncompressed size (<= @c ZXC_BLOCK_SIZE_MAX).
1800
 * @param[in]     opts          Per-call options (dict, checksum), or NULL.
1801
 * @return Decoded payload size in bytes on success, or a negative @ref zxc_error_t.
1802
 */
1803
int64_t zxc_decompress_block_safe(zxc_dctx* dctx, const void* RESTRICT src, const size_t src_size,
1804
                                  void* RESTRICT dst, const size_t dst_capacity,
1805
0
                                  const zxc_decompress_opts_t* opts) {
1806
0
    if (UNLIKELY(!dctx || !src || !dst || src_size < ZXC_BLOCK_HEADER_SIZE || dst_capacity == 0))
1807
0
        return ZXC_ERROR_NULL_INPUT;
1808
1809
    /* Strict-tail variant: dst_capacity matches the exact uncompressed size */
1810
0
    if (UNLIKELY(dst_capacity > ZXC_BLOCK_SIZE_MAX)) return ZXC_ERROR_BAD_BLOCK_SIZE;
1811
1812
    /* A dict needs the [dict|payload] bounce; route to the bounce-capable path. */
1813
0
    if (opts && opts->dict && opts->dict_size > 0) {
1814
0
        return zxc_decompress_block(dctx, src, src_size, dst, dst_capacity, opts);
1815
0
    }
1816
1817
0
    const uint8_t type = ((const uint8_t*)src)[0];
1818
    /* RAW never wild-writes past dst_capacity: route to the existing fast API. */
1819
0
    if (type == ZXC_BLOCK_RAW) {
1820
0
        return zxc_decompress_block(dctx, src, src_size, dst, dst_capacity, opts);
1821
0
    }
1822
1823
    /* GLO/GHI: use the strict-tail decoder (no bounce buffer required). */
1824
0
    const int checksum_enabled = opts ? opts->checksum_enabled : 0;
1825
0
    const size_t block_size = zxc_block_size_ceil(dst_capacity);
1826
0
    if (UNLIKELY(!dctx->initialized || dctx->last_block_size != block_size ||
1827
0
                 dctx->last_dict_size != 0)) {
1828
0
        if (dctx->initialized) {
1829
0
            zxc_cctx_free(&dctx->inner);
1830
0
            dctx->initialized = 0;
1831
0
        }
1832
        // LCOV_EXCL_START
1833
0
        if (UNLIKELY(zxc_cctx_init(&dctx->inner, block_size, 0, 0, checksum_enabled, 0) != ZXC_OK))
1834
0
            return ZXC_ERROR_MEMORY;
1835
        // LCOV_EXCL_STOP
1836
0
        dctx->last_block_size = block_size;
1837
0
        dctx->last_dict_size = 0;
1838
0
        dctx->initialized = 1;
1839
0
    } else {
1840
0
        dctx->inner.checksum_enabled = checksum_enabled;
1841
0
    }
1842
0
    dctx->inner.dict_size = 0;
1843
1844
0
    const int res = zxc_decompress_chunk_wrapper_safe_public(&dctx->inner, (const uint8_t*)src,
1845
0
                                                             src_size, (uint8_t*)dst, dst_capacity);
1846
0
    if (UNLIKELY(res < 0)) return res;
1847
0
    return (int64_t)res;
1848
0
}
1849
1850
/*
1851
 * ============================================================================
1852
 * STATIC CONTEXT API (caller-allocated workspace)
1853
 * ============================================================================
1854
 * Places the public handle struct at the start of the workspace, then carves
1855
 * the persistent buffer (via zxc_cctx_init_in_workspace) in the remaining
1856
 * cache-line-aligned tail.  The caller owns the whole workspace; free
1857
 * functions become no-ops via the owns_workspace flag.
1858
 */
1859
1860
/* Size occupied by the opaque handle at the start of the workspace, rounded
1861
 * up to a cache-line boundary so the persistent buffer (which expects 64 B
1862
 * alignment for the hot zones) starts aligned. */
1863
0
#define ZXC_STATIC_CCTX_HDR_SIZE ZXC_ALIGN_CL(sizeof(struct zxc_cctx_s))
1864
0
#define ZXC_STATIC_DCTX_HDR_SIZE ZXC_ALIGN_CL(sizeof(struct zxc_dctx_s))
1865
1866
/**
1867
 * @brief Workspace size needed for a static compression context.
1868
 *
1869
 * Public API; see @c zxc_buffer.h. Sum of the cache-line-aligned handle header
1870
 * and the persistent buffer that @ref zxc_init_static_cctx carves for the given
1871
 * @p block_size / @p level. Performs no allocation.
1872
 *
1873
 * @param[in] block_size  Block size the context will be pinned to.
1874
 * @param[in] level       Compression level.
1875
 * @return Required workspace size in bytes, or 0 if the parameters are invalid.
1876
 */
1877
0
size_t zxc_static_cctx_workspace_size(const size_t block_size, const int level) {
1878
0
    if (UNLIKELY(!zxc_validate_block_size(block_size))) return 0;
1879
0
    if (UNLIKELY(level < ZXC_LEVEL_FASTEST || level > ZXC_LEVEL_ULTRA)) return 0;
1880
0
    const size_t inner_sz = zxc_cctx_compute_workspace_size(block_size, 1, level, 0);
1881
0
    if (UNLIKELY(inner_sz == 0)) return 0;
1882
0
    return ZXC_STATIC_CCTX_HDR_SIZE + inner_sz;
1883
0
}
1884
1885
/**
1886
 * @brief Initialises a compression context inside a caller-supplied workspace.
1887
 *
1888
 * Public API; full contract in @c zxc_buffer.h. Places the opaque handle at the
1889
 * start of @p workspace and carves the persistent buffer in the aligned tail -
1890
 * no heap allocation. The block size is pinned for the context's lifetime, and
1891
 * @ref zxc_free_cctx becomes a no-op (the caller owns @p workspace).
1892
 *
1893
 * @param[in] workspace       Caller buffer (>= @ref zxc_static_cctx_workspace_size).
1894
 * @param[in] workspace_size  Capacity of @p workspace in bytes.
1895
 * @param[in] opts            Compression options (non-NULL: level, block_size,
1896
 *                            checksum).
1897
 * @return A ready context owned by @p workspace, or NULL on invalid input or an
1898
 *         undersized workspace.
1899
 */
1900
zxc_cctx* zxc_init_static_cctx(void* RESTRICT workspace, const size_t workspace_size,
1901
0
                               const zxc_compress_opts_t* RESTRICT opts) {
1902
0
    if (UNLIKELY(!workspace || !opts)) return NULL;
1903
1904
0
    const int level = (opts->level > 0) ? opts->level : ZXC_LEVEL_DEFAULT;
1905
0
    const size_t block_size = (opts->block_size > 0) ? opts->block_size : ZXC_BLOCK_SIZE_DEFAULT;
1906
0
    const int checksum_enabled = opts->checksum_enabled;
1907
1908
0
    if (UNLIKELY(!zxc_validate_block_size(block_size))) return NULL;
1909
0
    if (UNLIKELY(level < ZXC_LEVEL_FASTEST || level > ZXC_LEVEL_ULTRA)) return NULL;
1910
1911
0
    const size_t inner_sz = zxc_cctx_compute_workspace_size(block_size, 1, level, 0);
1912
0
    if (UNLIKELY(inner_sz == 0)) return NULL;
1913
0
    if (UNLIKELY(workspace_size < ZXC_STATIC_CCTX_HDR_SIZE + inner_sz)) return NULL;
1914
1915
0
    zxc_cctx* const cctx = (zxc_cctx*)workspace;
1916
0
    ZXC_MEMSET(cctx, 0, sizeof(*cctx));
1917
1918
0
    uint8_t* const inner_ws = (uint8_t*)workspace + ZXC_STATIC_CCTX_HDR_SIZE;
1919
0
    if (UNLIKELY(zxc_cctx_init_in_workspace(&cctx->inner, inner_ws, inner_sz, block_size, 1, level,
1920
0
                                            checksum_enabled, 0, 0) != ZXC_OK))
1921
0
        return NULL;
1922
1923
0
    cctx->owns_workspace = 1;
1924
0
    cctx->initialized = 1;
1925
0
    cctx->last_block_size = block_size;
1926
0
    cctx->stored_level = level;
1927
0
    cctx->stored_block_size = block_size;
1928
0
    cctx->stored_checksum = checksum_enabled;
1929
0
    return cctx;
1930
0
}
1931
1932
/**
1933
 * @brief Workspace size needed for a static decompression context.
1934
 *
1935
 * Public API; see @c zxc_buffer.h. Sum of the cache-line-aligned handle header
1936
 * and the persistent buffer that @ref zxc_init_static_dctx carves for the given
1937
 * @p block_size. Performs no allocation.
1938
 *
1939
 * @param[in] block_size  Block size the context will be pinned to.
1940
 * @return Required workspace size in bytes, or 0 if @p block_size is invalid.
1941
 */
1942
0
size_t zxc_static_dctx_workspace_size(const size_t block_size) {
1943
0
    if (UNLIKELY(!zxc_validate_block_size(block_size))) return 0;
1944
0
    const size_t inner_sz = zxc_cctx_compute_workspace_size(block_size, 0, 0, 0);
1945
0
    if (UNLIKELY(inner_sz == 0)) return 0;
1946
0
    return ZXC_STATIC_DCTX_HDR_SIZE + inner_sz;
1947
0
}
1948
1949
/**
1950
 * @brief Initialises a decompression context inside a caller-supplied workspace.
1951
 *
1952
 * Public API; full contract in @c zxc_buffer.h. Places the opaque handle at the
1953
 * start of @p workspace and carves the persistent buffer in the aligned tail -
1954
 * no heap allocation. The block size is pinned, so decoded archives must match
1955
 * it; @ref zxc_free_dctx becomes a no-op (the caller owns @p workspace).
1956
 *
1957
 * @param[in] workspace       Caller buffer (>= @ref zxc_static_dctx_workspace_size).
1958
 * @param[in] workspace_size  Capacity of @p workspace in bytes.
1959
 * @param[in] block_size      Block size to pin the context to.
1960
 * @return A ready context owned by @p workspace, or NULL on invalid input or an
1961
 *         undersized workspace.
1962
 */
1963
zxc_dctx* zxc_init_static_dctx(void* RESTRICT workspace, const size_t workspace_size,
1964
0
                               const size_t block_size) {
1965
0
    if (UNLIKELY(!workspace)) return NULL;
1966
0
    if (UNLIKELY(!zxc_validate_block_size(block_size))) return NULL;
1967
1968
0
    const size_t inner_sz = zxc_cctx_compute_workspace_size(block_size, 0, 0, 0);
1969
0
    if (UNLIKELY(inner_sz == 0)) return NULL;
1970
0
    if (UNLIKELY(workspace_size < ZXC_STATIC_DCTX_HDR_SIZE + inner_sz)) return NULL;
1971
1972
0
    zxc_dctx* const dctx = (zxc_dctx*)workspace;
1973
0
    ZXC_MEMSET(dctx, 0, sizeof(*dctx));
1974
1975
0
    uint8_t* const inner_ws = (uint8_t*)workspace + ZXC_STATIC_DCTX_HDR_SIZE;
1976
    /* mode == 0 init: checksum_enabled is updated per-call from the file
1977
     * header flags, so it does not need to be locked at workspace init. */
1978
0
    if (UNLIKELY(zxc_cctx_init_in_workspace(&dctx->inner, inner_ws, inner_sz, block_size, 0, 0, 0,
1979
0
                                            0, 0) != ZXC_OK))
1980
0
        return NULL;
1981
1982
0
    dctx->owns_workspace = 1;
1983
0
    dctx->initialized = 1;
1984
0
    dctx->last_block_size = block_size;
1985
0
    return dctx;
1986
0
}