@velox0/cerver 0.3.0 → 0.4.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/.github/workflows/publish.yml +4 -4
- package/README.md +5 -1
- package/lib/assets/embed.js +2 -1
- package/lib/codegen/dispatch_gen.js +189 -0
- package/lib/codegen/generator.js +12 -1
- package/lib/commands/new.js +92 -2
- package/lib/compiler/compile.js +29 -2
- package/package.json +1 -1
- package/runtime/cerver.h +85 -14
- package/runtime/http_parser.c +39 -35
- package/runtime/http_writer.c +46 -15
- package/runtime/router.c +70 -48
- package/runtime/server.c +449 -349
- package/runtime/static.c +96 -29
- package/templates/cerver.config.js +1 -0
- package/templates/cerver.png +0 -0
- package/templates/favicon.ico +0 -0
- package/test/run.js +1 -1
package/runtime/http_parser.c
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* http_parser.c — Minimal HTTP/1.1 request parser.
|
|
3
3
|
*
|
|
4
4
|
* Parses method, path, query string, and headers from a raw HTTP request.
|
|
5
|
-
*
|
|
5
|
+
* All parsing is done IN-PLACE on the caller's buffer — no copies are made.
|
|
6
|
+
* The caller must keep the buffer alive for the lifetime of the request.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
#include "cerver.h"
|
|
@@ -48,59 +49,65 @@ static void url_decode(char *str) {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
/* ------------------------------------------------------------------ */
|
|
51
|
-
/* Parse query string: "a=1&b=2" → key-value pairs
|
|
52
|
+
/* Parse query string IN-PLACE: "a=1&b=2" → key-value pairs */
|
|
52
53
|
/* ------------------------------------------------------------------ */
|
|
53
54
|
|
|
54
55
|
static void parse_query_string(char *qs, cerver_request_t *req) {
|
|
55
56
|
if (!qs || !*qs) return;
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
char *
|
|
58
|
+
/* Parse directly on the buffer — no strdup needed */
|
|
59
|
+
char *p = qs;
|
|
59
60
|
|
|
60
|
-
while (
|
|
61
|
-
char *
|
|
61
|
+
while (*p && req->query_count < CERVER_MAX_QUERY) {
|
|
62
|
+
char *pair_start = p;
|
|
63
|
+
|
|
64
|
+
/* Find end of pair (& or NUL) */
|
|
65
|
+
while (*p && *p != '&') p++;
|
|
66
|
+
if (*p == '&') *p++ = '\0';
|
|
67
|
+
|
|
68
|
+
char *eq = strchr(pair_start, '=');
|
|
62
69
|
if (eq) {
|
|
63
70
|
*eq = '\0';
|
|
64
|
-
req->query[req->query_count].key =
|
|
71
|
+
req->query[req->query_count].key = pair_start;
|
|
65
72
|
req->query[req->query_count].value = eq + 1;
|
|
66
73
|
url_decode((char *)req->query[req->query_count].key);
|
|
67
74
|
url_decode((char *)req->query[req->query_count].value);
|
|
68
75
|
} else {
|
|
69
|
-
req->query[req->query_count].key =
|
|
76
|
+
req->query[req->query_count].key = pair_start;
|
|
70
77
|
req->query[req->query_count].value = "";
|
|
71
78
|
}
|
|
72
79
|
req->query_count++;
|
|
73
|
-
pair = strtok_r(NULL, "&", &saveptr);
|
|
74
80
|
}
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
/* ------------------------------------------------------------------ */
|
|
78
|
-
/* Parse the HTTP request
|
|
84
|
+
/* Parse the HTTP request IN-PLACE */
|
|
79
85
|
/* ------------------------------------------------------------------ */
|
|
80
86
|
|
|
81
87
|
int cerver_parse_request(const char *raw, size_t len, cerver_request_t *req) {
|
|
82
88
|
if (!raw || len == 0) return -1;
|
|
83
89
|
|
|
84
|
-
/*
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
/*
|
|
91
|
+
* We parse in-place: the caller gives us a mutable buffer (cast away
|
|
92
|
+
* const — the caller's read_full_request already owns a mutable buffer).
|
|
93
|
+
* All internal pointers (headers, query, body) reference this buffer.
|
|
94
|
+
* The caller must keep it alive for the request's lifetime.
|
|
95
|
+
*/
|
|
96
|
+
char *buf = (char *)raw;
|
|
97
|
+
buf[len] = '\0'; /* caller ensures buf has capacity for len+1 */
|
|
98
|
+
|
|
99
|
+
/* We no longer allocate _raw_buf — the read buffer IS the raw buffer */
|
|
100
|
+
req->_raw_buf = NULL;
|
|
91
101
|
req->_raw_len = len;
|
|
92
102
|
|
|
93
103
|
/* ---- Request line: METHOD PATH HTTP/1.x ---- */
|
|
94
104
|
char *line_end = strstr(buf, "\r\n");
|
|
95
|
-
if (!line_end)
|
|
96
|
-
free(buf);
|
|
97
|
-
return -1;
|
|
98
|
-
}
|
|
105
|
+
if (!line_end) return -1;
|
|
99
106
|
*line_end = '\0';
|
|
100
107
|
|
|
101
108
|
/* Method */
|
|
102
109
|
char *sp1 = strchr(buf, ' ');
|
|
103
|
-
if (!sp1)
|
|
110
|
+
if (!sp1) return -1;
|
|
104
111
|
*sp1 = '\0';
|
|
105
112
|
|
|
106
113
|
size_t method_len = (size_t)(sp1 - buf);
|
|
@@ -117,8 +124,16 @@ int cerver_parse_request(const char *raw, size_t len, cerver_request_t *req) {
|
|
|
117
124
|
char *qmark = strchr(path_start, '?');
|
|
118
125
|
if (qmark) {
|
|
119
126
|
*qmark = '\0';
|
|
120
|
-
|
|
121
|
-
|
|
127
|
+
/* Point query_string directly into the buffer */
|
|
128
|
+
char *qs_start = qmark + 1;
|
|
129
|
+
size_t qs_len = strlen(qs_start);
|
|
130
|
+
if (qs_len >= sizeof(req->query_string)) qs_len = sizeof(req->query_string) - 1;
|
|
131
|
+
memcpy(req->query_string, qs_start, qs_len);
|
|
132
|
+
req->query_string[qs_len] = '\0';
|
|
133
|
+
|
|
134
|
+
/* Parse query params in-place from query_string
|
|
135
|
+
* (we copied to req->query_string so params point into req memory) */
|
|
136
|
+
parse_query_string(req->query_string, req);
|
|
122
137
|
}
|
|
123
138
|
|
|
124
139
|
/* Decode and store path */
|
|
@@ -132,17 +147,6 @@ int cerver_parse_request(const char *raw, size_t len, cerver_request_t *req) {
|
|
|
132
147
|
req->path[plen - 1] = '\0';
|
|
133
148
|
}
|
|
134
149
|
|
|
135
|
-
/* Parse query string */
|
|
136
|
-
if (req->query_string[0]) {
|
|
137
|
-
/* We need a mutable copy for strtok */
|
|
138
|
-
char *qs_copy = strdup(req->query_string);
|
|
139
|
-
if (qs_copy) {
|
|
140
|
-
parse_query_string(qs_copy, req);
|
|
141
|
-
/* Note: keys/values point into qs_copy which we leak intentionally
|
|
142
|
-
since the request's lifetime is short (one connection). */
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
150
|
/* ---- Headers ---- */
|
|
147
151
|
char *hdr_start = line_end + 2; /* skip \r\n */
|
|
148
152
|
size_t content_length = 0;
|
package/runtime/http_writer.c
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* http_writer.c — HTTP/1.1 response writer.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Uses writev() for zero-copy header+body writes.
|
|
5
|
+
* Supports keep-alive and Connection: close signaling.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
#include "cerver.h"
|
|
@@ -10,6 +11,7 @@
|
|
|
10
11
|
#include <stdlib.h>
|
|
11
12
|
#include <string.h>
|
|
12
13
|
#include <unistd.h>
|
|
14
|
+
#include <sys/uio.h>
|
|
13
15
|
|
|
14
16
|
/* ------------------------------------------------------------------ */
|
|
15
17
|
/* Status text lookup */
|
|
@@ -29,15 +31,16 @@ static const char *status_text(int code) {
|
|
|
29
31
|
case 404: return "Not Found";
|
|
30
32
|
case 405: return "Method Not Allowed";
|
|
31
33
|
case 500: return "Internal Server Error";
|
|
34
|
+
case 503: return "Service Unavailable";
|
|
32
35
|
default: return "Unknown";
|
|
33
36
|
}
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
/* ------------------------------------------------------------------ */
|
|
37
|
-
/* Write the full response to fd
|
|
40
|
+
/* Write the full response to fd using writev */
|
|
38
41
|
/* ------------------------------------------------------------------ */
|
|
39
42
|
|
|
40
|
-
int cerver_write_response(int fd, const cerver_response_t *res) {
|
|
43
|
+
int cerver_write_response(int fd, const cerver_response_t *res, int keepalive) {
|
|
41
44
|
/* Build the response header */
|
|
42
45
|
char header[4096];
|
|
43
46
|
int hlen = 0;
|
|
@@ -63,9 +66,14 @@ int cerver_write_response(int fd, const cerver_response_t *res) {
|
|
|
63
66
|
res->headers[i].key, res->headers[i].value);
|
|
64
67
|
}
|
|
65
68
|
|
|
66
|
-
/* Connection
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
/* Connection header — honor keep-alive state */
|
|
70
|
+
if (keepalive && !res->_force_close) {
|
|
71
|
+
hlen += snprintf(header + hlen, sizeof(header) - (size_t)hlen,
|
|
72
|
+
"Connection: keep-alive\r\n");
|
|
73
|
+
} else {
|
|
74
|
+
hlen += snprintf(header + hlen, sizeof(header) - (size_t)hlen,
|
|
75
|
+
"Connection: close\r\n");
|
|
76
|
+
}
|
|
69
77
|
|
|
70
78
|
/* Server header */
|
|
71
79
|
hlen += snprintf(header + hlen, sizeof(header) - (size_t)hlen,
|
|
@@ -74,18 +82,41 @@ int cerver_write_response(int fd, const cerver_response_t *res) {
|
|
|
74
82
|
/* End of headers */
|
|
75
83
|
hlen += snprintf(header + hlen, sizeof(header) - (size_t)hlen, "\r\n");
|
|
76
84
|
|
|
77
|
-
/*
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
/* Write body */
|
|
85
|
+
/*
|
|
86
|
+
* Use writev() to send header + body in a single syscall.
|
|
87
|
+
* This avoids Nagle interaction and reduces context switches.
|
|
88
|
+
*/
|
|
82
89
|
if (res->body && res->body_len > 0) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
struct iovec iov[2];
|
|
91
|
+
iov[0].iov_base = header;
|
|
92
|
+
iov[0].iov_len = (size_t)hlen;
|
|
93
|
+
iov[1].iov_base = (void *)res->body;
|
|
94
|
+
iov[1].iov_len = res->body_len;
|
|
95
|
+
|
|
96
|
+
size_t total = iov[0].iov_len + iov[1].iov_len;
|
|
97
|
+
size_t written = 0;
|
|
98
|
+
|
|
99
|
+
while (written < total) {
|
|
100
|
+
ssize_t n = writev(fd, iov, 2);
|
|
86
101
|
if (n < 0) return -1;
|
|
87
|
-
|
|
102
|
+
written += (size_t)n;
|
|
103
|
+
|
|
104
|
+
/* Adjust iov for partial writes */
|
|
105
|
+
if (written < iov[0].iov_len) {
|
|
106
|
+
iov[0].iov_base = header + written;
|
|
107
|
+
iov[0].iov_len -= (size_t)n;
|
|
108
|
+
} else {
|
|
109
|
+
/* Header fully sent, adjust body iov */
|
|
110
|
+
size_t body_sent = written - (size_t)hlen;
|
|
111
|
+
iov[0].iov_len = 0;
|
|
112
|
+
iov[1].iov_base = (void *)(res->body + body_sent);
|
|
113
|
+
iov[1].iov_len = res->body_len - body_sent;
|
|
114
|
+
}
|
|
88
115
|
}
|
|
116
|
+
} else {
|
|
117
|
+
/* No body — just send header */
|
|
118
|
+
ssize_t written = write(fd, header, (size_t)hlen);
|
|
119
|
+
if (written < 0) return -1;
|
|
89
120
|
}
|
|
90
121
|
|
|
91
122
|
return 0;
|
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"
|
|
@@ -42,6 +43,30 @@ const char *cerver_req_header(const cerver_request_t *req, const char *key) {
|
|
|
42
43
|
return NULL;
|
|
43
44
|
}
|
|
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;
|
|
68
|
+
}
|
|
69
|
+
|
|
45
70
|
/* ------------------------------------------------------------------ */
|
|
46
71
|
/* Pattern matching with dynamic segment extraction */
|
|
47
72
|
/* ------------------------------------------------------------------ */
|
|
@@ -50,11 +75,7 @@ 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
|
-
*
|
|
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
80
|
int cerver_route_match(const cerver_route_t *route, cerver_request_t *req) {
|
|
60
81
|
/* Method must match */
|
|
@@ -70,64 +91,58 @@ int cerver_route_match(const cerver_route_t *route, cerver_request_t *req) {
|
|
|
70
91
|
return 1;
|
|
71
92
|
}
|
|
72
93
|
|
|
73
|
-
/*
|
|
74
|
-
|
|
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
|
-
}
|
|
96
|
-
|
|
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
|
-
}
|
|
102
|
-
|
|
103
|
-
/* Segment counts must match */
|
|
104
|
-
if (pat_count != path_count) {
|
|
94
|
+
/* No dynamic segments? Then the strcmp above was definitive */
|
|
95
|
+
if (!strchr(pattern, ':')) {
|
|
105
96
|
return 0;
|
|
106
97
|
}
|
|
107
98
|
|
|
108
|
-
/*
|
|
109
|
-
|
|
99
|
+
/* Segment-by-segment matching without strtok_r */
|
|
100
|
+
const char *pp = pattern; /* pattern pointer */
|
|
101
|
+
const char *rp = path; /* request path pointer */
|
|
102
|
+
|
|
110
103
|
int saved_params = req->params_count;
|
|
111
104
|
|
|
112
|
-
|
|
113
|
-
|
|
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] == ':') {
|
|
114
121
|
/* Dynamic segment — extract parameter */
|
|
115
122
|
if (req->params_count < CERVER_MAX_PARAMS) {
|
|
116
|
-
|
|
117
|
-
/*
|
|
118
|
-
|
|
119
|
-
req->params[req->params_count].
|
|
120
|
-
req->params[req->params_count].value = path_segments[i];
|
|
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;
|
|
121
127
|
req->params_count++;
|
|
122
128
|
}
|
|
123
129
|
} else {
|
|
124
130
|
/* Static segment — must match exactly */
|
|
125
|
-
if (
|
|
126
|
-
/* Restore params on mismatch */
|
|
131
|
+
if (pp_len != rp_len || memcmp(pp_seg, rp_seg, pp_len) != 0) {
|
|
127
132
|
req->params_count = saved_params;
|
|
128
133
|
return 0;
|
|
129
134
|
}
|
|
130
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;
|
|
131
146
|
}
|
|
132
147
|
|
|
133
148
|
return 1;
|
|
@@ -138,6 +153,13 @@ int cerver_route_match(const cerver_route_t *route, cerver_request_t *req) {
|
|
|
138
153
|
/* ------------------------------------------------------------------ */
|
|
139
154
|
|
|
140
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 */
|
|
141
163
|
if (!srv->routes) return NULL;
|
|
142
164
|
|
|
143
165
|
for (int i = 0; i < srv->route_count; i++) {
|