@velox0/cerver 0.6.4 → 0.6.5

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.
@@ -421,7 +421,10 @@ function emitStringOpBlock(expr, pad, varName) {
421
421
  call = `""` ;
422
422
  }
423
423
 
424
- return [`${pad}char *${varName} = ${call};`];
424
+ return [
425
+ `${pad}char *${varName} = ${call};`,
426
+ `${pad}int ${varName}_owned = (${varName} != NULL);`,
427
+ ];
425
428
  }
426
429
 
427
430
  /**
@@ -447,13 +450,13 @@ function emitConcatBlock(concatExpr, pad, varName) {
447
450
  const format = cString(formatParts.join(""));
448
451
  const argList = args.length > 0 ? `, ${args.join(", ")}` : "";
449
452
 
450
- lines.push(`${pad}char *${varName} = malloc(1024);`);
451
- lines.push(`${pad}int ${varName}_owned = (${varName} != NULL);`);
453
+ const lenVarName = `${varName}_len`;
454
+ lines.push(`${pad}int ${lenVarName} = snprintf(NULL, 0, ${format}${argList});`);
455
+ lines.push(`${pad}char *${varName} = malloc(${lenVarName} + 1);`);
452
456
  lines.push(`${pad}if (${varName}) {`);
453
- lines.push(`${pad} snprintf(${varName}, 1024, ${format}${argList});`);
454
- lines.push(`${pad}} else {`);
455
- lines.push(`${pad} ${varName} = "";`);
457
+ lines.push(`${pad} snprintf(${varName}, ${lenVarName} + 1, ${format}${argList});`);
456
458
  lines.push(`${pad}}`);
459
+ lines.push(`${pad}int ${varName}_owned = (${varName} != NULL);`);
457
460
 
458
461
  return lines;
459
462
  }
@@ -512,17 +515,41 @@ function emitStatement(stmt, level, ctx) {
512
515
  const pad = indent(level);
513
516
  const lines = [];
514
517
 
518
+ const genCleanup = (padStr, skipVar = null) => {
519
+ const cl = [];
520
+ for (const ownedVar of ctx.ownedStrings) {
521
+ if (ownedVar !== skipVar) {
522
+ cl.push(`${padStr}if (${ownedVar}_owned) free((void *)${ownedVar});`);
523
+ }
524
+ }
525
+ return cl;
526
+ };
527
+
528
+ const genOomCheck = (varName) => {
529
+ const oomPad = pad + " ";
530
+ return [
531
+ `${pad}if (!${varName}) {`,
532
+ `${oomPad}cerver_res_text(res, 500, "Internal Server Error (OOM)");`,
533
+ ...genCleanup(oomPad, varName),
534
+ `${oomPad}return;`,
535
+ `${pad}}`
536
+ ];
537
+ };
538
+
515
539
  switch (stmt.type) {
516
540
  case "Return": {
517
541
  const fnName = `cerver_res_${stmt.responseType}`;
542
+ const returnedVarName = (stmt.value && stmt.value.type === "Identifier") ? stmt.value.name : null;
518
543
 
519
544
  /* Check if the return value involves a fetch() call */
520
545
  if (stmt.value && stmt.value.type === "Fetch") {
521
546
  const tempName = `_fetch_res_${ctx.fetchVarCounter++}`;
522
547
  const fetchBlock = emitFetchBlock(stmt.value, pad, tempName);
523
548
  lines.push(...fetchBlock.lines);
549
+ lines.push(...genOomCheck(tempName));
524
550
  lines.push(`${pad}${fnName}(res, ${stmt.status}, ${tempName});`);
525
551
  lines.push(`${pad}res->_body_owned = 1;`);
552
+ lines.push(...genCleanup(pad, tempName));
526
553
  lines.push(`${pad}return;`);
527
554
  } else if (
528
555
  stmt.value &&
@@ -531,8 +558,10 @@ function emitStatement(stmt, level, ctx) {
531
558
  ) {
532
559
  const tempName = `_concat_res_${ctx.concatVarCounter++}`;
533
560
  lines.push(...emitConcatBlock(stmt.value, pad, tempName));
561
+ lines.push(...genOomCheck(tempName));
534
562
  lines.push(`${pad}${fnName}(res, ${stmt.status}, ${tempName});`);
535
563
  lines.push(`${pad}if (${tempName}_owned) res->_body_owned = 1;`);
564
+ lines.push(...genCleanup(pad, tempName));
536
565
  lines.push(`${pad}return;`);
537
566
  } else if (
538
567
  stmt.value &&
@@ -542,8 +571,10 @@ function emitStatement(stmt, level, ctx) {
542
571
  /* Heap-allocated string op (tolower, toupper, trim, slice, replace) */
543
572
  const tempName = `_strop_res_${ctx.concatVarCounter++}`;
544
573
  lines.push(...emitStringOpBlock(stmt.value, pad, tempName));
545
- lines.push(`${pad}${fnName}(res, ${stmt.status}, ${tempName} ? ${tempName} : "");`);
546
- lines.push(`${pad}if (${tempName}) res->_body_owned = 1;`);
574
+ lines.push(...genOomCheck(tempName));
575
+ lines.push(`${pad}${fnName}(res, ${stmt.status}, ${tempName});`);
576
+ lines.push(`${pad}if (${tempName}_owned) res->_body_owned = 1;`);
577
+ lines.push(...genCleanup(pad, tempName));
547
578
  lines.push(`${pad}return;`);
548
579
  } else {
549
580
  assertInlineExpression(stmt.value);
@@ -556,6 +587,7 @@ function emitStatement(stmt, level, ctx) {
556
587
  ) {
557
588
  lines.push(`${pad}res->_body_owned = 1;`);
558
589
  }
590
+ lines.push(...genCleanup(pad, returnedVarName));
559
591
  lines.push(`${pad}return;`);
560
592
  }
561
593
  break;
@@ -632,6 +664,7 @@ function emitStatement(stmt, level, ctx) {
632
664
  if (stmt.initExpr && stmt.initExpr.type === "Fetch") {
633
665
  const fetchBlock = emitFetchBlock(stmt.initExpr, pad, stmt.name);
634
666
  lines.push(...fetchBlock.lines);
667
+ lines.push(...genOomCheck(stmt.name));
635
668
  ctx.ownedStrings.add(stmt.name);
636
669
  } else if (
637
670
  stmt.initExpr &&
@@ -639,6 +672,7 @@ function emitStatement(stmt, level, ctx) {
639
672
  !allLiteralConcat(stmt.initExpr)
640
673
  ) {
641
674
  lines.push(...emitConcatBlock(stmt.initExpr, pad, stmt.name));
675
+ lines.push(...genOomCheck(stmt.name));
642
676
  ctx.ownedStrings.add(stmt.name);
643
677
  } else if (
644
678
  stmt.initExpr &&
@@ -647,6 +681,7 @@ function emitStatement(stmt, level, ctx) {
647
681
  ) {
648
682
  /* Heap-allocated string op result stored in a mutable char * */
649
683
  lines.push(...emitStringOpBlock(stmt.initExpr, pad, stmt.name));
684
+ lines.push(...genOomCheck(stmt.name));
650
685
  ctx.ownedStrings.add(stmt.name);
651
686
  } else {
652
687
  assertInlineExpression(stmt.initExpr);
@@ -118,10 +118,11 @@ function validate(ast, filePath, source) {
118
118
  const decl = node.declaration;
119
119
  if (decl.type === "FunctionDeclaration") {
120
120
  const name = decl.id.name;
121
- if (!["GET", "POST"].includes(name)) {
121
+ const validMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"];
122
+ if (!validMethods.includes(name)) {
122
123
  addError(
123
124
  decl,
124
- `exported function "${name}" is not a valid HTTP method (use GET or POST)`,
125
+ `exported function "${name}" is not a valid HTTP method (use ${validMethods.join(", ")})`,
125
126
  );
126
127
  }
127
128
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velox0/cerver",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "description": "Compile restricted JavaScript server logic into optimized native C binaries (cross-platform: Linux, macOS, Windows)",
5
5
  "main": "bin/cerver.js",
6
6
  "bin": {
package/runtime/cerver.h CHANGED
@@ -303,8 +303,6 @@ char* cerver_str_replace(const char* s, const char* needle, const char* replacem
303
303
  int cerver_str_endswith(const char* s, const char* suffix);
304
304
  int cerver_str_indexof(const char* s, const char* needle);
305
305
 
306
-
307
-
308
306
  /* ------------------------------------------------------------------ */
309
307
  /* MIME (internal) */
310
308
  /* ------------------------------------------------------------------ */
package/runtime/fetch.c CHANGED
@@ -120,10 +120,19 @@ char* cerver_fetch(const char* url, const char* method, const char* body, const
120
120
  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fetch_write_cb);
121
121
  curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&buf);
122
122
  curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
123
+ curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L); /* cap redirect chain */
123
124
  curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
124
125
  curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
125
126
  curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); /* thread-safe */
126
127
  curl_easy_setopt(curl, CURLOPT_USERAGENT, "cerver/1.0");
128
+ /* Restrict to HTTP and HTTPS only — blocks file://, gopher://, dict://, etc. */
129
+ #if CURL_AT_LEAST_VERSION(7, 85, 0)
130
+ curl_easy_setopt(curl, CURLOPT_PROTOCOLS_STR, "http,https");
131
+ curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS_STR, "http,https");
132
+ #else
133
+ curl_easy_setopt(curl, CURLOPT_PROTOCOLS, (long)(CURLPROTO_HTTP | CURLPROTO_HTTPS));
134
+ curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS, (long)(CURLPROTO_HTTP | CURLPROTO_HTTPS));
135
+ #endif // CURL_AT_LEAST_VERSION(7, 85, 0)
127
136
 
128
137
  /* Set HTTP method */
129
138
  if (method) {
@@ -180,9 +180,13 @@ int cerver_parse_request(const char* raw, size_t len, cerver_request_t* req) {
180
180
  req->headers[req->header_count].value = val;
181
181
  req->header_count++;
182
182
 
183
- /* Track content-length */
183
+ /* Track content-length — use strtoul so negative values are rejected */
184
184
  if (strcasecmp(hdr_start, "Content-Length") == 0) {
185
- content_length = (size_t)atol(val);
185
+ char* endp = NULL;
186
+ unsigned long cl = strtoul(val, &endp, 10);
187
+ if (endp != val && *endp == '\0' && cl > 0 && cl <= (unsigned long)CERVER_READ_BUF_MAX) {
188
+ content_length = (size_t)cl;
189
+ }
186
190
  }
187
191
  }
188
192
  }
@@ -191,7 +195,8 @@ int cerver_parse_request(const char* raw, size_t len, cerver_request_t* req) {
191
195
  }
192
196
 
193
197
  /* ---- Body (for POST etc.) ---- */
194
- if (content_length > 0 && hdr_start < buf + len) {
198
+ if (content_length > 0 && hdr_start < buf + len && strcmp(req->method, "GET") != 0 &&
199
+ strcmp(req->method, "HEAD") != 0) {
195
200
  req->body = hdr_start;
196
201
  req->body_len = content_length;
197
202
  /* Ensure we don't read past the buffer */
@@ -142,28 +142,34 @@ static const char* status_text(int code) {
142
142
  int cerver_write_response(int fd, const cerver_response_t* res, int keepalive) {
143
143
  cerver_sock_t sfd = (cerver_sock_t)fd;
144
144
 
145
- char header[4096];
146
- int hlen = 0;
147
-
148
- hlen += snprintf(header + hlen, sizeof(header) - (size_t)hlen, "HTTP/1.1 %d %s\r\n", res->status,
149
- status_text(res->status));
150
- if (res->content_type)
151
- hlen += snprintf(header + hlen, sizeof(header) - (size_t)hlen, "Content-Type: %s\r\n",
152
- res->content_type);
153
- hlen += snprintf(header + hlen, sizeof(header) - (size_t)hlen, "Content-Length: %zu\r\n",
154
- res->body_len);
145
+ char header[4096];
146
+ size_t hlen = 0;
147
+
148
+ #define HDR_APPEND(...) \
149
+ do { \
150
+ if (hlen < sizeof(header)) { \
151
+ int _n = snprintf(header + hlen, sizeof(header) - hlen, __VA_ARGS__); \
152
+ if (_n > 0) hlen += (size_t)_n; \
153
+ if (hlen > sizeof(header)) hlen = sizeof(header); \
154
+ } \
155
+ } while (0)
156
+
157
+ HDR_APPEND("HTTP/1.1 %d %s\r\n", res->status, status_text(res->status));
158
+ if (res->content_type) HDR_APPEND("Content-Type: %s\r\n", res->content_type);
159
+ HDR_APPEND("Content-Length: %zu\r\n", res->body_len);
155
160
  for (int i = 0; i < res->header_count; i++)
156
- hlen += snprintf(header + hlen, sizeof(header) - (size_t)hlen, "%s: %s\r\n",
157
- res->headers[i].key, res->headers[i].value);
161
+ HDR_APPEND("%s: %s\r\n", res->headers[i].key, res->headers[i].value);
158
162
  if (keepalive && !res->_force_close)
159
- hlen += snprintf(header + hlen, sizeof(header) - (size_t)hlen, "Connection: keep-alive\r\n");
163
+ HDR_APPEND("Connection: keep-alive\r\n");
160
164
  else
161
- hlen += snprintf(header + hlen, sizeof(header) - (size_t)hlen, "Connection: close\r\n");
162
- hlen += snprintf(header + hlen, sizeof(header) - (size_t)hlen, "Server: cerver\r\n\r\n");
165
+ HDR_APPEND("Connection: close\r\n");
166
+ HDR_APPEND("Server: cerver\r\n\r\n");
167
+
168
+ #undef HDR_APPEND
163
169
 
164
170
  if (res->_body_owned == 3) {
165
171
  /* File-descriptor sendfile path */
166
- if (send_all(sfd, header, (size_t)hlen) < 0) return -1;
172
+ if (send_all(sfd, header, hlen) < 0) return -1;
167
173
  size_t total = res->body_len, sent = 0;
168
174
  while (sent < total) {
169
175
  ssize_t n = do_sendfile(sfd, res->_file_fd, (off_t)sent, total - sent);
@@ -177,17 +183,17 @@ int cerver_write_response(int fd, const cerver_response_t* res, int keepalive) {
177
183
  sent += (size_t)n;
178
184
  }
179
185
  } else if (res->body && res->body_len > 0) {
180
- if ((size_t)hlen + res->body_len <= sizeof(header)) {
186
+ if (hlen + res->body_len <= sizeof(header)) {
181
187
  /* Small response: one syscall */
182
188
  memcpy(header + hlen, res->body, res->body_len);
183
- if (send_all(sfd, header, (size_t)hlen + res->body_len) < 0) return -1;
189
+ if (send_all(sfd, header, hlen + res->body_len) < 0) return -1;
184
190
  } else {
185
191
  /* Large response: header then body */
186
- if (send_all(sfd, header, (size_t)hlen) < 0) return -1;
192
+ if (send_all(sfd, header, hlen) < 0) return -1;
187
193
  if (send_all(sfd, res->body, res->body_len) < 0) return -1;
188
194
  }
189
195
  } else {
190
- if (send_all(sfd, header, (size_t)hlen) < 0) return -1;
196
+ if (send_all(sfd, header, hlen) < 0) return -1;
191
197
  }
192
198
 
193
199
  return 0;
package/runtime/server.c CHANGED
@@ -33,6 +33,7 @@
33
33
  #include <netinet/in.h>
34
34
  #include <netinet/tcp.h>
35
35
  #include <arpa/inet.h>
36
+ #include <strings.h>
36
37
  #endif // !CERVER_PLATFORM_WINDOWS
37
38
 
38
39
  #if defined(__APPLE__) || defined(__FreeBSD__)
@@ -214,6 +215,7 @@ static char* read_full_request(cerver_sock_t fd, size_t* out_len) {
214
215
  per-connection in handle_connection). Just read normally. */
215
216
  #endif // CERVER_PLATFORM_WINDOWS
216
217
 
218
+ /* Phase 1: read until we have the full header section (\r\n\r\n). */
217
219
  while (len < (size_t)CERVER_READ_BUF_MAX) {
218
220
  ssize_t n = (ssize_t)cerver_sock_read(fd, buf + len, cap - len);
219
221
  if (n <= 0) break;
@@ -236,6 +238,75 @@ static char* read_full_request(cerver_sock_t fd, size_t* out_len) {
236
238
  free(buf);
237
239
  return NULL;
238
240
  }
241
+
242
+ /*
243
+ * Phase 2: if a Content-Length header is present, read the body.
244
+ *
245
+ * Scan the headers we just received for "Content-Length:" and compute
246
+ * how many body bytes we still need to pull from the socket. We do a
247
+ * minimal in-buffer scan here (before the real parser runs) so that the
248
+ * body always arrives in the same allocation that the parser will
249
+ * reference — preventing request smuggling over keep-alive connections.
250
+ */
251
+ buf[len] = '\0';
252
+ const char* hdr_end_ptr = (const char*)memmem(buf, len, "\r\n\r\n", 4);
253
+ if (hdr_end_ptr) {
254
+ size_t hdr_block_len = (size_t)(hdr_end_ptr - buf) + 4;
255
+ size_t content_length = 0;
256
+
257
+ /* Case-insensitive scan for Content-Length within the header block. */
258
+ const char* p = buf;
259
+ while (p < buf + hdr_block_len) {
260
+ const char* eol = (const char*)memmem(p, (size_t)(buf + hdr_block_len - p), "\r\n", 2);
261
+ if (!eol) break;
262
+ size_t line_len = (size_t)(eol - p);
263
+ if (line_len > 15 && (p[0] == 'C' || p[0] == 'c') &&
264
+ strncasecmp(p, "Content-Length:", 15) == 0) {
265
+ const char* val = p + 15;
266
+ char* endp = NULL;
267
+ while (*val == ' ' || *val == '\t') val++;
268
+ unsigned long cl = strtoul(val, &endp, 10);
269
+ if (endp != val && cl > 0 && cl <= (unsigned long)CERVER_READ_BUF_MAX) {
270
+ content_length = (size_t)cl;
271
+ }
272
+ break;
273
+ }
274
+ p = eol + 2;
275
+ }
276
+
277
+ if (content_length > 0) {
278
+ size_t body_received = (len > hdr_block_len) ? (len - hdr_block_len) : 0;
279
+ size_t body_needed = (content_length > body_received) ? (content_length - body_received) : 0;
280
+ size_t total_needed = len + body_needed;
281
+
282
+ if (total_needed > (size_t)CERVER_READ_BUF_MAX) {
283
+ /* Body would exceed hard limit — clamp to what we'll accept. */
284
+ total_needed = (size_t)CERVER_READ_BUF_MAX;
285
+ body_needed = (total_needed > len) ? (total_needed - len) : 0;
286
+ }
287
+
288
+ if (body_needed > 0) {
289
+ if (total_needed > cap) {
290
+ char* tmp = realloc(buf, total_needed + 1);
291
+ if (!tmp) {
292
+ free(buf);
293
+ return NULL;
294
+ }
295
+ buf = tmp;
296
+ cap = total_needed;
297
+ }
298
+
299
+ /* Read exactly the remaining body bytes. */
300
+ while (body_needed > 0) {
301
+ ssize_t n = (ssize_t)cerver_sock_read(fd, buf + len, body_needed);
302
+ if (n <= 0) break;
303
+ len += (size_t)n;
304
+ body_needed -= (size_t)n;
305
+ }
306
+ }
307
+ }
308
+ }
309
+
239
310
  buf[len] = '\0';
240
311
  *out_len = len;
241
312
  return buf;
@@ -261,10 +332,10 @@ static void handle_connection(cerver_server_t* srv, cerver_sock_t client_fd) {
261
332
  sizeof(nodelay));
262
333
  }
263
334
 
264
- int request_count = 0;
265
- int keepalive = 1;
335
+ unsigned int request_count = 0;
336
+ int keepalive = 1;
266
337
 
267
- while (keepalive && srv->running && request_count < CERVER_KEEPALIVE_MAX) {
338
+ while (keepalive && srv->running && request_count < (unsigned int)CERVER_KEEPALIVE_MAX) {
268
339
  int timeout_sec = (request_count == 0) ? 5 : CERVER_KEEPALIVE_TIMEOUT;
269
340
 
270
341
  #if CERVER_PLATFORM_WINDOWS
package/runtime/static.c CHANGED
@@ -43,7 +43,7 @@ static uint32_t fnv1a(const char* str) {
43
43
  /* ------------------------------------------------------------------ */
44
44
 
45
45
  static int path_is_safe(const char* path) {
46
- /* Reject paths with ".." */
46
+ /* Reject paths with ".." (literal traversal) */
47
47
  if (strstr(path, "..")) return 0;
48
48
 
49
49
  /* Reject paths with null bytes */
@@ -52,6 +52,34 @@ static int path_is_safe(const char* path) {
52
52
  /* Must start with "/" */
53
53
  if (path[0] != '/') return 0;
54
54
 
55
+ /* Reject encoded traversal sequences that survive url_decode:
56
+ * url_decode runs before us, but double-encoding (%252e) and
57
+ * mixed-case variants may slip through on some paths. Reject any
58
+ * remaining %XX sequences that decode to '.' or '/'. */
59
+ const char* p = path;
60
+ while (*p) {
61
+ if (*p == '%' && p[1] && p[2]) {
62
+ int hi = (p[1] >= '0' && p[1] <= '9') ? p[1] - '0'
63
+ : (p[1] >= 'a' && p[1] <= 'f') ? p[1] - 'a' + 10
64
+ : (p[1] >= 'A' && p[1] <= 'F') ? p[1] - 'A' + 10
65
+ : -1;
66
+ int lo = (p[2] >= '0' && p[2] <= '9') ? p[2] - '0'
67
+ : (p[2] >= 'a' && p[2] <= 'f') ? p[2] - 'a' + 10
68
+ : (p[2] >= 'A' && p[2] <= 'F') ? p[2] - 'A' + 10
69
+ : -1;
70
+ if (hi >= 0 && lo >= 0) {
71
+ int decoded = (hi << 4) | lo;
72
+ /* Reject if it would decode to '.' (0x2e), '/' (0x2f), or '\' (0x5c) */
73
+ if (decoded == 0x2e || decoded == 0x2f || decoded == 0x5c) return 0;
74
+ }
75
+ }
76
+ #if CERVER_PLATFORM_WINDOWS
77
+ /* Reject backslashes directly in the path */
78
+ if (*p == '\\') return 0;
79
+ #endif // CERVER_PLATFORM_WINDOWS
80
+ p++;
81
+ }
82
+
55
83
  return 1;
56
84
  }
57
85
 
@@ -64,13 +92,36 @@ typedef struct {
64
92
  int accepts_br;
65
93
  } encoding_prefs_t;
66
94
 
95
+ /*
96
+ * Check whether `token` appears as a standalone comma-separated token in
97
+ * the Accept-Encoding header value. Using strstr() would produce false
98
+ * positives (e.g. "br" matching inside "cobr" or "brotli").
99
+ */
100
+ static int ae_has_token(const char* ae, const char* token) {
101
+ size_t tlen = strlen(token);
102
+ const char* p = ae;
103
+ while (*p) {
104
+ /* Skip whitespace and commas */
105
+ while (*p == ' ' || *p == '\t' || *p == ',') p++;
106
+ if (!*p) break;
107
+ /* Find end of this token (stop at comma, semicolon, space, or NUL) */
108
+ const char* start = p;
109
+ while (*p && *p != ',' && *p != ';' && *p != ' ' && *p != '\t') p++;
110
+ size_t len = (size_t)(p - start);
111
+ if (len == tlen && memcmp(start, token, tlen) == 0) return 1;
112
+ /* Skip quality value if present (;q=0.9) */
113
+ while (*p && *p != ',') p++;
114
+ }
115
+ return 0;
116
+ }
117
+
67
118
  static encoding_prefs_t parse_accept_encoding(const cerver_request_t* req) {
68
119
  encoding_prefs_t prefs = {0, 0};
69
120
  const char* ae = cerver_req_header(req, "Accept-Encoding");
70
121
  if (!ae) return prefs;
71
122
 
72
- if (strstr(ae, "br")) prefs.accepts_br = 1;
73
- if (strstr(ae, "gzip")) prefs.accepts_gzip = 1;
123
+ if (ae_has_token(ae, "br")) prefs.accepts_br = 1;
124
+ if (ae_has_token(ae, "gzip")) prefs.accepts_gzip = 1;
74
125
 
75
126
  return prefs;
76
127
  }
@@ -222,28 +273,41 @@ static int serve_filesystem(cerver_server_t* srv, cerver_request_t* req, cerver_
222
273
  const char* path = req->path;
223
274
  if (!path_is_safe(path)) return -1;
224
275
 
225
- /* Build the full filesystem path */
276
+ /* Build the full filesystem path — reject if it would overflow. */
277
+ size_t dir_len = strlen(srv->public_dir);
278
+ size_t path_len = strlen(path);
279
+ if (dir_len + path_len + 1 > sizeof(((struct { char b[CERVER_MAX_PATH * 2]; }){}).b)) return -1;
280
+
226
281
  char full_path[CERVER_MAX_PATH * 2];
282
+ /* Bounds already verified above; silence -Wformat-truncation. */
283
+ #if defined(__GNUC__) && !defined(__clang__)
284
+ #pragma GCC diagnostic push
285
+ #pragma GCC diagnostic ignored "-Wformat-truncation"
286
+ #endif // __GNUC__ && !__clang__
227
287
  snprintf(full_path, sizeof(full_path), "%s%s", srv->public_dir, path);
288
+ #if defined(__GNUC__) && !defined(__clang__)
289
+ #pragma GCC diagnostic pop
290
+ #endif // __GNUC__ && !__clang__
228
291
 
229
292
  #if CERVER_PLATFORM_WINDOWS
230
293
  /* Normalize forward slashes to backslashes for native Windows APIs */
231
294
  for (char* p = full_path; *p; p++) {
232
295
  if (*p == '/') *p = '\\';
233
296
  }
234
- #endif
297
+ #endif // CERVER_PLATFORM_WINDOWS
235
298
 
236
299
  /* Check if it's a directory — try fallback path */
237
300
  struct stat st;
238
301
  if (stat(full_path, &st) == 0 && S_ISDIR(st.st_mode)) {
239
302
  char fallback_path[CERVER_MAX_PATH];
240
303
  get_fallback_path(path, fallback_path, sizeof(fallback_path));
304
+ if (dir_len + strlen(fallback_path) + 1 > sizeof(full_path)) return -1;
241
305
  snprintf(full_path, sizeof(full_path), "%s%s", srv->public_dir, fallback_path);
242
306
  #if CERVER_PLATFORM_WINDOWS
243
307
  for (char* p = full_path; *p; p++) {
244
308
  if (*p == '/') *p = '\\';
245
309
  }
246
- #endif
310
+ #endif // CERVER_PLATFORM_WINDOWS
247
311
  if (stat(full_path, &st) != 0) return -1;
248
312
  }
249
313
 
package/runtime/str_ops.c CHANGED
@@ -23,10 +23,10 @@
23
23
  * Return a malloc'd copy of `s` with all ASCII characters lowercased.
24
24
  * Returns NULL on allocation failure.
25
25
  */
26
- char *cerver_str_tolower(const char *s) {
26
+ char* cerver_str_tolower(const char* s) {
27
27
  if (!s) return NULL;
28
28
  size_t len = strlen(s);
29
- char *out = (char *)malloc(len + 1);
29
+ char* out = (char*)malloc(len + 1);
30
30
  if (!out) return NULL;
31
31
  for (size_t i = 0; i <= len; i++) {
32
32
  out[i] = (char)tolower((unsigned char)s[i]);
@@ -38,10 +38,10 @@ char *cerver_str_tolower(const char *s) {
38
38
  * Return a malloc'd copy of `s` with all ASCII characters uppercased.
39
39
  * Returns NULL on allocation failure.
40
40
  */
41
- char *cerver_str_toupper(const char *s) {
41
+ char* cerver_str_toupper(const char* s) {
42
42
  if (!s) return NULL;
43
43
  size_t len = strlen(s);
44
- char *out = (char *)malloc(len + 1);
44
+ char* out = (char*)malloc(len + 1);
45
45
  if (!out) return NULL;
46
46
  for (size_t i = 0; i <= len; i++) {
47
47
  out[i] = (char)toupper((unsigned char)s[i]);
@@ -57,18 +57,18 @@ char *cerver_str_toupper(const char *s) {
57
57
  * Return a malloc'd copy of `s` with leading and trailing ASCII
58
58
  * whitespace removed. Returns NULL on allocation failure.
59
59
  */
60
- char *cerver_str_trim(const char *s) {
60
+ char* cerver_str_trim(const char* s) {
61
61
  if (!s) return NULL;
62
62
 
63
63
  /* Skip leading whitespace */
64
64
  while (*s && isspace((unsigned char)*s)) s++;
65
65
 
66
- const char *end = s + strlen(s);
66
+ const char* end = s + strlen(s);
67
67
  /* Skip trailing whitespace */
68
68
  while (end > s && isspace((unsigned char)*(end - 1))) end--;
69
69
 
70
70
  size_t len = (size_t)(end - s);
71
- char *out = (char *)malloc(len + 1);
71
+ char* out = (char*)malloc(len + 1);
72
72
  if (!out) return NULL;
73
73
  memcpy(out, s, len);
74
74
  out[len] = '\0';
@@ -88,23 +88,27 @@ char *cerver_str_trim(const char *s) {
88
88
  *
89
89
  * Returns NULL on allocation failure.
90
90
  */
91
- char *cerver_str_slice(const char *s, int start, int end) {
91
+ char* cerver_str_slice(const char* s, int start, int end) {
92
92
  if (!s) return NULL;
93
93
  int len = (int)strlen(s);
94
94
 
95
95
  /* Resolve negative indices */
96
96
  if (start < 0) start = len + start;
97
- if (end < 0) end = (end == -1) ? len : len + end;
97
+ if (end < 0) end = (end == -1) ? len : len + end;
98
98
 
99
99
  /* Clamp */
100
- if (start < 0) start = 0;
100
+ if (start < 0) start = 0;
101
101
  if (start > len) start = len;
102
- if (end < 0) end = 0;
103
- if (end > len) end = len;
104
- if (start > end) { int t = start; start = end; end = t; }
102
+ if (end < 0) end = 0;
103
+ if (end > len) end = len;
104
+ if (start > end) {
105
+ int t = start;
106
+ start = end;
107
+ end = t;
108
+ }
105
109
 
106
- int out_len = end - start;
107
- char *out = (char *)malloc((size_t)out_len + 1);
110
+ int out_len = end - start;
111
+ char* out = (char*)malloc((size_t)out_len + 1);
108
112
  if (!out) return NULL;
109
113
  memcpy(out, s + start, (size_t)out_len);
110
114
  out[out_len] = '\0';
@@ -120,23 +124,22 @@ char *cerver_str_slice(const char *s, int start, int end) {
120
124
  * replaced by `replacement`. If `needle` is not found, returns a copy
121
125
  * of `s`. Returns NULL on allocation failure.
122
126
  */
123
- char *cerver_str_replace(const char *s, const char *needle,
124
- const char *replacement) {
127
+ char* cerver_str_replace(const char* s, const char* needle, const char* replacement) {
125
128
  if (!s) return NULL;
126
129
  if (!needle || *needle == '\0') {
127
130
  /* Empty needle — return a copy */
128
131
  size_t len = strlen(s);
129
- char *out = (char *)malloc(len + 1);
132
+ char* out = (char*)malloc(len + 1);
130
133
  if (!out) return NULL;
131
134
  memcpy(out, s, len + 1);
132
135
  return out;
133
136
  }
134
137
 
135
- const char *pos = strstr(s, needle);
138
+ const char* pos = strstr(s, needle);
136
139
  if (!pos) {
137
140
  /* Needle not found — return a copy of s */
138
141
  size_t len = strlen(s);
139
- char *out = (char *)malloc(len + 1);
142
+ char* out = (char*)malloc(len + 1);
140
143
  if (!out) return NULL;
141
144
  memcpy(out, s, len + 1);
142
145
  return out;
@@ -148,10 +151,10 @@ char *cerver_str_replace(const char *s, const char *needle,
148
151
  size_t after_len = strlen(pos + needle_len);
149
152
  size_t total = before_len + replacement_len + after_len + 1;
150
153
 
151
- char *out = (char *)malloc(total);
154
+ char* out = (char*)malloc(total);
152
155
  if (!out) return NULL;
153
156
 
154
- char *p = out;
157
+ char* p = out;
155
158
  memcpy(p, s, before_len);
156
159
  p += before_len;
157
160
  if (replacement_len) {
@@ -170,7 +173,7 @@ char *cerver_str_replace(const char *s, const char *needle,
170
173
  * Return 1 if `s` ends with `suffix`, 0 otherwise.
171
174
  * Mirrors JS String.prototype.endsWith().
172
175
  */
173
- int cerver_str_endswith(const char *s, const char *suffix) {
176
+ int cerver_str_endswith(const char* s, const char* suffix) {
174
177
  if (!s || !suffix) return 0;
175
178
  size_t slen = strlen(s);
176
179
  size_t suffixlen = strlen(suffix);
@@ -182,9 +185,9 @@ int cerver_str_endswith(const char *s, const char *suffix) {
182
185
  * Return the byte index of the first occurrence of `needle` in `s`,
183
186
  * or -1 if not found. Mirrors JS String.prototype.indexOf().
184
187
  */
185
- int cerver_str_indexof(const char *s, const char *needle) {
188
+ int cerver_str_indexof(const char* s, const char* needle) {
186
189
  if (!s || !needle) return -1;
187
- const char *pos = strstr(s, needle);
190
+ const char* pos = strstr(s, needle);
188
191
  if (!pos) return -1;
189
192
  return (int)(pos - s);
190
193
  }
@@ -60,7 +60,7 @@ static void cerver_test_tmpdir_template(char* buf, size_t len) {
60
60
  snprintf(buf, len, "%s\\cerver-test-XXXXXX", tmp);
61
61
  #else
62
62
  snprintf(buf, len, "/tmp/cerver-test-XXXXXX");
63
- #endif
63
+ #endif // CERVER_PLATFORM_WINDOWS
64
64
  }
65
65
 
66
66
  static const char* res_header(const cerver_response_t* res, const char* key) {
@@ -97,6 +97,9 @@ static inline void* cerver_memmem_win(const void* hay, size_t hlen, const void*
97
97
  #ifndef strcasecmp
98
98
  #define strcasecmp _stricmp
99
99
  #endif // strcasecmp
100
+ #ifndef strncasecmp
101
+ #define strncasecmp _strnicmp
102
+ #endif // strncasecmp
100
103
 
101
104
  /* clock_gettime / CLOCK_REALTIME are missing from some Windows CRTs. */
102
105
  #ifndef CLOCK_REALTIME
@@ -236,12 +239,12 @@ static inline int cerver_fetch_global_init_guard_run(cerver_fetch_global_init_gu
236
239
  typedef CRITICAL_SECTION cerver_mutex_t;
237
240
 
238
241
  typedef struct {
239
- HANDLE event; /* auto-reset event for signal */
240
- HANDLE bcast; /* manual-reset event for broadcast */
242
+ HANDLE event; /* auto-reset event for signal */
243
+ HANDLE bcast; /* manual-reset event for broadcast */
241
244
  } cerver_cond_t;
242
245
 
243
246
  typedef struct {
244
- volatile LONG done;
247
+ volatile LONG done;
245
248
  CRITICAL_SECTION lock;
246
249
  } cerver_fetch_global_init_guard_t;
247
250
 
@@ -249,8 +252,8 @@ typedef struct {
249
252
  we zero-init and rely on cerver_mutex_init() being called. For the
250
253
  two static structs in server.c that use CERVER_MUTEX_INITIALIZER we
251
254
  call cerver_mutex_init() at startup via cerver_init(). */
252
- #define CERVER_MUTEX_INITIALIZER {0}
253
- #define CERVER_COND_INITIALIZER {0}
255
+ #define CERVER_MUTEX_INITIALIZER {0}
256
+ #define CERVER_COND_INITIALIZER {0}
254
257
  #define CERVER_FETCH_GLOBAL_INIT_GUARD_INITIALIZER {0, {0}}
255
258
 
256
259
  static inline int cerver_mutex_init(cerver_mutex_t* m, void* attr) {
@@ -274,7 +277,7 @@ static inline int cerver_mutex_unlock(cerver_mutex_t* m) {
274
277
  static inline int cerver_cond_init(cerver_cond_t* c, void* attr) {
275
278
  (void)attr;
276
279
  c->event = CreateEvent(NULL, FALSE, FALSE, NULL); /* auto-reset */
277
- c->bcast = CreateEvent(NULL, TRUE, FALSE, NULL); /* manual-reset */
280
+ c->bcast = CreateEvent(NULL, TRUE, FALSE, NULL); /* manual-reset */
278
281
  return (c->event && c->bcast) ? 0 : -1;
279
282
  }
280
283
  static inline int cerver_cond_destroy(cerver_cond_t* c) {
@@ -302,7 +305,7 @@ static inline int cerver_cond_timedwait(cerver_cond_t* c, cerver_mutex_t* m,
302
305
  if (ms < 0) ms = 0;
303
306
  LeaveCriticalSection(m);
304
307
  HANDLE h[2] = {c->event, c->bcast};
305
- DWORD r = WaitForMultipleObjects(2, h, FALSE, (DWORD)ms);
308
+ DWORD r = WaitForMultipleObjects(2, h, FALSE, (DWORD)ms);
306
309
  EnterCriticalSection(m);
307
310
  return (r == WAIT_TIMEOUT) ? 110 : 0; /* 110 = ETIMEDOUT */
308
311
  }
@@ -323,13 +326,13 @@ static inline int cerver_fetch_global_init_guard_run(cerver_fetch_global_init_gu
323
326
  return 0;
324
327
  }
325
328
 
326
- #endif /* __MINGW64_VERSION_MAJOR || _MSC_VER */
329
+ #endif // __MINGW64_VERSION_MAJOR || _MSC_VER
327
330
 
328
331
  /* ---- thread helpers (shared across all Windows toolchains) -------- */
329
- typedef HANDLE cerver_connection_worker_thread_t;
330
- typedef HANDLE cerver_acceptor_thread_t;
331
- typedef size_t cerver_connection_worker_thread_attr_t;
332
- typedef size_t cerver_acceptor_thread_attr_t;
332
+ typedef HANDLE cerver_connection_worker_thread_t;
333
+ typedef HANDLE cerver_acceptor_thread_t;
334
+ typedef size_t cerver_connection_worker_thread_attr_t;
335
+ typedef size_t cerver_acceptor_thread_attr_t;
333
336
 
334
337
  static inline int cerver_connection_worker_thread_attr_init(
335
338
  cerver_connection_worker_thread_attr_t* attr) {