@velox0/cerver 0.4.0 → 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/.github/workflows/publish.yml +3 -4
- package/package.json +1 -1
- package/runtime/cerver.h +129 -129
- package/runtime/http_parser.c +152 -152
- package/runtime/http_writer.c +136 -126
- package/runtime/mime.c +48 -49
- package/runtime/router.c +98 -98
- package/runtime/server.c +593 -436
- package/runtime/static.c +174 -183
package/runtime/mime.c
CHANGED
|
@@ -8,77 +8,76 @@
|
|
|
8
8
|
#include <ctype.h>
|
|
9
9
|
|
|
10
10
|
typedef struct {
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
{
|
|
18
|
-
{
|
|
19
|
-
{
|
|
20
|
-
{
|
|
21
|
-
{
|
|
22
|
-
{
|
|
23
|
-
{
|
|
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
|
-
{
|
|
27
|
-
{
|
|
28
|
-
{
|
|
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
|
-
{
|
|
32
|
-
{
|
|
33
|
-
{
|
|
34
|
-
{
|
|
35
|
-
{
|
|
36
|
-
{
|
|
37
|
-
{
|
|
38
|
-
{
|
|
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
|
-
{
|
|
42
|
-
{
|
|
43
|
-
{
|
|
44
|
-
{
|
|
45
|
-
{
|
|
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
|
-
{
|
|
49
|
-
{
|
|
50
|
-
{
|
|
51
|
-
{
|
|
52
|
-
{
|
|
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
|
-
{
|
|
56
|
-
{
|
|
57
|
-
{
|
|
55
|
+
{".zip", "application/zip"},
|
|
56
|
+
{".gz", "application/gzip"},
|
|
57
|
+
{".tar", "application/x-tar"},
|
|
58
58
|
|
|
59
59
|
/* Documents */
|
|
60
|
-
{
|
|
60
|
+
{".pdf", "application/pdf"},
|
|
61
61
|
|
|
62
62
|
/* Misc */
|
|
63
|
-
{
|
|
64
|
-
{
|
|
63
|
+
{".wasm", "application/wasm"},
|
|
64
|
+
{".map", "application/json"},
|
|
65
65
|
|
|
66
|
-
{
|
|
67
|
-
};
|
|
66
|
+
{NULL, NULL}};
|
|
68
67
|
|
|
69
|
-
const char
|
|
70
|
-
|
|
68
|
+
const char* cerver_mime_from_path(const char* path) {
|
|
69
|
+
if (!path) return "application/octet-stream";
|
|
71
70
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
/* Find the last '.' in the path */
|
|
72
|
+
const char* dot = strrchr(path, '.');
|
|
73
|
+
if (!dot) return "application/octet-stream";
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
82
|
+
return "application/octet-stream";
|
|
84
83
|
}
|
package/runtime/router.c
CHANGED
|
@@ -16,31 +16,31 @@
|
|
|
16
16
|
/* Request accessor helpers */
|
|
17
17
|
/* ------------------------------------------------------------------ */
|
|
18
18
|
|
|
19
|
-
const char
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
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;
|
|
24
23
|
}
|
|
25
|
-
|
|
24
|
+
}
|
|
25
|
+
return "";
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const char
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
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;
|
|
33
32
|
}
|
|
34
|
-
|
|
33
|
+
}
|
|
34
|
+
return "";
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
const char
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
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;
|
|
42
41
|
}
|
|
43
|
-
|
|
42
|
+
}
|
|
43
|
+
return NULL;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/* ------------------------------------------------------------------ */
|
|
@@ -51,20 +51,20 @@ const char *cerver_req_header(const cerver_request_t *req, const char *key) {
|
|
|
51
51
|
* Returns 1 if the client sent "Connection: close" or is HTTP/1.0
|
|
52
52
|
* without an explicit "Connection: keep-alive".
|
|
53
53
|
*/
|
|
54
|
-
int cerver_req_wants_close(const cerver_request_t
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
60
|
}
|
|
61
61
|
|
|
62
62
|
/* ------------------------------------------------------------------ */
|
|
63
63
|
/* Server configuration helpers */
|
|
64
64
|
/* ------------------------------------------------------------------ */
|
|
65
65
|
|
|
66
|
-
void cerver_set_dispatch(cerver_server_t
|
|
67
|
-
|
|
66
|
+
void cerver_set_dispatch(cerver_server_t* srv, cerver_dispatch_fn fn) {
|
|
67
|
+
srv->dispatch_override = fn;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/* ------------------------------------------------------------------ */
|
|
@@ -77,96 +77,96 @@ void cerver_set_dispatch(cerver_server_t *srv, cerver_dispatch_fn fn) {
|
|
|
77
77
|
*
|
|
78
78
|
* Uses manual segment iteration instead of strtok_r for speed.
|
|
79
79
|
*/
|
|
80
|
-
int cerver_route_match(const cerver_route_t
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
+
}
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
const char* pattern = route->pattern;
|
|
87
|
+
const char* path = req->path;
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
/* Fast path: exact match */
|
|
90
|
+
if (strcmp(pattern, path) == 0) {
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
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;
|
|
96
133
|
return 0;
|
|
134
|
+
}
|
|
97
135
|
}
|
|
98
136
|
|
|
99
|
-
/*
|
|
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 '/' */
|
|
137
|
+
/* Skip '/' separator */
|
|
106
138
|
if (*pp == '/') pp++;
|
|
107
139
|
if (*rp == '/') rp++;
|
|
140
|
+
}
|
|
108
141
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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;
|
|
133
|
-
return 0;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/* Skip '/' separator */
|
|
138
|
-
if (*pp == '/') pp++;
|
|
139
|
-
if (*rp == '/') rp++;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/* Both must be consumed */
|
|
143
|
-
if (*pp || *rp) {
|
|
144
|
-
req->params_count = saved_params;
|
|
145
|
-
return 0;
|
|
146
|
-
}
|
|
142
|
+
/* Both must be consumed */
|
|
143
|
+
if (*pp || *rp) {
|
|
144
|
+
req->params_count = saved_params;
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
return 1;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
/* ------------------------------------------------------------------ */
|
|
152
152
|
/* Dispatch: find and return the handler for a request */
|
|
153
153
|
/* ------------------------------------------------------------------ */
|
|
154
154
|
|
|
155
|
-
cerver_handler_fn cerver_dispatch(cerver_server_t
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
161
|
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
/* Fall back to generic route table scan */
|
|
163
|
+
if (!srv->routes) return NULL;
|
|
164
164
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
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;
|
|
169
168
|
}
|
|
169
|
+
}
|
|
170
170
|
|
|
171
|
-
|
|
171
|
+
return NULL;
|
|
172
172
|
}
|