@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.
@@ -18,18 +18,18 @@ jobs:
18
18
 
19
19
  steps:
20
20
  - name: Checkout
21
- uses: actions/checkout@v4
21
+ uses: actions/checkout@v6
22
22
 
23
23
  - name: Setup pnpm
24
- uses: pnpm/action-setup@v4
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@v4
30
+ uses: actions/setup-node@v6
31
31
  with:
32
- node-version: 20
32
+ node-version: 24
33
33
  registry-url: https://registry.npmjs.org
34
34
  cache: pnpm
35
35
 
@@ -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 };
@@ -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(" if (!env_port) env_port = getenv(\"PORT\");");
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);"
@@ -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
- "-O2",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velox0/cerver",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Compile restricted JavaScript server logic into optimized native C binaries",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
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 256
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
- /* Server */
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
- /* Thread pool */
156
- int thread_count;
157
- pthread_t *threads;
158
- int task_queue[CERVER_TASK_QUEUE_SIZE];
159
- int tq_head;
160
- int tq_tail;
161
- int tq_count;
162
- pthread_mutex_t tq_mutex;
163
- pthread_cond_t tq_cond;
164
- } cerver_server_t;
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 */
@@ -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
- * No external dependencies.
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
- char *saveptr = NULL;
58
- char *pair = strtok_r(qs, "&", &saveptr);
58
+ /* Parse directly on the buffer — no strdup needed */
59
+ char *p = qs;
59
60
 
60
- while (pair && req->query_count < CERVER_MAX_QUERY) {
61
- char *eq = strchr(pair, '=');
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 = pair;
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 = pair;
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
- /* We need a mutable copy because we'll be inserting NUL terminators */
85
- char *buf = malloc(len + 1);
86
- if (!buf) return -1;
87
- memcpy(buf, raw, len);
88
- buf[len] = '\0';
89
-
90
- req->_raw_buf = buf;
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) { free(buf); return -1; }
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
- strncpy(req->query_string, qmark + 1, sizeof(req->query_string) - 1);
121
- req->query_string[sizeof(req->query_string) - 1] = '\0';
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;