@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/.github/workflows/publish.yml +7 -8
- 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 +171 -100
- package/runtime/http_parser.c +156 -152
- package/runtime/http_writer.c +138 -97
- package/runtime/mime.c +48 -49
- package/runtime/router.c +121 -99
- package/runtime/server.c +662 -405
- package/runtime/static.c +185 -127
- package/templates/cerver.config.js +1 -0
- package/test/run.js +1 -1
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
|
|
6
|
-
*
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
41
|
+
static int path_is_safe(const char* path) {
|
|
42
|
+
/* Reject paths with ".." */
|
|
43
|
+
if (strstr(path, "..")) return 0;
|
|
23
44
|
|
|
24
|
-
|
|
25
|
-
|
|
45
|
+
/* Reject paths with null bytes */
|
|
46
|
+
if (memchr(path, '\0', strlen(path))) return 0;
|
|
26
47
|
|
|
27
|
-
|
|
28
|
-
|
|
48
|
+
/* Must start with "/" */
|
|
49
|
+
if (path[0] != '/') return 0;
|
|
29
50
|
|
|
30
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
68
|
+
if (strstr(ae, "br")) prefs.accepts_br = 1;
|
|
69
|
+
if (strstr(ae, "gzip")) prefs.accepts_gzip = 1;
|
|
49
70
|
|
|
50
|
-
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
|
|
95
|
+
const char* path = req->path;
|
|
96
|
+
const cerver_asset_t* found = NULL;
|
|
77
97
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
136
|
-
|
|
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
|
-
|
|
140
|
-
|
|
164
|
+
const char* path = req->path;
|
|
165
|
+
if (!path_is_safe(path)) return -1;
|
|
141
166
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
/*
|
|
160
|
-
|
|
161
|
-
if (!fp) return -1;
|
|
207
|
+
/* Advise the kernel we'll read sequentially */
|
|
208
|
+
madvise(mapped, file_size, MADV_SEQUENTIAL);
|
|
162
209
|
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
167
|
-
|
|
222
|
+
close(fd);
|
|
223
|
+
return -1;
|
|
168
224
|
}
|
|
169
225
|
|
|
170
|
-
size_t
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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; /*
|
|
243
|
+
res->_body_owned = 1; /* malloc'd */
|
|
244
|
+
}
|
|
186
245
|
|
|
187
|
-
|
|
246
|
+
add_cache_headers(res, path);
|
|
188
247
|
|
|
189
|
-
|
|
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
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
202
|
-
|
|
259
|
+
/* Try embedded assets first */
|
|
260
|
+
if (serve_embedded(srv, req, res) == 0) return 0;
|
|
203
261
|
|
|
204
|
-
|
|
205
|
-
|
|
262
|
+
/* Fall back to filesystem */
|
|
263
|
+
if (serve_filesystem(srv, req, res) == 0) return 0;
|
|
206
264
|
|
|
207
|
-
|
|
265
|
+
return -1;
|
|
208
266
|
}
|
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);
|