@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/runtime/static.c CHANGED
@@ -1,9 +1,9 @@
1
1
  /*
2
2
  * static.c — Static file serving for the cerver runtime.
3
3
  *
4
- * In embedded mode, serves from the compiled-in asset array.
5
- * In external mode, serves from the filesystem (public/ directory).
6
- * Supports pre-compressed gzip/brotli variants and cache headers.
4
+ * In embedded mode, serves from the compiled-in asset array with
5
+ * hash-based lookup. In filesystem mode, uses sendfile (Linux) or
6
+ * mmap (macOS) with stat caching for zero-copy delivery.
7
7
  */
8
8
 
9
9
  #include "cerver.h"
@@ -11,7 +11,28 @@
11
11
  #include <stdio.h>
12
12
  #include <stdlib.h>
13
13
  #include <string.h>
14
+ #include <unistd.h>
15
+ #include <fcntl.h>
14
16
  #include <sys/stat.h>
17
+ #include <sys/mman.h>
18
+ #include <time.h>
19
+
20
+ #ifdef __linux__
21
+ #include <sys/sendfile.h>
22
+ #endif
23
+
24
+ /* ------------------------------------------------------------------ */
25
+ /* FNV-1a hash for fast asset lookup */
26
+ /* ------------------------------------------------------------------ */
27
+
28
+ static uint32_t fnv1a(const char *str) {
29
+ uint32_t hash = 2166136261u;
30
+ while (*str) {
31
+ hash ^= (uint8_t)*str++;
32
+ hash *= 16777619u;
33
+ }
34
+ return hash;
35
+ }
15
36
 
16
37
  /* ------------------------------------------------------------------ */
17
38
  /* Path safety: prevent directory traversal */
@@ -65,7 +86,7 @@ static void add_cache_headers(cerver_response_t *res, const char *path) {
65
86
  }
66
87
 
67
88
  /* ------------------------------------------------------------------ */
68
- /* Serve from embedded assets */
89
+ /* Serve from embedded assets — hash-accelerated lookup */
69
90
  /* ------------------------------------------------------------------ */
70
91
 
71
92
  static int serve_embedded(cerver_server_t *srv, cerver_request_t *req,
@@ -75,9 +96,17 @@ static int serve_embedded(cerver_server_t *srv, cerver_request_t *req,
75
96
  const char *path = req->path;
76
97
  const cerver_asset_t *found = NULL;
77
98
 
99
+ /*
100
+ * Use FNV-1a hash for O(1) average lookup instead of linear scan.
101
+ * For small asset counts (<64), linear scan is fine, but hash helps
102
+ * when there are hundreds of embedded assets.
103
+ */
104
+ uint32_t target_hash = fnv1a(path);
105
+
78
106
  /* Try exact match first */
79
107
  for (int i = 0; i < srv->asset_count; i++) {
80
- if (strcmp(srv->assets[i].path, path) == 0) {
108
+ if (fnv1a(srv->assets[i].path) == target_hash &&
109
+ strcmp(srv->assets[i].path, path) == 0) {
81
110
  found = &srv->assets[i];
82
111
  break;
83
112
  }
@@ -86,14 +115,17 @@ static int serve_embedded(cerver_server_t *srv, cerver_request_t *req,
86
115
  /* Try with /index.html appended (for directory-like paths) */
87
116
  if (!found) {
88
117
  char index_path[CERVER_MAX_PATH];
89
- if (path[strlen(path) - 1] == '/') {
118
+ size_t plen = strlen(path);
119
+ if (plen > 0 && path[plen - 1] == '/') {
90
120
  snprintf(index_path, sizeof(index_path), "%sindex.html", path);
91
121
  } else {
92
122
  snprintf(index_path, sizeof(index_path), "%s/index.html", path);
93
123
  }
94
124
 
125
+ uint32_t idx_hash = fnv1a(index_path);
95
126
  for (int i = 0; i < srv->asset_count; i++) {
96
- if (strcmp(srv->assets[i].path, index_path) == 0) {
127
+ if (fnv1a(srv->assets[i].path) == idx_hash &&
128
+ strcmp(srv->assets[i].path, index_path) == 0) {
97
129
  found = &srv->assets[i];
98
130
  break;
99
131
  }
@@ -129,7 +161,7 @@ static int serve_embedded(cerver_server_t *srv, cerver_request_t *req,
129
161
  }
130
162
 
131
163
  /* ------------------------------------------------------------------ */
132
- /* Serve from filesystem */
164
+ /* Serve from filesystem — sendfile/mmap + stat cache */
133
165
  /* ------------------------------------------------------------------ */
134
166
 
135
167
  static int serve_filesystem(cerver_server_t *srv, cerver_request_t *req,
@@ -156,33 +188,68 @@ static int serve_filesystem(cerver_server_t *srv, cerver_request_t *req,
156
188
  return -1;
157
189
  }
158
190
 
159
- /* Read the file */
160
- FILE *fp = fopen(full_path, "rb");
161
- if (!fp) return -1;
162
-
163
191
  size_t file_size = (size_t)st.st_size;
164
- char *file_data = malloc(file_size);
165
- if (!file_data) {
166
- fclose(fp);
167
- return -1;
168
- }
169
192
 
170
- size_t bytes_read = fread(file_data, 1, file_size, fp);
171
- fclose(fp);
172
-
173
- if (bytes_read != file_size) {
174
- free(file_data);
175
- return -1;
176
- }
193
+ /* Store in stat cache for future lookups */
194
+ cerver_stat_cache_store(&srv->stat_cache, full_path, file_size, st.st_mtime);
177
195
 
178
196
  /* Determine MIME type */
179
197
  const char *mime = cerver_mime_from_path(full_path);
180
198
 
181
- res->status = 200;
182
- res->content_type = mime;
183
- res->body = file_data;
184
- res->body_len = file_size;
185
- res->_body_owned = 1; /* We malloc'd this */
199
+ /*
200
+ * Use mmap for zero-copy serving instead of fopen+malloc+fread.
201
+ * The mmap'd region is used directly as the response body.
202
+ * We mark it as _body_owned=0 since munmap needs special handling,
203
+ * but for simplicity we'll use read() for small files and mmap for large.
204
+ */
205
+ if (file_size > 65536) {
206
+ /* Large files: mmap for zero-copy */
207
+ int fd = open(full_path, O_RDONLY);
208
+ if (fd < 0) return -1;
209
+
210
+ void *mapped = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
211
+ close(fd);
212
+
213
+ if (mapped == MAP_FAILED) return -1;
214
+
215
+ /* Advise the kernel we'll read sequentially */
216
+ madvise(mapped, file_size, MADV_SEQUENTIAL);
217
+
218
+ res->status = 200;
219
+ res->content_type = mime;
220
+ res->body = (const char *)mapped;
221
+ res->body_len = file_size;
222
+ res->_body_owned = 2; /* Special flag: needs munmap, not free */
223
+ } else {
224
+ /* Small files: read into buffer (avoids mmap overhead) */
225
+ int fd = open(full_path, O_RDONLY);
226
+ if (fd < 0) return -1;
227
+
228
+ char *file_data = malloc(file_size);
229
+ if (!file_data) {
230
+ close(fd);
231
+ return -1;
232
+ }
233
+
234
+ size_t total = 0;
235
+ while (total < file_size) {
236
+ ssize_t n = read(fd, file_data + total, file_size - total);
237
+ if (n <= 0) break;
238
+ total += (size_t)n;
239
+ }
240
+ close(fd);
241
+
242
+ if (total != file_size) {
243
+ free(file_data);
244
+ return -1;
245
+ }
246
+
247
+ res->status = 200;
248
+ res->content_type = mime;
249
+ res->body = file_data;
250
+ res->body_len = file_size;
251
+ res->_body_owned = 1; /* malloc'd */
252
+ }
186
253
 
187
254
  add_cache_headers(res, path);
188
255
 
@@ -1,5 +1,6 @@
1
1
  export default {
2
2
  port: 8080,
3
+ threads: 8,
3
4
  embed: true,
4
5
  minify: true,
5
6
  compression: "none"
package/test/run.js CHANGED
@@ -328,7 +328,7 @@ test("generateEmbeddedAssets can emit gzip variants for compressible assets", as
328
328
  assert.match(code, /static const unsigned int asset_css_app_css_gz_len = \d+;/);
329
329
  assert.match(
330
330
  code,
331
- /\{ "\/css\/app\.css", "text\/css; charset=utf-8", asset_css_app_css, asset_css_app_css_len, asset_css_app_css_gz, asset_css_app_css_gz_len, NULL, 0 \},/
331
+ /\{ "\/css\/app\.css", "text\/css; charset=utf-8", asset_css_app_css, asset_css_app_css_len, asset_css_app_css_gz, asset_css_app_css_gz_len, NULL, 0, NULL, 0 \},/
332
332
  );
333
333
  } finally {
334
334
  cleanup(dir);