@velox0/cerver 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/runtime/static.c CHANGED
@@ -25,30 +25,30 @@
25
25
  /* FNV-1a hash for fast asset lookup */
26
26
  /* ------------------------------------------------------------------ */
27
27
 
28
- static uint32_t fnv1a(const char *str) {
29
- uint32_t hash = 2166136261u;
30
- while (*str) {
31
- hash ^= (uint8_t)*str++;
32
- hash *= 16777619u;
33
- }
34
- return hash;
28
+ static uint32_t fnv1a(const char* str) {
29
+ uint32_t hash = 2166136261u;
30
+ while (*str) {
31
+ hash ^= (uint8_t)*str++;
32
+ hash *= 16777619u;
33
+ }
34
+ return hash;
35
35
  }
36
36
 
37
37
  /* ------------------------------------------------------------------ */
38
38
  /* Path safety: prevent directory traversal */
39
39
  /* ------------------------------------------------------------------ */
40
40
 
41
- static int path_is_safe(const char *path) {
42
- /* Reject paths with ".." */
43
- if (strstr(path, "..")) return 0;
41
+ static int path_is_safe(const char* path) {
42
+ /* Reject paths with ".." */
43
+ if (strstr(path, "..")) return 0;
44
44
 
45
- /* Reject paths with null bytes */
46
- if (memchr(path, '\0', strlen(path))) return 0;
45
+ /* Reject paths with null bytes */
46
+ if (memchr(path, '\0', strlen(path))) return 0;
47
47
 
48
- /* Must start with "/" */
49
- if (path[0] != '/') return 0;
48
+ /* Must start with "/" */
49
+ if (path[0] != '/') return 0;
50
50
 
51
- return 1;
51
+ return 1;
52
52
  }
53
53
 
54
54
  /* ------------------------------------------------------------------ */
@@ -56,220 +56,211 @@ static int path_is_safe(const char *path) {
56
56
  /* ------------------------------------------------------------------ */
57
57
 
58
58
  typedef struct {
59
- int accepts_gzip;
60
- int accepts_br;
59
+ int accepts_gzip;
60
+ int accepts_br;
61
61
  } encoding_prefs_t;
62
62
 
63
- static encoding_prefs_t parse_accept_encoding(const cerver_request_t *req) {
64
- encoding_prefs_t prefs = { 0, 0 };
65
- const char *ae = cerver_req_header(req, "Accept-Encoding");
66
- if (!ae) return prefs;
63
+ static encoding_prefs_t parse_accept_encoding(const cerver_request_t* req) {
64
+ encoding_prefs_t prefs = {0, 0};
65
+ const char* ae = cerver_req_header(req, "Accept-Encoding");
66
+ if (!ae) return prefs;
67
67
 
68
- if (strstr(ae, "br")) prefs.accepts_br = 1;
69
- if (strstr(ae, "gzip")) prefs.accepts_gzip = 1;
68
+ if (strstr(ae, "br")) prefs.accepts_br = 1;
69
+ if (strstr(ae, "gzip")) prefs.accepts_gzip = 1;
70
70
 
71
- return prefs;
71
+ return prefs;
72
72
  }
73
73
 
74
74
  /* ------------------------------------------------------------------ */
75
75
  /* Cache header helper */
76
76
  /* ------------------------------------------------------------------ */
77
77
 
78
- static void add_cache_headers(cerver_response_t *res, const char *path) {
79
- /* Hashed/versioned assets (in /static/) get long cache */
80
- if (strstr(path, "/static/") || strstr(path, "/assets/")) {
81
- cerver_res_header(res, "Cache-Control", "public, max-age=31536000, immutable");
82
- } else {
83
- /* HTML and other top-level files get short cache with revalidation */
84
- cerver_res_header(res, "Cache-Control", "public, max-age=3600, must-revalidate");
85
- }
78
+ static void add_cache_headers(cerver_response_t* res, const char* path) {
79
+ /* Hashed/versioned assets (in /static/) get long cache */
80
+ if (strstr(path, "/static/") || strstr(path, "/assets/")) {
81
+ cerver_res_header(res, "Cache-Control", "public, max-age=31536000, immutable");
82
+ } else {
83
+ /* HTML and other top-level files get short cache with revalidation */
84
+ cerver_res_header(res, "Cache-Control", "public, max-age=3600, must-revalidate");
85
+ }
86
86
  }
87
87
 
88
88
  /* ------------------------------------------------------------------ */
89
89
  /* Serve from embedded assets — hash-accelerated lookup */
90
90
  /* ------------------------------------------------------------------ */
91
91
 
92
- static int serve_embedded(cerver_server_t *srv, cerver_request_t *req,
93
- cerver_response_t *res) {
94
- if (!srv->assets || srv->asset_count == 0) return -1;
92
+ static int serve_embedded(cerver_server_t* srv, cerver_request_t* req, cerver_response_t* res) {
93
+ if (!srv->assets || srv->asset_count == 0) return -1;
95
94
 
96
- const char *path = req->path;
97
- const cerver_asset_t *found = NULL;
95
+ const char* path = req->path;
96
+ const cerver_asset_t* found = NULL;
98
97
 
99
- /*
100
- * Use FNV-1a hash for O(1) average lookup instead of linear scan.
101
- * For small asset counts (<64), linear scan is fine, but hash helps
102
- * when there are hundreds of embedded assets.
103
- */
104
- uint32_t target_hash = fnv1a(path);
105
-
106
- /* Try exact match first */
107
- for (int i = 0; i < srv->asset_count; i++) {
108
- if (fnv1a(srv->assets[i].path) == target_hash &&
109
- strcmp(srv->assets[i].path, path) == 0) {
110
- found = &srv->assets[i];
111
- break;
112
- }
113
- }
98
+ /*
99
+ * Use FNV-1a hash for O(1) average lookup instead of linear scan.
100
+ * For small asset counts (<64), linear scan is fine, but hash helps
101
+ * when there are hundreds of embedded assets.
102
+ */
103
+ uint32_t target_hash = fnv1a(path);
114
104
 
115
- /* Try with /index.html appended (for directory-like paths) */
116
- if (!found) {
117
- char index_path[CERVER_MAX_PATH];
118
- size_t plen = strlen(path);
119
- if (plen > 0 && path[plen - 1] == '/') {
120
- snprintf(index_path, sizeof(index_path), "%sindex.html", path);
121
- } else {
122
- snprintf(index_path, sizeof(index_path), "%s/index.html", path);
123
- }
124
-
125
- uint32_t idx_hash = fnv1a(index_path);
126
- for (int i = 0; i < srv->asset_count; i++) {
127
- if (fnv1a(srv->assets[i].path) == idx_hash &&
128
- strcmp(srv->assets[i].path, index_path) == 0) {
129
- found = &srv->assets[i];
130
- break;
131
- }
132
- }
105
+ /* Try exact match first */
106
+ for (int i = 0; i < srv->asset_count; i++) {
107
+ if (fnv1a(srv->assets[i].path) == target_hash && strcmp(srv->assets[i].path, path) == 0) {
108
+ found = &srv->assets[i];
109
+ break;
133
110
  }
134
-
135
- if (!found) return -1;
136
-
137
- /* Check for pre-compressed variants */
138
- encoding_prefs_t enc = parse_accept_encoding(req);
139
-
140
- if (enc.accepts_br && found->data_br && found->data_br_len > 0) {
141
- /* Serve brotli */
142
- cerver_res_file(res, 200, found->mime_type,
143
- found->data_br, found->data_br_len);
144
- cerver_res_header(res, "Content-Encoding", "br");
145
- cerver_res_header(res, "Vary", "Accept-Encoding");
146
- } else if (enc.accepts_gzip && found->data_gz && found->data_gz_len > 0) {
147
- /* Serve gzip */
148
- cerver_res_file(res, 200, found->mime_type,
149
- found->data_gz, found->data_gz_len);
150
- cerver_res_header(res, "Content-Encoding", "gzip");
151
- cerver_res_header(res, "Vary", "Accept-Encoding");
111
+ }
112
+
113
+ /* Try with /index.html appended (for directory-like paths) */
114
+ if (!found) {
115
+ char index_path[CERVER_MAX_PATH];
116
+ size_t plen = strlen(path);
117
+ if (plen > 0 && path[plen - 1] == '/') {
118
+ snprintf(index_path, sizeof(index_path), "%sindex.html", path);
152
119
  } else {
153
- /* Serve uncompressed */
154
- cerver_res_file(res, 200, found->mime_type,
155
- found->data, found->data_len);
120
+ snprintf(index_path, sizeof(index_path), "%s/index.html", path);
156
121
  }
157
122
 
158
- add_cache_headers(res, found->path);
159
-
160
- return 0;
123
+ uint32_t idx_hash = fnv1a(index_path);
124
+ for (int i = 0; i < srv->asset_count; i++) {
125
+ if (fnv1a(srv->assets[i].path) == idx_hash && strcmp(srv->assets[i].path, index_path) == 0) {
126
+ found = &srv->assets[i];
127
+ break;
128
+ }
129
+ }
130
+ }
131
+
132
+ if (!found) return -1;
133
+
134
+ /* Check for pre-compressed variants */
135
+ encoding_prefs_t enc = parse_accept_encoding(req);
136
+
137
+ if (enc.accepts_br && found->data_br && found->data_br_len > 0) {
138
+ /* Serve brotli */
139
+ cerver_res_file(res, 200, found->mime_type, found->data_br, found->data_br_len);
140
+ cerver_res_header(res, "Content-Encoding", "br");
141
+ cerver_res_header(res, "Vary", "Accept-Encoding");
142
+ } else if (enc.accepts_gzip && found->data_gz && found->data_gz_len > 0) {
143
+ /* Serve gzip */
144
+ cerver_res_file(res, 200, found->mime_type, found->data_gz, found->data_gz_len);
145
+ cerver_res_header(res, "Content-Encoding", "gzip");
146
+ cerver_res_header(res, "Vary", "Accept-Encoding");
147
+ } else {
148
+ /* Serve uncompressed */
149
+ cerver_res_file(res, 200, found->mime_type, found->data, found->data_len);
150
+ }
151
+
152
+ add_cache_headers(res, found->path);
153
+
154
+ return 0;
161
155
  }
162
156
 
163
157
  /* ------------------------------------------------------------------ */
164
158
  /* Serve from filesystem — sendfile/mmap + stat cache */
165
159
  /* ------------------------------------------------------------------ */
166
160
 
167
- static int serve_filesystem(cerver_server_t *srv, cerver_request_t *req,
168
- cerver_response_t *res) {
169
- if (!srv->public_dir) return -1;
161
+ static int serve_filesystem(cerver_server_t* srv, cerver_request_t* req, cerver_response_t* res) {
162
+ if (!srv->public_dir) return -1;
170
163
 
171
- const char *path = req->path;
172
- if (!path_is_safe(path)) return -1;
164
+ const char* path = req->path;
165
+ if (!path_is_safe(path)) return -1;
173
166
 
174
- /* Build the full filesystem path */
175
- char full_path[CERVER_MAX_PATH * 2];
176
- snprintf(full_path, sizeof(full_path), "%s%s", srv->public_dir, path);
167
+ /* Build the full filesystem path */
168
+ char full_path[CERVER_MAX_PATH * 2];
169
+ snprintf(full_path, sizeof(full_path), "%s%s", srv->public_dir, path);
177
170
 
178
- /* Check if it's a directory — try index.html */
179
- struct stat st;
180
- if (stat(full_path, &st) == 0 && S_ISDIR(st.st_mode)) {
181
- snprintf(full_path, sizeof(full_path), "%s%s/index.html",
182
- srv->public_dir, path);
183
- if (stat(full_path, &st) != 0) return -1;
184
- }
171
+ /* Check if it's a directory — try index.html */
172
+ struct stat st;
173
+ if (stat(full_path, &st) == 0 && S_ISDIR(st.st_mode)) {
174
+ snprintf(full_path, sizeof(full_path), "%s%s/index.html", srv->public_dir, path);
175
+ if (stat(full_path, &st) != 0) return -1;
176
+ }
185
177
 
186
- /* Must be a regular file */
187
- if (stat(full_path, &st) != 0 || !S_ISREG(st.st_mode)) {
188
- return -1;
178
+ /* Must be a regular file */
179
+ if (stat(full_path, &st) != 0 || !S_ISREG(st.st_mode)) {
180
+ return -1;
181
+ }
182
+
183
+ size_t file_size = (size_t)st.st_size;
184
+
185
+ /* Store in stat cache for future lookups */
186
+ cerver_stat_cache_store(&srv->stat_cache, full_path, file_size, st.st_mtime);
187
+
188
+ /* Determine MIME type */
189
+ const char* mime = cerver_mime_from_path(full_path);
190
+
191
+ /*
192
+ * Use mmap for zero-copy serving instead of fopen+malloc+fread.
193
+ * The mmap'd region is used directly as the response body.
194
+ * We mark it as _body_owned=0 since munmap needs special handling,
195
+ * but for simplicity we'll use read() for small files and mmap for large.
196
+ */
197
+ if (file_size > 65536) {
198
+ /* Large files: mmap for zero-copy */
199
+ int fd = open(full_path, O_RDONLY);
200
+ if (fd < 0) return -1;
201
+
202
+ void* mapped = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
203
+ close(fd);
204
+
205
+ if (mapped == MAP_FAILED) return -1;
206
+
207
+ /* Advise the kernel we'll read sequentially */
208
+ madvise(mapped, file_size, MADV_SEQUENTIAL);
209
+
210
+ res->status = 200;
211
+ res->content_type = mime;
212
+ res->body = (const char*)mapped;
213
+ res->body_len = file_size;
214
+ res->_body_owned = 2; /* Special flag: needs munmap, not free */
215
+ } else {
216
+ /* Small files: read into buffer (avoids mmap overhead) */
217
+ int fd = open(full_path, O_RDONLY);
218
+ if (fd < 0) return -1;
219
+
220
+ char* file_data = malloc(file_size);
221
+ if (!file_data) {
222
+ close(fd);
223
+ return -1;
189
224
  }
190
225
 
191
- size_t file_size = (size_t)st.st_size;
192
-
193
- /* Store in stat cache for future lookups */
194
- cerver_stat_cache_store(&srv->stat_cache, full_path, file_size, st.st_mtime);
195
-
196
- /* Determine MIME type */
197
- const char *mime = cerver_mime_from_path(full_path);
198
-
199
- /*
200
- * Use mmap for zero-copy serving instead of fopen+malloc+fread.
201
- * The mmap'd region is used directly as the response body.
202
- * We mark it as _body_owned=0 since munmap needs special handling,
203
- * but for simplicity we'll use read() for small files and mmap for large.
204
- */
205
- if (file_size > 65536) {
206
- /* Large files: mmap for zero-copy */
207
- int fd = open(full_path, O_RDONLY);
208
- if (fd < 0) return -1;
209
-
210
- void *mapped = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
211
- close(fd);
212
-
213
- if (mapped == MAP_FAILED) return -1;
214
-
215
- /* Advise the kernel we'll read sequentially */
216
- madvise(mapped, file_size, MADV_SEQUENTIAL);
226
+ size_t total = 0;
227
+ while (total < file_size) {
228
+ ssize_t n = read(fd, file_data + total, file_size - total);
229
+ if (n <= 0) break;
230
+ total += (size_t)n;
231
+ }
232
+ close(fd);
217
233
 
218
- res->status = 200;
219
- res->content_type = mime;
220
- res->body = (const char *)mapped;
221
- res->body_len = file_size;
222
- res->_body_owned = 2; /* Special flag: needs munmap, not free */
223
- } else {
224
- /* Small files: read into buffer (avoids mmap overhead) */
225
- int fd = open(full_path, O_RDONLY);
226
- if (fd < 0) return -1;
227
-
228
- char *file_data = malloc(file_size);
229
- if (!file_data) {
230
- close(fd);
231
- return -1;
232
- }
233
-
234
- size_t total = 0;
235
- while (total < file_size) {
236
- ssize_t n = read(fd, file_data + total, file_size - total);
237
- if (n <= 0) break;
238
- total += (size_t)n;
239
- }
240
- close(fd);
241
-
242
- if (total != file_size) {
243
- free(file_data);
244
- return -1;
245
- }
246
-
247
- res->status = 200;
248
- res->content_type = mime;
249
- res->body = file_data;
250
- res->body_len = file_size;
251
- res->_body_owned = 1; /* malloc'd */
234
+ if (total != file_size) {
235
+ free(file_data);
236
+ return -1;
252
237
  }
253
238
 
254
- add_cache_headers(res, path);
239
+ res->status = 200;
240
+ res->content_type = mime;
241
+ res->body = file_data;
242
+ res->body_len = file_size;
243
+ res->_body_owned = 1; /* malloc'd */
244
+ }
255
245
 
256
- return 0;
246
+ add_cache_headers(res, path);
247
+
248
+ return 0;
257
249
  }
258
250
 
259
251
  /* ------------------------------------------------------------------ */
260
252
  /* Main static serving entry point */
261
253
  /* ------------------------------------------------------------------ */
262
254
 
263
- int cerver_serve_static(cerver_server_t *srv, cerver_request_t *req,
264
- cerver_response_t *res) {
265
- /* Only serve GET requests for static files */
266
- if (strcmp(req->method, "GET") != 0) return -1;
255
+ int cerver_serve_static(cerver_server_t* srv, cerver_request_t* req, cerver_response_t* res) {
256
+ /* Only serve GET requests for static files */
257
+ if (strcmp(req->method, "GET") != 0) return -1;
267
258
 
268
- /* Try embedded assets first */
269
- if (serve_embedded(srv, req, res) == 0) return 0;
259
+ /* Try embedded assets first */
260
+ if (serve_embedded(srv, req, res) == 0) return 0;
270
261
 
271
- /* Fall back to filesystem */
272
- if (serve_filesystem(srv, req, res) == 0) return 0;
262
+ /* Fall back to filesystem */
263
+ if (serve_filesystem(srv, req, res) == 0) return 0;
273
264
 
274
- return -1;
265
+ return -1;
275
266
  }
@@ -0,0 +1,76 @@
1
+ #include "minunit.h"
2
+
3
+ #include <stdio.h>
4
+ #include <string.h>
5
+ #include <unistd.h>
6
+
7
+ int mu_tests_run = 0;
8
+ int mu_tests_failed = 0;
9
+ int mu_test_failed = 0;
10
+
11
+ static int mu_is_tty(FILE* f) { return isatty(fileno(f)); }
12
+
13
+ static const char* mu_color(FILE* f, const char* code) { return mu_is_tty(f) ? code : ""; }
14
+
15
+ const char* mu_snip(const char* s, char* buf, size_t cap) {
16
+ if (s == NULL) {
17
+ return "(null)";
18
+ }
19
+ if (cap == 0) {
20
+ return "";
21
+ }
22
+
23
+ size_t len = strlen(s);
24
+ if (len < cap) {
25
+ snprintf(buf, cap, "%s", s);
26
+ return buf;
27
+ }
28
+
29
+ if (cap <= 4) {
30
+ size_t keep = cap - 1;
31
+ memcpy(buf, s, keep);
32
+ buf[keep] = '\0';
33
+ return buf;
34
+ }
35
+
36
+ size_t keep = cap - 4;
37
+ memcpy(buf, s, keep);
38
+ memcpy(buf + keep, "...", 3);
39
+ buf[keep + 3] = '\0';
40
+ return buf;
41
+ }
42
+
43
+ void mu_fail(const char* file, int line, const char* msg) {
44
+ const char* red = mu_color(stderr, "\x1b[31m");
45
+ const char* reset = mu_color(stderr, "\x1b[0m");
46
+
47
+ mu_test_failed = 1;
48
+ fprintf(stderr, "%sFAIL%s %s:%d: %s\n", red, reset, file, line, msg);
49
+ }
50
+
51
+ void mu_run(const char* name, mu_test_fn fn) {
52
+ mu_tests_run++;
53
+ mu_test_failed = 0;
54
+ fn();
55
+ if (mu_test_failed) {
56
+ mu_tests_failed++;
57
+ fprintf(stderr, " in %s\n", name);
58
+ } else {
59
+ const char* green = mu_color(stdout, "\x1b[32m");
60
+ const char* reset = mu_color(stdout, "\x1b[0m");
61
+ printf("%sok%s %s\n", green, reset, name);
62
+ }
63
+ }
64
+
65
+ int mu_report(void) {
66
+ const char* reset = mu_color(stdout, "\x1b[0m");
67
+ if (mu_tests_failed) {
68
+ const char* red = mu_color(stdout, "\x1b[31m");
69
+ printf("%sSummary:%s %d tests, %s%d failures%s\n", red, reset, mu_tests_run, red,
70
+ mu_tests_failed, reset);
71
+ } else {
72
+ const char* green = mu_color(stdout, "\x1b[32m");
73
+ printf("%sSummary:%s %d tests, %s0 failures%s\n", green, reset, mu_tests_run, green, reset);
74
+ }
75
+ return mu_tests_failed ? 1 : 0;
76
+ }
@@ -0,0 +1,64 @@
1
+ #ifndef CERVER_MINUNIT_H
2
+ #define CERVER_MINUNIT_H
3
+
4
+ #include <stddef.h>
5
+ #include <stdio.h>
6
+ #include <string.h>
7
+
8
+ typedef void (*mu_test_fn)(void);
9
+
10
+ #define MU_SNIP_MAX 80
11
+
12
+ extern int mu_tests_run;
13
+ extern int mu_tests_failed;
14
+ extern int mu_test_failed;
15
+
16
+ void mu_fail(const char* file, int line, const char* msg);
17
+ void mu_run(const char* name, mu_test_fn fn);
18
+ int mu_report(void);
19
+ const char* mu_snip(const char* s, char* buf, size_t cap);
20
+
21
+ #define MU_ASSERT(cond) \
22
+ do { \
23
+ if (!(cond)) { \
24
+ mu_fail(__FILE__, __LINE__, #cond); \
25
+ return; \
26
+ } \
27
+ } while (0)
28
+
29
+ #define MU_ASSERT_EQ_INT(a, b) \
30
+ do { \
31
+ if ((a) != (b)) { \
32
+ char _mu_buf[128]; \
33
+ snprintf(_mu_buf, sizeof(_mu_buf), "got %d expected %d", (int)(a), (int)(b)); \
34
+ mu_fail(__FILE__, __LINE__, _mu_buf); \
35
+ return; \
36
+ } \
37
+ } while (0)
38
+
39
+ #define MU_ASSERT_EQ_SIZE(a, b) \
40
+ do { \
41
+ if ((a) != (b)) { \
42
+ char _mu_buf[128]; \
43
+ snprintf(_mu_buf, sizeof(_mu_buf), "got %zu expected %zu", (size_t)(a), (size_t)(b)); \
44
+ mu_fail(__FILE__, __LINE__, _mu_buf); \
45
+ return; \
46
+ } \
47
+ } while (0)
48
+
49
+ #define MU_ASSERT_STREQ(a, b) \
50
+ do { \
51
+ if (((a) == NULL && (b) != NULL) || ((a) != NULL && (b) == NULL) || \
52
+ ((a) != NULL && (b) != NULL && strcmp((a), (b)) != 0)) { \
53
+ char _mu_buf[256]; \
54
+ char _mu_a[MU_SNIP_MAX]; \
55
+ char _mu_b[MU_SNIP_MAX]; \
56
+ const char* _mu_as = mu_snip((a), _mu_a, sizeof(_mu_a)); \
57
+ const char* _mu_bs = mu_snip((b), _mu_b, sizeof(_mu_b)); \
58
+ snprintf(_mu_buf, sizeof(_mu_buf), "got '%s' expected '%s'", _mu_as, _mu_bs); \
59
+ mu_fail(__FILE__, __LINE__, _mu_buf); \
60
+ return; \
61
+ } \
62
+ } while (0)
63
+
64
+ #endif