@velox0/cerver 0.3.1 → 0.4.1

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/mime.c CHANGED
@@ -8,77 +8,76 @@
8
8
  #include <ctype.h>
9
9
 
10
10
  typedef struct {
11
- const char *ext;
12
- const char *mime;
11
+ const char* ext;
12
+ const char* mime;
13
13
  } mime_entry_t;
14
14
 
15
15
  static const mime_entry_t mime_table[] = {
16
16
  /* Web essentials */
17
- { ".html", "text/html; charset=utf-8" },
18
- { ".htm", "text/html; charset=utf-8" },
19
- { ".css", "text/css; charset=utf-8" },
20
- { ".js", "application/javascript; charset=utf-8" },
21
- { ".mjs", "application/javascript; charset=utf-8" },
22
- { ".json", "application/json; charset=utf-8" },
23
- { ".xml", "application/xml; charset=utf-8" },
17
+ {".html", "text/html; charset=utf-8"},
18
+ {".htm", "text/html; charset=utf-8"},
19
+ {".css", "text/css; charset=utf-8"},
20
+ {".js", "application/javascript; charset=utf-8"},
21
+ {".mjs", "application/javascript; charset=utf-8"},
22
+ {".json", "application/json; charset=utf-8"},
23
+ {".xml", "application/xml; charset=utf-8"},
24
24
 
25
25
  /* Text */
26
- { ".txt", "text/plain; charset=utf-8" },
27
- { ".csv", "text/csv; charset=utf-8" },
28
- { ".md", "text/markdown; charset=utf-8" },
26
+ {".txt", "text/plain; charset=utf-8"},
27
+ {".csv", "text/csv; charset=utf-8"},
28
+ {".md", "text/markdown; charset=utf-8"},
29
29
 
30
30
  /* Images */
31
- { ".png", "image/png" },
32
- { ".jpg", "image/jpeg" },
33
- { ".jpeg", "image/jpeg" },
34
- { ".gif", "image/gif" },
35
- { ".svg", "image/svg+xml" },
36
- { ".ico", "image/x-icon" },
37
- { ".webp", "image/webp" },
38
- { ".avif", "image/avif" },
31
+ {".png", "image/png"},
32
+ {".jpg", "image/jpeg"},
33
+ {".jpeg", "image/jpeg"},
34
+ {".gif", "image/gif"},
35
+ {".svg", "image/svg+xml"},
36
+ {".ico", "image/x-icon"},
37
+ {".webp", "image/webp"},
38
+ {".avif", "image/avif"},
39
39
 
40
40
  /* Fonts */
41
- { ".woff", "font/woff" },
42
- { ".woff2", "font/woff2" },
43
- { ".ttf", "font/ttf" },
44
- { ".otf", "font/otf" },
45
- { ".eot", "application/vnd.ms-fontobject" },
41
+ {".woff", "font/woff"},
42
+ {".woff2", "font/woff2"},
43
+ {".ttf", "font/ttf"},
44
+ {".otf", "font/otf"},
45
+ {".eot", "application/vnd.ms-fontobject"},
46
46
 
47
47
  /* Media */
48
- { ".mp4", "video/mp4" },
49
- { ".webm", "video/webm" },
50
- { ".ogg", "audio/ogg" },
51
- { ".mp3", "audio/mpeg" },
52
- { ".wav", "audio/wav" },
48
+ {".mp4", "video/mp4"},
49
+ {".webm", "video/webm"},
50
+ {".ogg", "audio/ogg"},
51
+ {".mp3", "audio/mpeg"},
52
+ {".wav", "audio/wav"},
53
53
 
54
54
  /* Archives */
55
- { ".zip", "application/zip" },
56
- { ".gz", "application/gzip" },
57
- { ".tar", "application/x-tar" },
55
+ {".zip", "application/zip"},
56
+ {".gz", "application/gzip"},
57
+ {".tar", "application/x-tar"},
58
58
 
59
59
  /* Documents */
60
- { ".pdf", "application/pdf" },
60
+ {".pdf", "application/pdf"},
61
61
 
62
62
  /* Misc */
63
- { ".wasm", "application/wasm" },
64
- { ".map", "application/json" },
63
+ {".wasm", "application/wasm"},
64
+ {".map", "application/json"},
65
65
 
66
- { NULL, NULL }
67
- };
66
+ {NULL, NULL}};
68
67
 
69
- const char *cerver_mime_from_path(const char *path) {
70
- if (!path) return "application/octet-stream";
68
+ const char* cerver_mime_from_path(const char* path) {
69
+ if (!path) return "application/octet-stream";
71
70
 
72
- /* Find the last '.' in the path */
73
- const char *dot = strrchr(path, '.');
74
- if (!dot) return "application/octet-stream";
71
+ /* Find the last '.' in the path */
72
+ const char* dot = strrchr(path, '.');
73
+ if (!dot) return "application/octet-stream";
75
74
 
76
- /* Case-insensitive extension match */
77
- for (const mime_entry_t *entry = mime_table; entry->ext; entry++) {
78
- if (strcasecmp(dot, entry->ext) == 0) {
79
- return entry->mime;
80
- }
75
+ /* Case-insensitive extension match */
76
+ for (const mime_entry_t* entry = mime_table; entry->ext; entry++) {
77
+ if (strcasecmp(dot, entry->ext) == 0) {
78
+ return entry->mime;
81
79
  }
80
+ }
82
81
 
83
- return "application/octet-stream";
82
+ return "application/octet-stream";
84
83
  }
package/runtime/router.c CHANGED
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Matches incoming requests against registered route patterns.
5
5
  * Supports static paths and dynamic segments (:param).
6
+ * Supports dispatch override for compile-time generated dispatch.
6
7
  */
7
8
 
8
9
  #include "cerver.h"
@@ -15,31 +16,55 @@
15
16
  /* Request accessor helpers */
16
17
  /* ------------------------------------------------------------------ */
17
18
 
18
- const char *cerver_req_param(const cerver_request_t *req, const char *key) {
19
- for (int i = 0; i < req->params_count; i++) {
20
- if (strcmp(req->params[i].key, key) == 0) {
21
- return req->params[i].value;
22
- }
19
+ const char* cerver_req_param(const cerver_request_t* req, const char* key) {
20
+ for (int i = 0; i < req->params_count; i++) {
21
+ if (strcmp(req->params[i].key, key) == 0) {
22
+ return req->params[i].value;
23
23
  }
24
- return "";
24
+ }
25
+ return "";
25
26
  }
26
27
 
27
- const char *cerver_req_query(const cerver_request_t *req, const char *key) {
28
- for (int i = 0; i < req->query_count; i++) {
29
- if (strcmp(req->query[i].key, key) == 0) {
30
- return req->query[i].value;
31
- }
28
+ const char* cerver_req_query(const cerver_request_t* req, const char* key) {
29
+ for (int i = 0; i < req->query_count; i++) {
30
+ if (strcmp(req->query[i].key, key) == 0) {
31
+ return req->query[i].value;
32
32
  }
33
- return "";
33
+ }
34
+ return "";
34
35
  }
35
36
 
36
- const char *cerver_req_header(const cerver_request_t *req, const char *key) {
37
- for (int i = 0; i < req->header_count; i++) {
38
- if (strcasecmp(req->headers[i].key, key) == 0) {
39
- return req->headers[i].value;
40
- }
37
+ const char* cerver_req_header(const cerver_request_t* req, const char* key) {
38
+ for (int i = 0; i < req->header_count; i++) {
39
+ if (strcasecmp(req->headers[i].key, key) == 0) {
40
+ return req->headers[i].value;
41
41
  }
42
- return NULL;
42
+ }
43
+ return NULL;
44
+ }
45
+
46
+ /* ------------------------------------------------------------------ */
47
+ /* Connection lifecycle helpers */
48
+ /* ------------------------------------------------------------------ */
49
+
50
+ /*
51
+ * Returns 1 if the client sent "Connection: close" or is HTTP/1.0
52
+ * without an explicit "Connection: keep-alive".
53
+ */
54
+ int cerver_req_wants_close(const cerver_request_t* req) {
55
+ const char* conn = cerver_req_header(req, "Connection");
56
+ if (conn && strcasecmp(conn, "close") == 0) return 1;
57
+ /* HTTP/1.0 without explicit keep-alive → close */
58
+ /* (We don't track HTTP version separately, so default keep-alive for 1.1) */
59
+ return 0;
60
+ }
61
+
62
+ /* ------------------------------------------------------------------ */
63
+ /* Server configuration helpers */
64
+ /* ------------------------------------------------------------------ */
65
+
66
+ void cerver_set_dispatch(cerver_server_t* srv, cerver_dispatch_fn fn) {
67
+ srv->dispatch_override = fn;
43
68
  }
44
69
 
45
70
  /* ------------------------------------------------------------------ */
@@ -50,101 +75,98 @@ const char *cerver_req_header(const cerver_request_t *req, const char *key) {
50
75
  * Match a route pattern against a request path.
51
76
  * Pattern segments starting with ':' are dynamic and extract values.
52
77
  *
53
- * Examples:
54
- * pattern="/items/:id" path="/items/123" → match, id="123"
55
- * pattern="/" path="/" → match
56
- * pattern="/api/data" path="/api/data" → match
57
- * pattern="/api/data" path="/api/other" → no match
78
+ * Uses manual segment iteration instead of strtok_r for speed.
58
79
  */
59
- int cerver_route_match(const cerver_route_t *route, cerver_request_t *req) {
60
- /* Method must match */
61
- if (strcmp(route->method, req->method) != 0) {
62
- return 0;
63
- }
64
-
65
- const char *pattern = route->pattern;
66
- const char *path = req->path;
67
-
68
- /* Fast path: exact match */
69
- if (strcmp(pattern, path) == 0) {
70
- return 1;
71
- }
72
-
73
- /* Segment-by-segment matching */
74
- /* We'll work with copies so we can tokenize */
75
- char pat_buf[CERVER_MAX_PATH];
76
- char path_buf[CERVER_MAX_PATH];
77
- strncpy(pat_buf, pattern, sizeof(pat_buf) - 1);
78
- pat_buf[sizeof(pat_buf) - 1] = '\0';
79
- strncpy(path_buf, path, sizeof(path_buf) - 1);
80
- path_buf[sizeof(path_buf) - 1] = '\0';
81
-
82
- /* Split into segments */
83
- char *pat_segments[64];
84
- char *path_segments[64];
85
- int pat_count = 0;
86
- int path_count = 0;
87
-
88
- char *saveptr;
89
- char *tok;
90
-
91
- tok = strtok_r(pat_buf, "/", &saveptr);
92
- while (tok && pat_count < 64) {
93
- pat_segments[pat_count++] = tok;
94
- tok = strtok_r(NULL, "/", &saveptr);
95
- }
80
+ int cerver_route_match(const cerver_route_t* route, cerver_request_t* req) {
81
+ /* Method must match */
82
+ if (strcmp(route->method, req->method) != 0) {
83
+ return 0;
84
+ }
96
85
 
97
- tok = strtok_r(path_buf, "/", &saveptr);
98
- while (tok && path_count < 64) {
99
- path_segments[path_count++] = tok;
100
- tok = strtok_r(NULL, "/", &saveptr);
101
- }
86
+ const char* pattern = route->pattern;
87
+ const char* path = req->path;
102
88
 
103
- /* Segment counts must match */
104
- if (pat_count != path_count) {
89
+ /* Fast path: exact match */
90
+ if (strcmp(pattern, path) == 0) {
91
+ return 1;
92
+ }
93
+
94
+ /* No dynamic segments? Then the strcmp above was definitive */
95
+ if (!strchr(pattern, ':')) {
96
+ return 0;
97
+ }
98
+
99
+ /* Segment-by-segment matching without strtok_r */
100
+ const char* pp = pattern; /* pattern pointer */
101
+ const char* rp = path; /* request path pointer */
102
+
103
+ int saved_params = req->params_count;
104
+
105
+ /* Skip leading '/' */
106
+ if (*pp == '/') pp++;
107
+ if (*rp == '/') rp++;
108
+
109
+ while (*pp && *rp) {
110
+ /* Extract pattern segment */
111
+ const char* pp_seg = pp;
112
+ while (*pp && *pp != '/') pp++;
113
+ size_t pp_len = (size_t)(pp - pp_seg);
114
+
115
+ /* Extract path segment */
116
+ const char* rp_seg = rp;
117
+ while (*rp && *rp != '/') rp++;
118
+ size_t rp_len = (size_t)(rp - rp_seg);
119
+
120
+ if (pp_seg[0] == ':') {
121
+ /* Dynamic segment — extract parameter */
122
+ if (req->params_count < CERVER_MAX_PARAMS) {
123
+ req->params[req->params_count].key = pp_seg + 1;
124
+ /* Temporarily NUL-terminate the key at the slash */
125
+ /* The key points into the route pattern (static/const) */
126
+ req->params[req->params_count].value = rp_seg;
127
+ req->params_count++;
128
+ }
129
+ } else {
130
+ /* Static segment — must match exactly */
131
+ if (pp_len != rp_len || memcmp(pp_seg, rp_seg, pp_len) != 0) {
132
+ req->params_count = saved_params;
105
133
  return 0;
134
+ }
106
135
  }
107
136
 
108
- /* Match each segment */
109
- /* Reset params before populating */
110
- int saved_params = req->params_count;
111
-
112
- for (int i = 0; i < pat_count; i++) {
113
- if (pat_segments[i][0] == ':') {
114
- /* Dynamic segment — extract parameter */
115
- if (req->params_count < CERVER_MAX_PARAMS) {
116
- /* The key is the segment name without ':' */
117
- /* We need stable storage — use the request's internal structures.
118
- Since we're in the request's lifetime, we can use strdup. */
119
- req->params[req->params_count].key = pat_segments[i] + 1;
120
- req->params[req->params_count].value = path_segments[i];
121
- req->params_count++;
122
- }
123
- } else {
124
- /* Static segment — must match exactly */
125
- if (strcmp(pat_segments[i], path_segments[i]) != 0) {
126
- /* Restore params on mismatch */
127
- req->params_count = saved_params;
128
- return 0;
129
- }
130
- }
131
- }
137
+ /* Skip '/' separator */
138
+ if (*pp == '/') pp++;
139
+ if (*rp == '/') rp++;
140
+ }
132
141
 
133
- return 1;
142
+ /* Both must be consumed */
143
+ if (*pp || *rp) {
144
+ req->params_count = saved_params;
145
+ return 0;
146
+ }
147
+
148
+ return 1;
134
149
  }
135
150
 
136
151
  /* ------------------------------------------------------------------ */
137
152
  /* Dispatch: find and return the handler for a request */
138
153
  /* ------------------------------------------------------------------ */
139
154
 
140
- cerver_handler_fn cerver_dispatch(cerver_server_t *srv, cerver_request_t *req) {
141
- if (!srv->routes) return NULL;
155
+ cerver_handler_fn cerver_dispatch(cerver_server_t* srv, cerver_request_t* req) {
156
+ /* Try the generated compile-time dispatch first */
157
+ if (srv->dispatch_override) {
158
+ cerver_handler_fn h = srv->dispatch_override(req);
159
+ if (h) return h;
160
+ }
161
+
162
+ /* Fall back to generic route table scan */
163
+ if (!srv->routes) return NULL;
142
164
 
143
- for (int i = 0; i < srv->route_count; i++) {
144
- if (cerver_route_match(&srv->routes[i], req)) {
145
- return srv->routes[i].handler;
146
- }
165
+ for (int i = 0; i < srv->route_count; i++) {
166
+ if (cerver_route_match(&srv->routes[i], req)) {
167
+ return srv->routes[i].handler;
147
168
  }
169
+ }
148
170
 
149
- return NULL;
171
+ return NULL;
150
172
  }