@velox0/cerver 0.1.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.
@@ -0,0 +1,137 @@
1
+ /*
2
+ * http_writer.c — HTTP/1.1 response writer.
3
+ *
4
+ * Formats a cerver_response_t into raw HTTP bytes and writes to a socket fd.
5
+ */
6
+
7
+ #include "cerver.h"
8
+
9
+ #include <stdio.h>
10
+ #include <stdlib.h>
11
+ #include <string.h>
12
+ #include <unistd.h>
13
+
14
+ /* ------------------------------------------------------------------ */
15
+ /* Status text lookup */
16
+ /* ------------------------------------------------------------------ */
17
+
18
+ static const char *status_text(int code) {
19
+ switch (code) {
20
+ case 200: return "OK";
21
+ case 201: return "Created";
22
+ case 204: return "No Content";
23
+ case 301: return "Moved Permanently";
24
+ case 302: return "Found";
25
+ case 304: return "Not Modified";
26
+ case 400: return "Bad Request";
27
+ case 401: return "Unauthorized";
28
+ case 403: return "Forbidden";
29
+ case 404: return "Not Found";
30
+ case 405: return "Method Not Allowed";
31
+ case 500: return "Internal Server Error";
32
+ default: return "Unknown";
33
+ }
34
+ }
35
+
36
+ /* ------------------------------------------------------------------ */
37
+ /* Write the full response to fd */
38
+ /* ------------------------------------------------------------------ */
39
+
40
+ int cerver_write_response(int fd, const cerver_response_t *res) {
41
+ /* Build the response header */
42
+ char header[4096];
43
+ int hlen = 0;
44
+
45
+ /* Status line */
46
+ hlen += snprintf(header + hlen, sizeof(header) - (size_t)hlen,
47
+ "HTTP/1.1 %d %s\r\n", res->status, status_text(res->status));
48
+
49
+ /* Content-Type */
50
+ if (res->content_type) {
51
+ hlen += snprintf(header + hlen, sizeof(header) - (size_t)hlen,
52
+ "Content-Type: %s\r\n", res->content_type);
53
+ }
54
+
55
+ /* Content-Length */
56
+ hlen += snprintf(header + hlen, sizeof(header) - (size_t)hlen,
57
+ "Content-Length: %zu\r\n", res->body_len);
58
+
59
+ /* Extra headers */
60
+ for (int i = 0; i < res->header_count; i++) {
61
+ hlen += snprintf(header + hlen, sizeof(header) - (size_t)hlen,
62
+ "%s: %s\r\n",
63
+ res->headers[i].key, res->headers[i].value);
64
+ }
65
+
66
+ /* Connection: close (we don't do keep-alive in v0.1) */
67
+ hlen += snprintf(header + hlen, sizeof(header) - (size_t)hlen,
68
+ "Connection: close\r\n");
69
+
70
+ /* Server header */
71
+ hlen += snprintf(header + hlen, sizeof(header) - (size_t)hlen,
72
+ "Server: cerver\r\n");
73
+
74
+ /* End of headers */
75
+ hlen += snprintf(header + hlen, sizeof(header) - (size_t)hlen, "\r\n");
76
+
77
+ /* Write header */
78
+ ssize_t written = write(fd, header, (size_t)hlen);
79
+ if (written < 0) return -1;
80
+
81
+ /* Write body */
82
+ if (res->body && res->body_len > 0) {
83
+ size_t total = 0;
84
+ while (total < res->body_len) {
85
+ ssize_t n = write(fd, res->body + total, res->body_len - total);
86
+ if (n < 0) return -1;
87
+ total += (size_t)n;
88
+ }
89
+ }
90
+
91
+ return 0;
92
+ }
93
+
94
+ /* ------------------------------------------------------------------ */
95
+ /* Response helper functions */
96
+ /* ------------------------------------------------------------------ */
97
+
98
+ void cerver_res_text(cerver_response_t *res, int status, const char *text) {
99
+ res->status = status;
100
+ res->content_type = "text/plain; charset=utf-8";
101
+ res->body = text;
102
+ res->body_len = strlen(text);
103
+ res->_body_owned = 0;
104
+ }
105
+
106
+ void cerver_res_json(cerver_response_t *res, int status, const char *json) {
107
+ res->status = status;
108
+ res->content_type = "application/json; charset=utf-8";
109
+ res->body = json;
110
+ res->body_len = strlen(json);
111
+ res->_body_owned = 0;
112
+ }
113
+
114
+ void cerver_res_html(cerver_response_t *res, int status, const char *html) {
115
+ res->status = status;
116
+ res->content_type = "text/html; charset=utf-8";
117
+ res->body = html;
118
+ res->body_len = strlen(html);
119
+ res->_body_owned = 0;
120
+ }
121
+
122
+ void cerver_res_file(cerver_response_t *res, int status, const char *mime,
123
+ const unsigned char *data, size_t len) {
124
+ res->status = status;
125
+ res->content_type = mime;
126
+ res->body = (const char *)data;
127
+ res->body_len = len;
128
+ res->_body_owned = 0;
129
+ }
130
+
131
+ void cerver_res_header(cerver_response_t *res, const char *key, const char *val) {
132
+ if (res->header_count < CERVER_MAX_HEADERS) {
133
+ res->headers[res->header_count].key = key;
134
+ res->headers[res->header_count].value = val;
135
+ res->header_count++;
136
+ }
137
+ }
package/runtime/mime.c ADDED
@@ -0,0 +1,84 @@
1
+ /*
2
+ * mime.c — MIME type detection by file extension.
3
+ */
4
+
5
+ #include "cerver.h"
6
+
7
+ #include <string.h>
8
+ #include <ctype.h>
9
+
10
+ typedef struct {
11
+ const char *ext;
12
+ const char *mime;
13
+ } mime_entry_t;
14
+
15
+ static const mime_entry_t mime_table[] = {
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" },
24
+
25
+ /* Text */
26
+ { ".txt", "text/plain; charset=utf-8" },
27
+ { ".csv", "text/csv; charset=utf-8" },
28
+ { ".md", "text/markdown; charset=utf-8" },
29
+
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" },
39
+
40
+ /* Fonts */
41
+ { ".woff", "font/woff" },
42
+ { ".woff2", "font/woff2" },
43
+ { ".ttf", "font/ttf" },
44
+ { ".otf", "font/otf" },
45
+ { ".eot", "application/vnd.ms-fontobject" },
46
+
47
+ /* Media */
48
+ { ".mp4", "video/mp4" },
49
+ { ".webm", "video/webm" },
50
+ { ".ogg", "audio/ogg" },
51
+ { ".mp3", "audio/mpeg" },
52
+ { ".wav", "audio/wav" },
53
+
54
+ /* Archives */
55
+ { ".zip", "application/zip" },
56
+ { ".gz", "application/gzip" },
57
+ { ".tar", "application/x-tar" },
58
+
59
+ /* Documents */
60
+ { ".pdf", "application/pdf" },
61
+
62
+ /* Misc */
63
+ { ".wasm", "application/wasm" },
64
+ { ".map", "application/json" },
65
+
66
+ { NULL, NULL }
67
+ };
68
+
69
+ const char *cerver_mime_from_path(const char *path) {
70
+ if (!path) return "application/octet-stream";
71
+
72
+ /* Find the last '.' in the path */
73
+ const char *dot = strrchr(path, '.');
74
+ if (!dot) return "application/octet-stream";
75
+
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
+ }
81
+ }
82
+
83
+ return "application/octet-stream";
84
+ }
@@ -0,0 +1,150 @@
1
+ /*
2
+ * router.c — Route matching and dispatch.
3
+ *
4
+ * Matches incoming requests against registered route patterns.
5
+ * Supports static paths and dynamic segments (:param).
6
+ */
7
+
8
+ #include "cerver.h"
9
+
10
+ #include <stdio.h>
11
+ #include <stdlib.h>
12
+ #include <string.h>
13
+
14
+ /* ------------------------------------------------------------------ */
15
+ /* Request accessor helpers */
16
+ /* ------------------------------------------------------------------ */
17
+
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
+ }
23
+ }
24
+ return "";
25
+ }
26
+
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
+ }
32
+ }
33
+ return "";
34
+ }
35
+
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
+ }
41
+ }
42
+ return NULL;
43
+ }
44
+
45
+ /* ------------------------------------------------------------------ */
46
+ /* Pattern matching with dynamic segment extraction */
47
+ /* ------------------------------------------------------------------ */
48
+
49
+ /*
50
+ * Match a route pattern against a request path.
51
+ * Pattern segments starting with ':' are dynamic and extract values.
52
+ *
53
+ * Examples:
54
+ * pattern="/art/:key" path="/art/sunset" → match, key="sunset"
55
+ * pattern="/" path="/" → match
56
+ * pattern="/api/data" path="/api/data" → match
57
+ * pattern="/api/data" path="/api/other" → no match
58
+ */
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
+ }
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) {
105
+ return 0;
106
+ }
107
+
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
+ }
132
+
133
+ return 1;
134
+ }
135
+
136
+ /* ------------------------------------------------------------------ */
137
+ /* Dispatch: find and return the handler for a request */
138
+ /* ------------------------------------------------------------------ */
139
+
140
+ cerver_handler_fn cerver_dispatch(cerver_server_t *srv, cerver_request_t *req) {
141
+ if (!srv->routes) return NULL;
142
+
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
+ }
147
+ }
148
+
149
+ return NULL;
150
+ }