@velox0/cerver 0.4.2 → 0.5.0

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
@@ -85,6 +85,64 @@ static void add_cache_headers(cerver_response_t* res, const char* path) {
85
85
  }
86
86
  }
87
87
 
88
+ /* ------------------------------------------------------------------ */
89
+ /* Helper to resolve fallback paths for directory/clean-URL routes. */
90
+ /* - "/" -> "/index.html" */
91
+ /* - "/page" or "/page/" -> "/page/page.html" */
92
+ /* ------------------------------------------------------------------ */
93
+
94
+ static void get_fallback_path(const char* path, char* out, size_t out_len) {
95
+ if (strcmp(path, "/") == 0 || strcmp(path, "") == 0) {
96
+ snprintf(out, out_len, "/index.html");
97
+ return;
98
+ }
99
+
100
+ size_t len = strlen(path);
101
+ while (len > 0 && path[len - 1] == '/') {
102
+ len--;
103
+ }
104
+
105
+ if (len == 0) {
106
+ snprintf(out, out_len, "/index.html");
107
+ return;
108
+ }
109
+
110
+ int last_slash = -1;
111
+ for (int i = (int)len - 1; i >= 0; i--) {
112
+ if (path[i] == '/') {
113
+ last_slash = i;
114
+ break;
115
+ }
116
+ }
117
+
118
+ size_t segment_len = len - (last_slash + 1);
119
+ if (segment_len == 0) {
120
+ snprintf(out, out_len, "/index.html");
121
+ return;
122
+ }
123
+
124
+ /* Extract the prefix and segment safely */
125
+ char prefix[1024];
126
+ if (len < sizeof(prefix)) {
127
+ memcpy(prefix, path, len);
128
+ prefix[len] = '\0';
129
+ } else {
130
+ snprintf(out, out_len, "/index.html");
131
+ return;
132
+ }
133
+
134
+ char segment[256];
135
+ if (segment_len < sizeof(segment)) {
136
+ memcpy(segment, path + last_slash + 1, segment_len);
137
+ segment[segment_len] = '\0';
138
+ } else {
139
+ snprintf(out, out_len, "/index.html");
140
+ return;
141
+ }
142
+
143
+ snprintf(out, out_len, "%s/%s.html", prefix, segment);
144
+ }
145
+
88
146
  /* ------------------------------------------------------------------ */
89
147
  /* Serve from embedded assets — hash-accelerated lookup */
90
148
  /* ------------------------------------------------------------------ */
@@ -110,19 +168,15 @@ static int serve_embedded(cerver_server_t* srv, cerver_request_t* req, cerver_re
110
168
  }
111
169
  }
112
170
 
113
- /* Try with /index.html appended (for directory-like paths) */
171
+ /* Try with fallback path (for directory-like paths) */
114
172
  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);
119
- } else {
120
- snprintf(index_path, sizeof(index_path), "%s/index.html", path);
121
- }
173
+ char fallback_path[CERVER_MAX_PATH];
174
+ get_fallback_path(path, fallback_path, sizeof(fallback_path));
122
175
 
123
- uint32_t idx_hash = fnv1a(index_path);
176
+ uint32_t idx_hash = fnv1a(fallback_path);
124
177
  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) {
178
+ if (fnv1a(srv->assets[i].path) == idx_hash &&
179
+ strcmp(srv->assets[i].path, fallback_path) == 0) {
126
180
  found = &srv->assets[i];
127
181
  break;
128
182
  }
@@ -168,10 +222,12 @@ static int serve_filesystem(cerver_server_t* srv, cerver_request_t* req, cerver_
168
222
  char full_path[CERVER_MAX_PATH * 2];
169
223
  snprintf(full_path, sizeof(full_path), "%s%s", srv->public_dir, path);
170
224
 
171
- /* Check if it's a directory — try index.html */
225
+ /* Check if it's a directory — try fallback path */
172
226
  struct stat st;
173
227
  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);
228
+ char fallback_path[CERVER_MAX_PATH];
229
+ get_fallback_path(path, fallback_path, sizeof(fallback_path));
230
+ snprintf(full_path, sizeof(full_path), "%s%s", srv->public_dir, fallback_path);
175
231
  if (stat(full_path, &st) != 0) return -1;
176
232
  }
177
233
 
@@ -189,59 +245,19 @@ static int serve_filesystem(cerver_server_t* srv, cerver_request_t* req, cerver_
189
245
  const char* mime = cerver_mime_from_path(full_path);
190
246
 
191
247
  /*
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.
248
+ * Use sendfile for zero-copy filesystem static serving.
249
+ * The file is opened and its descriptor is stored in the response structure
250
+ * for streaming directly to the client socket in the writer.
196
251
  */
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;
224
- }
225
-
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);
233
-
234
- if (total != file_size) {
235
- free(file_data);
236
- return -1;
237
- }
238
-
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
- }
252
+ int fd = open(full_path, O_RDONLY);
253
+ if (fd < 0) return -1;
254
+
255
+ res->status = 200;
256
+ res->content_type = mime;
257
+ res->body = NULL;
258
+ res->body_len = file_size;
259
+ res->_body_owned = 3; /* Special flag: sendfile, close fd */
260
+ res->_file_fd = fd;
245
261
 
246
262
  add_cache_headers(res, path);
247
263
 
@@ -224,6 +224,22 @@ static void test_route_match_mismatch_resets_params(void) {
224
224
  MU_ASSERT_EQ_INT(0, req.params_count);
225
225
  }
226
226
 
227
+ static void test_route_match_multi_segment(void) {
228
+ cerver_route_t route;
229
+ route.method = "GET";
230
+ route.pattern = "/users/:id/profile";
231
+ route.handler = handler_a;
232
+
233
+ cerver_request_t req;
234
+ memset(&req, 0, sizeof(req));
235
+ strcpy(req.method, "GET");
236
+ strcpy(req.path, "/users/123/profile");
237
+
238
+ MU_ASSERT(cerver_route_match(&route, &req) == 1);
239
+ MU_ASSERT_EQ_INT(1, req.params_count);
240
+ MU_ASSERT_STREQ("123", cerver_req_param(&req, "id"));
241
+ }
242
+
227
243
  static void test_request_header_helpers(void) {
228
244
  cerver_request_t req;
229
245
  memset(&req, 0, sizeof(req));
@@ -272,9 +288,9 @@ static void test_static_embedded_index_fallback(void) {
272
288
  cerver_server_t srv;
273
289
  cerver_init(&srv, 8080, 1);
274
290
 
275
- static const unsigned char data[] = "docs";
291
+ static const unsigned char data[] = "about";
276
292
  cerver_asset_t assets[1];
277
- assets[0].path = "/docs/index.html";
293
+ assets[0].path = "/about/about.html";
278
294
  assets[0].mime_type = "text/html";
279
295
  assets[0].data = data;
280
296
  assets[0].data_len = sizeof(data) - 1;
@@ -289,13 +305,91 @@ static void test_static_embedded_index_fallback(void) {
289
305
  memset(&req, 0, sizeof(req));
290
306
  memset(&res, 0, sizeof(res));
291
307
  strcpy(req.method, "GET");
292
- strcpy(req.path, "/docs/");
308
+ strcpy(req.path, "/about/");
293
309
 
294
310
  MU_ASSERT_EQ_INT(0, cerver_serve_static(&srv, &req, &res));
295
311
  MU_ASSERT(res.body == (const char*)data);
296
312
  MU_ASSERT_STREQ("text/html", res.content_type);
297
313
  }
298
314
 
315
+ static void test_static_filesystem_directory_fallback(void) {
316
+ char dir_template[] = "/tmp/cerver-test-XXXXXX";
317
+ char* dir = mkdtemp(dir_template);
318
+ MU_ASSERT(dir != NULL);
319
+
320
+ /* Create public/index.html (maps to /) */
321
+ char file_path[PATH_MAX];
322
+ snprintf(file_path, sizeof(file_path), "%s/index.html", dir);
323
+ MU_ASSERT_EQ_INT(0, write_file(file_path, "root index", 10));
324
+
325
+ /* Create public/about/about.html (maps to /about or /about/) */
326
+ char sub_dir[PATH_MAX];
327
+ snprintf(sub_dir, sizeof(sub_dir), "%s/about", dir);
328
+ MU_ASSERT_EQ_INT(0, mkdir(sub_dir, 0700));
329
+
330
+ char file_path2[PATH_MAX];
331
+ snprintf(file_path2, sizeof(file_path2), "%s/about/about.html", dir);
332
+ MU_ASSERT_EQ_INT(0, write_file(file_path2, "about content", 13));
333
+
334
+ /* Create public/about/index.html (should NOT map to /about, since index.html under subdirectory
335
+ * doesn't alias) */
336
+ char file_path3[PATH_MAX];
337
+ snprintf(file_path3, sizeof(file_path3), "%s/about/index.html", dir);
338
+ MU_ASSERT_EQ_INT(0, write_file(file_path3, "about index", 11));
339
+
340
+ cerver_server_t srv;
341
+ cerver_init(&srv, 8080, 1);
342
+ cerver_set_public_dir(&srv, dir);
343
+
344
+ /* Test / -> index.html fallback */
345
+ {
346
+ cerver_request_t req;
347
+ cerver_response_t res;
348
+ memset(&req, 0, sizeof(req));
349
+ memset(&res, 0, sizeof(res));
350
+ strcpy(req.method, "GET");
351
+ strcpy(req.path, "/");
352
+
353
+ MU_ASSERT_EQ_INT(0, cerver_serve_static(&srv, &req, &res));
354
+ MU_ASSERT_EQ_SIZE(10, res.body_len);
355
+ if (res._body_owned == 3 && res._file_fd >= 0) close(res._file_fd);
356
+ }
357
+
358
+ /* Test /about -> about/about.html fallback */
359
+ {
360
+ cerver_request_t req;
361
+ cerver_response_t res;
362
+ memset(&req, 0, sizeof(req));
363
+ memset(&res, 0, sizeof(res));
364
+ strcpy(req.method, "GET");
365
+ strcpy(req.path, "/about");
366
+
367
+ MU_ASSERT_EQ_INT(0, cerver_serve_static(&srv, &req, &res));
368
+ MU_ASSERT_EQ_SIZE(13, res.body_len);
369
+ if (res._body_owned == 3 && res._file_fd >= 0) close(res._file_fd);
370
+ }
371
+
372
+ /* Test /about/ -> about/about.html fallback */
373
+ {
374
+ cerver_request_t req;
375
+ cerver_response_t res;
376
+ memset(&req, 0, sizeof(req));
377
+ memset(&res, 0, sizeof(res));
378
+ strcpy(req.method, "GET");
379
+ strcpy(req.path, "/about/");
380
+
381
+ MU_ASSERT_EQ_INT(0, cerver_serve_static(&srv, &req, &res));
382
+ MU_ASSERT_EQ_SIZE(13, res.body_len);
383
+ if (res._body_owned == 3 && res._file_fd >= 0) close(res._file_fd);
384
+ }
385
+
386
+ unlink(file_path);
387
+ unlink(file_path2);
388
+ unlink(file_path3);
389
+ rmdir(sub_dir);
390
+ rmdir(dir);
391
+ }
392
+
299
393
  static void test_static_filesystem_small(void) {
300
394
  char dir_template[] = "/tmp/cerver-test-XXXXXX";
301
395
  char* dir = mkdtemp(dir_template);
@@ -318,10 +412,27 @@ static void test_static_filesystem_small(void) {
318
412
 
319
413
  MU_ASSERT_EQ_INT(0, cerver_serve_static(&srv, &req, &res));
320
414
  MU_ASSERT_EQ_SIZE(5, res.body_len);
321
- MU_ASSERT(memcmp(res.body, "small", 5) == 0);
322
- MU_ASSERT_EQ_INT(1, res._body_owned);
415
+ MU_ASSERT(res.body == NULL);
416
+ MU_ASSERT_EQ_INT(3, res._body_owned);
417
+ MU_ASSERT(res._file_fd >= 0);
418
+
419
+ int fds[2];
420
+ MU_ASSERT(pipe(fds) == 0);
421
+ MU_ASSERT_EQ_INT(0, cerver_write_response(fds[1], &res, 1));
422
+ close(fds[1]);
423
+
424
+ char out[1024];
425
+ ssize_t n = read_all(fds[0], out, sizeof(out));
426
+ MU_ASSERT(n > 0);
427
+ close(fds[0]);
428
+
429
+ MU_ASSERT(strstr(out, "HTTP/1.1 200 OK\r\n") != NULL);
430
+ MU_ASSERT(strstr(out, "Content-Length: 5\r\n") != NULL);
431
+ MU_ASSERT(strstr(out, "\r\nsmall") != NULL);
323
432
 
324
- free((void*)res.body);
433
+ if (res._body_owned == 3 && res._file_fd >= 0) {
434
+ close(res._file_fd);
435
+ }
325
436
  unlink(file_path);
326
437
  rmdir(dir);
327
438
  }
@@ -334,10 +445,10 @@ static void test_static_filesystem_large(void) {
334
445
  char file_path[PATH_MAX];
335
446
  snprintf(file_path, sizeof(file_path), "%s/large.bin", dir);
336
447
 
337
- char* payload = (char*)malloc(70000);
448
+ char* payload = (char*)malloc(32000);
338
449
  MU_ASSERT(payload != NULL);
339
- memset(payload, 'a', 70000);
340
- MU_ASSERT_EQ_INT(0, write_file(file_path, payload, 70000));
450
+ memset(payload, 'a', 32000);
451
+ MU_ASSERT_EQ_INT(0, write_file(file_path, payload, 32000));
341
452
  free(payload);
342
453
 
343
454
  cerver_server_t srv;
@@ -352,11 +463,27 @@ static void test_static_filesystem_large(void) {
352
463
  strcpy(req.path, "/large.bin");
353
464
 
354
465
  MU_ASSERT_EQ_INT(0, cerver_serve_static(&srv, &req, &res));
355
- MU_ASSERT_EQ_SIZE(70000, res.body_len);
356
- MU_ASSERT_EQ_INT(2, res._body_owned);
357
- MU_ASSERT(((const char*)res.body)[0] == 'a');
466
+ MU_ASSERT_EQ_SIZE(32000, res.body_len);
467
+ MU_ASSERT(res.body == NULL);
468
+ MU_ASSERT_EQ_INT(3, res._body_owned);
469
+ MU_ASSERT(res._file_fd >= 0);
470
+
471
+ int fds[2];
472
+ MU_ASSERT(pipe(fds) == 0);
473
+ MU_ASSERT_EQ_INT(0, cerver_write_response(fds[1], &res, 1));
474
+ close(fds[1]);
475
+
476
+ char out[35000];
477
+ ssize_t n = read_all(fds[0], out, sizeof(out));
478
+ MU_ASSERT(n > 0);
479
+ close(fds[0]);
358
480
 
359
- munmap((void*)res.body, res.body_len);
481
+ MU_ASSERT(strstr(out, "HTTP/1.1 200 OK\r\n") != NULL);
482
+ MU_ASSERT(strstr(out, "Content-Length: 32000\r\n") != NULL);
483
+
484
+ if (res._body_owned == 3 && res._file_fd >= 0) {
485
+ close(res._file_fd);
486
+ }
360
487
  unlink(file_path);
361
488
  rmdir(dir);
362
489
  }
@@ -402,11 +529,13 @@ int main(void) {
402
529
  mu_run("write_response_force_close", test_write_response_force_close);
403
530
  mu_run("route_match_and_dispatch", test_route_match_and_dispatch);
404
531
  mu_run("route_match_mismatch_resets_params", test_route_match_mismatch_resets_params);
532
+ mu_run("route_match_multi_segment", test_route_match_multi_segment);
405
533
  mu_run("request_header_helpers", test_request_header_helpers);
406
534
  mu_run("static_embedded_prefers_br", test_static_embedded_prefers_br);
407
535
  mu_run("static_embedded_index_fallback", test_static_embedded_index_fallback);
408
536
  mu_run("static_filesystem_small", test_static_filesystem_small);
409
537
  mu_run("static_filesystem_large", test_static_filesystem_large);
538
+ mu_run("static_filesystem_directory_fallback", test_static_filesystem_directory_fallback);
410
539
  mu_run("static_rejects_unsafe_path", test_static_rejects_unsafe_path);
411
540
  mu_run("stat_cache_store_lookup", test_stat_cache_store_lookup);
412
541
  mu_run("cerver_init_fields", test_cerver_init_fields);
@@ -1,35 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- pull_request:
6
-
7
- jobs:
8
- tests:
9
- name: Tests
10
- runs-on: ubuntu-latest
11
-
12
- steps:
13
- - name: Checkout
14
- uses: actions/checkout@v6
15
-
16
- - name: Setup pnpm
17
- uses: pnpm/action-setup@v6
18
- with:
19
- version: 10
20
- run_install: false
21
-
22
- - name: Setup Node.js
23
- uses: actions/setup-node@v6
24
- with:
25
- node-version: 24
26
- cache: pnpm
27
-
28
- - name: Install dependencies
29
- run: pnpm install --frozen-lockfile
30
-
31
- - name: Run runtime tests
32
- run: make test-runtime
33
-
34
- - name: Run JS tests
35
- run: pnpm test
@@ -1,50 +0,0 @@
1
- name: Publish package
2
-
3
- on:
4
- push:
5
- tags:
6
- - "v*"
7
- workflow_dispatch:
8
-
9
- permissions:
10
- contents: write
11
- id-token: write
12
-
13
- jobs:
14
- publish:
15
- name: Publish to npm
16
- runs-on: ubuntu-latest
17
-
18
- steps:
19
- - name: Checkout
20
- uses: actions/checkout@v6
21
-
22
- - name: Setup pnpm
23
- uses: pnpm/action-setup@v6
24
- with:
25
- version: 10
26
- run_install: false
27
-
28
- - name: Setup Node.js
29
- uses: actions/setup-node@v6
30
- with:
31
- node-version: 24
32
- registry-url: https://registry.npmjs.org
33
- cache: pnpm
34
-
35
- - name: Install dependencies
36
- run: pnpm install --frozen-lockfile
37
-
38
- - name: Run runtime tests
39
- run: make test-runtime
40
-
41
- - name: Run tests
42
- run: pnpm test
43
-
44
- - name: Verify package contents
45
- run: npm pack --dry-run
46
-
47
- - name: Publish to npm
48
- run: npm publish --access public --provenance
49
- env:
50
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}