lnm/src/http/lnm_http_router.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);
}