cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
compress.c
Go to the documentation of this file.
1
6#define _POSIX_C_SOURCE 200809L
10#include <zlib.h>
11#include <brotli/encode.h>
12#include <zstd.h>
13#include <string.h>
14#include <strings.h>
15#include <stdio.h>
16#include <stdlib.h>
17#include "../../net/http/async_internal.h"
18
19#define CWIST_MAX_BACKENDS 8
20
21static const cwist_compress_backend *g_backends[CWIST_MAX_BACKENDS];
22static int g_backend_count = 0;
23static size_t g_compress_min_size = 0;
24
31 if (!backend || g_backend_count >= CWIST_MAX_BACKENDS) return;
32 g_backends[g_backend_count++] = backend;
33}
34
39 g_backend_count = 0;
40}
41
42/* --- zlib/gzip backend --- */
43
50static int zlib_gzip_init(void **state) {
51 z_stream *zs = (z_stream *)calloc(1, sizeof(z_stream));
52 if (!zs) return -1;
54 Z_OK) {
55 free(zs);
56 return -1;
57 }
58 *state = zs;
59 return 0;
60}
61
68static int zlib_deflate_init(void **state) {
69 z_stream *zs = (z_stream *)calloc(1, sizeof(z_stream));
70 if (!zs) return -1;
72 free(zs);
73 return -1;
74 }
75 *state = zs;
76 return 0;
77}
78
90static int zlib_compress(void *state, const char *in, size_t in_len, char *out, size_t *out_len,
91 int flush) {
92 z_stream *zs = (z_stream *)state;
93 zs->next_in = (Bytef *)in;
94 zs->avail_in = (uInt)in_len;
95 zs->next_out = (Bytef *)out;
96 zs->avail_out = (uInt)(*out_len);
98 /* All-or-nothing: the caller advances the input by the full chunk, so a
99 * partially consumed chunk would silently drop the tail of the body.
100 * Report an error instead; the middleware then serves the body
101 * uncompressed. */
102 if (flush) {
103 if (rc != Z_STREAM_END) return -1;
104 } else {
105 if (rc != Z_OK || zs->avail_in != 0) return -1;
106 }
107 *out_len = (size_t)(*out_len - zs->avail_out);
108 return 0;
109}
110
119static int zlib_finish(void *state, char *out, size_t *out_len) {
120 z_stream *zs = (z_stream *)state;
121 zs->next_in = NULL;
122 zs->avail_in = 0;
123 zs->next_out = (Bytef *)out;
124 zs->avail_out = (uInt)(*out_len);
125 int rc = deflate(zs, Z_FINISH);
126 /* Z_BUF_ERROR here means the trailer did not fit; accepting it would
127 * emit a truncated gzip stream. */
128 if (rc != Z_STREAM_END) return -1;
129 *out_len = (size_t)(*out_len - zs->avail_out);
130 return 0;
131}
132
137static void zlib_cleanup(void *state) {
138 z_stream *zs = (z_stream *)state;
139 if (zs) {
140 deflateEnd(zs);
141 free(zs);
142 }
143}
144
145static const cwist_compress_backend cwist_backend_gzip = {
146 .encoding_name = "gzip",
147 .init = zlib_gzip_init,
148 .compress = zlib_compress,
149 .finish = zlib_finish,
150 .cleanup = zlib_cleanup,
151};
152
153static const cwist_compress_backend cwist_backend_deflate = {
154 .encoding_name = "deflate",
155 .init = zlib_deflate_init,
156 .compress = zlib_compress,
157 .finish = zlib_finish,
158 .cleanup = zlib_cleanup,
159};
160
166 return &cwist_backend_gzip;
167}
168
174 return &cwist_backend_deflate;
175}
176
177/* --- Brotli backend --- */
178
179typedef struct {
180 char *accum;
181 size_t accum_len;
182 size_t accum_cap;
183 char *out;
184 size_t out_len;
185 size_t out_pos;
186} cwist_brotli_state_t;
187
194static int brotli_init(void **state) {
195 cwist_brotli_state_t *bs = (cwist_brotli_state_t *)calloc(1, sizeof(*bs));
196 if (!bs) return -1;
197 *state = bs;
198 return 0;
199}
200
208static int brotli_ensure_accum(cwist_brotli_state_t *bs, size_t need) {
209 if (!bs) return -1;
210 size_t required = bs->accum_len + need;
211 if (required > bs->accum_cap) {
212 size_t new_cap = bs->accum_cap ? bs->accum_cap * 2 : 4096;
213 while (new_cap < required) new_cap *= 2;
214 char *tmp = (char *)realloc(bs->accum, new_cap);
215 if (!tmp) return -1;
216 bs->accum = tmp;
217 bs->accum_cap = new_cap;
218 }
219 return 0;
220}
221
239static int brotli_compress(void *state, const char *in, size_t in_len, char *out, size_t *out_len,
240 int flush) {
241 cwist_brotli_state_t *bs = (cwist_brotli_state_t *)state;
242 if (!bs || !out || !out_len) return -1;
243
244 if (!flush) {
245 if (in_len == 0) {
246 *out_len = 0;
247 return 0;
248 }
249 if (brotli_ensure_accum(bs, in_len) != 0) return -1;
250 memcpy(bs->accum + bs->accum_len, in, in_len);
251 bs->accum_len += in_len;
252 *out_len = 0;
253 return 0;
254 }
255
256 /* On flush, compress accumulated input plus the final chunk. */
257 size_t total_in_len = bs->accum_len + in_len;
258 const uint8_t *total_in = NULL;
260 if (bs->accum_len > 0) {
262 if (!merged) return -1;
263 if (bs->accum_len > 0) memcpy(merged, bs->accum, bs->accum_len);
264 if (in_len > 0) memcpy(merged + bs->accum_len, in, in_len);
266 } else {
267 total_in = (const uint8_t *)in;
268 }
269
271 if (encoded_size == 0) {
272 free(merged);
273 return -1;
274 }
275
277 if (!encoded) {
278 free(merged);
279 return -1;
280 }
281
284 free(encoded);
285 free(merged);
286 return -1;
287 }
288
289 free(merged);
290 bs->accum_len = 0;
291
292 /* Replace any previous output buffer with the new one. */
293 free(bs->out);
294 bs->out = (char *)encoded;
295 bs->out_len = encoded_size;
296 bs->out_pos = 0;
297
298 size_t to_copy = encoded_size;
299 if (to_copy > *out_len) to_copy = *out_len;
300 if (to_copy > 0 && out) memcpy(out, encoded, to_copy);
301 bs->out_pos = to_copy;
302 *out_len = to_copy;
303 return 0;
304}
305
314static int brotli_finish(void *state, char *out, size_t *out_len) {
315 cwist_brotli_state_t *bs = (cwist_brotli_state_t *)state;
316 if (!bs || !out || !out_len) return -1;
317 size_t remaining = bs->out_len - bs->out_pos;
318 size_t to_copy = remaining;
319 if (to_copy > *out_len) to_copy = *out_len;
320 if (to_copy > 0) memcpy(out, bs->out + bs->out_pos, to_copy);
321 bs->out_pos += to_copy;
322 *out_len = to_copy;
323 return 0;
324}
325
330static void brotli_cleanup(void *state) {
331 cwist_brotli_state_t *bs = (cwist_brotli_state_t *)state;
332 if (!bs) return;
333 free(bs->accum);
334 free(bs->out);
335 free(bs);
336}
337
338static const cwist_compress_backend cwist_backend_brotli = {
339 .encoding_name = "br",
340 .init = brotli_init,
341 .compress = brotli_compress,
342 .finish = brotli_finish,
343 .cleanup = brotli_cleanup,
344};
345
351 return &cwist_backend_brotli;
352}
353
354/* --- Zstandard (zstd) backend --- */
355
356typedef struct {
357 ZSTD_CStream *cstream;
358 char *accum;
359 size_t accum_len;
360 size_t accum_cap;
361 char *out;
362 size_t out_len;
363 size_t out_pos;
364} cwist_zstd_state_t;
365
372static int zstd_init(void **state) {
373 cwist_zstd_state_t *zs = (cwist_zstd_state_t *)calloc(1, sizeof(*zs));
374 if (!zs) return -1;
375 zs->cstream = ZSTD_createCStream();
376 if (!zs->cstream) {
377 free(zs);
378 return -1;
379 }
380 size_t rc = ZSTD_initCStream(zs->cstream, ZSTD_CLEVEL_DEFAULT);
381 if (ZSTD_isError(rc)) {
382 ZSTD_freeCStream(zs->cstream);
383 free(zs);
384 return -1;
385 }
386 *state = zs;
387 return 0;
388}
389
397static int zstd_ensure_accum(cwist_zstd_state_t *zs, size_t need) {
398 size_t required = zs->accum_len + need;
399 if (required > zs->accum_cap) {
400 size_t new_cap = zs->accum_cap ? zs->accum_cap * 2 : 4096;
401 while (new_cap < required) new_cap *= 2;
402 char *tmp = (char *)realloc(zs->accum, new_cap);
403 if (!tmp) return -1;
404 zs->accum = tmp;
405 zs->accum_cap = new_cap;
406 }
407 return 0;
408}
409
427static int zstd_compress(void *state, const char *in, size_t in_len, char *out, size_t *out_len,
428 int flush) {
429 cwist_zstd_state_t *zs = (cwist_zstd_state_t *)state;
430 if (!zs || !out || !out_len) return -1;
431
432 if (!flush) {
433 if (in_len == 0) {
434 *out_len = 0;
435 return 0;
436 }
437 if (zstd_ensure_accum(zs, in_len) != 0) return -1;
438 memcpy(zs->accum + zs->accum_len, in, in_len);
439 zs->accum_len += in_len;
440 *out_len = 0;
441 return 0;
442 }
443
444 size_t total_in_len = zs->accum_len + in_len;
445 char *total_in = (char *)malloc(total_in_len);
446 if (!total_in) return -1;
447 if (zs->accum_len > 0) memcpy(total_in, zs->accum, zs->accum_len);
448 if (in_len > 0) memcpy(total_in + zs->accum_len, in, in_len);
449
450 size_t out_cap = ZSTD_compressBound(total_in_len);
451 char *out_buf = (char *)malloc(out_cap);
452 if (!out_buf) {
453 free(total_in);
454 return -1;
455 }
456
458 ZSTD_outBuffer ob = {out_buf, out_cap, 0};
459
460 size_t rc = ZSTD_compressStream2(zs->cstream, &ob, &ib, ZSTD_e_end);
461 free(total_in);
462 if (ZSTD_isError(rc)) {
463 free(out_buf);
464 return -1;
465 }
466
467 zs->accum_len = 0;
468 free(zs->out);
469 zs->out = out_buf;
470 zs->out_len = ob.pos;
471 zs->out_pos = 0;
472
473 size_t to_copy = (ob.pos < *out_len) ? ob.pos : *out_len;
474 memcpy(out, out_buf, to_copy);
475 zs->out_pos = to_copy;
476 *out_len = to_copy;
477 return 0;
478}
479
488static int zstd_finish(void *state, char *out, size_t *out_len) {
489 cwist_zstd_state_t *zs = (cwist_zstd_state_t *)state;
490 if (!zs || !out || !out_len) return -1;
491 size_t remaining = zs->out_len - zs->out_pos;
492 size_t to_copy = (remaining < *out_len) ? remaining : *out_len;
493 if (to_copy > 0) memcpy(out, zs->out + zs->out_pos, to_copy);
494 zs->out_pos += to_copy;
495 *out_len = to_copy;
496 return 0;
497}
498
503static void zstd_cleanup(void *state) {
504 cwist_zstd_state_t *zs = (cwist_zstd_state_t *)state;
505 if (!zs) return;
506 if (zs->cstream) ZSTD_freeCStream(zs->cstream);
507 free(zs->accum);
508 free(zs->out);
509 free(zs);
510}
511
512static const cwist_compress_backend cwist_backend_zstd = {
513 .encoding_name = "zstd",
514 .init = zstd_init,
515 .compress = zstd_compress,
516 .finish = zstd_finish,
517 .cleanup = zstd_cleanup,
518};
519
525 return &cwist_backend_zstd;
526}
527/* --- Middleware helpers --- */
528
536static int str_contains_ci(const char *haystack, const char *needle) {
537 if (!haystack || !needle) return 0;
538 size_t hlen = strlen(haystack);
539 size_t nlen = strlen(needle);
540 if (nlen == 0) return 1;
541 if (hlen < nlen) return 0;
542 for (size_t i = 0; i <= hlen - nlen; ++i) {
543 size_t j = 0;
544 for (; j < nlen; ++j) {
545 char a = haystack[i + j];
546 char b = needle[j];
547 if (a >= 'A' && a <= 'Z') a = a - 'A' + 'a';
548 if (b >= 'A' && b <= 'Z') b = b - 'A' + 'a';
549 if (a != b) break;
550 }
551 if (j == nlen) return 1;
552 }
553 return 0;
554}
555
562static const cwist_compress_backend *select_backend(const char *accept_encoding) {
563 if (!accept_encoding) return NULL;
564 /* Prefer gzip, then deflate */
565 for (int i = 0; i < g_backend_count; ++i) {
566 if (str_contains_ci(accept_encoding, g_backends[i]->encoding_name)) {
567 return g_backends[i];
568 }
569 }
570 return NULL;
571}
572
582static void remove_header(cwist_http_header_node **head, const char *key) {
584 while (*curr) {
585 if ((*curr)->key && (*curr)->key->data && strcasecmp((*curr)->key->data, key) == 0) {
587 *curr = (*curr)->next;
590 if (!to_free->arena_owned) {
592 }
593 continue;
594 }
595 curr = &(*curr)->next;
596 }
597}
598
613static void cwist_mw_compress_handler(cwist_http_request *req, cwist_http_response *res,
614 cwist_handler_func next) {
615 const char *accept = cwist_http_header_get(req->headers, "Accept-Encoding");
616 const cwist_compress_backend *backend = select_backend(accept);
617
618 next(req, res);
619
620 /* A deferred response belongs to its cwist_async completion, which may
621 * be writing it on another thread right now (cwist_async_respond(), the
622 * timeout job); leave it alone. */
623 if (cwist_async_res_deferred(res)) return;
624 if (!backend) return;
625 if (!res->body || res->body->size == 0) return;
626 if (res->is_ptr_body || res->use_file_stream) return;
627 if (res->status_code < 200 || res->status_code >= 300) return;
628 if (res->body->size < g_compress_min_size) return;
629
630 void *state = NULL;
631 if (backend->init(&state) != 0) return;
632
633 size_t out_cap = res->body->size + 256;
634 char *out_buf = (char *)cwist_malloc(out_cap);
635 if (!out_buf) {
636 backend->cleanup(state);
637 return;
638 }
639
640 size_t total_out = 0;
641 int ok = 1;
642
643 /* Stream the entire body through the compressor */
644 size_t in_offset = 0;
645 while (in_offset < res->body->size && ok) {
646 size_t chunk_in = res->body->size - in_offset;
647 size_t chunk_out = out_cap - total_out;
648 if (chunk_out == 0) {
649 /* Grow output buffer */
650 size_t new_cap = out_cap * 2;
651 char *new_buf = (char *)cwist_realloc(out_buf, new_cap);
652 if (!new_buf) {
653 ok = 0;
654 break;
655 }
656 out_buf = new_buf;
657 out_cap = new_cap;
658 chunk_out = out_cap - total_out;
659 }
660 int flush = (in_offset + chunk_in >= res->body->size) ? 1 : 0;
661 size_t written = chunk_out;
662 if (backend->compress(state, res->body->data + in_offset, chunk_in, out_buf + total_out,
663 &written, flush) != 0) {
664 ok = 0;
665 break;
666 }
669 }
670
671 if (ok) {
672 /* Drain trailing bytes, growing the buffer until the backend is
673 * exhausted (brotli/zstd buffer the whole stream internally, so a
674 * single fixed-size tail can truncate the output). */
675 for (;;) {
676 size_t tail = out_cap - total_out;
677 if (tail == 0) {
678 size_t new_cap = out_cap * 2;
679 char *new_buf = (char *)cwist_realloc(out_buf, new_cap);
680 if (!new_buf) {
681 ok = 0;
682 break;
683 }
684 out_buf = new_buf;
685 out_cap = new_cap;
686 continue;
687 }
688 size_t wrote = tail;
689 if (backend->finish(state, out_buf + total_out, &wrote) != 0) {
690 ok = 0;
691 break;
692 }
693 total_out += wrote;
694 if (wrote < tail) break; /* drained */
695 }
696 }
697
698 backend->cleanup(state);
699
700 if (ok && total_out > 0 && total_out < res->body->size) {
702 cwist_http_header_add(&res->headers, "Content-Encoding", backend->encoding_name);
703 remove_header(&res->headers, "Content-Length");
704 }
705 cwist_free(out_buf);
706}
707
714 g_compress_min_size = min_body_size;
715 return cwist_mw_compress_handler;
716}
Memory allocation wrappers backed by libttak.
void cwist_free(void *ptr)
Free memory obtained via cwist_alloc/cwist_strdup/etc.
Definition alloc.c:52
void * cwist_realloc(void *ptr, size_t new_size)
Resize an existing allocation (libttak tracked).
Definition alloc.c:27
void * cwist_malloc(size_t size)
Allocate zeroed memory via the CWIST owner context.
Definition alloc.c:13
bool cwist_async_res_deferred(const cwist_http_response *res)
Definition async_tls.c:71
const cwist_compress_backend * cwist_compress_backend_zstd(void)
Get the built-in zstd backend.
Definition compress.c:524
const cwist_compress_backend * cwist_compress_backend_deflate(void)
Get the built-in deflate backend.
Definition compress.c:173
void cwist_compress_register_backend(const cwist_compress_backend *backend)
Register a compression backend for use by the middleware.
Definition compress.c:30
const cwist_compress_backend * cwist_compress_backend_brotli(void)
Get the built-in Brotli backend.
Definition compress.c:350
cwist_middleware_func cwist_mw_compress(size_t min_body_size)
Create the response compression middleware.
Definition compress.c:713
void cwist_compress_unregister_all(void)
Remove all registered compression backends.
Definition compress.c:38
#define CWIST_MAX_BACKENDS
Definition compress.c:19
const cwist_compress_backend * cwist_compress_backend_gzip(void)
Get the built-in gzip backend.
Definition compress.c:165
Compression middleware with swappable backend interface.
cwist_error_t cwist_sstring_assign_len(cwist_sstring *str, const char *data, size_t len)
Assign raw bytes with length.
Definition sstring.c:88
void cwist_sstring_destroy(cwist_sstring *str)
Destroy an sstring instance.
Definition sstring.c:823
char * cwist_http_header_get(cwist_http_header_node *head, const char *key)
Find a header value by key.
Definition http.c:1880
cwist_error_t cwist_http_header_add(cwist_http_header_node **head, const char *key, const char *value)
Add a header to the list.
Definition http.c:1861
HTTP Protocol Definitions and Helpers.
size_t head
Definition otel.c:57
size_t cap
Definition otel.c:56
Compression backend vtable.
Definition compress.h:23
const char * encoding_name
Definition compress.h:24
int(* init)(void **state)
Initialize compressor state.
Definition compress.h:27
int(* compress)(void *state, const char *in, size_t in_len, char *out, size_t *out_len, int flush)
Compress one chunk.
Definition compress.h:39
int(* finish)(void *state, char *out, size_t *out_len)
Finish compression and write any trailing bytes.
Definition compress.h:49
void(* cleanup)(void *state)
Clean up compressor state.
Definition compress.h:52
Linked list for headers to handle multiple headers easily.
Definition http.h:59
struct cwist_http_header_node * next
Definition http.h:62
cwist_http_header_node * headers
Definition http.h:72
HTTP Response Object. Supports standard string body or Zero-Copy pointer body.
Definition http.h:82
bool use_file_stream
Fast File Streaming.
Definition http.h:216
cwist_sstring * body
Definition http.h:87
cwist_http_status_t status_code
Definition http.h:84
cwist_http_header_node * headers
Definition http.h:86
bool is_ptr_body
Zero-Copy Pointer Body.
Definition http.h:209
size_t size
Definition sstring.h:20
char * data
Access directly only when raw handling is necessary.
Definition sstring.h:15
void(* cwist_handler_func)(cwist_http_request *req, cwist_http_response *res)
Function pointer type for HTTP route handlers.
Definition app.h:32
void(* cwist_middleware_func)(cwist_http_request *req, cwist_http_response *res, cwist_handler_func next)
Middleware type that receives req/res pair and the next stage in the chain.
Definition app.h:115