@velox0/cerver 0.4.3 → 0.5.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/README.md +15 -5
- package/lib/assets/embed.js +33 -8
- package/lib/codegen/dispatch_gen.js +1 -1
- package/lib/codegen/generator.js +90 -55
- package/lib/codegen/route_table.js +2 -2
- package/lib/commands/build.js +31 -8
- package/lib/commands/dev.js +9 -2
- package/lib/commands/new.js +667 -258
- package/lib/commands/run.js +8 -1
- package/lib/compiler/compile.js +9 -4
- package/lib/config.js +20 -1
- package/lib/parser/discover.js +6 -6
- package/package.json +1 -1
- package/runtime/cerver.h +3 -2
- package/runtime/http_writer.c +8 -8
- package/runtime/router.c +17 -15
- package/runtime/server.c +16 -0
- package/runtime/static.c +68 -12
- package/runtime/tests/runtime_tests.c +84 -5
- package/.github/workflows/ci.yml +0 -35
- package/.github/workflows/publish.yml +0 -50
- package/test/run.js +0 -366
package/lib/commands/run.js
CHANGED
|
@@ -3,12 +3,19 @@
|
|
|
3
3
|
const { execFileSync } = require("child_process");
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const fs = require("fs");
|
|
6
|
+
const { findProjectRoot } = require("../config");
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Run the compiled binary from dist/.
|
|
9
10
|
*/
|
|
10
11
|
function run(opts) {
|
|
11
|
-
const projectDir =
|
|
12
|
+
const projectDir = findProjectRoot();
|
|
13
|
+
if (!projectDir) {
|
|
14
|
+
console.log("Not a cerver project");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
process.chdir(projectDir);
|
|
18
|
+
|
|
12
19
|
const binaryPath = path.join(projectDir, "dist", "server");
|
|
13
20
|
|
|
14
21
|
if (!fs.existsSync(binaryPath)) {
|
package/lib/compiler/compile.js
CHANGED
|
@@ -45,12 +45,17 @@ function supportsFlag(cc, flag) {
|
|
|
45
45
|
*/
|
|
46
46
|
function compile(distDir, runtimeDir, opts) {
|
|
47
47
|
const cc = detectCompiler();
|
|
48
|
-
const serverC = path.join(distDir, "server.c");
|
|
49
48
|
const outputBin = path.join(distDir, "server");
|
|
50
49
|
|
|
51
|
-
|
|
50
|
+
/* Collect all generated source files from distDir */
|
|
51
|
+
const generatedSources = fs
|
|
52
|
+
.readdirSync(distDir)
|
|
53
|
+
.filter((f) => f.endsWith(".c"))
|
|
54
|
+
.map((f) => path.join(distDir, f));
|
|
55
|
+
|
|
56
|
+
if (generatedSources.length === 0) {
|
|
52
57
|
throw new Error(
|
|
53
|
-
`cerver: generated source
|
|
58
|
+
`cerver: no generated C source files found in ${distDir}`
|
|
54
59
|
);
|
|
55
60
|
}
|
|
56
61
|
|
|
@@ -68,7 +73,7 @@ function compile(distDir, runtimeDir, opts) {
|
|
|
68
73
|
"-Wno-unused-parameter",
|
|
69
74
|
"-o",
|
|
70
75
|
outputBin,
|
|
71
|
-
|
|
76
|
+
...generatedSources,
|
|
72
77
|
...runtimeSources,
|
|
73
78
|
`-I${runtimeDir}`,
|
|
74
79
|
"-lpthread",
|
package/lib/config.js
CHANGED
|
@@ -53,4 +53,23 @@ function loadConfig(projectDir) {
|
|
|
53
53
|
return config;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
function findProjectRoot(startDir = process.cwd()) {
|
|
57
|
+
let currentDir = startDir;
|
|
58
|
+
|
|
59
|
+
while (true) {
|
|
60
|
+
if (fs.existsSync(path.join(currentDir, "cerver.config.js"))) {
|
|
61
|
+
return currentDir;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const parentDir = path.dirname(currentDir);
|
|
65
|
+
if (parentDir === currentDir) {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
currentDir = parentDir;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { loadConfig, findProjectRoot, DEFAULTS };
|
|
75
|
+
|
package/lib/parser/discover.js
CHANGED
|
@@ -4,15 +4,15 @@ const fs = require("fs");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Discover route files under
|
|
7
|
+
* Discover route files under routes/ and map them to URL paths.
|
|
8
8
|
*
|
|
9
9
|
* File-based routing convention:
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* routes/index.js → /
|
|
11
|
+
* routes/page.js → /page
|
|
12
|
+
* routes/group/item.js → /group/item
|
|
13
|
+
* routes/item/[id].js → /item/:id
|
|
14
14
|
*
|
|
15
|
-
* @param {string} routesDir - Absolute path to
|
|
15
|
+
* @param {string} routesDir - Absolute path to routes/
|
|
16
16
|
* @returns {Array<{ filePath: string, urlPath: string }>}
|
|
17
17
|
*/
|
|
18
18
|
function discoverRoutes(routesDir) {
|
package/package.json
CHANGED
package/runtime/cerver.h
CHANGED
|
@@ -238,7 +238,7 @@ struct cerver_server {
|
|
|
238
238
|
cerver_worker_t* workers;
|
|
239
239
|
|
|
240
240
|
/* Route trie for radix/trie-based routing */
|
|
241
|
-
void*
|
|
241
|
+
void* route_trie;
|
|
242
242
|
};
|
|
243
243
|
|
|
244
244
|
/* Server lifecycle */
|
|
@@ -269,7 +269,8 @@ int cerver_write_response(int fd, const cerver_response_t* res, int keepalive);
|
|
|
269
269
|
int cerver_route_match(const cerver_route_t* route, cerver_request_t* req);
|
|
270
270
|
cerver_handler_fn cerver_dispatch(cerver_server_t* srv, cerver_request_t* req);
|
|
271
271
|
void* cerver_trie_create(void);
|
|
272
|
-
void cerver_trie_insert(void* trie, const char* pattern, const char* method,
|
|
272
|
+
void cerver_trie_insert(void* trie, const char* pattern, const char* method,
|
|
273
|
+
cerver_handler_fn handler);
|
|
273
274
|
void cerver_trie_free(void* trie);
|
|
274
275
|
|
|
275
276
|
/* ------------------------------------------------------------------ */
|
package/runtime/http_writer.c
CHANGED
|
@@ -25,7 +25,7 @@ static ssize_t cerver_sendfile(int out_fd, int in_fd, off_t offset, size_t count
|
|
|
25
25
|
#include <sys/socket.h>
|
|
26
26
|
static ssize_t cerver_sendfile(int out_fd, int in_fd, off_t offset, size_t count) {
|
|
27
27
|
off_t len = (off_t)count;
|
|
28
|
-
int
|
|
28
|
+
int res = sendfile(in_fd, out_fd, offset, &len, NULL, 0);
|
|
29
29
|
if (res == 0) {
|
|
30
30
|
return (ssize_t)len;
|
|
31
31
|
}
|
|
@@ -35,8 +35,8 @@ static ssize_t cerver_sendfile(int out_fd, int in_fd, off_t offset, size_t count
|
|
|
35
35
|
/* Fallback to read-write copy if not a socket or unsupported on this descriptor type */
|
|
36
36
|
char buf[8192];
|
|
37
37
|
if (lseek(in_fd, offset, SEEK_SET) == -1) return -1;
|
|
38
|
-
size_t
|
|
39
|
-
ssize_t n_read
|
|
38
|
+
size_t to_read = count > sizeof(buf) ? sizeof(buf) : count;
|
|
39
|
+
ssize_t n_read = read(in_fd, buf, to_read);
|
|
40
40
|
if (n_read <= 0) return n_read;
|
|
41
41
|
|
|
42
42
|
size_t written = 0;
|
|
@@ -54,8 +54,8 @@ static ssize_t cerver_sendfile(int out_fd, int in_fd, off_t offset, size_t count
|
|
|
54
54
|
static ssize_t cerver_sendfile(int out_fd, int in_fd, off_t offset, size_t count) {
|
|
55
55
|
char buf[8192];
|
|
56
56
|
if (lseek(in_fd, offset, SEEK_SET) == -1) return -1;
|
|
57
|
-
size_t
|
|
58
|
-
ssize_t n_read
|
|
57
|
+
size_t to_read = count > sizeof(buf) ? sizeof(buf) : count;
|
|
58
|
+
ssize_t n_read = read(in_fd, buf, to_read);
|
|
59
59
|
if (n_read <= 0) return n_read;
|
|
60
60
|
|
|
61
61
|
size_t written = 0;
|
|
@@ -156,7 +156,7 @@ int cerver_write_response(int fd, const cerver_response_t* res, int keepalive) {
|
|
|
156
156
|
*/
|
|
157
157
|
if (res->_body_owned == 3) {
|
|
158
158
|
/* Send header first */
|
|
159
|
-
size_t header_total
|
|
159
|
+
size_t header_total = (size_t)hlen;
|
|
160
160
|
size_t header_written = 0;
|
|
161
161
|
while (header_written < header_total) {
|
|
162
162
|
ssize_t n = write(fd, header + header_written, header_total - header_written);
|
|
@@ -169,7 +169,7 @@ int cerver_write_response(int fd, const cerver_response_t* res, int keepalive) {
|
|
|
169
169
|
|
|
170
170
|
/* Zero-copy body sending via sendfile(2) */
|
|
171
171
|
size_t body_total = res->body_len;
|
|
172
|
-
size_t body_sent
|
|
172
|
+
size_t body_sent = 0;
|
|
173
173
|
while (body_sent < body_total) {
|
|
174
174
|
ssize_t n = cerver_sendfile(fd, res->_file_fd, (off_t)body_sent, body_total - body_sent);
|
|
175
175
|
if (n < 0) {
|
|
@@ -183,7 +183,7 @@ int cerver_write_response(int fd, const cerver_response_t* res, int keepalive) {
|
|
|
183
183
|
if ((size_t)hlen + res->body_len <= sizeof(header)) {
|
|
184
184
|
/* Small response optimization: copy body into header buffer and send in one syscall */
|
|
185
185
|
memcpy(header + hlen, res->body, res->body_len);
|
|
186
|
-
size_t total
|
|
186
|
+
size_t total = (size_t)hlen + res->body_len;
|
|
187
187
|
size_t written = 0;
|
|
188
188
|
while (written < total) {
|
|
189
189
|
ssize_t n = write(fd, header + written, total - written);
|
package/runtime/router.c
CHANGED
|
@@ -108,9 +108,9 @@ int cerver_route_match(const cerver_route_t* route, cerver_request_t* req) {
|
|
|
108
108
|
const char* pp = pattern; /* pattern pointer */
|
|
109
109
|
const char* rp = path; /* request path pointer */
|
|
110
110
|
|
|
111
|
-
int
|
|
111
|
+
int saved_params = req->params_count;
|
|
112
112
|
char* param_slashes[CERVER_MAX_PARAMS];
|
|
113
|
-
int
|
|
113
|
+
int param_slash_count = 0;
|
|
114
114
|
|
|
115
115
|
/* Skip leading '/' */
|
|
116
116
|
if (*pp == '/') pp++;
|
|
@@ -186,18 +186,18 @@ typedef struct trie_node trie_node_t;
|
|
|
186
186
|
|
|
187
187
|
struct trie_node {
|
|
188
188
|
char* segment;
|
|
189
|
-
int
|
|
189
|
+
int is_param;
|
|
190
190
|
char* param_name;
|
|
191
191
|
|
|
192
192
|
struct {
|
|
193
|
-
const char*
|
|
193
|
+
const char* method;
|
|
194
194
|
cerver_handler_fn handler;
|
|
195
195
|
} handlers[16];
|
|
196
196
|
int handler_count;
|
|
197
197
|
|
|
198
198
|
trie_node_t** children;
|
|
199
|
-
int
|
|
200
|
-
int
|
|
199
|
+
int children_count;
|
|
200
|
+
int children_cap;
|
|
201
201
|
};
|
|
202
202
|
|
|
203
203
|
void* cerver_trie_create(void) {
|
|
@@ -210,17 +210,18 @@ static trie_node_t* trie_create_node(const char* segment, size_t len) {
|
|
|
210
210
|
if (node && segment) {
|
|
211
211
|
node->segment = trie_strndup(segment, len);
|
|
212
212
|
if (node->segment[0] == ':') {
|
|
213
|
-
node->is_param
|
|
213
|
+
node->is_param = 1;
|
|
214
214
|
node->param_name = trie_strndup(node->segment + 1, len - 1);
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
return node;
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
void cerver_trie_insert(void* trie, const char* pattern, const char* method,
|
|
220
|
+
void cerver_trie_insert(void* trie, const char* pattern, const char* method,
|
|
221
|
+
cerver_handler_fn handler) {
|
|
221
222
|
if (!trie) return;
|
|
222
223
|
trie_node_t* curr = (trie_node_t*)trie;
|
|
223
|
-
const char*
|
|
224
|
+
const char* p = pattern;
|
|
224
225
|
while (*p == '/') p++;
|
|
225
226
|
|
|
226
227
|
while (*p) {
|
|
@@ -246,7 +247,7 @@ void cerver_trie_insert(void* trie, const char* pattern, const char* method, cer
|
|
|
246
247
|
child = trie_create_node(seg_start, len);
|
|
247
248
|
if (curr->children_count >= curr->children_cap) {
|
|
248
249
|
curr->children_cap = curr->children_cap == 0 ? 4 : curr->children_cap * 2;
|
|
249
|
-
curr->children
|
|
250
|
+
curr->children = realloc(curr->children, curr->children_cap * sizeof(trie_node_t*));
|
|
250
251
|
}
|
|
251
252
|
curr->children[curr->children_count++] = child;
|
|
252
253
|
}
|
|
@@ -257,7 +258,7 @@ void cerver_trie_insert(void* trie, const char* pattern, const char* method, cer
|
|
|
257
258
|
|
|
258
259
|
// Add handler to leaf
|
|
259
260
|
if (curr->handler_count < 16) {
|
|
260
|
-
curr->handlers[curr->handler_count].method
|
|
261
|
+
curr->handlers[curr->handler_count].method = method;
|
|
261
262
|
curr->handlers[curr->handler_count].handler = handler;
|
|
262
263
|
curr->handler_count++;
|
|
263
264
|
}
|
|
@@ -275,7 +276,8 @@ void cerver_trie_free(void* trie) {
|
|
|
275
276
|
free(node);
|
|
276
277
|
}
|
|
277
278
|
|
|
278
|
-
static int trie_match_recursive(trie_node_t* node, const char* path, cerver_request_t* req,
|
|
279
|
+
static int trie_match_recursive(trie_node_t* node, const char* path, cerver_request_t* req,
|
|
280
|
+
cerver_handler_fn* out_handler, int param_start_idx) {
|
|
279
281
|
// Skip leading slashes
|
|
280
282
|
while (*path == '/') path++;
|
|
281
283
|
|
|
@@ -283,7 +285,7 @@ static int trie_match_recursive(trie_node_t* node, const char* path, cerver_requ
|
|
|
283
285
|
// Check if node has a handler for req->method
|
|
284
286
|
for (int i = 0; i < node->handler_count; i++) {
|
|
285
287
|
if (strcmp(node->handlers[i].method, req->method) == 0) {
|
|
286
|
-
*out_handler
|
|
288
|
+
*out_handler = node->handlers[i].handler;
|
|
287
289
|
req->params_count = param_start_idx;
|
|
288
290
|
return 1;
|
|
289
291
|
}
|
|
@@ -313,7 +315,7 @@ static int trie_match_recursive(trie_node_t* node, const char* path, cerver_requ
|
|
|
313
315
|
trie_node_t* child = node->children[i];
|
|
314
316
|
if (child->is_param) {
|
|
315
317
|
if (param_start_idx < CERVER_MAX_PARAMS) {
|
|
316
|
-
req->params[param_start_idx].key
|
|
318
|
+
req->params[param_start_idx].key = child->param_name;
|
|
317
319
|
req->params[param_start_idx].value = seg_start;
|
|
318
320
|
}
|
|
319
321
|
if (trie_match_recursive(child, path, req, out_handler, param_start_idx + 1)) {
|
|
@@ -340,7 +342,7 @@ cerver_handler_fn cerver_dispatch(cerver_server_t* srv, cerver_request_t* req) {
|
|
|
340
342
|
if (!srv->route_trie) return NULL;
|
|
341
343
|
|
|
342
344
|
cerver_handler_fn handler = NULL;
|
|
343
|
-
req->params_count
|
|
345
|
+
req->params_count = 0;
|
|
344
346
|
if (trie_match_recursive((trie_node_t*)srv->route_trie, req->path, req, &handler, 0)) {
|
|
345
347
|
// NUL-terminate extracted values in-place inside req->path
|
|
346
348
|
for (int i = 0; i < req->params_count; i++) {
|
package/runtime/server.c
CHANGED
|
@@ -771,6 +771,22 @@ int cerver_listen(cerver_server_t* srv) {
|
|
|
771
771
|
printf("cerver: %d acceptor(s), %d connection workers, keep-alive max %d req/conn\n",
|
|
772
772
|
acceptor_count, pool_size, CERVER_KEEPALIVE_MAX);
|
|
773
773
|
|
|
774
|
+
for (int i = 0; i < srv->route_count; i++) {
|
|
775
|
+
const char* method = srv->routes[i].method;
|
|
776
|
+
const char* color = "\x1B[35m";
|
|
777
|
+
if (strcmp(method, "GET") == 0) {
|
|
778
|
+
color = "\x1B[32m";
|
|
779
|
+
} else if (strcmp(method, "POST") == 0) {
|
|
780
|
+
color = "\x1B[33m";
|
|
781
|
+
} else if (strcmp(method, "PUT") == 0) {
|
|
782
|
+
color = "\x1B[36m";
|
|
783
|
+
} else if (strcmp(method, "DELETE") == 0) {
|
|
784
|
+
color = "\x1B[31m";
|
|
785
|
+
}
|
|
786
|
+
printf(" → Mapped {%s, %s%s\x1B[0m} route\n", srv->routes[i].pattern, color, method);
|
|
787
|
+
}
|
|
788
|
+
fflush(stdout);
|
|
789
|
+
|
|
774
790
|
release_acceptors();
|
|
775
791
|
|
|
776
792
|
pthread_attr_destroy(&attr);
|
package/runtime/static.c
CHANGED
|
@@ -85,6 +85,64 @@ static void add_cache_headers(cerver_response_t* res, const char* path) {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/* ------------------------------------------------------------------ */
|
|
89
|
+
/* Helper to resolve fallback paths for directory/clean-URL routes. */
|
|
90
|
+
/* - "/" -> "/index.html" */
|
|
91
|
+
/* - "/page" or "/page/" -> "/page/page.html" */
|
|
92
|
+
/* ------------------------------------------------------------------ */
|
|
93
|
+
|
|
94
|
+
static void get_fallback_path(const char* path, char* out, size_t out_len) {
|
|
95
|
+
if (strcmp(path, "/") == 0 || strcmp(path, "") == 0) {
|
|
96
|
+
snprintf(out, out_len, "/index.html");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
size_t len = strlen(path);
|
|
101
|
+
while (len > 0 && path[len - 1] == '/') {
|
|
102
|
+
len--;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (len == 0) {
|
|
106
|
+
snprintf(out, out_len, "/index.html");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
int last_slash = -1;
|
|
111
|
+
for (int i = (int)len - 1; i >= 0; i--) {
|
|
112
|
+
if (path[i] == '/') {
|
|
113
|
+
last_slash = i;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
size_t segment_len = len - (last_slash + 1);
|
|
119
|
+
if (segment_len == 0) {
|
|
120
|
+
snprintf(out, out_len, "/index.html");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Extract the prefix and segment safely */
|
|
125
|
+
char prefix[1024];
|
|
126
|
+
if (len < sizeof(prefix)) {
|
|
127
|
+
memcpy(prefix, path, len);
|
|
128
|
+
prefix[len] = '\0';
|
|
129
|
+
} else {
|
|
130
|
+
snprintf(out, out_len, "/index.html");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
char segment[256];
|
|
135
|
+
if (segment_len < sizeof(segment)) {
|
|
136
|
+
memcpy(segment, path + last_slash + 1, segment_len);
|
|
137
|
+
segment[segment_len] = '\0';
|
|
138
|
+
} else {
|
|
139
|
+
snprintf(out, out_len, "/index.html");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
snprintf(out, out_len, "%s/%s.html", prefix, segment);
|
|
144
|
+
}
|
|
145
|
+
|
|
88
146
|
/* ------------------------------------------------------------------ */
|
|
89
147
|
/* Serve from embedded assets — hash-accelerated lookup */
|
|
90
148
|
/* ------------------------------------------------------------------ */
|
|
@@ -110,19 +168,15 @@ static int serve_embedded(cerver_server_t* srv, cerver_request_t* req, cerver_re
|
|
|
110
168
|
}
|
|
111
169
|
}
|
|
112
170
|
|
|
113
|
-
/* Try with
|
|
171
|
+
/* Try with fallback path (for directory-like paths) */
|
|
114
172
|
if (!found) {
|
|
115
|
-
char
|
|
116
|
-
|
|
117
|
-
if (plen > 0 && path[plen - 1] == '/') {
|
|
118
|
-
snprintf(index_path, sizeof(index_path), "%sindex.html", path);
|
|
119
|
-
} else {
|
|
120
|
-
snprintf(index_path, sizeof(index_path), "%s/index.html", path);
|
|
121
|
-
}
|
|
173
|
+
char fallback_path[CERVER_MAX_PATH];
|
|
174
|
+
get_fallback_path(path, fallback_path, sizeof(fallback_path));
|
|
122
175
|
|
|
123
|
-
uint32_t idx_hash = fnv1a(
|
|
176
|
+
uint32_t idx_hash = fnv1a(fallback_path);
|
|
124
177
|
for (int i = 0; i < srv->asset_count; i++) {
|
|
125
|
-
if (fnv1a(srv->assets[i].path) == idx_hash &&
|
|
178
|
+
if (fnv1a(srv->assets[i].path) == idx_hash &&
|
|
179
|
+
strcmp(srv->assets[i].path, fallback_path) == 0) {
|
|
126
180
|
found = &srv->assets[i];
|
|
127
181
|
break;
|
|
128
182
|
}
|
|
@@ -168,10 +222,12 @@ static int serve_filesystem(cerver_server_t* srv, cerver_request_t* req, cerver_
|
|
|
168
222
|
char full_path[CERVER_MAX_PATH * 2];
|
|
169
223
|
snprintf(full_path, sizeof(full_path), "%s%s", srv->public_dir, path);
|
|
170
224
|
|
|
171
|
-
/* Check if it's a directory — try
|
|
225
|
+
/* Check if it's a directory — try fallback path */
|
|
172
226
|
struct stat st;
|
|
173
227
|
if (stat(full_path, &st) == 0 && S_ISDIR(st.st_mode)) {
|
|
174
|
-
|
|
228
|
+
char fallback_path[CERVER_MAX_PATH];
|
|
229
|
+
get_fallback_path(path, fallback_path, sizeof(fallback_path));
|
|
230
|
+
snprintf(full_path, sizeof(full_path), "%s%s", srv->public_dir, fallback_path);
|
|
175
231
|
if (stat(full_path, &st) != 0) return -1;
|
|
176
232
|
}
|
|
177
233
|
|
|
@@ -288,9 +288,9 @@ static void test_static_embedded_index_fallback(void) {
|
|
|
288
288
|
cerver_server_t srv;
|
|
289
289
|
cerver_init(&srv, 8080, 1);
|
|
290
290
|
|
|
291
|
-
static const unsigned char data[] = "
|
|
291
|
+
static const unsigned char data[] = "about";
|
|
292
292
|
cerver_asset_t assets[1];
|
|
293
|
-
assets[0].path = "/
|
|
293
|
+
assets[0].path = "/about/about.html";
|
|
294
294
|
assets[0].mime_type = "text/html";
|
|
295
295
|
assets[0].data = data;
|
|
296
296
|
assets[0].data_len = sizeof(data) - 1;
|
|
@@ -305,13 +305,91 @@ static void test_static_embedded_index_fallback(void) {
|
|
|
305
305
|
memset(&req, 0, sizeof(req));
|
|
306
306
|
memset(&res, 0, sizeof(res));
|
|
307
307
|
strcpy(req.method, "GET");
|
|
308
|
-
strcpy(req.path, "/
|
|
308
|
+
strcpy(req.path, "/about/");
|
|
309
309
|
|
|
310
310
|
MU_ASSERT_EQ_INT(0, cerver_serve_static(&srv, &req, &res));
|
|
311
311
|
MU_ASSERT(res.body == (const char*)data);
|
|
312
312
|
MU_ASSERT_STREQ("text/html", res.content_type);
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
static void test_static_filesystem_directory_fallback(void) {
|
|
316
|
+
char dir_template[] = "/tmp/cerver-test-XXXXXX";
|
|
317
|
+
char* dir = mkdtemp(dir_template);
|
|
318
|
+
MU_ASSERT(dir != NULL);
|
|
319
|
+
|
|
320
|
+
/* Create public/index.html (maps to /) */
|
|
321
|
+
char file_path[PATH_MAX];
|
|
322
|
+
snprintf(file_path, sizeof(file_path), "%s/index.html", dir);
|
|
323
|
+
MU_ASSERT_EQ_INT(0, write_file(file_path, "root index", 10));
|
|
324
|
+
|
|
325
|
+
/* Create public/about/about.html (maps to /about or /about/) */
|
|
326
|
+
char sub_dir[PATH_MAX];
|
|
327
|
+
snprintf(sub_dir, sizeof(sub_dir), "%s/about", dir);
|
|
328
|
+
MU_ASSERT_EQ_INT(0, mkdir(sub_dir, 0700));
|
|
329
|
+
|
|
330
|
+
char file_path2[PATH_MAX];
|
|
331
|
+
snprintf(file_path2, sizeof(file_path2), "%s/about/about.html", dir);
|
|
332
|
+
MU_ASSERT_EQ_INT(0, write_file(file_path2, "about content", 13));
|
|
333
|
+
|
|
334
|
+
/* Create public/about/index.html (should NOT map to /about, since index.html under subdirectory
|
|
335
|
+
* doesn't alias) */
|
|
336
|
+
char file_path3[PATH_MAX];
|
|
337
|
+
snprintf(file_path3, sizeof(file_path3), "%s/about/index.html", dir);
|
|
338
|
+
MU_ASSERT_EQ_INT(0, write_file(file_path3, "about index", 11));
|
|
339
|
+
|
|
340
|
+
cerver_server_t srv;
|
|
341
|
+
cerver_init(&srv, 8080, 1);
|
|
342
|
+
cerver_set_public_dir(&srv, dir);
|
|
343
|
+
|
|
344
|
+
/* Test / -> index.html fallback */
|
|
345
|
+
{
|
|
346
|
+
cerver_request_t req;
|
|
347
|
+
cerver_response_t res;
|
|
348
|
+
memset(&req, 0, sizeof(req));
|
|
349
|
+
memset(&res, 0, sizeof(res));
|
|
350
|
+
strcpy(req.method, "GET");
|
|
351
|
+
strcpy(req.path, "/");
|
|
352
|
+
|
|
353
|
+
MU_ASSERT_EQ_INT(0, cerver_serve_static(&srv, &req, &res));
|
|
354
|
+
MU_ASSERT_EQ_SIZE(10, res.body_len);
|
|
355
|
+
if (res._body_owned == 3 && res._file_fd >= 0) close(res._file_fd);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/* Test /about -> about/about.html fallback */
|
|
359
|
+
{
|
|
360
|
+
cerver_request_t req;
|
|
361
|
+
cerver_response_t res;
|
|
362
|
+
memset(&req, 0, sizeof(req));
|
|
363
|
+
memset(&res, 0, sizeof(res));
|
|
364
|
+
strcpy(req.method, "GET");
|
|
365
|
+
strcpy(req.path, "/about");
|
|
366
|
+
|
|
367
|
+
MU_ASSERT_EQ_INT(0, cerver_serve_static(&srv, &req, &res));
|
|
368
|
+
MU_ASSERT_EQ_SIZE(13, res.body_len);
|
|
369
|
+
if (res._body_owned == 3 && res._file_fd >= 0) close(res._file_fd);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/* Test /about/ -> about/about.html fallback */
|
|
373
|
+
{
|
|
374
|
+
cerver_request_t req;
|
|
375
|
+
cerver_response_t res;
|
|
376
|
+
memset(&req, 0, sizeof(req));
|
|
377
|
+
memset(&res, 0, sizeof(res));
|
|
378
|
+
strcpy(req.method, "GET");
|
|
379
|
+
strcpy(req.path, "/about/");
|
|
380
|
+
|
|
381
|
+
MU_ASSERT_EQ_INT(0, cerver_serve_static(&srv, &req, &res));
|
|
382
|
+
MU_ASSERT_EQ_SIZE(13, res.body_len);
|
|
383
|
+
if (res._body_owned == 3 && res._file_fd >= 0) close(res._file_fd);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
unlink(file_path);
|
|
387
|
+
unlink(file_path2);
|
|
388
|
+
unlink(file_path3);
|
|
389
|
+
rmdir(sub_dir);
|
|
390
|
+
rmdir(dir);
|
|
391
|
+
}
|
|
392
|
+
|
|
315
393
|
static void test_static_filesystem_small(void) {
|
|
316
394
|
char dir_template[] = "/tmp/cerver-test-XXXXXX";
|
|
317
395
|
char* dir = mkdtemp(dir_template);
|
|
@@ -343,7 +421,7 @@ static void test_static_filesystem_small(void) {
|
|
|
343
421
|
MU_ASSERT_EQ_INT(0, cerver_write_response(fds[1], &res, 1));
|
|
344
422
|
close(fds[1]);
|
|
345
423
|
|
|
346
|
-
char
|
|
424
|
+
char out[1024];
|
|
347
425
|
ssize_t n = read_all(fds[0], out, sizeof(out));
|
|
348
426
|
MU_ASSERT(n > 0);
|
|
349
427
|
close(fds[0]);
|
|
@@ -395,7 +473,7 @@ static void test_static_filesystem_large(void) {
|
|
|
395
473
|
MU_ASSERT_EQ_INT(0, cerver_write_response(fds[1], &res, 1));
|
|
396
474
|
close(fds[1]);
|
|
397
475
|
|
|
398
|
-
char
|
|
476
|
+
char out[35000];
|
|
399
477
|
ssize_t n = read_all(fds[0], out, sizeof(out));
|
|
400
478
|
MU_ASSERT(n > 0);
|
|
401
479
|
close(fds[0]);
|
|
@@ -457,6 +535,7 @@ int main(void) {
|
|
|
457
535
|
mu_run("static_embedded_index_fallback", test_static_embedded_index_fallback);
|
|
458
536
|
mu_run("static_filesystem_small", test_static_filesystem_small);
|
|
459
537
|
mu_run("static_filesystem_large", test_static_filesystem_large);
|
|
538
|
+
mu_run("static_filesystem_directory_fallback", test_static_filesystem_directory_fallback);
|
|
460
539
|
mu_run("static_rejects_unsafe_path", test_static_rejects_unsafe_path);
|
|
461
540
|
mu_run("stat_cache_store_lookup", test_stat_cache_store_lookup);
|
|
462
541
|
mu_run("cerver_init_fields", test_cerver_init_fields);
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
pull_request:
|
|
6
|
-
|
|
7
|
-
jobs:
|
|
8
|
-
tests:
|
|
9
|
-
name: Tests
|
|
10
|
-
runs-on: ubuntu-latest
|
|
11
|
-
|
|
12
|
-
steps:
|
|
13
|
-
- name: Checkout
|
|
14
|
-
uses: actions/checkout@v6
|
|
15
|
-
|
|
16
|
-
- name: Setup pnpm
|
|
17
|
-
uses: pnpm/action-setup@v6
|
|
18
|
-
with:
|
|
19
|
-
version: 10
|
|
20
|
-
run_install: false
|
|
21
|
-
|
|
22
|
-
- name: Setup Node.js
|
|
23
|
-
uses: actions/setup-node@v6
|
|
24
|
-
with:
|
|
25
|
-
node-version: 24
|
|
26
|
-
cache: pnpm
|
|
27
|
-
|
|
28
|
-
- name: Install dependencies
|
|
29
|
-
run: pnpm install --frozen-lockfile
|
|
30
|
-
|
|
31
|
-
- name: Run runtime tests
|
|
32
|
-
run: make test-runtime
|
|
33
|
-
|
|
34
|
-
- name: Run JS tests
|
|
35
|
-
run: pnpm test
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
name: Publish package
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
tags:
|
|
6
|
-
- "v*"
|
|
7
|
-
workflow_dispatch:
|
|
8
|
-
|
|
9
|
-
permissions:
|
|
10
|
-
contents: write
|
|
11
|
-
id-token: write
|
|
12
|
-
|
|
13
|
-
jobs:
|
|
14
|
-
publish:
|
|
15
|
-
name: Publish to npm
|
|
16
|
-
runs-on: ubuntu-latest
|
|
17
|
-
|
|
18
|
-
steps:
|
|
19
|
-
- name: Checkout
|
|
20
|
-
uses: actions/checkout@v6
|
|
21
|
-
|
|
22
|
-
- name: Setup pnpm
|
|
23
|
-
uses: pnpm/action-setup@v6
|
|
24
|
-
with:
|
|
25
|
-
version: 10
|
|
26
|
-
run_install: false
|
|
27
|
-
|
|
28
|
-
- name: Setup Node.js
|
|
29
|
-
uses: actions/setup-node@v6
|
|
30
|
-
with:
|
|
31
|
-
node-version: 24
|
|
32
|
-
registry-url: https://registry.npmjs.org
|
|
33
|
-
cache: pnpm
|
|
34
|
-
|
|
35
|
-
- name: Install dependencies
|
|
36
|
-
run: pnpm install --frozen-lockfile
|
|
37
|
-
|
|
38
|
-
- name: Run runtime tests
|
|
39
|
-
run: make test-runtime
|
|
40
|
-
|
|
41
|
-
- name: Run tests
|
|
42
|
-
run: pnpm test
|
|
43
|
-
|
|
44
|
-
- name: Verify package contents
|
|
45
|
-
run: npm pack --dry-run
|
|
46
|
-
|
|
47
|
-
- name: Publish to npm
|
|
48
|
-
run: npm publish --access public --provenance
|
|
49
|
-
env:
|
|
50
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|