Skip to content

Update Readme - #41

Closed
RaphiaRa wants to merge 59 commits into
mainfrom
refactor
Closed

Update Readme#41
RaphiaRa wants to merge 59 commits into
mainfrom
refactor

Conversation

@RaphiaRa

Copy link
Copy Markdown
Owner

No description provided.

RaphiaRa added 30 commits July 30, 2026 18:13
- 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.
RaphiaRa added 27 commits August 4, 2026 04:42
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
@RaphiaRa RaphiaRa closed this Aug 10, 2026
@RaphiaRa
RaphiaRa deleted the refactor branch August 10, 2026 00:12
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 83.84401% with 638 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/th_ssl_ops.c 0.00% 66 Missing ⚠️
src/th_socket.c 25.37% 50 Missing ⚠️
src/th_server.c 0.00% 40 Missing ⚠️
src/th_file.c 47.94% 36 Missing and 2 partials ⚠️
src/th_request.c 53.65% 32 Missing and 6 partials ⚠️
src/th_poll.c 78.73% 18 Missing and 19 partials ⚠️
src/th_multipart_parser.c 81.38% 13 Missing and 22 partials ⚠️
src/th_response.c 40.67% 18 Missing and 17 partials ⚠️
src/th_ssl_session.c 67.39% 22 Missing and 8 partials ⚠️
src/th_listener.c 0.00% 24 Missing ⚠️
... and 40 more

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants