383 lines
9.9 KiB
C
383 lines
9.9 KiB
C
#include <string.h>
|
|
|
|
#include "lnm/common.h"
|
|
#include "lnm/http/loop_internal.h"
|
|
|
|
lnm_err lnm_http_router_init(lnm_http_router **out) {
|
|
lnm_http_router *router = calloc(1, sizeof(lnm_http_router));
|
|
|
|
if (router == NULL) {
|
|
return lnm_err_failed_alloc;
|
|
}
|
|
|
|
*out = router;
|
|
|
|
return lnm_err_ok;
|
|
}
|
|
|
|
void lnm_http_router_free(lnm_http_router *router) {
|
|
if (router == NULL) {
|
|
return;
|
|
}
|
|
|
|
for (size_t i = 0; i < 128; i++) {
|
|
lnm_http_router_free(router->exact_children[i]);
|
|
}
|
|
|
|
lnm_http_router_free(router->single_segment_child);
|
|
|
|
for (size_t i = 0; i < lnm_http_method_total; i++) {
|
|
lnm_http_route_free(router->routes[i]);
|
|
lnm_http_route_free(router->multi_segment_routes[i]);
|
|
}
|
|
|
|
free(router);
|
|
}
|
|
|
|
static bool is_ascii(const char *s) {
|
|
while (*s != '\0') {
|
|
if (*s < 0) {
|
|
return false;
|
|
}
|
|
|
|
s++;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
lnm_err lnm_http_router_add(lnm_http_route **out, lnm_http_router *http_router,
|
|
lnm_http_method method, const char *path) {
|
|
if (path[0] != '/' || !is_ascii(path)) {
|
|
return lnm_err_invalid_route;
|
|
}
|
|
|
|
lnm_http_route *route;
|
|
LNM_RES(lnm_http_route_init(&route));
|
|
|
|
size_t key_segments_count = 0;
|
|
lnm_err res = lnm_err_ok;
|
|
|
|
while (*path != '\0') {
|
|
unsigned char c = *path;
|
|
|
|
switch (c) {
|
|
case ':': {
|
|
const char *next_slash_ptr = strchr(path + 1, '/');
|
|
const char *new_path =
|
|
next_slash_ptr == NULL ? strchr(path + 1, '\0') : next_slash_ptr;
|
|
size_t key_len = new_path - (path + 1);
|
|
|
|
if (key_len == 0) {
|
|
res = lnm_err_invalid_route;
|
|
|
|
goto end;
|
|
}
|
|
|
|
res = lnm_http_route_key_segment_insert(route, path + 1, key_len,
|
|
key_segments_count);
|
|
|
|
if (res != lnm_err_ok) {
|
|
goto end;
|
|
}
|
|
|
|
key_segments_count++;
|
|
|
|
if (http_router->single_segment_child == NULL) {
|
|
res = lnm_http_router_init(&http_router->single_segment_child);
|
|
|
|
if (res != lnm_err_ok) {
|
|
goto end;
|
|
}
|
|
}
|
|
|
|
http_router = http_router->single_segment_child;
|
|
path = new_path;
|
|
} break;
|
|
case '*': {
|
|
const char *next_slash_ptr = strchr(path + 1, '/');
|
|
|
|
// Star match should be at end of route
|
|
if (next_slash_ptr != NULL) {
|
|
res = lnm_err_invalid_route;
|
|
|
|
goto end;
|
|
}
|
|
|
|
const char *end = strchr(path + 1, '\0');
|
|
size_t key_len = end - (path + 1);
|
|
|
|
if (key_len == 0) {
|
|
res = lnm_err_invalid_route;
|
|
|
|
goto end;
|
|
}
|
|
|
|
res = lnm_http_route_key_segment_insert(route, path + 1, key_len,
|
|
key_segments_count);
|
|
|
|
if (res != lnm_err_ok) {
|
|
goto end;
|
|
}
|
|
|
|
key_segments_count++;
|
|
|
|
if (http_router->multi_segment_routes[method] != NULL) {
|
|
res = lnm_err_overlapping_route;
|
|
}
|
|
|
|
http_router->multi_segment_routes[method] = route;
|
|
goto end;
|
|
|
|
} break;
|
|
default:
|
|
if (http_router->exact_children[c] == NULL) {
|
|
res = lnm_http_router_init(&http_router->exact_children[c]);
|
|
|
|
if (res != lnm_err_ok) {
|
|
goto end;
|
|
}
|
|
}
|
|
|
|
http_router = http_router->exact_children[c];
|
|
path++;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (http_router->routes[method] != NULL) {
|
|
res = lnm_err_overlapping_route;
|
|
|
|
goto end;
|
|
}
|
|
|
|
http_router->routes[method] = route;
|
|
http_router->represents_route = true;
|
|
|
|
end:
|
|
if (res != lnm_err_ok) {
|
|
lnm_http_route_free(route);
|
|
|
|
return res;
|
|
}
|
|
|
|
if (out != NULL) {
|
|
*out = route;
|
|
}
|
|
|
|
return lnm_err_ok;
|
|
}
|
|
|
|
static lnm_http_route_err __lnm_http_router_route(lnm_http_route_match *out,
|
|
const lnm_http_router *router,
|
|
lnm_http_method method,
|
|
const char *path,
|
|
size_t path_index,
|
|
size_t matched_key_segments) {
|
|
if (path[path_index] == '\0') {
|
|
if (!router->represents_route) {
|
|
return lnm_http_route_err_unknown_route;
|
|
}
|
|
|
|
if (out != NULL) {
|
|
out->route = router->routes[method];
|
|
}
|
|
|
|
return router->routes[method] == NULL ? lnm_http_route_err_unknown_method
|
|
: lnm_http_route_err_match;
|
|
}
|
|
|
|
lnm_http_route_err res = lnm_http_route_err_unknown_route;
|
|
const lnm_http_router *exact_router =
|
|
router->exact_children[(unsigned char)path[path_index]];
|
|
|
|
if (exact_router != NULL) {
|
|
lnm_http_route_err sub_res = __lnm_http_router_route(
|
|
out, exact_router, method, path, path_index + 1, matched_key_segments);
|
|
|
|
if (sub_res == lnm_http_route_err_match) {
|
|
return lnm_http_route_err_match;
|
|
}
|
|
|
|
res = LNM_MAX(res, sub_res);
|
|
}
|
|
|
|
const lnm_http_router *single_segment_router = router->single_segment_child;
|
|
|
|
if (single_segment_router != NULL) {
|
|
const char *next_slash_ptr = strchr(path + path_index, '/');
|
|
const char *new_path = next_slash_ptr == NULL
|
|
? strchr(path + path_index, '\0')
|
|
: next_slash_ptr;
|
|
size_t segment_len = new_path - (path + path_index);
|
|
|
|
if (segment_len > 0) {
|
|
lnm_http_route_err sub_res = __lnm_http_router_route(
|
|
out, single_segment_router, method, path, path_index + segment_len,
|
|
matched_key_segments + 1);
|
|
|
|
if (sub_res == lnm_http_route_err_match) {
|
|
if (out != NULL) {
|
|
out->key_segments[matched_key_segments].start = path_index;
|
|
out->key_segments[matched_key_segments].len = segment_len;
|
|
}
|
|
|
|
return lnm_http_route_err_match;
|
|
}
|
|
|
|
res = LNM_MAX(res, sub_res);
|
|
}
|
|
}
|
|
|
|
const lnm_http_route *multi_segment_route =
|
|
router->multi_segment_routes[method];
|
|
|
|
if (multi_segment_route != NULL) {
|
|
const char *end_ptr = strchr(path + path_index, '\0');
|
|
size_t segment_len = end_ptr - (path + path_index);
|
|
|
|
// TODO do 405's make sense for star matches?
|
|
|
|
if (segment_len > 0) {
|
|
if (out != NULL) {
|
|
out->route = multi_segment_route;
|
|
out->key_segments[matched_key_segments].start = path_index;
|
|
out->key_segments[matched_key_segments].len = segment_len;
|
|
}
|
|
|
|
return lnm_http_route_err_match;
|
|
}
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
lnm_http_route_err lnm_http_router_route(lnm_http_route_match *out,
|
|
const lnm_http_router *router,
|
|
lnm_http_method method,
|
|
const char *path) {
|
|
if (!is_ascii(path)) {
|
|
return lnm_http_route_err_unknown_route;
|
|
}
|
|
|
|
return __lnm_http_router_route(out, router, method, path, 0, 0);
|
|
}
|
|
|
|
const lnm_http_route_match_segment *
|
|
lnm_http_route_match_get(lnm_http_route_match *match, const char *key) {
|
|
if (match->route->key_segments == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
lnm_http_route_segment_trie *trie = match->route->key_segments;
|
|
|
|
while (*key != '\0') {
|
|
trie = trie->children[(unsigned char)*key];
|
|
|
|
if (trie == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
key++;
|
|
}
|
|
|
|
if (!trie->represents_segment) {
|
|
return NULL;
|
|
}
|
|
|
|
return &match->key_segments[trie->index];
|
|
}
|
|
|
|
bool lnm_http_router_conflicts(const lnm_http_router *r1,
|
|
const lnm_http_router *r2) {
|
|
// First check the literal routes for the routers
|
|
for (lnm_http_method m = 0; m < lnm_http_method_total; m++) {
|
|
if ((r1->routes[m] != NULL && r2->routes[m] != NULL) ||
|
|
(r1->multi_segment_routes[m] != NULL &&
|
|
r2->multi_segment_routes[m] != NULL)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if ((r1->single_segment_child != NULL && r2->single_segment_child != NULL) &&
|
|
lnm_http_router_conflicts(r1->single_segment_child,
|
|
r2->single_segment_child)) {
|
|
return true;
|
|
}
|
|
|
|
for (unsigned char c = 0; c < 128; c++) {
|
|
if ((r1->exact_children[c] != NULL && r2->exact_children[c] != NULL) &&
|
|
lnm_http_router_conflicts(r1->exact_children[c],
|
|
r2->exact_children[c])) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void __lnm_http_router_merge(lnm_http_router *r1, lnm_http_router *r2) {
|
|
for (lnm_http_method m = 0; m < lnm_http_method_total; m++) {
|
|
if (r2->routes[m] != NULL) {
|
|
r1->routes[m] = r2->routes[m];
|
|
}
|
|
|
|
if (r2->multi_segment_routes[m] != NULL) {
|
|
r1->multi_segment_routes[m] = r2->multi_segment_routes[m];
|
|
}
|
|
}
|
|
|
|
if (r1->single_segment_child != NULL && r2->single_segment_child != NULL) {
|
|
__lnm_http_router_merge(r1->single_segment_child, r2->single_segment_child);
|
|
} else if (r2->single_segment_child != NULL) {
|
|
r1->single_segment_child = r2->single_segment_child;
|
|
}
|
|
|
|
for (unsigned char c = 0; c < 128; c++) {
|
|
if (r1->exact_children[c] != NULL && r2->exact_children[c] != NULL) {
|
|
__lnm_http_router_merge(r1->exact_children[c], r2->exact_children[c]);
|
|
} else if (r2->exact_children[c] != NULL) {
|
|
r1->exact_children[c] = r2->exact_children[c];
|
|
}
|
|
}
|
|
|
|
r1->represents_route = r1->represents_route || r2->represents_route;
|
|
|
|
free(r2);
|
|
}
|
|
|
|
lnm_err lnm_http_router_merge(lnm_http_router *r1, lnm_http_router *r2) {
|
|
if (lnm_http_router_conflicts(r1, r2)) {
|
|
return lnm_err_overlapping_route;
|
|
}
|
|
|
|
__lnm_http_router_merge(r1, r2);
|
|
|
|
return lnm_err_ok;
|
|
}
|
|
|
|
lnm_err lnm_http_router_nest(lnm_http_router *parent, lnm_http_router *child,
|
|
const char *prefix) {
|
|
if (!is_ascii(prefix) || prefix[0] != '/') {
|
|
return lnm_err_invalid_route;
|
|
}
|
|
|
|
lnm_http_router *router = parent;
|
|
|
|
// The child router's routes are also mounted on '/', so we stop one earlier
|
|
// as we need to merge it with the router representing the '/'
|
|
while (*prefix != '\0') {
|
|
unsigned char c = *prefix;
|
|
|
|
if (router->exact_children[c] == NULL) {
|
|
LNM_RES(lnm_http_router_init(&router->exact_children[c]));
|
|
}
|
|
|
|
router = router->exact_children[c];
|
|
prefix++;
|
|
}
|
|
|
|
return lnm_http_router_merge(router, child);
|
|
}
|