Conversation
- th_string (non-owning view type) is now th_str - th_heap_string (owning SSO string type) is now th_string - Update all call sites and CMakeLists.txt source lists accordingly
- TH_ENABLE_COVERAGE option instruments tiny_http_test with --coverage - New 'coverage' target runs ctest then gcovr, producing a terminal summary and an HTML report under build/coverage/ - Off by default, no effect on normal builds
…emaining/less - th_clock is a small vtable (monotonic_now); th_clock_os() is the real OS-backed implementation (POSIX clock_gettime / Windows GetTickCount64) - th_timer now takes a th_clock* at init instead of calling the OS clock directly, so tests can inject a fully controllable fake clock - Add th_timer_from_duration, th_timer_remaining, th_timer_less - Add src/th_timer_test.c (no test previously existed for this module)
- TH_TEST_BEGIN wraps the test body in a loop, re-executing it once per case so shared setup is reconstructed fresh each time instead of being manually reset - th_timer_test.c now sets up its fake clock once and mutates it per case - Fixes th_dir_mgr_test.c's last case being closed with TH_TEST_END instead of TH_TEST_CASE_END, which silently skipped its leak check
- th_reactor.h: reactor/handle vtables, decoupled from th_io_task - th_op.h: th_op (task + abort callback + read/write type + completion flags), the unit of work submitted to a th_handle. TH_OP_COMPLETED lets th_op_perform double as both "do I/O" and "finalize" depending on whether it's set, so a completion always gets posted to th_loop and finalizes on a later drain instead of running inline - bounds stack depth when I/O completes immediately, repeatedly. - th_loop.h/.c: task queue that owns a reactor and polls it when idle; th_loop_run polls with a zero timeout until no work is left - th_poll.h/.c: poll(2)-based reactor with an injected th_pollops syscall table, so tests can control fd readiness without real fds. th_handle_submit now runs the op immediately and only waits for readiness on TH_EAGAIN/TH_EWOULDBLOCK - Add th_loop_test.c and th_poll_test.c using fake reactors/pollops/clocks Not yet wired into th_context/th_server - this lands standalone, alongside the existing th_io_service/th_runner stack, ahead of the th_socket/th_acceptor port that will replace it. Also: - Make tiny_http_test's TH_CONFIG_OS_MOCK compile definition PUBLIC so th_test (which links against it) sees the same macro-dependent constants (e.g. th_system_error.h's TH_E*); previously th_poll_test.c and th_poll.c could disagree on what TH_EIO expands to - Fix th_tcp_socket_test.c comparing against raw EIO instead of TH_EIO, latent since it happened to match by coincidence before - Fix th_poll's reactor: check per-handle I/O timeouts even when poll() returns 0 (previously only checked when fds were also ready, so a silent connection could hang past its timeout)
- th_socket.h/.c: non-blocking TCP connection over a th_handle, with an injected th_socket_ops vtable (send/sendvec/recv/sendfile) so tests can fake a socket without a real fd. th_socket_ops_os() is the production impl; holds a th_loop* (not just a reactor) so ops can defer completion via th_socket_post instead of running it inline. - th_recv.h/.c, th_send.h/.c, th_sendvec.h/.c, th_sendfile.h/.c: th_op based retry state machines (retry on TH_EAGAIN/TH_EWOULDBLOCK until done), completing via a plain callback + user_data - no dependency on th_io_task/th_io_handler. Sendfile covers the two strategies live on this platform (buffered pread+sendmsg for small transfers, mmap for large ones); BSD/Linux sendfile() syscalls are deferred to the kqueue follow-up phase. - Real, populated tests for all of the above (fake ops/reactor/loop) - the refactored/ snapshot this design is based on left these empty. Renamed the current (still in use) th_socket.h/.c to th_socket_legacy, freeing up the th_socket name for the new port; th_tcp_socket, th_conn, th_response, th_http, th_listener, th_acceptor and th_ssl_socket now reference th_socket_legacy. Deleted wholesale in the cutover phase. Standalone, not yet wired into th_context/th_server.
- th_acceptor_ops (open/accept) injected at construction, mirroring th_socket_ops; production impl th_acceptor_ops_os() consolidates the getaddrinfo/socket/setsockopt/bind/listen and accept/fcntl syscalls - th_accept_op: th_op-based retry state machine completing via plain callback + user_data, same shape as th_recv/send/sendvec/sendfile - th_address: plain sockaddr_storage + addrlen struct, no string parsing (th_bind's addr/port two-string signature is unchanged, resolved via getaddrinfo in th_acceptor_ops_os_open) - rename current th_acceptor.h/.c to th_acceptor_legacy to free up the name, matching th_socket/th_socket_legacy from the previous phase; th_listener still uses th_acceptor_legacy until the conn/listener cutover phase - real th_acceptor_test.c/th_accept_test.c with fake ops/reactor/loop
- th_conn_methods gains a unified recv/send (send dispatches on an optional th_file* to pick sendvec vs sendfile); th_response/th_http call th_conn directly instead of reaching through to a socket type - th_tcp_conn.c written fresh over th_socket, taking an already-constructed th_socket by value so tests can inject fake th_socket_ops - th_context, th_io_service/task/composite/op*, th_runner, th_poll_service, th_kqueue_service, th_mock_service, th_socket_legacy, th_acceptor_legacy, th_tcp_socket removed; th_server/th_listener hold a th_loop/th_reactor directly - SSL disabled for now (th_ssl_socket.c/.h removed from the build, kept as a reference copy under refactored/th_ssl_socket_backup/); SSL support returns in a later phase ported onto th_conn/th_socket - restore TH_OP_IMMEDIATE (dropped in an earlier phase as apparently dead): th_handle_submit now only performs an op inline on its very first attempt, avoiding unbounded submit-recursion when an op keeps hitting EAGAIN with nothing to change that (e.g. accept() with no pending connection) - th_poll_reactor now tells th_loop about ops it holds pending for readiness (th_loop_increase/decrease_task_count), fixing th_loop_poll returning EOF immediately instead of ever polling - add th_conn_test.c/th_tcp_conn_test.c, rewrite th_response_test.c against a fake th_conn Verified end-to-end against a live echo server (curl GET/POST).
- th_dir_ops (open/close) is injected at construction, same DI pattern as th_socket_ops/th_acceptor_ops; th_dir_ops_os() for production - th_dir_mgr no longer owns/constructs th_dir itself: th_dir_mgr_add takes an already-open th_dir by value and moves it in, deiniting it on any failure path so callers never touch it again either way - th_fcache holds a non-owning th_dir_mgr*; th_server owns the th_dir_mgr and calls th_dir_mgr_add directly, dropping the passthrough th_fcache_add_dir - add th_dir_test.c; rework th_dir_mgr_test.c/th_fcache_test.c to use fake th_dir_ops instead of th_mock_syscall for directory-open
- th_fcache_get(cache, dir, path, out) takes an already-resolved th_dir* directly instead of a root label string; th_fcache no longer holds a th_dir_mgr* or does its own label lookups, and the th_fcache_find_dir passthrough is gone - callers that need a label -> th_dir* lookup (th_response.c, th_upload.c) now do it themselves against a th_dir_mgr*, threaded alongside th_fcache* through th_server -> th_listener -> th_http -> th_request/th_response/th_upload
- Path containment is now handled by openat() staying relative to an already-open directory fd plus O_NOFOLLOW, not by resolving and string-comparing against a realpath'd jail root - Fixes a latent bug where flags = O_NOFOLLOW was silently overwritten instead of OR'd in, leaving O_NOFOLLOW as the only real protection now in place - Removes th_file.c's last dependency on th_path.c
- th_file_ops (openat/read/write/mmap/munmap/stat_hash/close) is injected at construction, matching the th_dir_ops precedent; th_file_ops_os() provides the production syscalls - th_fcache holds the th_file_ops* it opens files with; th_upload_save reuses fcache's ops instead of threading a second param through - th_mock_syscall.c/.h are unreachable now (th_file.c was their last consumer) - deleted both, which left th_path.c with no caller either (th_dir_open's resolve-its-own-path step was the last one) - deleted th_path.c/.h and the now-pointless th_dir_get_path/stored path on th_dir - TH_CONFIG_OS_MOCK is not used anymore, so tiny_http_test now builds as a plain TH_CONFIG_OS_POSIX target; dropped the compile definition and the dead mock branches in th_config.h/th_system_error.h - th_dir is now just a thin fd-holding wrapper, so th_dir_test.c only re-verified the fake-ops harness itself; removed it (th_dir stays covered via th_dir_mgr_test.c and th_fcache_test.c)
- th_poll_handle_submit fell through and double-registered an op if its inline TH_OP_IMMEDIATE attempt resubmitted instead of completing - th_poll_reactor_run silently dropped a pollfd pushed by a synchronous resubmit mid-loop, since the final compaction only counted entries within the pre-poll snapshot - both left a resubmitted op with no live pollfd, so poll() would never wake it again; added a regression test in th_poll_test.c
- th_socket_ops_os_sendfile always uses the buffered pread+sendmsg path now; the mmap path added little for the extra complexity - th_file drops its mmap/munmap ops and sliding-window view machinery (th_file_get_view, th_fileview, th_file_mmap) entirely
- th_upload only ever needed fcache for its file_ops table (never used it as a cache), so it now holds a th_file_ops* directly instead - new th_upload_test.c covers init/getters, set_name/filename/ content_type reflected in th_upload_get_info, th_upload_save's success/unknown-dir/open-error/write-error paths via fake dir/file ops
th_request only needed fcache for its file_ops table (for uploads), never as an actual cache. Hold th_file_ops* directly instead.
Each case re-runs the whole test function, so the shared request/ parser setup only needs to happen once at the top.
Fake th_conn defers recv/send callbacks instead of invoking them inline, letting each test step the exchange one call at a time and change the pending request in between (needed for the keep-alive case). Covers routing to a handler, 404/400 error responses, exact status-line/header/body checks via memcmp, connection teardown after Connection: close, and a second request surviving on a kept-alive connection.
Splits a request across two recv calls, mid-header and mid-body, to check th_http_handle_read_request correctly waits for and reassembles data spread across multiple reads. Also registers a POST route so the partial-body case (a POST) actually reaches the handler.
Checks the Allow header for a specific route (only listing methods th_router_would_handle confirms) and for the * wildcard (listing all methods unconditionally).
th_fcache_erase was only reachable indirectly and untested: add cases for a stale cached fd (stat hash mismatch, forcing reopen) and LRU eviction once the cache is full (max_cached reached).
Covers multiple cookies in one Cookie header, trimming of extra whitespace around name/value, and the missing-'=' bad-request error.
- Move Cookie-header parsing out of th_request_parser into a standalone, incremental th_cookie_parser. - fix: quoted cookie-values now have their surrounding quotes stripped.
- th_test now links the tiny_http library directly instead of a duplicate tiny_http_test target that recompiled all core sources. - ASAN is now an opt-in TH_ENABLE_ASAN option (off by default) applied to tiny_http itself, rather than hardcoded into the removed target.
th_loop_init never initialized reactor_task, leaving its destroy function pointer as uninitialized memory. th_loop_deinit calls th_task_destroy on every queued task including reactor_task, so this could jump through garbage and crash.
Off by default, mirrors TH_ENABLE_ASAN.
Use an int accumulator for the two hex digits, narrowing to char only once when writing *out.
- move multipart/form-data parsing out of th_request_parser into a standalone, request-agnostic parser (mirrors th_cookie_parser) - add spec-driven tests covering RFC 7578/2046 boundary/part parsing, explicit Content-Length vs boundary-scan paths, and malformed input
- TH_ENABLE_BENCHMARKS builds th_bench, mirroring the th_test driver pattern but timing a whole N-iteration loop once and reporting the average (no per-iteration clock_gettime, no min/max) - add benches for th_multipart_parser and th_str_find_first(_of)
- th_str_find_first: use memchr instead of a scalar byte loop - th_str_find_first_of: hoist strlen(chars) out of the inner loop instead of re-checking chars[j] != '\0' on every haystack byte Cuts th_multipart_parser's ten-fields bench from ~1.7us to ~1.2us and its 4kb-boundary-scan bench from ~3.1us to ~0.3us.
BREAKING CHANGE: th_upload, th_upload_info, th_find_upload, th_upload_get_info, th_upload_get_data, th_upload_save, and th_upload_iter are removed. - multipart/form-data parts (both plain fields and file uploads) are now stored uniformly on th_request and exposed as th_part, with th_find_part/th_part_iter and accessors th_part_name/_filename/ _content_type/_content - formvars are unaffected - they still only come from application/x-www-form-urlencoded bodies - th_upload_save is replaced by the more general th_save_to_disk(req, data, dir_label, filepath), which writes any th_buffer to a registered directory
find_package(Python3) was picking the system interpreter even when a virtualenv with networkx installed was active, so amalgamation was disabled unnecessarily.
th_detail_small_string_resize computed new_len - self->len unconditionally, underflowing when shrinking and passing a huge size to memset. - test: cover th_string_resize (small grow/shrink, small->large promotion, large grow/shrink) and th_string_eq (small and large)
th_route_consume_trail advanced the raw path trail by the decoded segment's length instead of the raw (pre-decode) length, breaking matches for any route containing a URL-encoded segment that decodes shorter than its raw form. Also skip URL-decoding entirely for segments with no '%', avoiding an allocation on the common case. - test: add benchmark cases for th_router
Replace the ROUTER_TEST_CASE_BEGIN wrapper and the shared required/num_required/success statics with a local route_expectations struct passed through th_router_add_route's user_data, checked directly in the handler's return value.
- incomplete input at each parser state, byte-by-byte and chunked feeding - bad line endings, invalid/control chars in header names, non-printable header values - unknown method, header name too large - GET/HEAD requests with a body - malformed HTTP-version strings, incl. per-character corruption sweep fix: reject HTTP-version tokens with extra trailing digits (e.g. HTTP/1.10)
Push consecutive non-escaped bytes in one th_string_append call instead of one th_string_push_back per byte, cutting per-byte call and branch overhead for the common case of long unescaped stretches. - add th_url_decode_bench with representative decode shapes - revive th_url_decode_test against the current th_url_decode_string API (previously called a since-removed inplace function and wasn't wired into CMakeLists.txt, so it never built or ran) - drop the dead th_url_decode_inplace declaration
Enable 18 more -W flags on the tiny_http target (tests/bench excluded): format, null-deref, cast-align, write-strings, redundant-decls, duplicated-cond/branches, old-style-definition, nested-externs, bad-function-cast, undef, vla, double-promotion, float-equal, inline, array-bounds=2, stringop-overflow=4, jump-misses-init, switch-default. - th_strerror: missing default case let an unrecognized TH_ERR_CATEGORY_OTHER code fall through into the SYSTEM branch and call strerror() on an internal error code - th_http_error: same missing-default pattern on its outer switch - th_response_async_write: goto cleanup skipped iovcnt's initializer; moved the declaration above the jumps Skipped -Waggregate-return, -Wswitch-enum, -Wcast-qual, and -Wunused-macros - each either fights an established idiom in this codebase or is dominated by false positives.
- th_filepath validates/normalizes a path (rejects absolute, "..", leading/trailing slash, embedded NUL) before it reaches a syscall - th_file_ops reduced to plain syscall wrappers; seek/stat use POSIX types directly instead of OS-independent wrappers
It only ever needed the server's dir_mgr/fcache, not request/response state. Drops th_request's now-unused dir_mgr/file_ops fields.
Minor allocation savings not worth the header/pointer-arithmetic bookkeeping; th_server now just uses the plain allocator. Also drops th_pool_allocator (its only caller) and renames TH_DEFINE_OBJ_POOL_ALLOCATOR to TH_DEFINE_POOL_ALLOCATOR since it's now the only pool allocator left.
Covers find (populate then random lookups), random insert/delete churn, and pure delete (populate then random-order removal, all hits).
th_acceptor_accept and th_accept_op now take a th_socket* and set its fd on success, instead of handing back a raw int fd for the caller to thread through by hand. th_conn gains get_socket (mirroring get_address), so th_listener no longer needs a per-conn-type set_fd — th_tcp_conn_set_fd is removed entirely. This makes the accept path identical for any future th_conn implementation (e.g. SSL): accept into the conn's socket, then start().
- th_ssl_ops wraps every OpenSSL call (SSL_CTX/SSL/BIO), injectable for testing without a real cert, key, or socket - th_ssl_session drives handshake/read/write over memory BIOs, with no knowledge of th_conn/th_socket/the reactor - th_ssl_io_op is a single th_op state machine shuttling ciphertext to/from the real socket between session steps; th_ssl_recv_op/ th_ssl_send_op add exact-length retry and iov/file chunking on top - th_ssl_conn implements th_conn, running the handshake in start() before upgrading (or destroying itself on handshake failure) - th_listener branches th_ssl_conn_create vs th_tcp_conn_create; the accept path itself is identical for both - th_ssl_context now takes th_ssl_ops too, so SSL_CTX setup is mockable - OpenSSL error-stack logging moved into th_ssl_ops itself, next to the calls that can actually produce queue entries - added th_ssl_smem_bio_test.c; fixed th_smem_ensure_buf_size returning the requested size instead of the buffer's actual size on no-op calls Also fixes a pre-existing use-after-free: th_loop_poll ran a task's fn then read task->destroy afterward, which is a dangling read whenever fn frees the object embedding the task (e.g. a connection destroying itself from its own recv completion). Since every call site always passed NULL for destroy, removed the field entirely instead of just reordering the read.
- some -W flags (e.g. -Wduplicated-cond, -Warray-bounds=2) are GCC-only and error out as unknown on Apple Clang - check each candidate with check_c_compiler_flag and only add it if supported, instead of hardcoding the full list
msghdr.msg_iovlen is int on macOS/BSD (vs. size_t-ish on Linux), so assigning iovcnt/veclen directly trips -Wconversion.
th_response_set_body_va forwards a non-literal fmt into vsnprintf, which -Wformat-nonliteral flags on Clang. Annotate th_printf_body itself with a printf format attribute (via new TH_PRINTF_FMT macro, no-op on non-GCC/Clang) so the compiler treats the forward as checked.
-Wformat-nonliteral, -Wformat-pedantic lead to multiple warnings treated as errors on clang. -Wformat-nonliteral is resolved by marking our internal printf functions as safe, while -Wformat-pedantic is resolved by converting pointers to void*
Applications using this library with openssl enabled need to link to openssl anyway. So it makes sense make the linking public
- replaces TH_DISABLE_AMALGAMATION with TH_ENABLE_AMALGAMATION, off by default, so a plain build no longer needs Python3/networkx at all - missing Python3/modules is now a hard error when explicitly enabled, instead of silently downgrading to disabled
codecov-action@v4 dropped the gcov input; reuse the existing coverage cmake target (ctest + gcovr) and upload its XML report.
- gcov 14 needs --merge-mode-functions=separate for functions whose signature spans multiple lines (e.g. attribute on its own line) - _bench.c files were leaking into the coverage report - gperf-generated .c files, and the .gperf grammars they carry #line directives back to, are excluded too - neither is hand-written
- Add coverage badge - Minor rewording
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.