From 57340562880f3ac8a0d88a8550ebe1dcd61801a8 Mon Sep 17 00:00:00 2001 From: Raphael Schlarb Date: Wed, 12 Aug 2026 06:29:03 -0500 Subject: [PATCH 1/4] refactor: mirror getaddrinfo/socket syscalls directly in acceptor ops - extract th_addrinfo (getaddrinfo/freeaddrinfo mirrored 1:1) into th_address, resolved once by th_listener before opening the acceptor - add TH_ERR_CATEGORY_EAI for getaddrinfo's EAI_* codes, distinct from errno; EAI_SYSTEM still maps to TH_ERR_SYSTEM(errno) - split th_acceptor_ops.open into socket/setsockopt/set_nonblocking/ bind/listen/close, one op per syscall, so tests can fake each step - add TH_EADDRINUSE --- CMakeLists.txt | 2 + include/th.h | 2 + src/th_accept_test.c | 63 +++++++++++++++-- src/th_acceptor.c | 154 ++++++++++++++++++++++++----------------- src/th_acceptor.h | 28 ++++---- src/th_acceptor_test.c | 120 +++++++++++++++++++++++++------- src/th_address.c | 62 +++++++++++++++++ src/th_address.h | 26 +++++++ src/th_address_test.c | 110 +++++++++++++++++++++++++++++ src/th_error.c | 3 + src/th_listener.c | 7 +- src/th_system_error.h | 2 + 12 files changed, 470 insertions(+), 109 deletions(-) create mode 100644 src/th_address.c create mode 100644 src/th_address_test.c diff --git a/CMakeLists.txt b/CMakeLists.txt index cb221de..232fa1d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,6 +45,7 @@ SET(TH_CORE_SRC src/th_send.c src/th_sendvec.c src/th_sendfile.c + src/th_address.c src/th_acceptor.c src/th_accept.c src/th_tcp_conn.c @@ -234,6 +235,7 @@ if (NOT TH_DISABLE_TESTS) src/th_send_test.c src/th_sendvec_test.c src/th_sendfile_test.c + src/th_address_test.c src/th_acceptor_test.c src/th_accept_test.c src/th_conn_test.c diff --git a/include/th.h b/include/th.h index e95f4cd..b106465 100644 --- a/include/th.h +++ b/include/th.h @@ -45,6 +45,7 @@ typedef struct th_buffer { #define TH_ERR_CATEGORY_SYSTEM 1 // system error, corresponds to errno or GetLastError #define TH_ERR_CATEGORY_HTTP 2 // http protocol error #define TH_ERR_CATEGORY_SSL 3 // ssl error +#define TH_ERR_CATEGORY_EAI 4 // getaddrinfo error, EAI_* code (not errno) /* other error codes */ #define TH_ERRC_OK 0 @@ -78,6 +79,7 @@ typedef enum th_err { #define TH_ERR_SYSTEM(code) TH_ERR(TH_ERR_CATEGORY_SYSTEM, code) #define TH_ERR_HTTP(code) TH_ERR(TH_ERR_CATEGORY_HTTP, code) #define TH_ERR_SSL(code) TH_ERR(TH_ERR_CATEGORY_SSL, code) +#define TH_ERR_EAI(code) TH_ERR(TH_ERR_CATEGORY_EAI, code) #define TH_ERR_CATEGORY(err) (err >> TH_ERR_CATEGORY_SHIFT) #define TH_ERR_CODE(err) (err & TH_ERR_CODE_MASK) diff --git a/src/th_accept_test.c b/src/th_accept_test.c index 4fc4ebf..c79966d 100644 --- a/src/th_accept_test.c +++ b/src/th_accept_test.c @@ -87,15 +87,62 @@ typedef struct th_fake_acceptor_ops { } th_fake_acceptor_ops; static th_err -th_fake_acceptor_open(void* self, const char* addr, const char* port, int* out_fd) +th_fake_acceptor_socket(void* self, int domain, int type, int protocol, int* out_fd) { - (void)addr; - (void)port; + (void)domain; + (void)type; + (void)protocol; th_fake_acceptor_ops* ops = self; *out_fd = ops->open_fd; return TH_ERR_OK; } +static th_err +th_fake_acceptor_setsockopt(void* self, int fd, int level, int optname, const void* optval, socklen_t optlen) +{ + (void)self; + (void)fd; + (void)level; + (void)optname; + (void)optval; + (void)optlen; + return TH_ERR_OK; +} + +static th_err +th_fake_acceptor_set_nonblocking(void* self, int fd) +{ + (void)self; + (void)fd; + return TH_ERR_OK; +} + +static th_err +th_fake_acceptor_bind(void* self, int fd, const struct sockaddr* addr, socklen_t addrlen) +{ + (void)self; + (void)fd; + (void)addr; + (void)addrlen; + return TH_ERR_OK; +} + +static th_err +th_fake_acceptor_listen(void* self, int fd, int backlog) +{ + (void)self; + (void)fd; + (void)backlog; + return TH_ERR_OK; +} + +static void +th_fake_acceptor_close(void* self, int fd) +{ + (void)self; + (void)fd; +} + static th_err th_fake_acceptor_accept(void* self, int fd, th_address* addr, int* out_fd) { @@ -114,7 +161,12 @@ th_fake_acceptor_accept(void* self, int fd, th_address* addr, int* out_fd) static void th_fake_acceptor_ops_init(th_fake_acceptor_ops* ops) { - ops->base.open = th_fake_acceptor_open; + ops->base.socket = th_fake_acceptor_socket; + ops->base.setsockopt = th_fake_acceptor_setsockopt; + ops->base.set_nonblocking = th_fake_acceptor_set_nonblocking; + ops->base.bind = th_fake_acceptor_bind; + ops->base.listen = th_fake_acceptor_listen; + ops->base.close = th_fake_acceptor_close; ops->base.accept = th_fake_acceptor_accept; ops->open_fd = 9; ops->accept_err = TH_ERR_OK; @@ -158,7 +210,8 @@ TH_TEST_BEGIN(accept) th_fake_acceptor_ops_init(&ops); th_acceptor acceptor; th_acceptor_init(&acceptor, &loop, &ops.base); - th_acceptor_open(&acceptor, "127.0.0.1", "8080"); + th_addrinfo info = {0}; + th_acceptor_open(&acceptor, &info); TH_TEST_CASE_BEGIN(accept_completes_with_new_fd) { diff --git a/src/th_acceptor.c b/src/th_acceptor.c index ad37cb1..6fdf941 100644 --- a/src/th_acceptor.c +++ b/src/th_acceptor.c @@ -5,81 +5,63 @@ #if defined(TH_CONFIG_OS_POSIX) #include #include -#include #include -#include #include #include #include TH_LOCAL(th_err) -th_acceptor_ops_os_set_nonblocking(int fd) +th_acceptor_ops_os_socket(void* self, int domain, int type, int protocol, int* out_fd) { + (void)self; + int fd = socket(domain, type, protocol); + if (fd < 0) + return TH_ERR_SYSTEM(errno); + *out_fd = fd; + return TH_ERR_OK; +} + +TH_LOCAL(th_err) +th_acceptor_ops_os_setsockopt(void* self, int fd, int level, int optname, const void* optval, socklen_t optlen) +{ + (void)self; + if (setsockopt(fd, level, optname, optval, optlen) < 0) + return TH_ERR_SYSTEM(errno); + return TH_ERR_OK; +} + +TH_LOCAL(th_err) +th_acceptor_ops_os_set_nonblocking(void* self, int fd) +{ + (void)self; if (fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK) < 0) return TH_ERR_SYSTEM(errno); return TH_ERR_OK; } TH_LOCAL(th_err) -th_acceptor_ops_os_open(void* self, const char* addr, const char* port, int* out_fd) +th_acceptor_ops_os_bind(void* self, int fd, const struct sockaddr* addr, socklen_t addrlen) { (void)self; - struct addrinfo hints = {0}; - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_STREAM; - hints.ai_flags = AI_PASSIVE; - struct addrinfo* res = NULL; - if (getaddrinfo(addr, port, &hints, &res) != 0) + if (bind(fd, addr, addrlen) < 0) return TH_ERR_SYSTEM(errno); + return TH_ERR_OK; +} - th_err err = TH_ERR_OK; - int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); - if (fd < 0) { - err = TH_ERR_SYSTEM(errno); - goto cleanup_addrinfo; - } -#if TH_CONFIG_REUSE_ADDR - { - int optval = 1; - if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0) { - err = TH_ERR_SYSTEM(errno); - goto cleanup_fd; - } - } -#endif -#if TH_CONFIG_REUSE_PORT - { -#if defined(SO_REUSEPORT) - int optval = 1; - if (setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval)) < 0) { - err = TH_ERR_SYSTEM(errno); - goto cleanup_fd; - } -#else - TH_LOG_FATAL("SO_REUSEPORT is not supported on this platform"); - err = TH_ERR_NOSUPPORT; - goto cleanup_fd; -#endif - } -#endif - if ((err = th_acceptor_ops_os_set_nonblocking(fd)) != TH_ERR_OK) - goto cleanup_fd; - if (bind(fd, res->ai_addr, res->ai_addrlen) < 0) { - err = TH_ERR_SYSTEM(errno); - goto cleanup_fd; - } - if (listen(fd, 1024) < 0) { - err = TH_ERR_SYSTEM(errno); - goto cleanup_fd; - } - freeaddrinfo(res); - *out_fd = fd; +TH_LOCAL(th_err) +th_acceptor_ops_os_listen(void* self, int fd, int backlog) +{ + (void)self; + if (listen(fd, backlog) < 0) + return TH_ERR_SYSTEM(errno); return TH_ERR_OK; -cleanup_fd: +} + +TH_LOCAL(void) +th_acceptor_ops_os_close(void* self, int fd) +{ + (void)self; close(fd); -cleanup_addrinfo: - freeaddrinfo(res); - return err; } TH_LOCAL(th_err) @@ -89,7 +71,7 @@ th_acceptor_ops_os_accept(void* self, int fd, th_address* addr, int* out_fd) int conn_fd = accept(fd, (struct sockaddr*)&addr->addr, &addr->addrlen); if (conn_fd < 0) return TH_ERR_SYSTEM(errno); - th_err err = th_acceptor_ops_os_set_nonblocking(conn_fd); + th_err err = th_acceptor_ops_os_set_nonblocking(self, conn_fd); if (err != TH_ERR_OK) { close(conn_fd); return err; @@ -102,7 +84,12 @@ TH_PRIVATE(th_acceptor_ops*) th_acceptor_ops_os(void) { static th_acceptor_ops ops = { - .open = th_acceptor_ops_os_open, + .socket = th_acceptor_ops_os_socket, + .setsockopt = th_acceptor_ops_os_setsockopt, + .set_nonblocking = th_acceptor_ops_os_set_nonblocking, + .bind = th_acceptor_ops_os_bind, + .listen = th_acceptor_ops_os_listen, + .close = th_acceptor_ops_os_close, .accept = th_acceptor_ops_os_accept, }; return &ops; @@ -110,6 +97,49 @@ th_acceptor_ops_os(void) #endif /* TH_CONFIG_OS_POSIX */ +TH_LOCAL(th_err) +th_acceptor_open_socket(th_acceptor* acceptor, const th_addrinfo* info, int* out_fd) +{ + th_acceptor_ops* ops = acceptor->ops; + int fd = -1; + th_err err = ops->socket(ops, info->family, info->socktype, info->protocol, &fd); + if (err != TH_ERR_OK) + return err; + +#if TH_CONFIG_REUSE_ADDR + { + int optval = 1; + if ((err = ops->setsockopt(ops, fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval))) != TH_ERR_OK) + goto cleanup_fd; + } +#endif +#if TH_CONFIG_REUSE_PORT + { +#if defined(SO_REUSEPORT) + int optval = 1; + if ((err = ops->setsockopt(ops, fd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval))) != TH_ERR_OK) + goto cleanup_fd; +#else + TH_LOG_FATAL("SO_REUSEPORT is not supported on this platform"); + err = TH_ERR_NOSUPPORT; + goto cleanup_fd; +#endif + } +#endif + if ((err = ops->set_nonblocking(ops, fd)) != TH_ERR_OK) + goto cleanup_fd; + if ((err = ops->bind(ops, fd, (const struct sockaddr*)&info->addr.addr, info->addr.addrlen)) != TH_ERR_OK) + goto cleanup_fd; + if ((err = ops->listen(ops, fd, 1024)) != TH_ERR_OK) + goto cleanup_fd; + + *out_fd = fd; + return TH_ERR_OK; +cleanup_fd: + ops->close(ops, fd); + return err; +} + TH_PRIVATE(void) th_acceptor_init(th_acceptor* acceptor, th_loop* loop, th_acceptor_ops* ops) { @@ -119,18 +149,16 @@ th_acceptor_init(th_acceptor* acceptor, th_loop* loop, th_acceptor_ops* ops) } TH_PRIVATE(th_err) -th_acceptor_open(th_acceptor* acceptor, const char* addr, const char* port) +th_acceptor_open(th_acceptor* acceptor, const th_addrinfo* info) { int fd = -1; - th_err err = acceptor->ops->open(acceptor->ops, addr, port, &fd); + th_err err = th_acceptor_open_socket(acceptor, info, &fd); if (err != TH_ERR_OK) return err; th_acceptor_close(acceptor); err = th_reactor_create_handle(acceptor->loop->reactor, &acceptor->handle, fd); if (err != TH_ERR_OK) { -#if defined(TH_CONFIG_OS_POSIX) - close(fd); -#endif + acceptor->ops->close(acceptor->ops, fd); return err; } th_handle_enable_timeout(acceptor->handle, false); diff --git a/src/th_acceptor.h b/src/th_acceptor.h index fdf380f..e8fb708 100644 --- a/src/th_acceptor.h +++ b/src/th_acceptor.h @@ -9,21 +9,17 @@ #include "th_socket.h" /** th_acceptor_ops - * @brief The raw listen-socket syscalls a th_acceptor performs. Injected - * at construction time so tests can fake an acceptor without a real fd. + * @brief Mirrors the raw listen-socket syscalls directly, one op each, + * so tests can fake a th_acceptor without a real fd. */ typedef struct th_acceptor_ops { - /** open - * @brief Resolves addr/port, creates a non-blocking listening socket - * bound and listening on it, and writes its fd to *fd. - */ - th_err (*open)(void* self, const char* addr, const char* port, int* fd); - - /** accept - * @brief Accepts one pending connection on fd, writes the peer - * address to addr and the new non-blocking socket's fd to *out_fd. - * TH_ERR_SYSTEM(TH_EAGAIN)/TH_EWOULDBLOCK when none is pending. - */ + th_err (*socket)(void* self, int domain, int type, int protocol, int* out_fd); + th_err (*setsockopt)(void* self, int fd, int level, int optname, const void* optval, socklen_t optlen); + th_err (*set_nonblocking)(void* self, int fd); + th_err (*bind)(void* self, int fd, const struct sockaddr* addr, socklen_t addrlen); + th_err (*listen)(void* self, int fd, int backlog); + void (*close)(void* self, int fd); + // TH_ERR_SYSTEM(TH_EAGAIN)/TH_EWOULDBLOCK when nothing is pending. th_err (*accept)(void* self, int fd, th_address* addr, int* out_fd); } th_acceptor_ops; @@ -46,11 +42,11 @@ TH_PRIVATE(void) th_acceptor_init(th_acceptor* acceptor, th_loop* loop, th_acceptor_ops* ops); /** th_acceptor_open - * @brief Resolves addr/port and registers the resulting listening socket - * with the acceptor's reactor, replacing any fd previously set. + * @brief Opens a listening socket matching info and registers it with + * the acceptor's reactor, replacing any fd previously set. */ TH_PRIVATE(th_err) -th_acceptor_open(th_acceptor* acceptor, const char* addr, const char* port); +th_acceptor_open(th_acceptor* acceptor, const th_addrinfo* info); TH_INLINE(int) th_acceptor_get_fd(const th_acceptor* acceptor) diff --git a/src/th_acceptor_test.c b/src/th_acceptor_test.c index de69e7d..6dc27b2 100644 --- a/src/th_acceptor_test.c +++ b/src/th_acceptor_test.c @@ -90,25 +90,78 @@ th_fake_reactor_init(th_fake_reactor* reactor) typedef struct th_fake_acceptor_ops { th_acceptor_ops base; - th_err open_err; - int open_fd; + th_err socket_err; + int socket_fd; + th_err setsockopt_err; + th_err set_nonblocking_err; + th_err bind_err; + th_err listen_err; + bool closed; th_err accept_err; int accept_fd; int last_fd; } th_fake_acceptor_ops; static th_err -th_fake_acceptor_open(void* self, const char* addr, const char* port, int* out_fd) +th_fake_acceptor_socket(void* self, int domain, int type, int protocol, int* out_fd) { - (void)addr; - (void)port; + (void)domain; + (void)type; + (void)protocol; th_fake_acceptor_ops* ops = self; - if (ops->open_err != TH_ERR_OK) - return ops->open_err; - *out_fd = ops->open_fd; + if (ops->socket_err != TH_ERR_OK) + return ops->socket_err; + *out_fd = ops->socket_fd; return TH_ERR_OK; } +static th_err +th_fake_acceptor_setsockopt(void* self, int fd, int level, int optname, const void* optval, socklen_t optlen) +{ + (void)fd; + (void)level; + (void)optname; + (void)optval; + (void)optlen; + th_fake_acceptor_ops* ops = self; + return ops->setsockopt_err; +} + +static th_err +th_fake_acceptor_set_nonblocking(void* self, int fd) +{ + (void)fd; + th_fake_acceptor_ops* ops = self; + return ops->set_nonblocking_err; +} + +static th_err +th_fake_acceptor_bind(void* self, int fd, const struct sockaddr* addr, socklen_t addrlen) +{ + (void)fd; + (void)addr; + (void)addrlen; + th_fake_acceptor_ops* ops = self; + return ops->bind_err; +} + +static th_err +th_fake_acceptor_listen(void* self, int fd, int backlog) +{ + (void)fd; + (void)backlog; + th_fake_acceptor_ops* ops = self; + return ops->listen_err; +} + +static void +th_fake_acceptor_close(void* self, int fd) +{ + (void)fd; + th_fake_acceptor_ops* ops = self; + ops->closed = true; +} + static th_err th_fake_acceptor_accept(void* self, int fd, th_address* addr, int* out_fd) { @@ -124,10 +177,20 @@ th_fake_acceptor_accept(void* self, int fd, th_address* addr, int* out_fd) static void th_fake_acceptor_ops_init(th_fake_acceptor_ops* ops) { - ops->base.open = th_fake_acceptor_open; + ops->base.socket = th_fake_acceptor_socket; + ops->base.setsockopt = th_fake_acceptor_setsockopt; + ops->base.set_nonblocking = th_fake_acceptor_set_nonblocking; + ops->base.bind = th_fake_acceptor_bind; + ops->base.listen = th_fake_acceptor_listen; + ops->base.close = th_fake_acceptor_close; ops->base.accept = th_fake_acceptor_accept; - ops->open_err = TH_ERR_OK; - ops->open_fd = 9; + ops->socket_err = TH_ERR_OK; + ops->socket_fd = 9; + ops->setsockopt_err = TH_ERR_OK; + ops->set_nonblocking_err = TH_ERR_OK; + ops->bind_err = TH_ERR_OK; + ops->listen_err = TH_ERR_OK; + ops->closed = false; ops->accept_err = TH_ERR_OK; ops->accept_fd = -1; ops->last_fd = -1; @@ -143,6 +206,7 @@ TH_TEST_BEGIN(acceptor) th_fake_acceptor_ops_init(&ops); th_acceptor acceptor; th_acceptor_init(&acceptor, &loop, &ops.base); + th_addrinfo info = {0}; TH_TEST_CASE_BEGIN(acceptor_init_has_no_fd) { @@ -151,37 +215,45 @@ TH_TEST_BEGIN(acceptor) TH_TEST_CASE_END TH_TEST_CASE_BEGIN(acceptor_open_registers_handle_without_timeout) { - TH_EXPECT(th_acceptor_open(&acceptor, "127.0.0.1", "8080") == TH_ERR_OK); + TH_EXPECT(th_acceptor_open(&acceptor, &info) == TH_ERR_OK); TH_EXPECT(th_acceptor_get_fd(&acceptor) == 9); TH_EXPECT(reactor.handle.timeout_enabled == false); } TH_TEST_CASE_END - TH_TEST_CASE_BEGIN(acceptor_open_propagates_ops_error) + TH_TEST_CASE_BEGIN(acceptor_open_propagates_socket_error) + { + ops.socket_err = TH_ERR_SYSTEM(TH_EIO); + TH_EXPECT(th_acceptor_open(&acceptor, &info) == TH_ERR_SYSTEM(TH_EIO)); + TH_EXPECT(th_acceptor_get_fd(&acceptor) == -1); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(acceptor_open_propagates_bind_error_and_closes_fd) { - ops.open_err = TH_ERR_SYSTEM(TH_EIO); - TH_EXPECT(th_acceptor_open(&acceptor, "127.0.0.1", "8080") == TH_ERR_SYSTEM(TH_EIO)); + ops.bind_err = TH_ERR_SYSTEM(TH_EADDRINUSE); + TH_EXPECT(th_acceptor_open(&acceptor, &info) == TH_ERR_SYSTEM(TH_EADDRINUSE)); TH_EXPECT(th_acceptor_get_fd(&acceptor) == -1); + TH_EXPECT(ops.closed); } TH_TEST_CASE_END TH_TEST_CASE_BEGIN(acceptor_open_propagates_reactor_error) { reactor.create_handle_err = TH_ERR_SYSTEM(TH_EBADF); - TH_EXPECT(th_acceptor_open(&acceptor, "127.0.0.1", "8080") == TH_ERR_SYSTEM(TH_EBADF)); + TH_EXPECT(th_acceptor_open(&acceptor, &info) == TH_ERR_SYSTEM(TH_EBADF)); TH_EXPECT(th_acceptor_get_fd(&acceptor) == -1); } TH_TEST_CASE_END TH_TEST_CASE_BEGIN(acceptor_open_closes_previous_handle) { - ops.open_fd = 1; - TH_EXPECT(th_acceptor_open(&acceptor, "127.0.0.1", "8080") == TH_ERR_OK); - ops.open_fd = 2; - TH_EXPECT(th_acceptor_open(&acceptor, "127.0.0.1", "8080") == TH_ERR_OK); + ops.socket_fd = 1; + TH_EXPECT(th_acceptor_open(&acceptor, &info) == TH_ERR_OK); + ops.socket_fd = 2; + TH_EXPECT(th_acceptor_open(&acceptor, &info) == TH_ERR_OK); TH_EXPECT(th_acceptor_get_fd(&acceptor) == 2); } TH_TEST_CASE_END TH_TEST_CASE_BEGIN(acceptor_accept_calls_ops_with_fd) { - TH_EXPECT(th_acceptor_open(&acceptor, "127.0.0.1", "8080") == TH_ERR_OK); + TH_EXPECT(th_acceptor_open(&acceptor, &info) == TH_ERR_OK); ops.accept_fd = 42; th_address addr; th_socket socket; @@ -193,7 +265,7 @@ TH_TEST_BEGIN(acceptor) TH_TEST_CASE_END TH_TEST_CASE_BEGIN(acceptor_accept_propagates_eagain) { - TH_EXPECT(th_acceptor_open(&acceptor, "127.0.0.1", "8080") == TH_ERR_OK); + TH_EXPECT(th_acceptor_open(&acceptor, &info) == TH_ERR_OK); ops.accept_err = TH_ERR_SYSTEM(TH_EAGAIN); th_address addr; th_socket socket; @@ -203,14 +275,14 @@ TH_TEST_BEGIN(acceptor) TH_TEST_CASE_END TH_TEST_CASE_BEGIN(acceptor_cancel_forwards_to_handle) { - TH_EXPECT(th_acceptor_open(&acceptor, "127.0.0.1", "8080") == TH_ERR_OK); + TH_EXPECT(th_acceptor_open(&acceptor, &info) == TH_ERR_OK); th_acceptor_cancel(&acceptor); TH_EXPECT(reactor.handle.cancelled); } TH_TEST_CASE_END TH_TEST_CASE_BEGIN(acceptor_close_destroys_handle_and_clears_fd) { - TH_EXPECT(th_acceptor_open(&acceptor, "127.0.0.1", "8080") == TH_ERR_OK); + TH_EXPECT(th_acceptor_open(&acceptor, &info) == TH_ERR_OK); th_acceptor_close(&acceptor); TH_EXPECT(reactor.handle.destroyed); TH_EXPECT(th_acceptor_get_fd(&acceptor) == -1); diff --git a/src/th_address.c b/src/th_address.c new file mode 100644 index 0000000..19fb1b5 --- /dev/null +++ b/src/th_address.c @@ -0,0 +1,62 @@ +#include "th_address.h" + +#if defined(TH_CONFIG_OS_POSIX) +#include +#include + +TH_LOCAL(th_err) +th_addrinfo_ops_os_getaddrinfo(void* self, const char* host, const char* port, const struct addrinfo* hints, struct addrinfo** res) +{ + (void)self; + int rc = getaddrinfo(host, port, hints, res); + if (rc == 0) + return TH_ERR_OK; + return rc == EAI_SYSTEM ? TH_ERR_SYSTEM(errno) : TH_ERR_EAI(rc); +} + +TH_LOCAL(void) +th_addrinfo_ops_os_freeaddrinfo(void* self, struct addrinfo* res) +{ + (void)self; + freeaddrinfo(res); +} + +TH_PRIVATE(th_addrinfo_ops*) +th_addrinfo_ops_os(void) +{ + static th_addrinfo_ops ops = { + .getaddrinfo = th_addrinfo_ops_os_getaddrinfo, + .freeaddrinfo = th_addrinfo_ops_os_freeaddrinfo, + }; + return &ops; +} + +TH_PRIVATE(const char*) +th_addrinfo_strerror(int code) +{ + return gai_strerror(code); +} + +#endif /* TH_CONFIG_OS_POSIX */ + +TH_PRIVATE(th_err) +th_addrinfo_from_str(th_addrinfo* info, const char* host, const char* port, th_addrinfo_ops* ops) +{ + struct addrinfo hints = {0}; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_PASSIVE; + + struct addrinfo* res = NULL; + th_err err = ops->getaddrinfo(ops, host, port, &hints, &res); + if (err != TH_ERR_OK) + return err; + + info->family = res->ai_family; + info->socktype = res->ai_socktype; + info->protocol = res->ai_protocol; + info->addr.addrlen = (socklen_t)res->ai_addrlen; + memcpy(&info->addr.addr, res->ai_addr, res->ai_addrlen); + ops->freeaddrinfo(ops, res); + return TH_ERR_OK; +} diff --git a/src/th_address.h b/src/th_address.h index d07e300..5224909 100644 --- a/src/th_address.h +++ b/src/th_address.h @@ -5,6 +5,7 @@ #include "th_config.h" +#include #include /** th_address @@ -21,4 +22,29 @@ th_address_init(th_address* addr) addr->addrlen = sizeof(addr->addr); } +typedef struct th_addrinfo_ops { + th_err (*getaddrinfo)(void* self, const char* host, const char* port, const struct addrinfo* hints, struct addrinfo** res); + void (*freeaddrinfo)(void* self, struct addrinfo* res); +} th_addrinfo_ops; + +TH_PRIVATE(th_addrinfo_ops*) +th_addrinfo_ops_os(void); + +/** th_addrinfo_strerror + * @brief gai_strerror for an EAI_* code, i.e. TH_ERR_EAI's code - not a + * POSIX errno, so th_system_strerror doesn't apply. + */ +TH_PRIVATE(const char*) +th_addrinfo_strerror(int code); + +typedef struct th_addrinfo { + th_address addr; + int family; + int socktype; + int protocol; +} th_addrinfo; + +TH_PRIVATE(th_err) +th_addrinfo_from_str(th_addrinfo* info, const char* host, const char* port, th_addrinfo_ops* ops); + #endif diff --git a/src/th_address_test.c b/src/th_address_test.c new file mode 100644 index 0000000..3fceee6 --- /dev/null +++ b/src/th_address_test.c @@ -0,0 +1,110 @@ +#include "th_address.h" +#include "th_system_error.h" +#include "th_test.h" + +#include +#include + +typedef struct th_fake_addrinfo_ops { + th_addrinfo_ops base; + th_err resolve_err; + const char* last_host; + const char* last_port; + struct sockaddr_in sockaddr; + struct addrinfo addrinfo; + bool freed; +} th_fake_addrinfo_ops; + +static th_err +th_fake_getaddrinfo(void* self, const char* host, const char* port, const struct addrinfo* hints, struct addrinfo** res) +{ + (void)hints; + th_fake_addrinfo_ops* ops = self; + ops->last_host = host; + ops->last_port = port; + if (ops->resolve_err != TH_ERR_OK) + return ops->resolve_err; + + ops->sockaddr = (struct sockaddr_in){.sin_family = AF_INET}; + ops->addrinfo = (struct addrinfo){ + .ai_family = AF_INET, + .ai_socktype = SOCK_STREAM, + .ai_protocol = 0, + .ai_addrlen = sizeof(ops->sockaddr), + .ai_addr = (struct sockaddr*)&ops->sockaddr, + }; + *res = &ops->addrinfo; + return TH_ERR_OK; +} + +static void +th_fake_freeaddrinfo(void* self, struct addrinfo* res) +{ + (void)res; + th_fake_addrinfo_ops* ops = self; + ops->freed = true; +} + +static void +th_fake_addrinfo_ops_init(th_fake_addrinfo_ops* ops) +{ + ops->base.getaddrinfo = th_fake_getaddrinfo; + ops->base.freeaddrinfo = th_fake_freeaddrinfo; + ops->resolve_err = TH_ERR_OK; + ops->last_host = NULL; + ops->last_port = NULL; + ops->freed = false; +} + +TH_TEST_BEGIN(address) +{ + th_fake_addrinfo_ops ops; + th_fake_addrinfo_ops_init(&ops); + + TH_TEST_CASE_BEGIN(addrinfo_from_str_forwards_host_port_to_ops) + { + th_addrinfo info; + TH_EXPECT(th_addrinfo_from_str(&info, "example.com", "8080", &ops.base) == TH_ERR_OK); + TH_EXPECT(strcmp(ops.last_host, "example.com") == 0); + TH_EXPECT(strcmp(ops.last_port, "8080") == 0); + TH_EXPECT(info.family == AF_INET); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(addrinfo_from_str_frees_the_resolved_list) + { + th_addrinfo info; + TH_EXPECT(th_addrinfo_from_str(&info, "example.com", "8080", &ops.base) == TH_ERR_OK); + TH_EXPECT(ops.freed); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(addrinfo_from_str_propagates_ops_error) + { + ops.resolve_err = TH_ERR_SYSTEM(TH_EIO); + th_addrinfo info; + TH_EXPECT(th_addrinfo_from_str(&info, "example.com", "8080", &ops.base) == TH_ERR_SYSTEM(TH_EIO)); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(addrinfo_from_str_resolves_ipv4_literal) + { + th_addrinfo info; + TH_EXPECT(th_addrinfo_from_str(&info, "127.0.0.1", "8080", th_addrinfo_ops_os()) == TH_ERR_OK); + TH_EXPECT(info.family == AF_INET); + TH_EXPECT(info.addr.addrlen == sizeof(struct sockaddr_in)); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(addrinfo_from_str_resolves_ipv6_literal) + { + th_addrinfo info; + TH_EXPECT(th_addrinfo_from_str(&info, "::1", "8080", th_addrinfo_ops_os()) == TH_ERR_OK); + TH_EXPECT(info.family == AF_INET6); + TH_EXPECT(info.addr.addrlen == sizeof(struct sockaddr_in6)); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(addrinfo_from_str_rejects_unresolvable_host) + { + th_addrinfo info; + TH_EXPECT(th_addrinfo_from_str(&info, "this.host.does.not.resolve.invalid", "8080", th_addrinfo_ops_os()) != TH_ERR_OK); + } + TH_TEST_CASE_END +} +TH_TEST_END diff --git a/src/th_error.c b/src/th_error.c index bf817de..4c41fa9 100644 --- a/src/th_error.c +++ b/src/th_error.c @@ -1,6 +1,7 @@ #include #include +#include "th_address.h" #include "th_http_error.h" #include "th_ssl_error.h" #include "th_utility.h" @@ -36,6 +37,8 @@ th_strerror(th_err err) TH_ASSERT(0 && "SSL not enabled"); return NULL; #endif + case TH_ERR_CATEGORY_EAI: + return th_addrinfo_strerror(TH_ERR_CODE(err)); default: break; } diff --git a/src/th_listener.c b/src/th_listener.c index f957643..e3ebbe6 100644 --- a/src/th_listener.c +++ b/src/th_listener.c @@ -43,8 +43,13 @@ th_listener_init(th_listener* listener, th_loop* loop, listener->ssl_enabled = false; listener->allocator = allocator ? allocator : th_default_allocator_get(); th_err err = TH_ERR_OK; + + th_addrinfo info; + if ((err = th_addrinfo_from_str(&info, host, port, th_addrinfo_ops_os())) != TH_ERR_OK) + return err; + th_acceptor_init(&listener->acceptor, loop, th_acceptor_ops_os()); - if ((err = th_acceptor_open(&listener->acceptor, host, port)) != TH_ERR_OK) + if ((err = th_acceptor_open(&listener->acceptor, &info)) != TH_ERR_OK) return err; if (opt && opt->key_file && opt->cert_file) { if ((err = th_listener_enable_ssl(listener, opt->key_file, opt->cert_file)) != TH_ERR_OK) diff --git a/src/th_system_error.h b/src/th_system_error.h index 6ee53d6..d556aac 100644 --- a/src/th_system_error.h +++ b/src/th_system_error.h @@ -39,6 +39,7 @@ th_system_strerror(int errc) #define TH_ETIMEDOUT ETIMEDOUT #define TH_ECANCELED ECANCELED #define TH_EPROTO EPROTO +#define TH_EADDRINUSE EADDRINUSE #elif defined(TH_CONFIG_OS_WIN) #define TH_ENOENT ERROR_FILE_NOT_FOUND #define TH_EINTR ERROR_INTERRUPT @@ -52,6 +53,7 @@ th_system_strerror(int errc) #define TH_ETIMEDOUT ERROR_TIMEOUT #define TH_ECANCELED ERROR_CANCELLED #define TH_EPROTO ERROR_INVALID_DATA +#define TH_EADDRINUSE WSAEADDRINUSE #endif #endif From a89c3c5b1eb54bacadd146826eb8558f373f035e Mon Sep 17 00:00:00 2001 From: Raphael Schlarb Date: Wed, 12 Aug 2026 06:57:33 -0500 Subject: [PATCH 2/4] test: add fail_after allocation-failure injection to test allocator - th_test_allocator_fail_after(n) makes the Nth alloc/realloc call return NULL, self-resetting once triggered; also reset at the start of every test case so a leftover countdown can't leak into the next - add th_dir_mgr_add coverage looping fail_after over every step, verifying the eventual success is actually visible via a get --- src/th_dir_mgr_test.c | 19 +++++++++++++++++++ src/th_test.c | 38 +++++++++++++++++++++++++++++++++++--- src/th_test.h | 9 ++++++++- 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/th_dir_mgr_test.c b/src/th_dir_mgr_test.c index 96f97d6..5ac1f79 100644 --- a/src/th_dir_mgr_test.c +++ b/src/th_dir_mgr_test.c @@ -53,6 +53,25 @@ TH_TEST_BEGIN(dir_mgr) th_dir_mgr_deinit(&mgr); } TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(dir_mgr_add_survives_alloc_failure_at_every_step) + { + int n = 0; + th_err err; + do { + th_dir dir; + th_dir_init(&dir, &ops.base); + TH_EXPECT(th_dir_open(&dir, TH_STR("/")) == TH_ERR_OK); + + th_test_allocator_fail_after(n++); + err = th_dir_mgr_add(&mgr, TH_STR("justsomelongdirnametotriggeralloc"), dir); + TH_EXPECT(err == TH_ERR_OK || err == TH_ERR_BAD_ALLOC); + TH_EXPECT(n < 1000); + } while (err != TH_ERR_OK); + + TH_EXPECT(th_dir_mgr_get(&mgr, TH_STR("justsomelongdirnametotriggeralloc")) != NULL); + th_dir_mgr_deinit(&mgr); + } + TH_TEST_CASE_END TH_TEST_CASE_BEGIN(dir_mgr_add_duplicate_label) { th_dir dir1; diff --git a/src/th_test.c b/src/th_test.c index 3af33de..01abbc7 100644 --- a/src/th_test.c +++ b/src/th_test.c @@ -12,12 +12,25 @@ typedef struct th_alloc_list { typedef struct th_test_allocator { th_allocator base; th_alloc_list* list; + int fail_countdown; /* -1 = disabled; 0 = fail the next alloc/realloc */ } th_test_allocator; +/* -1 (disabled) never reaches 0, so it's left untouched. */ +static bool +th_test_allocator_should_fail(th_test_allocator* allocator) +{ + if (allocator->fail_countdown < 0) + return false; + if (allocator->fail_countdown-- == 0) { + allocator->fail_countdown = -1; + return true; + } + return false; +} + static void* -th_test_allocator_alloc(void* self, size_t size) +th_test_allocator_track(th_test_allocator* allocator, size_t size) { - th_test_allocator* allocator = self; void* ptr = calloc(1, size); if (!ptr) return NULL; @@ -32,10 +45,21 @@ th_test_allocator_alloc(void* self, size_t size) return ptr; } +static void* +th_test_allocator_alloc(void* self, size_t size) +{ + th_test_allocator* allocator = self; + if (th_test_allocator_should_fail(allocator)) + return NULL; + return th_test_allocator_track(allocator, size); +} + static void* th_test_allocator_realloc(void* self, void* ptr, size_t size) { th_test_allocator* allocator = self; + if (th_test_allocator_should_fail(allocator)) + return NULL; for (th_alloc_list* node = allocator->list; node != NULL; node = node->next) { if (node->ptr == ptr) { void* new_ptr = realloc(ptr, size); @@ -45,7 +69,7 @@ th_test_allocator_realloc(void* self, void* ptr, size_t size) return new_ptr; } } - return th_test_allocator_alloc(self, size); + return th_test_allocator_track(allocator, size); } static void @@ -77,6 +101,12 @@ int th_test_allocator_outstanding(void) return count; } +void th_test_allocator_fail_after(int n) +{ + th_test_allocator* allocator = (th_test_allocator*)th_default_allocator_get(); + allocator->fail_countdown = n; +} + void th_test_setup(void) { static th_test_allocator allocator = { @@ -86,6 +116,7 @@ void th_test_setup(void) .free = th_test_allocator_free, }, .list = NULL, + .fail_countdown = -1, }; th_default_allocator_set(&allocator.base); } @@ -100,5 +131,6 @@ void th_test_teardown(void) node = next; } allocator->list = NULL; + allocator->fail_countdown = -1; th_default_allocator_set(NULL); } diff --git a/src/th_test.h b/src/th_test.h index e344919..4907998 100644 --- a/src/th_test.h +++ b/src/th_test.h @@ -31,6 +31,12 @@ void th_test_teardown(void); */ int th_test_allocator_outstanding(void); +/** th_test_allocator_fail_after + * @brief Makes the Nth alloc/realloc call from now return NULL (0 = the + * very next call). Self-resets to never-fail once that call happens. + */ +void th_test_allocator_fail_after(int n); + /* Re-runs the whole function once per case, skipping all but the selected * one, so shared setup is fresh for every case. */ #define TH_TEST_BEGIN(name) \ @@ -40,7 +46,8 @@ int th_test_allocator_outstanding(void); (void)argv; \ for (size_t th_target = 0;; th_target++) { \ size_t th_index = 0; \ - bool th_ran = false; + bool th_ran = false; \ + th_test_allocator_fail_after(-1); /* Leak check runs once per iteration, not in TH_TEST_CASE_END, so shared * fixture state isn't flagged as a leak mid-case. */ From e8c0bf36f3e617766cdeac0cbe03735a0e9756d5 Mon Sep 17 00:00:00 2001 From: Raphael Schlarb Date: Wed, 12 Aug 2026 07:09:07 -0500 Subject: [PATCH 3/4] test: add th_conn_tracker unit tests Covers tracking on init/deinit, count across multiple conns, async_wait's fire-on-any-destroy-while-armed/once-only behavior, and cancel_all - using a fake th_conn_observable, no real socket needed. --- CMakeLists.txt | 1 + src/th_conn_tracker_test.c | 169 +++++++++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 src/th_conn_tracker_test.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 232fa1d..1b5d7a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -239,6 +239,7 @@ if (NOT TH_DISABLE_TESTS) src/th_acceptor_test.c src/th_accept_test.c src/th_conn_test.c + src/th_conn_tracker_test.c src/th_tcp_conn_test.c src/th_router_test.c src/th_allocator_test.c diff --git a/src/th_conn_tracker_test.c b/src/th_conn_tracker_test.c new file mode 100644 index 0000000..81d14fa --- /dev/null +++ b/src/th_conn_tracker_test.c @@ -0,0 +1,169 @@ +#include "th_conn_tracker.h" +#include "th_test.h" + +typedef struct th_fake_conn { + th_conn_observable base; + bool destroyed; + bool cancelled; +} th_fake_conn; + +static void +th_fake_conn_cancel(void* self) +{ + th_fake_conn* conn = self; + conn->cancelled = true; +} + +static void +th_fake_conn_free(void* self) +{ + th_fake_conn* conn = self; + conn->destroyed = true; +} + +static const th_conn_methods th_fake_conn_methods = { + .get_address = NULL, + .start = NULL, + .recv = NULL, + .send = NULL, + .cancel = th_fake_conn_cancel, + .destroy = th_conn_observable_destroy, +}; + +static void +th_fake_conn_init(th_fake_conn* conn, th_conn_observer* observer) +{ + conn->destroyed = false; + conn->cancelled = false; + th_conn_observable_init(&conn->base, &th_fake_conn_methods, th_fake_conn_free, observer); +} + +typedef struct th_recording_task { + th_task base; + int complete_count; +} th_recording_task; + +static void +th_recording_task_fn(void* self) +{ + th_recording_task* task = self; + ++task->complete_count; +} + +static void +th_recording_task_init(th_recording_task* task) +{ + th_task_init(&task->base, th_recording_task_fn); + task->complete_count = 0; +} + +TH_TEST_BEGIN(conn_tracker) +{ + th_conn_tracker tracker; + th_conn_tracker_init(&tracker); + + TH_TEST_CASE_BEGIN(conn_tracker_init_has_zero_count) + { + TH_EXPECT(th_conn_tracker_count(&tracker) == 0); + th_conn_tracker_deinit(&tracker); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(conn_tracker_tracks_conn_on_init) + { + th_fake_conn conn; + th_fake_conn_init(&conn, &tracker.base); + + TH_EXPECT(th_conn_tracker_count(&tracker) == 1); + + th_conn_destroy(&conn.base.base); + th_conn_tracker_deinit(&tracker); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(conn_tracker_untracks_conn_on_destroy) + { + th_fake_conn conn; + th_fake_conn_init(&conn, &tracker.base); + + th_conn_destroy(&conn.base.base); + + TH_EXPECT(th_conn_tracker_count(&tracker) == 0); + TH_EXPECT(conn.destroyed); + th_conn_tracker_deinit(&tracker); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(conn_tracker_tracks_multiple_conns) + { + th_fake_conn conn1, conn2; + th_fake_conn_init(&conn1, &tracker.base); + th_fake_conn_init(&conn2, &tracker.base); + + TH_EXPECT(th_conn_tracker_count(&tracker) == 2); + + th_conn_destroy(&conn1.base.base); + TH_EXPECT(th_conn_tracker_count(&tracker) == 1); + + th_conn_destroy(&conn2.base.base); + TH_EXPECT(th_conn_tracker_count(&tracker) == 0); + th_conn_tracker_deinit(&tracker); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(conn_tracker_async_wait_completes_task_on_any_destroy) + { + th_fake_conn conn1, conn2; + th_fake_conn_init(&conn1, &tracker.base); + th_fake_conn_init(&conn2, &tracker.base); + + th_recording_task task; + th_recording_task_init(&task); + th_conn_tracker_async_wait(&tracker, &task.base); + + th_conn_destroy(&conn1.base.base); + + TH_EXPECT(task.complete_count == 1); + TH_EXPECT(th_conn_tracker_count(&tracker) == 1); + + th_conn_destroy(&conn2.base.base); + th_conn_tracker_deinit(&tracker); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(conn_tracker_async_wait_only_completes_task_once) + { + th_fake_conn conn1, conn2; + th_fake_conn_init(&conn1, &tracker.base); + th_fake_conn_init(&conn2, &tracker.base); + + th_recording_task task; + th_recording_task_init(&task); + th_conn_tracker_async_wait(&tracker, &task.base); + + th_conn_destroy(&conn1.base.base); + th_conn_destroy(&conn2.base.base); + + TH_EXPECT(task.complete_count == 1); + th_conn_tracker_deinit(&tracker); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(conn_tracker_cancel_all_cancels_every_tracked_conn) + { + th_fake_conn conn1, conn2; + th_fake_conn_init(&conn1, &tracker.base); + th_fake_conn_init(&conn2, &tracker.base); + + th_conn_tracker_cancel_all(&tracker); + + TH_EXPECT(conn1.cancelled); + TH_EXPECT(conn2.cancelled); + + th_conn_destroy(&conn1.base.base); + th_conn_destroy(&conn2.base.base); + th_conn_tracker_deinit(&tracker); + } + TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(conn_tracker_cancel_all_on_empty_tracker_is_a_noop) + { + th_conn_tracker_cancel_all(&tracker); + th_conn_tracker_deinit(&tracker); + } + TH_TEST_CASE_END +} +TH_TEST_END From 9d47639831a69f18e84d33417cb60e983304fe6a Mon Sep 17 00:00:00 2001 From: Raphael Schlarb Date: Wed, 12 Aug 2026 07:34:31 -0500 Subject: [PATCH 4/4] test: add missing cookie parser edge cases Cover a cookie-name starting with a separator, tabs before the value, and a control character inside a quoted value. --- src/th_cookie_parser_test.c | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/th_cookie_parser_test.c b/src/th_cookie_parser_test.c index 8faac17..6976d7b 100644 --- a/src/th_cookie_parser_test.c +++ b/src/th_cookie_parser_test.c @@ -88,6 +88,17 @@ TH_TEST_BEGIN(cookie_parser) TH_EXPECT(th_cookie_parser_done(&parser)); } TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(tolerates_tabs_before_the_value) + { + th_cookie_parser parser; + th_cookie_parser_init(&parser, TH_STR("name=\t\tvalue")); + + th_str key, value; + TH_EXPECT(th_cookie_parser_next(&parser, &key, &value) == TH_ERR_OK); + TH_EXPECT(TH_STR_EQ(key, "name") && TH_STR_EQ(value, "value")); + TH_EXPECT(th_cookie_parser_done(&parser)); + } + TH_TEST_CASE_END TH_TEST_CASE_BEGIN(empty_cookie_value_is_valid) { th_cookie_parser parser; @@ -136,6 +147,16 @@ TH_TEST_BEGIN(cookie_parser) TH_EXPECT(th_cookie_parser_done(&parser)); } TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(quoted_cookie_value_containing_control_character_is_a_bad_request) + { + th_cookie_parser parser; + th_cookie_parser_init(&parser, TH_STR("name=\"\t\"")); + + th_str key, value; + TH_EXPECT(th_cookie_parser_next(&parser, &key, &value) == TH_ERR_HTTP(TH_CODE_BAD_REQUEST)); + TH_EXPECT(th_cookie_parser_done(&parser)); + } + TH_TEST_CASE_END TH_TEST_CASE_BEGIN(cookie_value_may_contain_full_octet_range) { // cookie-octet excludes only space, DQUOTE, comma, semicolon, backslash - @@ -211,6 +232,16 @@ TH_TEST_BEGIN(cookie_parser) TH_EXPECT(th_cookie_parser_done(&parser)); } TH_TEST_CASE_END + TH_TEST_CASE_BEGIN(cookie_name_starting_with_a_separator_is_a_bad_request) + { + th_cookie_parser parser; + th_cookie_parser_init(&parser, TH_STR("(name=1")); + + th_str key, value; + TH_EXPECT(th_cookie_parser_next(&parser, &key, &value) == TH_ERR_HTTP(TH_CODE_BAD_REQUEST)); + TH_EXPECT(th_cookie_parser_done(&parser)); + } + TH_TEST_CASE_END TH_TEST_CASE_BEGIN(cookie_name_containing_a_control_character_is_a_bad_request) { th_cookie_parser parser;