@velox0/cerver 0.3.1 → 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/lib/assets/embed.js +2 -1
- package/lib/codegen/dispatch_gen.js +189 -0
- package/lib/codegen/generator.js +12 -1
- 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/test/run.js +1 -1
|
@@ -18,18 +18,18 @@ jobs:
|
|
|
18
18
|
|
|
19
19
|
steps:
|
|
20
20
|
- name: Checkout
|
|
21
|
-
uses: actions/checkout@
|
|
21
|
+
uses: actions/checkout@v6
|
|
22
22
|
|
|
23
23
|
- name: Setup pnpm
|
|
24
|
-
uses: pnpm/action-setup@
|
|
24
|
+
uses: pnpm/action-setup@v6
|
|
25
25
|
with:
|
|
26
26
|
version: 10
|
|
27
27
|
run_install: false
|
|
28
28
|
|
|
29
29
|
- name: Setup Node.js
|
|
30
|
-
uses: actions/setup-node@
|
|
30
|
+
uses: actions/setup-node@v6
|
|
31
31
|
with:
|
|
32
|
-
node-version:
|
|
32
|
+
node-version: 24
|
|
33
33
|
registry-url: https://registry.npmjs.org
|
|
34
34
|
cache: pnpm
|
|
35
35
|
|
package/lib/assets/embed.js
CHANGED
|
@@ -179,7 +179,8 @@ async function generateEmbeddedAssets(assets, shouldMinify, compression) {
|
|
|
179
179
|
` { "${entry.servePath}", "${entry.mime}", ` +
|
|
180
180
|
`${entry.name}, ${entry.name}_len, ` +
|
|
181
181
|
`${entry.gzName}, ${entry.gzLen}, ` +
|
|
182
|
-
`${entry.brName}, ${entry.brLen}
|
|
182
|
+
`${entry.brName}, ${entry.brLen}, ` +
|
|
183
|
+
`NULL, 0 },`
|
|
183
184
|
);
|
|
184
185
|
}
|
|
185
186
|
lines.push("};");
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { cString, handlerName } = require("./emit");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate a compile-time dispatch function that avoids the generic router.
|
|
7
|
+
*
|
|
8
|
+
* For static routes: method check → length check → memcmp (known lengths).
|
|
9
|
+
* For dynamic routes: segment-count check → per-segment inline comparisons.
|
|
10
|
+
*
|
|
11
|
+
* Falls through to generic router for unmatched paths.
|
|
12
|
+
*
|
|
13
|
+
* @param {IRRoute[]} routes - All IR routes
|
|
14
|
+
* @returns {string} - C source for cerver_generated_dispatch()
|
|
15
|
+
*/
|
|
16
|
+
function generateDispatch(routes) {
|
|
17
|
+
const lines = [];
|
|
18
|
+
|
|
19
|
+
/* Separate static and dynamic routes */
|
|
20
|
+
const staticRoutes = routes.filter((r) => !r.urlPath.includes(":"));
|
|
21
|
+
const dynamicRoutes = routes.filter((r) => r.urlPath.includes(":"));
|
|
22
|
+
|
|
23
|
+
/* Forward declarations for handler functions */
|
|
24
|
+
lines.push("/* ---- Generated Fast Dispatch ---- */");
|
|
25
|
+
lines.push("");
|
|
26
|
+
|
|
27
|
+
/* Group static routes by method */
|
|
28
|
+
const byMethod = {};
|
|
29
|
+
for (const route of staticRoutes) {
|
|
30
|
+
if (!byMethod[route.method]) byMethod[route.method] = [];
|
|
31
|
+
byMethod[route.method].push(route);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Group dynamic routes by method */
|
|
35
|
+
const dynByMethod = {};
|
|
36
|
+
for (const route of dynamicRoutes) {
|
|
37
|
+
if (!dynByMethod[route.method]) dynByMethod[route.method] = [];
|
|
38
|
+
dynByMethod[route.method].push(route);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Generate the dispatch function */
|
|
42
|
+
lines.push(
|
|
43
|
+
"static cerver_handler_fn cerver_generated_dispatch(cerver_request_t *req) {"
|
|
44
|
+
);
|
|
45
|
+
lines.push(" const char *path = req->path;");
|
|
46
|
+
lines.push(" size_t path_len = 0;");
|
|
47
|
+
lines.push(" { const char *p = path; while (*p) { path_len++; p++; } }");
|
|
48
|
+
lines.push("");
|
|
49
|
+
|
|
50
|
+
const allMethods = new Set([
|
|
51
|
+
...Object.keys(byMethod),
|
|
52
|
+
...Object.keys(dynByMethod),
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
let firstMethod = true;
|
|
56
|
+
for (const method of allMethods) {
|
|
57
|
+
const methodStatic = byMethod[method] || [];
|
|
58
|
+
const methodDynamic = dynByMethod[method] || [];
|
|
59
|
+
|
|
60
|
+
/* Method check */
|
|
61
|
+
const methodLen = method.length;
|
|
62
|
+
lines.push(
|
|
63
|
+
` ${firstMethod ? "" : "} else "}if (req->method[0] == '${method[0]}' && memcmp(req->method, ${cString(method)}, ${methodLen + 1}) == 0) {`
|
|
64
|
+
);
|
|
65
|
+
firstMethod = false;
|
|
66
|
+
|
|
67
|
+
/* ---- Static routes: group by path length for fast rejection ---- */
|
|
68
|
+
if (methodStatic.length > 0) {
|
|
69
|
+
/* Group by length */
|
|
70
|
+
const byLength = {};
|
|
71
|
+
for (const route of methodStatic) {
|
|
72
|
+
const len = route.urlPath.length;
|
|
73
|
+
if (!byLength[len]) byLength[len] = [];
|
|
74
|
+
byLength[len].push(route);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
lines.push(" /* Static routes */");
|
|
78
|
+
|
|
79
|
+
const lengths = Object.keys(byLength)
|
|
80
|
+
.map(Number)
|
|
81
|
+
.sort((a, b) => a - b);
|
|
82
|
+
|
|
83
|
+
for (const len of lengths) {
|
|
84
|
+
const routesAtLen = byLength[len];
|
|
85
|
+
|
|
86
|
+
lines.push(` if (path_len == ${len}) {`);
|
|
87
|
+
|
|
88
|
+
for (const route of routesAtLen) {
|
|
89
|
+
const name = handlerName(route.method, route.urlPath);
|
|
90
|
+
if (route.urlPath === "/") {
|
|
91
|
+
lines.push(
|
|
92
|
+
` if (path[0] == '/' && path[1] == '\\0') return ${name};`
|
|
93
|
+
);
|
|
94
|
+
} else {
|
|
95
|
+
lines.push(
|
|
96
|
+
` if (memcmp(path, ${cString(route.urlPath)}, ${len}) == 0) return ${name};`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
lines.push(" }");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* ---- Dynamic routes: inline segment matching ---- */
|
|
106
|
+
if (methodDynamic.length > 0) {
|
|
107
|
+
lines.push("");
|
|
108
|
+
lines.push(" /* Dynamic routes */");
|
|
109
|
+
|
|
110
|
+
for (const route of methodDynamic) {
|
|
111
|
+
const segments = route.urlPath.split("/").filter(Boolean);
|
|
112
|
+
const name = handlerName(route.method, route.urlPath);
|
|
113
|
+
|
|
114
|
+
lines.push(" {");
|
|
115
|
+
lines.push(` /* ${route.urlPath} */`);
|
|
116
|
+
lines.push(` const char *p = path;`);
|
|
117
|
+
lines.push(` if (*p == '/') p++;`);
|
|
118
|
+
lines.push(` int match = 1;`);
|
|
119
|
+
|
|
120
|
+
for (let i = 0; i < segments.length; i++) {
|
|
121
|
+
const seg = segments[i];
|
|
122
|
+
const isLast = i === segments.length - 1;
|
|
123
|
+
|
|
124
|
+
if (seg.startsWith(":")) {
|
|
125
|
+
const paramName = seg.slice(1);
|
|
126
|
+
/* Dynamic segment: find the segment boundaries */
|
|
127
|
+
lines.push(` const char *seg${i}_start = p;`);
|
|
128
|
+
lines.push(` while (*p && *p != '/') p++;`);
|
|
129
|
+
lines.push(` size_t seg${i}_len = (size_t)(p - seg${i}_start);`);
|
|
130
|
+
lines.push(` if (seg${i}_len == 0) match = 0;`);
|
|
131
|
+
|
|
132
|
+
if (!isLast) {
|
|
133
|
+
lines.push(` if (match && *p == '/') p++; else if (!${isLast}) match = 0;`);
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
/* Static segment: compare directly */
|
|
137
|
+
const segLen = seg.length;
|
|
138
|
+
lines.push(
|
|
139
|
+
` if (match && memcmp(p, ${cString(seg)}, ${segLen}) == 0 && (p[${segLen}] == '/' || p[${segLen}] == '\\0')) {`
|
|
140
|
+
);
|
|
141
|
+
lines.push(` p += ${segLen};`);
|
|
142
|
+
lines.push(` if (*p == '/') p++;`);
|
|
143
|
+
lines.push(` } else { match = 0; }`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* Ensure path is fully consumed */
|
|
148
|
+
lines.push(` if (match && *p != '\\0') match = 0;`);
|
|
149
|
+
|
|
150
|
+
/* Extract params on match */
|
|
151
|
+
lines.push(` if (match) {`);
|
|
152
|
+
|
|
153
|
+
/* Re-iterate to set params */
|
|
154
|
+
let paramIdx = 0;
|
|
155
|
+
for (let i = 0; i < segments.length; i++) {
|
|
156
|
+
const seg = segments[i];
|
|
157
|
+
if (seg.startsWith(":")) {
|
|
158
|
+
const paramName = seg.slice(1);
|
|
159
|
+
lines.push(
|
|
160
|
+
` req->params[req->params_count].key = ${cString(paramName)};`
|
|
161
|
+
);
|
|
162
|
+
lines.push(
|
|
163
|
+
` req->params[req->params_count].value = seg${i}_start;`
|
|
164
|
+
);
|
|
165
|
+
lines.push(` req->params_count++;`);
|
|
166
|
+
paramIdx++;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
lines.push(` return ${name};`);
|
|
171
|
+
lines.push(` }`);
|
|
172
|
+
lines.push(` }`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!firstMethod) {
|
|
178
|
+
lines.push(" }");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
lines.push("");
|
|
182
|
+
lines.push(" return NULL; /* No match — fall through to generic router */");
|
|
183
|
+
lines.push("}");
|
|
184
|
+
lines.push("");
|
|
185
|
+
|
|
186
|
+
return lines.join("\n");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = { generateDispatch };
|
package/lib/codegen/generator.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { handlerName, emitStatement } = require("./emit");
|
|
4
4
|
const { generateRouteTable } = require("./route_table");
|
|
5
|
+
const { generateDispatch } = require("./dispatch_gen");
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Generate the complete C server source file from IR routes.
|
|
@@ -70,6 +71,11 @@ function generateServer(routes, config, assetsCode) {
|
|
|
70
71
|
lines.push("");
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
/* ---- Generated fast dispatch ---- */
|
|
75
|
+
lines.push("/* ---- Generated Fast Dispatch (compile-time optimized) ---- */");
|
|
76
|
+
lines.push("");
|
|
77
|
+
lines.push(generateDispatch(routes));
|
|
78
|
+
|
|
73
79
|
/* ---- Main function ---- */
|
|
74
80
|
lines.push("/* ---- Entry Point ---- */");
|
|
75
81
|
lines.push("");
|
|
@@ -81,7 +87,7 @@ function generateServer(routes, config, assetsCode) {
|
|
|
81
87
|
lines.push("");
|
|
82
88
|
lines.push(" /* Allow PORT env override */");
|
|
83
89
|
lines.push(' const char *env_port = getenv("CERVER_PORT");');
|
|
84
|
-
lines.push(
|
|
90
|
+
lines.push(' if (!env_port) env_port = getenv("PORT");');
|
|
85
91
|
lines.push(" if (env_port) port = atoi(env_port);");
|
|
86
92
|
lines.push("");
|
|
87
93
|
lines.push(" cerver_server_t srv;");
|
|
@@ -92,6 +98,11 @@ function generateServer(routes, config, assetsCode) {
|
|
|
92
98
|
);
|
|
93
99
|
lines.push("");
|
|
94
100
|
|
|
101
|
+
/* Wire up generated dispatch */
|
|
102
|
+
lines.push(" /* Use generated compile-time dispatch for fast routing */");
|
|
103
|
+
lines.push(" cerver_set_dispatch(&srv, cerver_generated_dispatch);");
|
|
104
|
+
lines.push("");
|
|
105
|
+
|
|
95
106
|
if (assetsCode) {
|
|
96
107
|
lines.push(
|
|
97
108
|
" cerver_set_assets(&srv, cerver_embedded_assets, cerver_embedded_asset_count);"
|
package/lib/compiler/compile.js
CHANGED
|
@@ -22,6 +22,20 @@ function detectCompiler() {
|
|
|
22
22
|
);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Check if a compiler supports a given flag.
|
|
27
|
+
*/
|
|
28
|
+
function supportsFlag(cc, flag) {
|
|
29
|
+
try {
|
|
30
|
+
execSync(`echo 'int main(){}' | ${cc} ${flag} -x c -o /dev/null - 2>/dev/null`, {
|
|
31
|
+
stdio: "ignore",
|
|
32
|
+
});
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
25
39
|
/**
|
|
26
40
|
* Compile the generated C source into a binary.
|
|
27
41
|
*
|
|
@@ -46,9 +60,9 @@ function compile(distDir, runtimeDir, opts) {
|
|
|
46
60
|
.filter((f) => f.endsWith(".c"))
|
|
47
61
|
.map((f) => path.join(runtimeDir, f));
|
|
48
62
|
|
|
49
|
-
/* Build the compiler command */
|
|
63
|
+
/* Build the compiler command with aggressive optimization */
|
|
50
64
|
const args = [
|
|
51
|
-
"-
|
|
65
|
+
"-O3",
|
|
52
66
|
"-Wall",
|
|
53
67
|
"-Wextra",
|
|
54
68
|
"-Wno-unused-parameter",
|
|
@@ -60,10 +74,23 @@ function compile(distDir, runtimeDir, opts) {
|
|
|
60
74
|
"-lpthread",
|
|
61
75
|
];
|
|
62
76
|
|
|
77
|
+
/* Add LTO if supported */
|
|
78
|
+
if (supportsFlag(cc, "-flto")) {
|
|
79
|
+
args.splice(1, 0, "-flto");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Add march=native if supported (for SIMD/hardware-specific opts) */
|
|
83
|
+
if (supportsFlag(cc, "-march=native")) {
|
|
84
|
+
args.push("-march=native");
|
|
85
|
+
}
|
|
86
|
+
|
|
63
87
|
if (opts && opts.static) {
|
|
64
88
|
args.push("-static");
|
|
65
89
|
}
|
|
66
90
|
|
|
91
|
+
/* On macOS, we need to define _GNU_SOURCE for some functions */
|
|
92
|
+
args.push("-D_GNU_SOURCE");
|
|
93
|
+
|
|
67
94
|
console.log(` compiling with ${cc}...`);
|
|
68
95
|
console.log(` ${cc} ${args.join(" ")}`);
|
|
69
96
|
|
package/package.json
CHANGED
package/runtime/cerver.h
CHANGED
|
@@ -26,8 +26,21 @@
|
|
|
26
26
|
#define CERVER_READ_BUF_MAX (1 << 20) /* 1 MB hard limit */
|
|
27
27
|
#define CERVER_MAX_ROUTES 256
|
|
28
28
|
|
|
29
|
+
/* Keep-alive settings */
|
|
30
|
+
#define CERVER_KEEPALIVE_MAX 10000 /* max requests per connection */
|
|
31
|
+
#define CERVER_KEEPALIVE_TIMEOUT 5 /* seconds idle between requests */
|
|
32
|
+
|
|
33
|
+
/* Event loop tuning */
|
|
34
|
+
#define CERVER_MAX_EVENTS 256
|
|
35
|
+
#define CERVER_LISTEN_BACKLOG 4096
|
|
36
|
+
|
|
37
|
+
/* Worker architecture */
|
|
29
38
|
#define CERVER_THREAD_POOL_DEFAULT 4
|
|
30
|
-
#define CERVER_TASK_QUEUE_SIZE
|
|
39
|
+
#define CERVER_TASK_QUEUE_SIZE 1024
|
|
40
|
+
|
|
41
|
+
/* Stat cache for filesystem serving */
|
|
42
|
+
#define CERVER_STAT_CACHE_SIZE 256
|
|
43
|
+
#define CERVER_STAT_CACHE_TTL 60 /* seconds */
|
|
31
44
|
|
|
32
45
|
/* ------------------------------------------------------------------ */
|
|
33
46
|
/* Key-value pair (used for headers, query params, route params) */
|
|
@@ -68,7 +81,7 @@ typedef struct {
|
|
|
68
81
|
const char *body;
|
|
69
82
|
size_t body_len;
|
|
70
83
|
|
|
71
|
-
/* Internal: raw buffer ownership */
|
|
84
|
+
/* Internal: raw buffer ownership (NULL if in-place parsing used) */
|
|
72
85
|
char *_raw_buf;
|
|
73
86
|
size_t _raw_len;
|
|
74
87
|
} cerver_request_t;
|
|
@@ -91,6 +104,9 @@ typedef struct {
|
|
|
91
104
|
|
|
92
105
|
/* Internal flag: was body malloc'd? */
|
|
93
106
|
int _body_owned;
|
|
107
|
+
|
|
108
|
+
/* Keep-alive control: set to 1 to force close after response */
|
|
109
|
+
int _force_close;
|
|
94
110
|
} cerver_response_t;
|
|
95
111
|
|
|
96
112
|
/* Response helpers — called by generated handler code */
|
|
@@ -109,6 +125,9 @@ const char *cerver_req_param(const cerver_request_t *req, const char *key);
|
|
|
109
125
|
const char *cerver_req_query(const cerver_request_t *req, const char *key);
|
|
110
126
|
const char *cerver_req_header(const cerver_request_t *req, const char *key);
|
|
111
127
|
|
|
128
|
+
/* Check if client wants to close after this request */
|
|
129
|
+
int cerver_req_wants_close(const cerver_request_t *req);
|
|
130
|
+
|
|
112
131
|
/* ------------------------------------------------------------------ */
|
|
113
132
|
/* Route definition */
|
|
114
133
|
/* ------------------------------------------------------------------ */
|
|
@@ -136,13 +155,54 @@ typedef struct {
|
|
|
136
155
|
size_t data_gz_len;
|
|
137
156
|
const unsigned char *data_br;
|
|
138
157
|
size_t data_br_len;
|
|
158
|
+
|
|
159
|
+
/* Pre-computed response header (NULL if not generated) */
|
|
160
|
+
const char *prebuilt_header;
|
|
161
|
+
size_t prebuilt_header_len;
|
|
139
162
|
} cerver_asset_t;
|
|
140
163
|
|
|
141
164
|
/* ------------------------------------------------------------------ */
|
|
142
|
-
/*
|
|
165
|
+
/* Stat cache for filesystem serving */
|
|
166
|
+
/* ------------------------------------------------------------------ */
|
|
167
|
+
|
|
168
|
+
typedef struct {
|
|
169
|
+
char path[CERVER_MAX_PATH];
|
|
170
|
+
size_t file_size;
|
|
171
|
+
time_t mtime;
|
|
172
|
+
time_t cached_at;
|
|
173
|
+
int valid;
|
|
174
|
+
} cerver_stat_entry_t;
|
|
175
|
+
|
|
176
|
+
typedef struct {
|
|
177
|
+
cerver_stat_entry_t entries[CERVER_STAT_CACHE_SIZE];
|
|
178
|
+
pthread_mutex_t lock;
|
|
179
|
+
} cerver_stat_cache_t;
|
|
180
|
+
|
|
181
|
+
/* ------------------------------------------------------------------ */
|
|
182
|
+
/* Generated dispatch (compile-time route optimization) */
|
|
183
|
+
/* ------------------------------------------------------------------ */
|
|
184
|
+
|
|
185
|
+
typedef cerver_handler_fn (*cerver_dispatch_fn)(cerver_request_t *req);
|
|
186
|
+
|
|
143
187
|
/* ------------------------------------------------------------------ */
|
|
188
|
+
/* Worker state (per-core event loop) */
|
|
189
|
+
/* ------------------------------------------------------------------ */
|
|
190
|
+
|
|
191
|
+
typedef struct cerver_server cerver_server_t;
|
|
144
192
|
|
|
145
193
|
typedef struct {
|
|
194
|
+
int id;
|
|
195
|
+
int event_fd; /* kqueue or epoll fd */
|
|
196
|
+
int listen_fd; /* per-worker on Linux, shared on macOS */
|
|
197
|
+
cerver_server_t *srv;
|
|
198
|
+
pthread_t thread;
|
|
199
|
+
} cerver_worker_t;
|
|
200
|
+
|
|
201
|
+
/* ------------------------------------------------------------------ */
|
|
202
|
+
/* Server */
|
|
203
|
+
/* ------------------------------------------------------------------ */
|
|
204
|
+
|
|
205
|
+
struct cerver_server {
|
|
146
206
|
int port;
|
|
147
207
|
int sock_fd;
|
|
148
208
|
cerver_route_t *routes;
|
|
@@ -152,22 +212,23 @@ typedef struct {
|
|
|
152
212
|
const char *public_dir; /* NULL if embedded mode */
|
|
153
213
|
volatile int running;
|
|
154
214
|
|
|
155
|
-
/*
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
215
|
+
/* Generated dispatch override (faster than generic router) */
|
|
216
|
+
cerver_dispatch_fn dispatch_override;
|
|
217
|
+
|
|
218
|
+
/* Stat cache for filesystem serving */
|
|
219
|
+
cerver_stat_cache_t stat_cache;
|
|
220
|
+
|
|
221
|
+
/* Worker pool */
|
|
222
|
+
int worker_count;
|
|
223
|
+
cerver_worker_t *workers;
|
|
224
|
+
};
|
|
165
225
|
|
|
166
226
|
/* Server lifecycle */
|
|
167
227
|
int cerver_init(cerver_server_t *srv, int port, int threads);
|
|
168
228
|
int cerver_add_routes(cerver_server_t *srv, cerver_route_t *routes, int count);
|
|
169
229
|
int cerver_set_assets(cerver_server_t *srv, cerver_asset_t *assets, int count);
|
|
170
230
|
void cerver_set_public_dir(cerver_server_t *srv, const char *dir);
|
|
231
|
+
void cerver_set_dispatch(cerver_server_t *srv, cerver_dispatch_fn fn);
|
|
171
232
|
int cerver_listen(cerver_server_t *srv);
|
|
172
233
|
void cerver_shutdown(cerver_server_t *srv);
|
|
173
234
|
|
|
@@ -181,7 +242,7 @@ int cerver_parse_request(const char *raw, size_t len, cerver_request_t *req);
|
|
|
181
242
|
/* HTTP writer (internal) */
|
|
182
243
|
/* ------------------------------------------------------------------ */
|
|
183
244
|
|
|
184
|
-
int cerver_write_response(int fd, const cerver_response_t *res);
|
|
245
|
+
int cerver_write_response(int fd, const cerver_response_t *res, int keepalive);
|
|
185
246
|
|
|
186
247
|
/* ------------------------------------------------------------------ */
|
|
187
248
|
/* Router (internal) */
|
|
@@ -203,4 +264,14 @@ const char *cerver_mime_from_path(const char *path);
|
|
|
203
264
|
int cerver_serve_static(cerver_server_t *srv, cerver_request_t *req,
|
|
204
265
|
cerver_response_t *res);
|
|
205
266
|
|
|
267
|
+
/* ------------------------------------------------------------------ */
|
|
268
|
+
/* Stat cache (internal) */
|
|
269
|
+
/* ------------------------------------------------------------------ */
|
|
270
|
+
|
|
271
|
+
void cerver_stat_cache_init(cerver_stat_cache_t *cache);
|
|
272
|
+
int cerver_stat_cache_lookup(cerver_stat_cache_t *cache, const char *path,
|
|
273
|
+
size_t *file_size);
|
|
274
|
+
void cerver_stat_cache_store(cerver_stat_cache_t *cache, const char *path,
|
|
275
|
+
size_t file_size, time_t mtime);
|
|
276
|
+
|
|
206
277
|
#endif /* CERVER_H */
|
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;
|