2 ex__http__router_8c
gullradriel edited this page 2026-10-29 20:54:40 +01:00

examples/ex_http_router.c

Functions

Name
void test_match(const N_ROUTER * r)
matching on the blog's tables
void test_checks(void )
the start-up checks
int process_args(int argc, char ** argv)
read -V
int param_is(const N_ROUTE_MATCH * m, const char * name, const char * want)
param name of the last match equals want
int main(int argc, char ** argv)
run the checks
int h(void * ctx, const N_ROUTE_MATCH * m)
a handler that does nothing (the test only routes)
void expect_true(const char * what, int ok)
count a failed check
void expect_route(const N_ROUTER * r, n_http_req_method method, const char * path, const char * want, N_ROUTE_MATCH * m)
route path with method and expect a route data tag (NULL: not found)

Attributes

Name
int log_level
log level from -V
int failures
failed checks
const N_ROUTE[] PUBLIC
the public routes of the Nilorea blog, in a deliberately unhelpful order: the catch-all "/{slug}/" comes first
const N_ROUTE[] ADMIN
a few admin routes

Functions Documentation

function test_match

static void test_match(
    const N_ROUTER * r
)

matching on the blog's tables

function test_checks

static void test_checks(
    void 
)

the start-up checks

function process_args

static int process_args(
    int argc,
    char ** argv
)

read -V

Parameters:

  • argc argument count
  • argv arguments

Return: 0, or -1 on a bad argument

function param_is

static int param_is(
    const N_ROUTE_MATCH * m,
    const char * name,
    const char * want
)

param name of the last match equals want

Parameters:

  • m the match
  • name placeholder
  • want expected decoded value

Return: non-zero when equal

function main

int main(
    int argc,
    char ** argv
)

run the checks

Parameters:

  • argc argument count
  • argv arguments

Return: 0 when every check passes

function h

static int h(
    void * ctx,
    const N_ROUTE_MATCH * m
)

a handler that does nothing (the test only routes)

Parameters:

  • ctx unused
  • m unused

Return: 0

function expect_true

static void expect_true(
    const char * what,
    int ok
)

count a failed check

Parameters:

  • what the check
  • ok its result

function expect_route

static void expect_route(
    const N_ROUTER * r,
    n_http_req_method method,
    const char * path,
    const char * want,
    N_ROUTE_MATCH * m
)

route path with method and expect a route data tag (NULL: not found)

Parameters:

  • r router
  • method method
  • path the path
  • want expected data tag
  • m receives the match

Attributes Documentation

variable log_level

static int log_level = 3;

log level from -V

variable failures

static int failures = 0;

failed checks

variable PUBLIC

static const N_ROUTE[] PUBLIC                             = {
    { (1u << (unsigned)(N_HTTP_REQ_GET)) , "/{slug:slug}/", h, "page"},
    { (1u << (unsigned)(N_HTTP_REQ_GET)) , "/", h, "home"},
    { (1u << (unsigned)(N_HTTP_REQ_GET)) , "/page/{n:int6}/", h, "home-paged"},
    { (1u << (unsigned)(N_HTTP_REQ_GET)) , "/category/{slug:slug}/", h, "category"},
    { (1u << (unsigned)(N_HTTP_REQ_GET)) , "/category/{slug:slug}/page/{n:int6}/", h, "category-paged"},
    { (1u << (unsigned)(N_HTTP_REQ_GET)) , "/{y:d4}/{m:d2}/", h, "month"},
    { (1u << (unsigned)(N_HTTP_REQ_GET)) , "/{y:d4}/{m:d2}/{d:d2}/{slug:slug}/", h, "post"},
    { (1u << (unsigned)(N_HTTP_REQ_GET)) , "/feed/", h, "feed"},
    { (1u << (unsigned)(N_HTTP_REQ_GET)) , "/sitemap.xml", h, "sitemap"},
    { (1u << (unsigned)(N_HTTP_REQ_GET)) , "/media/{id:hex16}/{name:seg}", h, "media"},
    { (1u << (unsigned)(N_HTTP_REQ_GET)) , "/media/{id:hex16}/{w:int4}/{name:seg}", h, "variant"},
    { (1u << (unsigned)(N_HTTP_REQ_GET)) , "/wp-content/uploads/{path:rest}", h, "legacy-upload"},
};

the public routes of the Nilorea blog, in a deliberately unhelpful order: the catch-all "/{slug}/" comes first

variable ADMIN

static const N_ROUTE[] ADMIN                            = {
    { (1u << (unsigned)(N_HTTP_REQ_GET)) , "/admin/login", h, "login-form"},
    { (1u << (unsigned)(N_HTTP_REQ_POST)) , "/admin/login", h, "login"},
    { (1u << (unsigned)(N_HTTP_REQ_GET)) | (1u << (unsigned)(N_HTTP_REQ_POST)) , "/admin/posts/{id:int9}", h, "post-edit"},
    { (1u << (unsigned)(N_HTTP_REQ_POST)) , "/admin/posts/{id:int9}/trash", h, "post-trash"},
};

a few admin routes

Source code

/*
 * Nilorea Library
 * Copyright (C) 2005-2026 Castagnier Mickael
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * permissions and limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 */


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "nilorea/n_common.h"
#include "nilorea/n_http_router.h"
#include "nilorea/n_log.h"

static int log_level = LOG_ERR;
static int failures = 0;

static void expect_true(const char* what, int ok) {
    if (!ok) {
        n_log(LOG_ERR, "FAIL: %s", what);
        failures++;
    } else {
        n_log(LOG_INFO, "ok: %s", what);
    }
}

static int process_args(int argc, char** argv) {
    int opt = 0;

    while ((opt = getopt(argc, argv, "V:")) != -1) {
        if (opt == 'V') {
            log_level = atoi(optarg);
        } else {
            return -1;
        }
    }
    return 0;
}

static int h(void* ctx, const N_ROUTE_MATCH* m) {
    (void)ctx;
    (void)m;
    return 0;
}

static const N_ROUTE PUBLIC[] = {
    {N_ROUTE_GET, "/{slug:slug}/", h, "page"},
    {N_ROUTE_GET, "/", h, "home"},
    {N_ROUTE_GET, "/page/{n:int6}/", h, "home-paged"},
    {N_ROUTE_GET, "/category/{slug:slug}/", h, "category"},
    {N_ROUTE_GET, "/category/{slug:slug}/page/{n:int6}/", h, "category-paged"},
    {N_ROUTE_GET, "/{y:d4}/{m:d2}/", h, "month"},
    {N_ROUTE_GET, "/{y:d4}/{m:d2}/{d:d2}/{slug:slug}/", h, "post"},
    {N_ROUTE_GET, "/feed/", h, "feed"},
    {N_ROUTE_GET, "/sitemap.xml", h, "sitemap"},
    {N_ROUTE_GET, "/media/{id:hex16}/{name:seg}", h, "media"},
    {N_ROUTE_GET, "/media/{id:hex16}/{w:int4}/{name:seg}", h, "variant"},
    {N_ROUTE_GET, "/wp-content/uploads/{path:rest}", h, "legacy-upload"},
};

static const N_ROUTE ADMIN[] = {
    {N_ROUTE_GET, "/admin/login", h, "login-form"},
    {N_ROUTE_POST, "/admin/login", h, "login"},
    {N_ROUTE_GET | N_ROUTE_POST, "/admin/posts/{id:int9}", h, "post-edit"},
    {N_ROUTE_POST, "/admin/posts/{id:int9}/trash", h, "post-trash"},
};

static void expect_route(const N_ROUTER* r, n_http_req_method method, const char* path, const char* want, N_ROUTE_MATCH* m) {
    N_ROUTER_RESULT res = n_router_match(r, method, path, strlen(path), m, NULL);
    int ok = want ? (res == N_ROUTER_FOUND && strcmp((const char*)m->route->data, want) == 0) : res == N_ROUTER_NOT_FOUND;
    if (!ok) {
        n_log(LOG_ERR, "FAIL: %s -> result %d, route %s, wanted %s", path, (int)res,
              res == N_ROUTER_FOUND ? (const char*)m->route->data : "-", want ? want : "not found");
        failures++;
    }
}

static int param_is(const N_ROUTE_MATCH* m, const char* name, const char* want) {
    size_t len = 0;
    const char* v = n_route_param(m, name, &len);
    return v && len == strlen(want) && strcmp(v, want) == 0;
}

static void test_match(const N_ROUTER* r) {
    static N_ROUTE_MATCH m;
    unsigned allowed = 0;
    char allow[32];

    expect_route(r, N_HTTP_REQ_GET, "/", "home", &m);
    expect_route(r, N_HTTP_REQ_GET, "/about/", "page", &m);
    expect_true("slug captured", param_is(&m, "slug", "about"));
    expect_route(r, N_HTTP_REQ_GET, "/feed/", "feed", &m);
    expect_route(r, N_HTTP_REQ_GET, "/page/3/", "home-paged", &m);
    expect_true("int captured", param_is(&m, "n", "3"));
    expect_route(r, N_HTTP_REQ_GET, "/page/1234567/", NULL, &m);
    expect_route(r, N_HTTP_REQ_GET, "/category/c_code/", "category", &m);
    expect_route(r, N_HTTP_REQ_GET, "/category/c_code/page/2/", "category-paged", &m);
    expect_route(r, N_HTTP_REQ_GET, "/2026/10/", "month", &m);
    expect_route(r, N_HTTP_REQ_GET, "/2026/10/23/clock_gettime/", "post", &m);
    expect_true("post placeholders", param_is(&m, "y", "2026") && param_is(&m, "m", "10") && param_is(&m, "d", "23") &&
                                         param_is(&m, "slug", "clock_gettime") && m.nb_params == 4);
    expect_route(r, N_HTTP_REQ_GET, "/2026/1/23/x/", NULL, &m);
    expect_route(r, N_HTTP_REQ_GET, "/2026/10/23/x", NULL, &m);
    expect_route(r, N_HTTP_REQ_GET, "/feed", NULL, &m);
    expect_route(r, N_HTTP_REQ_GET, "/sitemap.xml", "sitemap", &m);
    expect_route(r, N_HTTP_REQ_GET, "/sitemap.xml/", NULL, &m);
    expect_route(r, N_HTTP_REQ_GET, "/About/", NULL, &m);
    expect_route(r, N_HTTP_REQ_GET, "/ab%63/", NULL, &m);
    expect_route(r, N_HTTP_REQ_GET, "/media/0123456789abcdef/photo%20one.jpg", "media", &m);
    expect_true("seg decoded", param_is(&m, "name", "photo one.jpg"));
    expect_route(r, N_HTTP_REQ_GET, "/media/0123456789abcdef/960/photo.webp", "variant", &m);
    expect_route(r, N_HTTP_REQ_GET, "/media/0123456789ABCDEF/photo.jpg", NULL, &m);
    expect_route(r, N_HTTP_REQ_GET, "/media/0123456789abcdef/a%2Fb", "media", &m);
    expect_true("%2F decodes inside a seg value", param_is(&m, "name", "a/b"));
    expect_route(r, N_HTTP_REQ_GET, "/wp-content/uploads/2019/05/img-300x200.jpg", "legacy-upload", &m);
    expect_true("rest takes the remainder", param_is(&m, "path", "2019/05/img-300x200.jpg"));
    expect_route(r, N_HTTP_REQ_GET, "/wp-content/uploads/", NULL, &m);
    expect_route(r, N_HTTP_REQ_GET, "/wp-content/uploads/caf%C3%A9.png", "legacy-upload", &m);
    expect_true("rest decoded", param_is(&m, "path", "caf\xc3\xa9.png"));
    expect_route(r, N_HTTP_REQ_GET, "//", NULL, &m);
    expect_route(r, N_HTTP_REQ_HEAD, "/feed/", "feed", &m);
    expect_route(r, N_HTTP_REQ_GET, "/admin/login", "login-form", &m);
    expect_route(r, N_HTTP_REQ_POST, "/admin/login", "login", &m);
    expect_route(r, N_HTTP_REQ_POST, "/admin/posts/12", "post-edit", &m);
    expect_route(r, N_HTTP_REQ_POST, "/admin/posts/12/trash", "post-trash", &m);

    expect_true("POST on a GET route: 405",
                n_router_match(r, N_HTTP_REQ_POST, "/feed/", 6, &m, &allowed) == N_ROUTER_METHOD_NOT_ALLOWED && allowed == (N_ROUTE_GET | N_ROUTE_HEAD));
    expect_true("Allow header", n_router_allow_header(allowed, allow, sizeof(allow)) && strcmp(allow, "GET, HEAD") == 0);
    expect_true("GET on a POST-only route: 405 POST",
                n_router_match(r, N_HTTP_REQ_GET, "/admin/posts/1/trash", 20, &m, &allowed) == N_ROUTER_METHOD_NOT_ALLOWED &&
                    n_router_allow_header(allowed, allow, sizeof(allow)) && strcmp(allow, "POST") == 0);
    expect_true("Allow for both", n_router_allow_header(N_ROUTE_GET | N_ROUTE_POST, allow, sizeof(allow)) && strcmp(allow, "GET, HEAD, POST") == 0);
    expect_true("Allow buffer too small", !n_router_allow_header(N_ROUTE_GET, allow, 4));
    expect_true("malformed escape: 400", n_router_match(r, N_HTTP_REQ_GET, "/a%zz/", 7, &m, NULL) == N_ROUTER_BAD_REQUEST);
    expect_true("escape cut short: 400", n_router_match(r, N_HTTP_REQ_GET, "/a%4", 4, &m, NULL) == N_ROUTER_BAD_REQUEST);
    expect_true("decoded NUL: 400", n_router_match(r, N_HTTP_REQ_GET, "/a%00b/", 7, &m, NULL) == N_ROUTER_BAD_REQUEST);
    expect_true("ill-formed UTF-8 anywhere: 400", n_router_match(r, N_HTTP_REQ_GET, "/x/%E9/", 7, &m, NULL) == N_ROUTER_BAD_REQUEST);
    expect_true("UTF-8 split across segments: 400", n_router_match(r, N_HTTP_REQ_GET, "/%C3/%A9", 8, &m, NULL) == N_ROUTER_BAD_REQUEST);
    expect_true("no leading '/': 400", n_router_match(r, N_HTTP_REQ_GET, "feed/", 5, &m, NULL) == N_ROUTER_BAD_REQUEST);
    expect_true("empty path: 400", n_router_match(r, N_HTTP_REQ_GET, "", 0, &m, NULL) == N_ROUTER_BAD_REQUEST);
    expect_true("NULL router: 400", n_router_match(NULL, N_HTTP_REQ_GET, "/", 1, &m, NULL) == N_ROUTER_BAD_REQUEST);
    expect_true("absent param", n_route_param(&m, "nope", NULL) == NULL);
    {
        char deep[200];
        size_t i, o = 0;
        for (i = 0; i < 40; i++) {
            deep[o++] = '/';
            deep[o++] = 'a';
        }
        expect_true("too many segments: 404", n_router_match(r, N_HTTP_REQ_GET, deep, o, &m, NULL) == N_ROUTER_NOT_FOUND);
    }
}

static void test_checks(void) {
    char err[256];
    N_ROUTER* r = n_router_new();
    static const N_ROUTE dup[] = {{N_ROUTE_GET, "/a/{x:slug}/", h, NULL}, {N_ROUTE_GET, "/a/{y:slug}/", h, NULL}};
    static const N_ROUTE dhex[] = {{N_ROUTE_GET, "/b/{x:d4}", h, NULL}, {N_ROUTE_GET, "/b/{y:hex4}", h, NULL}};
    static const N_ROUTE dlen[] = {{N_ROUTE_GET, "/c/{x:d4}", h, NULL}, {N_ROUTE_GET, "/c/{y:hex16}", h, NULL}};
    static const N_ROUTE methods[] = {{N_ROUTE_GET, "/d/{x:slug}", h, NULL}, {N_ROUTE_POST, "/d/{y:slug}", h, NULL}};
    static const N_ROUTE headget[] = {{N_ROUTE_GET, "/e", h, NULL}, {N_ROUTE_HEAD, "/e", h, NULL}};

    n_router_add(r, dup, 2);
    expect_true("same shape twice is ambiguous", n_router_check(r, err, sizeof(err)) == FALSE && strstr(err, "/a/{x:slug}/") != NULL);
    n_router_free(&r);
    r = n_router_new();
    n_router_add(r, dhex, 2);
    expect_true("d4 against hex4 is ambiguous", n_router_check(r, err, sizeof(err)) == FALSE);
    n_router_free(&r);
    r = n_router_new();
    n_router_add(r, dlen, 2);
    expect_true("d4 against hex16 is not", n_router_check(r, err, sizeof(err)) == TRUE && err[0] == '\0');
    n_router_free(&r);
    r = n_router_new();
    n_router_add(r, methods, 2);
    expect_true("same shape, other method, is not", n_router_check(r, err, sizeof(err)) == TRUE);
    n_router_free(&r);
    r = n_router_new();
    n_router_add(r, headget, 2);
    expect_true("HEAD route beside a GET one is ambiguous", n_router_check(r, err, sizeof(err)) == FALSE);
    n_router_free(&r);
    expect_true("free sets NULL", r == NULL);

    {
        static const char* bad[] = {
            "noslash", "/a//b", "/{x", "/{x}y", "/{}", "/{X:seg}", "/{x:what}", "/{x:d0}", "/{x:d19}", "/{x:hex65}",
            "/{x:int}", "/{x:seg}/{x:seg}", "/{r:rest}/more", "/{r:rest}/", "/a%20b", "/a b", "/a?b",
            "/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}", "/{abcdefghijklmnopqrstuvwxyz012345}"};
        size_t i;
        for (i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
            N_ROUTE one = {N_ROUTE_GET, bad[i], h, NULL};
            r = n_router_new();
            if (n_router_add(r, &one, 1) != FALSE || n_router_count(r) != 0) {
                n_log(LOG_ERR, "FAIL: pattern '%s' accepted", bad[i]);
                failures++;
            }
            n_router_free(&r);
        }
    }
    {
        N_ROUTE nomethod = {0, "/x", h, NULL};
        N_ROUTE nohandler = {N_ROUTE_GET, "/x", NULL, NULL};
        N_ROUTE badbit = {1u << 9, "/x", h, NULL};
        N_ROUTE mixed[] = {{N_ROUTE_GET, "/ok", h, NULL}, {N_ROUTE_GET, "/bad//", h, NULL}};
        r = n_router_new();
        expect_true("route without method refused", n_router_add(r, &nomethod, 1) == FALSE);
        expect_true("route without handler refused", n_router_add(r, &nohandler, 1) == FALSE);
        expect_true("unknown method bit refused", n_router_add(r, &badbit, 1) == FALSE);
        expect_true("one bad route: nothing of the table added", n_router_add(r, mixed, 2) == FALSE && n_router_count(r) == 0);
        expect_true("empty table is fine", n_router_add(r, NULL, 0) == TRUE);
        n_router_free(&r);
    }
}

int main(int argc, char** argv) {
    N_ROUTER* r = NULL;
    char err[256];

    if (process_args(argc, argv) != 0) {
        fprintf(stderr, "usage: %s [-V LOG_LEVEL]\n", argv[0]);
        return 1;
    }
    set_log_level(log_level);

    r = n_router_new();
    expect_true("public table added", n_router_add(r, PUBLIC, sizeof(PUBLIC) / sizeof(PUBLIC[0])));
    expect_true("admin table added", n_router_add(r, ADMIN, sizeof(ADMIN) / sizeof(ADMIN[0])));
    expect_true("route count", n_router_count(r) == 16);
    expect_true("route access", n_router_route(r, 1) == &PUBLIC[1] && n_router_route(r, 12) == &ADMIN[0] && n_router_route(r, 16) == NULL);
    expect_true("the blog's tables are not ambiguous", n_router_check(r, err, sizeof(err)) == TRUE);
    if (err[0])
        n_log(LOG_ERR, "%s", err);
    test_match(r);
    n_router_free(&r);
    test_checks();

    if (failures) {
        n_log(LOG_ERR, "ex_http_router: %d failure(s)", failures);
        return 1;
    }
    n_log(LOG_NOTICE, "ex_http_router: all checks passed");
    return 0;
}