cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
assets.c
Go to the documentation of this file.
1
11#include "assets_internal.h"
15#include <stdatomic.h>
16#include <stdio.h>
17#include <string.h>
18
19#define ASSET_HASH_HEX 16
20
21struct cwist_asset {
22 char *name;
23 char *hashed_name;
24 char *url;
25 char *content_type;
26 char etag[ASSET_HASH_HEX + 3];
27 unsigned char *data;
28 size_t len;
29 struct cwist_asset *next;
30};
31
32typedef struct cwist_asset_registry {
33 char *prefix;
34 _Atomic(struct cwist_asset *) head;
36
38static cwist_error_t asset_result(int16_t code) {
40 err.error.err_i16 = code;
41 return err;
42}
43
45static char *join3(const char *a, const char *b, const char *c) {
46 size_t la = strlen(a), lb = strlen(b), lc = strlen(c);
47 char *out = (char *)cwist_alloc(la + lb + lc + 1);
48 if (!out) return NULL;
49 memcpy(out, a, la);
50 memcpy(out + la, b, lb);
51 memcpy(out + la + lb, c, lc + 1);
52 return out;
53}
54
56static char *normalize_prefix(const char *url_prefix) {
57 if (!url_prefix || url_prefix[0] != '/') return NULL;
58 size_t len = strlen(url_prefix);
59 while (len > 0 && url_prefix[len - 1] == '/') len--;
60
61 for (size_t i = 0; i < len; i++) {
62 unsigned char c = (unsigned char)url_prefix[i];
63 if (c <= 0x20 || c == 0x7f || c == '?' || c == '#' || c == '%') return NULL;
64 }
65
66 char *out = (char *)cwist_alloc(len + 1);
67 if (!out) return NULL;
68
69 memcpy(out, url_prefix, len);
70 out[len] = '\0';
71 return out;
72}
73
75static bool name_is_valid(const char *name) {
76 if (!name || !*name || name[0] == '/') return false;
77 const char *seg = name;
78 for (const char *p = name;; p++) {
79 char c = *p;
80 if (c == '/' || c == '\0') {
81 size_t seg_len = (size_t)(p - seg);
82 if (seg_len == 0) return false;
83 if (seg_len == 1 && seg[0] == '.') return false;
84 if (seg_len == 2 && seg[0] == '.' && seg[1] == '.') return false;
85 if (c == '\0') return true;
86 seg = p + 1;
87 continue;
88 }
89 bool ok = (c >= 'a' && c <= 'z')
90 || (c >= 'A' && c <= 'Z')
91 || (c >= '0' && c <= '9')
92 || c == '.' || c == '-'
93 || c == '_' || c == '~';
94 if (!ok) return false;
95 }
96}
97
99static size_t extension_offset(const char *name) {
100 const char *base = strrchr(name, '/');
101 base = base ? base + 1 : name;
102 const char *dot = strrchr(base, '.');
103 /* A leading dot names a dotfile, not an extension. */
104 if (!dot || dot == base) return strlen(name);
105 return (size_t)(dot - name);
106}
107
109static const char *guess_content_type(const char *name) {
110 static const struct {
111 const char *ext;
112 const char *type;
113 } table[] = {
114 {".css", "text/css; charset=utf-8"},
115 {".js", "text/javascript; charset=utf-8"},
116 {".mjs", "text/javascript; charset=utf-8"},
117 {".json", "application/json"},
118 {".map", "application/json"},
119 {".html", "text/html; charset=utf-8"},
120 {".htm", "text/html; charset=utf-8"},
121 {".txt", "text/plain; charset=utf-8"},
122 {".svg", "image/svg+xml"},
123 {".png", "image/png"},
124 {".jpg", "image/jpeg"},
125 {".jpeg", "image/jpeg"},
126 {".gif", "image/gif"},
127 {".webp", "image/webp"},
128 {".ico", "image/x-icon"},
129 {".woff", "font/woff"},
130 {".woff2", "font/woff2"},
131 {".wasm", "application/wasm"},
132 };
133 const char *ext = name + extension_offset(name);
134 for (size_t i = 0; i < sizeof(table) / sizeof(table[0]); i++) {
135 const char *a = ext, *b = table[i].ext;
136 while (*a && *b && (*a | 0x20) == *b) {
137 a++;
138 b++;
139 }
140 if (*a == '\0' && *b == '\0') return table[i].type;
141 }
142 return "application/octet-stream";
143}
144
146static void content_hash(const void *data, size_t len, char out[ASSET_HASH_HEX + 1]) {
147 static const char hex[] = "0123456789abcdef";
150 cwist_sha256_init(&ctx);
151 if (len > 0) cwist_sha256_update(&ctx, (const uint8_t *)data, len);
152 cwist_sha256_final(&ctx, digest);
153 for (size_t i = 0; i < ASSET_HASH_HEX / 2; i++) {
154 out[2 * i] = hex[digest[i] >> 4];
155 out[2 * i + 1] = hex[digest[i] & 0xf];
156 }
157 out[ASSET_HASH_HEX] = '\0';
158}
159
161static void asset_free(struct cwist_asset *asset) {
162 if (!asset) return;
163 cwist_free(asset->name);
164 cwist_free(asset->hashed_name);
165 cwist_free(asset->url);
166 cwist_free(asset->content_type);
167 cwist_free(asset->data);
168 cwist_free(asset);
169}
170
172static cwist_asset_registry *registry_get(cwist_app *app, bool create) {
173 if (app->assets || !create) return (cwist_asset_registry *)app->assets;
175 if (!reg) return NULL;
177 if (!reg->prefix) {
179 return NULL;
180 }
181 atomic_init(&reg->head, NULL);
182 app->assets = reg;
183 return reg;
184}
185
197cwist_error_t cwist_app_asset_prefix(cwist_app *app, const char *url_prefix) {
198 if (!app) return asset_result(-1);
199 char *prefix = normalize_prefix(url_prefix);
200 if (!prefix) return asset_result(-1);
201 cwist_asset_registry *reg = registry_get(app, true);
203 cwist_free(prefix);
204 return asset_result(-1);
205 }
206 cwist_free(reg->prefix);
207 reg->prefix = prefix;
208 return asset_result(0);
209}
210
212static struct cwist_asset *asset_build(const cwist_asset_registry *reg, const char *name,
213 unsigned char *data, size_t len, const char *content_type,
214 const char *hash) {
215 struct cwist_asset *asset = (struct cwist_asset *)cwist_alloc(sizeof(*asset));
216 if (!asset) {
217 cwist_free(data);
218 return NULL;
219 }
220 asset->data = data;
221 asset->len = len;
222
223 size_t ext = extension_offset(name);
224 char *stem = (char *)cwist_alloc(ext + 1);
225 if (stem) {
226 memcpy(stem, name, ext);
227 stem[ext] = '\0';
228 }
229 char *stem_hash = stem ? join3(stem, ".", hash) : NULL;
231
232 asset->name = cwist_strdup(name);
233 asset->hashed_name = stem_hash ? join3(stem_hash, name + ext, "") : NULL;
235 asset->url = asset->hashed_name ? join3(reg->prefix, "/", asset->hashed_name) : NULL;
236 asset->content_type = cwist_strdup(content_type ? content_type : guess_content_type(name));
237 snprintf(asset->etag, sizeof(asset->etag), "\"%s\"", hash);
238
239 if (!asset->name || !asset->hashed_name || !asset->url || !asset->content_type) {
240 asset_free(asset);
241 return NULL;
242 }
243 return asset;
244}
245
247static bool header_value_is_valid(const char *value) {
248 for (const unsigned char *p = (const unsigned char *)value; *p; p++) {
249 if (*p < 0x20 || *p == 0x7f) return false;
250 }
251 return true;
252}
253
255static cwist_error_t asset_publish(cwist_app *app, const char *name, unsigned char *data,
256 size_t len, const char *content_type) {
257 cwist_asset_registry *reg = registry_get(app, true);
258 if (!reg) {
259 cwist_free(data);
260 return asset_result(-1);
261 }
262
263 char hash[ASSET_HASH_HEX + 1];
264 content_hash(data, len, hash);
265
266 /* The newest asset under this name already has this content and type:
267 * nothing to publish. An older match does not count, since the name
268 * currently resolves to something else. */
269 const char *type = content_type ? content_type : guess_content_type(name);
270 for (struct cwist_asset *a = atomic_load_explicit(&reg->head, memory_order_acquire); a;
271 a = a->next) {
272 if (strcmp(a->name, name) != 0) continue;
273 if (memcmp(a->etag + 1, hash, ASSET_HASH_HEX) == 0 && a->len == len &&
274 (len == 0 || memcmp(a->data, data, len) == 0) && strcmp(a->content_type, type) == 0) {
275 cwist_free(data);
276 return asset_result(0);
277 }
278 break;
279 }
280
281 struct cwist_asset *asset = asset_build(reg, name, data, len, content_type, hash);
282 if (!asset) return asset_result(-1);
283
284 struct cwist_asset *head = atomic_load_explicit(&reg->head, memory_order_relaxed);
285 do {
286 asset->next = head;
289 return asset_result(0);
290}
291
306cwist_error_t cwist_app_asset_add(cwist_app *app, const char *name, const void *data, size_t len,
307 const char *content_type) {
308 if (!app || !name_is_valid(name)
309 || (!data && len > 0))
310 return asset_result(-1);
311
312 if (content_type
313 && (!*content_type || !header_value_is_valid(content_type))) {
314 return asset_result(-1);
315 }
316 unsigned char *copy = (unsigned char *)cwist_alloc(len ? len : 1);
317
318 if (!copy) return asset_result(-1);
319 if (len > 0) memcpy(copy, data, len);
320 return asset_publish(app, name, copy, len, content_type);
321}
322
336cwist_error_t cwist_app_asset_add_file(cwist_app *app, const char *name, const char *path,
337 const char *content_type) {
338 if (!app || !name_is_valid(name) || !path) return asset_result(-1);
339 if (content_type && (!*content_type || !header_value_is_valid(content_type))) {
340 return asset_result(-1);
341 }
342
343 FILE *f = fopen(path, "rb");
344 if (!f) return asset_result(-1);
345
346 size_t cap = 4096, len = 0;
347 unsigned char *buf = (unsigned char *)cwist_alloc(cap);
348 bool ok = buf != NULL;
349
350 while (ok) {
351 if (len == cap) {
352 unsigned char *grown = (unsigned char *)cwist_realloc(buf, cap * 2);
353 if (!grown) {
354 ok = false;
355 break;
356 }
357 buf = grown;
358 cap *= 2;
359 }
360 size_t n = fread(buf + len, 1, cap - len, f);
361 len += n;
362 if (n == 0) {
363 ok = !ferror(f);
364 break;
365 }
366 }
367 fclose(f);
368
369 if (!ok) {
370 cwist_free(buf);
371 return asset_result(-1);
372 }
373
374 return asset_publish(app, name, buf, len, content_type);
375}
376
386const char *cwist_app_asset_url(cwist_app *app, const char *name) {
387 if (!app || !name) return NULL;
388
389 cwist_asset_registry *reg = registry_get(app, false);
390 if (!reg) return NULL;
391
392 for (struct cwist_asset *a = atomic_load_explicit(&reg->head, memory_order_acquire); a;
393 a = a->next) {
394 if (strcmp(a->name, name) == 0) return a->url;
395 }
396
397 return NULL;
398}
399
411 if (!app || !app->assets || !req || !req->path || !req->path->data) return false;
412 if (req->method != CWIST_HTTP_GET && req->method != CWIST_HTTP_HEAD) return false;
414
415 const char *path = req->path->data;
416 size_t plen = strlen(reg->prefix);
417 if (strncmp(path, reg->prefix, plen) != 0 || path[plen] != '/') return false;
418 const char *rest = path + plen + 1;
419
420 const struct cwist_asset *logical = NULL;
421 for (struct cwist_asset *a = atomic_load_explicit(&reg->head, memory_order_acquire); a;
422 a = a->next) {
423 if (strcmp(a->hashed_name, rest) == 0) {
424 out->asset = a;
425 out->immutable = true;
426 return true;
427 }
428 if (!logical && strcmp(a->name, rest) == 0) logical = a;
429 }
430
431 if (!logical) return false;
432 out->asset = logical;
433 out->immutable = false;
434
435 return true;
436}
437
439static bool etag_matches(const char *if_none_match, const char *etag) {
440 size_t elen = strlen(etag);
441 const char *p = if_none_match;
442 while (*p) {
443 while (*p == ' ' || *p == '\t' || *p == ',') p++;
444 if (!*p) break;
445 const char *start = p;
446 while (*p && *p != ',') p++;
447 const char *end = p;
448 while (end > start && (end[-1] == ' ' || end[-1] == '\t')) end--;
449 if (end - start == 1 && *start == '*') return true;
450 if (end - start > 2 && start[0] == 'W' && start[1] == '/') start += 2;
451 if ((size_t)(end - start) == elen && memcmp(start, etag, elen) == 0) return true;
452 }
453 return false;
454}
455
457static bool assets_add_header(cwist_http_response *res, const char *name, const char *value) {
458 cwist_error_t err = cwist_http_header_add(&res->headers, name, value);
459 bool ok = cwist_error_is_ok(&err);
461 return ok;
462}
463
476 const cwist_asset_match *match) {
477 if (!req || !res) return;
478 if (!match || !match->asset) {
480 return;
481 }
482 const struct cwist_asset *asset = match->asset;
483 const char *cache = match->immutable ? CWIST_ASSET_IMMUTABLE_CACHE : "no-cache";
484 const char *inm = cwist_http_header_get(req->headers, "If-None-Match");
485 bool not_modified = inm && etag_matches(inm, asset->etag);
486
487 bool ok = assets_add_header(res, "ETag", asset->etag) && assets_add_header(res, "Cache-Control", cache);
488 if (ok && !not_modified) ok = assets_add_header(res, "Content-Type", asset->content_type);
489 if (!ok) {
491 return;
492 }
493
494 if (not_modified) {
498 return;
499 }
501 /* Assets live until the app is destroyed, so no release hook is needed. */
502 cwist_http_response_set_body_ptr(res, asset->data, asset->len);
503}
504
514int cwist_assets_clone(void **dst, const void *src) {
515 *dst = NULL;
516 if (!src) return 0;
517
518 const cwist_asset_registry *from = (const cwist_asset_registry *)src;
520 if (!reg) return -1;
521
522 reg->prefix = cwist_strdup(from->prefix);
523 atomic_init(&reg->head, NULL);
524
525 if (!reg->prefix) {
527 return -1;
528 }
529
530 /* Keep the newest-first order: append each copy at the tail. */
531 struct cwist_asset *head = NULL, **tail = &head;
532 bool ok = true;
533 for (struct cwist_asset *a =
535 a && ok; a = a->next) {
536 unsigned char *data = (unsigned char *)cwist_alloc(a->len ? a->len : 1);
537 if (!data) {
538 ok = false;
539 break;
540 }
541
542 if (a->len) memcpy(data, a->data, a->len);
543
544 char hash[ASSET_HASH_HEX + 1];
545 memcpy(hash, a->etag + 1, ASSET_HASH_HEX);
546 hash[ASSET_HASH_HEX] = '\0';
547 struct cwist_asset *copy = asset_build(reg, a->name, data, a->len, a->content_type, hash);
548
549 if (!copy) {
550 ok = false;
551 break;
552 }
553
554 copy->next = NULL;
555 *tail = copy;
556 tail = &copy->next;
557 }
558
559 atomic_init(&reg->head, head);
560 if (!ok) {
562 return -1;
563 }
564
565 *dst = reg;
566 return 0;
567}
568
572 if (!reg) return;
573
574 struct cwist_asset *a = atomic_load_explicit(&reg->head, memory_order_acquire);
575 while (a) {
576 struct cwist_asset *next = a->next;
577 asset_free(a);
578 a = next;
579 }
580
581 cwist_free(reg->prefix);
583}
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
char * cwist_strdup(const char *src)
Duplicate a string using libttak-backed storage.
Definition alloc.c:32
void * cwist_alloc(size_t size)
Allocate zeroed memory tracked by libttak.
Definition alloc.c:17
#define ASSET_HASH_HEX
Definition assets.c:19
void cwist_assets_destroy(void *registry)
Definition assets.c:570
const char * cwist_app_asset_url(cwist_app *app, const char *name)
Return the URL of the newest asset registered under name.
Definition assets.c:386
void cwist_assets_respond(cwist_http_request *req, cwist_http_response *res, const cwist_asset_match *match)
Fill a response for a matched asset.
Definition assets.c:475
cwist_error_t cwist_app_asset_add_file(cwist_app *app, const char *name, const char *path, const char *content_type)
Read a file from disk and publish it as an asset.
Definition assets.c:336
cwist_error_t cwist_app_asset_prefix(cwist_app *app, const char *url_prefix)
Set the URL prefix under which assets are served.
Definition assets.c:197
struct cwist_asset_registry cwist_asset_registry
int cwist_assets_clone(void **dst, const void *src)
Deep-copy a registry (used when an app is cloned).
Definition assets.c:514
bool cwist_assets_match(cwist_app *app, const cwist_http_request *req, cwist_asset_match *out)
Match an HTTP request to a registered asset.
Definition assets.c:410
cwist_error_t cwist_app_asset_add(cwist_app *app, const char *name, const void *data, size_t len, const char *content_type)
Publish an in-memory buffer as an asset.
Definition assets.c:306
Content-hashed, in-memory static assets served by a cwist_app.
#define CWIST_ASSET_DEFAULT_PREFIX
Definition assets.h:33
#define CWIST_ASSET_IMMUTABLE_CACHE
Definition assets.h:36
Private wiring between app.c and the asset registry (assets.c).
cwist_error_t cwist_sstring_assign(cwist_sstring *str, const char *data)
Assign a C string.
Definition sstring.c:504
@ CWIST_ERR_INT16
used when checking common errcodes in Unix/Linux
Definition cwist_err.h:16
cwist_error_t make_error(cwist_errtype_t type)
Creates an error object initialized with the specified type.
Definition error.c:14
char * cwist_http_header_get(cwist_http_header_node *head, const char *key)
Find a header value by key.
Definition http.c:1880
@ CWIST_HTTP_INTERNAL_ERROR
Definition http.h:35
@ CWIST_HTTP_OK
Definition http.h:28
@ CWIST_HTTP_HEAD
Definition http.h:22
@ CWIST_HTTP_GET
Definition http.h:17
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
void cwist_http_response_set_body_ptr(cwist_http_response *res, const void *ptr, size_t len)
Sets a direct pointer for the response body (Zero Copy). Use this when serving large files from memor...
Definition http.c:2346
@ CWIST_HTTP_NOT_MODIFIED
Definition http.h:71
size_t head
Definition otel.c:57
size_t cap
Definition otel.c:56
Minimal self-contained SHA-256 and HMAC-SHA256 (RFC 4634 style).
#define CWIST_SHA256_DIGEST_LEN
Definition sha256.h:21
Main Application Context.
Definition app.h:155
void * assets
Content-hashed in-memory assets (see <cwist/sys/app/assets.h>).
Definition app.h:222
const struct cwist_asset * asset
Integrated error structure used throughout the system.
Definition cwist_err.h:72
cwist_sstring * path
e.g., "/users/1"
Definition http.h:67
cwist_http_header_node * headers
Definition http.h:72
cwist_http_method_t method
Definition http.h:66
HTTP Response Object. Supports standard string body or Zero-Copy pointer body.
Definition http.h:82
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
char * data
Access directly only when raw handling is necessary.
Definition sstring.h:15
void cwist_error_dispose(cwist_error_t *err)
Release resources owned by an error value. Currently only CWIST_ERR_JSON owns memory (the cJSON paylo...
Definition error.c:43