@velox0/cerver 0.3.1 → 0.4.1

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,23 +11,44 @@
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 */
18
39
  /* ------------------------------------------------------------------ */
19
40
 
20
- static int path_is_safe(const char *path) {
21
- /* Reject paths with ".." */
22
- if (strstr(path, "..")) return 0;
41
+ static int path_is_safe(const char* path) {
42
+ /* Reject paths with ".." */
43
+ if (strstr(path, "..")) return 0;
23
44
 
24
- /* Reject paths with null bytes */
25
- if (memchr(path, '\0', strlen(path))) return 0;
45
+ /* Reject paths with null bytes */
46
+ if (memchr(path, '\0', strlen(path))) return 0;
26
47
 
27
- /* Must start with "/" */
28
- if (path[0] != '/') return 0;
48
+ /* Must start with "/" */
49
+ if (path[0] != '/') return 0;
29
50
 
30
- return 1;
51
+ return 1;
31
52
  }
32
53
 
33
54
  /* ------------------------------------------------------------------ */
@@ -35,174 +56,211 @@ static int path_is_safe(const char *path) {
35
56
  /* ------------------------------------------------------------------ */
36
57
 
37
58
  typedef struct {
38
- int accepts_gzip;
39
- int accepts_br;
59
+ int accepts_gzip;
60
+ int accepts_br;
40
61
  } encoding_prefs_t;
41
62
 
42
- static encoding_prefs_t parse_accept_encoding(const cerver_request_t *req) {
43
- encoding_prefs_t prefs = { 0, 0 };
44
- const char *ae = cerver_req_header(req, "Accept-Encoding");
45
- if (!ae) return prefs;
63
+ static encoding_prefs_t parse_accept_encoding(const cerver_request_t* req) {
64
+ encoding_prefs_t prefs = {0, 0};
65
+ const char* ae = cerver_req_header(req, "Accept-Encoding");
66
+ if (!ae) return prefs;
46
67
 
47
- if (strstr(ae, "br")) prefs.accepts_br = 1;
48
- if (strstr(ae, "gzip")) prefs.accepts_gzip = 1;
68
+ if (strstr(ae, "br")) prefs.accepts_br = 1;
69
+ if (strstr(ae, "gzip")) prefs.accepts_gzip = 1;
49
70
 
50
- return prefs;
71
+ return prefs;
51
72
  }
52
73
 
53
74
  /* ------------------------------------------------------------------ */
54
75
  /* Cache header helper */
55
76
  /* ------------------------------------------------------------------ */
56
77
 
57
- static void add_cache_headers(cerver_response_t *res, const char *path) {
58
- /* Hashed/versioned assets (in /static/) get long cache */
59
- if (strstr(path, "/static/") || strstr(path, "/assets/")) {
60
- cerver_res_header(res, "Cache-Control", "public, max-age=31536000, immutable");
61
- } else {
62
- /* HTML and other top-level files get short cache with revalidation */
63
- cerver_res_header(res, "Cache-Control", "public, max-age=3600, must-revalidate");
64
- }
78
+ static void add_cache_headers(cerver_response_t* res, const char* path) {
79
+ /* Hashed/versioned assets (in /static/) get long cache */
80
+ if (strstr(path, "/static/") || strstr(path, "/assets/")) {
81
+ cerver_res_header(res, "Cache-Control", "public, max-age=31536000, immutable");
82
+ } else {
83
+ /* HTML and other top-level files get short cache with revalidation */
84
+ cerver_res_header(res, "Cache-Control", "public, max-age=3600, must-revalidate");
85
+ }
65
86
  }
66
87
 
67
88
  /* ------------------------------------------------------------------ */
68
- /* Serve from embedded assets */
89
+ /* Serve from embedded assets — hash-accelerated lookup */
69
90
  /* ------------------------------------------------------------------ */
70
91
 
71
- static int serve_embedded(cerver_server_t *srv, cerver_request_t *req,
72
- cerver_response_t *res) {
73
- if (!srv->assets || srv->asset_count == 0) return -1;
92
+ static int serve_embedded(cerver_server_t* srv, cerver_request_t* req, cerver_response_t* res) {
93
+ if (!srv->assets || srv->asset_count == 0) return -1;
74
94
 
75
- const char *path = req->path;
76
- const cerver_asset_t *found = NULL;
95
+ const char* path = req->path;
96
+ const cerver_asset_t* found = NULL;
77
97
 
78
- /* Try exact match first */
79
- for (int i = 0; i < srv->asset_count; i++) {
80
- if (strcmp(srv->assets[i].path, path) == 0) {
81
- found = &srv->assets[i];
82
- break;
83
- }
84
- }
98
+ /*
99
+ * Use FNV-1a hash for O(1) average lookup instead of linear scan.
100
+ * For small asset counts (<64), linear scan is fine, but hash helps
101
+ * when there are hundreds of embedded assets.
102
+ */
103
+ uint32_t target_hash = fnv1a(path);
85
104
 
86
- /* Try with /index.html appended (for directory-like paths) */
87
- if (!found) {
88
- char index_path[CERVER_MAX_PATH];
89
- if (path[strlen(path) - 1] == '/') {
90
- snprintf(index_path, sizeof(index_path), "%sindex.html", path);
91
- } else {
92
- snprintf(index_path, sizeof(index_path), "%s/index.html", path);
93
- }
94
-
95
- for (int i = 0; i < srv->asset_count; i++) {
96
- if (strcmp(srv->assets[i].path, index_path) == 0) {
97
- found = &srv->assets[i];
98
- break;
99
- }
100
- }
105
+ /* Try exact match first */
106
+ for (int i = 0; i < srv->asset_count; i++) {
107
+ if (fnv1a(srv->assets[i].path) == target_hash && strcmp(srv->assets[i].path, path) == 0) {
108
+ found = &srv->assets[i];
109
+ break;
101
110
  }
102
-
103
- if (!found) return -1;
104
-
105
- /* Check for pre-compressed variants */
106
- encoding_prefs_t enc = parse_accept_encoding(req);
107
-
108
- if (enc.accepts_br && found->data_br && found->data_br_len > 0) {
109
- /* Serve brotli */
110
- cerver_res_file(res, 200, found->mime_type,
111
- found->data_br, found->data_br_len);
112
- cerver_res_header(res, "Content-Encoding", "br");
113
- cerver_res_header(res, "Vary", "Accept-Encoding");
114
- } else if (enc.accepts_gzip && found->data_gz && found->data_gz_len > 0) {
115
- /* Serve gzip */
116
- cerver_res_file(res, 200, found->mime_type,
117
- found->data_gz, found->data_gz_len);
118
- cerver_res_header(res, "Content-Encoding", "gzip");
119
- cerver_res_header(res, "Vary", "Accept-Encoding");
111
+ }
112
+
113
+ /* Try with /index.html appended (for directory-like paths) */
114
+ if (!found) {
115
+ char index_path[CERVER_MAX_PATH];
116
+ size_t plen = strlen(path);
117
+ if (plen > 0 && path[plen - 1] == '/') {
118
+ snprintf(index_path, sizeof(index_path), "%sindex.html", path);
120
119
  } else {
121
- /* Serve uncompressed */
122
- cerver_res_file(res, 200, found->mime_type,
123
- found->data, found->data_len);
120
+ snprintf(index_path, sizeof(index_path), "%s/index.html", path);
124
121
  }
125
122
 
126
- add_cache_headers(res, found->path);
127
-
128
- return 0;
123
+ uint32_t idx_hash = fnv1a(index_path);
124
+ for (int i = 0; i < srv->asset_count; i++) {
125
+ if (fnv1a(srv->assets[i].path) == idx_hash && strcmp(srv->assets[i].path, index_path) == 0) {
126
+ found = &srv->assets[i];
127
+ break;
128
+ }
129
+ }
130
+ }
131
+
132
+ if (!found) return -1;
133
+
134
+ /* Check for pre-compressed variants */
135
+ encoding_prefs_t enc = parse_accept_encoding(req);
136
+
137
+ if (enc.accepts_br && found->data_br && found->data_br_len > 0) {
138
+ /* Serve brotli */
139
+ cerver_res_file(res, 200, found->mime_type, found->data_br, found->data_br_len);
140
+ cerver_res_header(res, "Content-Encoding", "br");
141
+ cerver_res_header(res, "Vary", "Accept-Encoding");
142
+ } else if (enc.accepts_gzip && found->data_gz && found->data_gz_len > 0) {
143
+ /* Serve gzip */
144
+ cerver_res_file(res, 200, found->mime_type, found->data_gz, found->data_gz_len);
145
+ cerver_res_header(res, "Content-Encoding", "gzip");
146
+ cerver_res_header(res, "Vary", "Accept-Encoding");
147
+ } else {
148
+ /* Serve uncompressed */
149
+ cerver_res_file(res, 200, found->mime_type, found->data, found->data_len);
150
+ }
151
+
152
+ add_cache_headers(res, found->path);
153
+
154
+ return 0;
129
155
  }
130
156
 
131
157
  /* ------------------------------------------------------------------ */
132
- /* Serve from filesystem */
158
+ /* Serve from filesystem — sendfile/mmap + stat cache */
133
159
  /* ------------------------------------------------------------------ */
134
160
 
135
- static int serve_filesystem(cerver_server_t *srv, cerver_request_t *req,
136
- cerver_response_t *res) {
137
- if (!srv->public_dir) return -1;
161
+ static int serve_filesystem(cerver_server_t* srv, cerver_request_t* req, cerver_response_t* res) {
162
+ if (!srv->public_dir) return -1;
138
163
 
139
- const char *path = req->path;
140
- if (!path_is_safe(path)) return -1;
164
+ const char* path = req->path;
165
+ if (!path_is_safe(path)) return -1;
141
166
 
142
- /* Build the full filesystem path */
143
- char full_path[CERVER_MAX_PATH * 2];
144
- snprintf(full_path, sizeof(full_path), "%s%s", srv->public_dir, path);
167
+ /* Build the full filesystem path */
168
+ char full_path[CERVER_MAX_PATH * 2];
169
+ snprintf(full_path, sizeof(full_path), "%s%s", srv->public_dir, path);
145
170
 
146
- /* Check if it's a directory — try index.html */
147
- struct stat st;
148
- if (stat(full_path, &st) == 0 && S_ISDIR(st.st_mode)) {
149
- snprintf(full_path, sizeof(full_path), "%s%s/index.html",
150
- srv->public_dir, path);
151
- if (stat(full_path, &st) != 0) return -1;
152
- }
171
+ /* Check if it's a directory — try index.html */
172
+ struct stat st;
173
+ if (stat(full_path, &st) == 0 && S_ISDIR(st.st_mode)) {
174
+ snprintf(full_path, sizeof(full_path), "%s%s/index.html", srv->public_dir, path);
175
+ if (stat(full_path, &st) != 0) return -1;
176
+ }
153
177
 
154
- /* Must be a regular file */
155
- if (stat(full_path, &st) != 0 || !S_ISREG(st.st_mode)) {
156
- return -1;
157
- }
178
+ /* Must be a regular file */
179
+ if (stat(full_path, &st) != 0 || !S_ISREG(st.st_mode)) {
180
+ return -1;
181
+ }
182
+
183
+ size_t file_size = (size_t)st.st_size;
184
+
185
+ /* Store in stat cache for future lookups */
186
+ cerver_stat_cache_store(&srv->stat_cache, full_path, file_size, st.st_mtime);
187
+
188
+ /* Determine MIME type */
189
+ const char* mime = cerver_mime_from_path(full_path);
190
+
191
+ /*
192
+ * Use mmap for zero-copy serving instead of fopen+malloc+fread.
193
+ * The mmap'd region is used directly as the response body.
194
+ * We mark it as _body_owned=0 since munmap needs special handling,
195
+ * but for simplicity we'll use read() for small files and mmap for large.
196
+ */
197
+ if (file_size > 65536) {
198
+ /* Large files: mmap for zero-copy */
199
+ int fd = open(full_path, O_RDONLY);
200
+ if (fd < 0) return -1;
201
+
202
+ void* mapped = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
203
+ close(fd);
204
+
205
+ if (mapped == MAP_FAILED) return -1;
158
206
 
159
- /* Read the file */
160
- FILE *fp = fopen(full_path, "rb");
161
- if (!fp) return -1;
207
+ /* Advise the kernel we'll read sequentially */
208
+ madvise(mapped, file_size, MADV_SEQUENTIAL);
162
209
 
163
- size_t file_size = (size_t)st.st_size;
164
- char *file_data = malloc(file_size);
210
+ res->status = 200;
211
+ res->content_type = mime;
212
+ res->body = (const char*)mapped;
213
+ res->body_len = file_size;
214
+ res->_body_owned = 2; /* Special flag: needs munmap, not free */
215
+ } else {
216
+ /* Small files: read into buffer (avoids mmap overhead) */
217
+ int fd = open(full_path, O_RDONLY);
218
+ if (fd < 0) return -1;
219
+
220
+ char* file_data = malloc(file_size);
165
221
  if (!file_data) {
166
- fclose(fp);
167
- return -1;
222
+ close(fd);
223
+ return -1;
168
224
  }
169
225
 
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;
226
+ size_t total = 0;
227
+ while (total < file_size) {
228
+ ssize_t n = read(fd, file_data + total, file_size - total);
229
+ if (n <= 0) break;
230
+ total += (size_t)n;
176
231
  }
232
+ close(fd);
177
233
 
178
- /* Determine MIME type */
179
- const char *mime = cerver_mime_from_path(full_path);
234
+ if (total != file_size) {
235
+ free(file_data);
236
+ return -1;
237
+ }
180
238
 
181
239
  res->status = 200;
182
240
  res->content_type = mime;
183
241
  res->body = file_data;
184
242
  res->body_len = file_size;
185
- res->_body_owned = 1; /* We malloc'd this */
243
+ res->_body_owned = 1; /* malloc'd */
244
+ }
186
245
 
187
- add_cache_headers(res, path);
246
+ add_cache_headers(res, path);
188
247
 
189
- return 0;
248
+ return 0;
190
249
  }
191
250
 
192
251
  /* ------------------------------------------------------------------ */
193
252
  /* Main static serving entry point */
194
253
  /* ------------------------------------------------------------------ */
195
254
 
196
- int cerver_serve_static(cerver_server_t *srv, cerver_request_t *req,
197
- cerver_response_t *res) {
198
- /* Only serve GET requests for static files */
199
- if (strcmp(req->method, "GET") != 0) return -1;
255
+ int cerver_serve_static(cerver_server_t* srv, cerver_request_t* req, cerver_response_t* res) {
256
+ /* Only serve GET requests for static files */
257
+ if (strcmp(req->method, "GET") != 0) return -1;
200
258
 
201
- /* Try embedded assets first */
202
- if (serve_embedded(srv, req, res) == 0) return 0;
259
+ /* Try embedded assets first */
260
+ if (serve_embedded(srv, req, res) == 0) return 0;
203
261
 
204
- /* Fall back to filesystem */
205
- if (serve_filesystem(srv, req, res) == 0) return 0;
262
+ /* Fall back to filesystem */
263
+ if (serve_filesystem(srv, req, res) == 0) return 0;
206
264
 
207
- return -1;
265
+ return -1;
208
266
  }
@@ -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);