libcpp reuse + audit/normalize tooling + real-time platform layer#1
Merged
Conversation
…ket) + str/secure
…98::BufferedSocket Client's recv/send buffers delegate to BufferedSocket; every extracted line passes one sanitizer that strips stray CR and NUL bytes, so a parameter can never smuggle a forged IRC line to peers. Mixed CRLF/LF input now yields clean per-'\n' lines (no embedded newline in a line). CTCP \x01 payloads (DCC) pass untouched.
Nicks, channel names and invite entries now compare case-insensitively (matching the CASEMAPPING=ascii token advertised in 005): channels are keyed by their casemapped name (display case preserved in Channel::_name), findClientByNick/findMember/isInvited fold ASCII case, and 'NICK BOB' collides with 'bob'. Shared TCP test harness extracted to tests/TestHarness.hpp for reuse across protocol-level suites.
MODE +l parses via strtol with full-string + range checks (rejects '12abc', negatives, overflow; caps at MAX_USERLIMIT) and echoes the canonical number. MODE +k rejects malformed keys (empty, >MAX_KEYLEN, space/comma/control chars) with ERR_INVALIDKEY 525. TOPIC is truncated to the TOPICLEN=390 advertised in 005.
… password Per-client send buffer capped at MAX_SENDQ (64 KiB): overflow latches, excess lines are dropped and the peer is disconnected at the next sweep point (output drain / end of input loop / timeout pass) — never mid-broadcast. acceptClient rejects connection #1025 with a best-effort 'ERROR :Server full'. epoll_ctl MOD/DEL return values are now checked. Password verification uses libcpp::str::eq_consttime.
A bus peer streaming >8 KiB without a newline is dropped with 'ERR line too long' instead of growing memory without bound; line framing delegates to libcpp98::LineBuffer. The AUTH secret check now uses eq_consttime.
C++98 observer interface (include/ext/IServerExtension.hpp) with lifecycle, channel-event, interception (onCommand/onPrivmsg/ reservesNick), foreign-fd, and audit hooks. Server owns registered extensions (deleted in reverse order), fires every hook at its seam point, and exposes registerExternalFd for extensions that multiplex their own sockets into the single epoll. Purely additive: legacy Bot/PlatformBus/AuditLog wiring still in place, removed next.
Bot now plugs in via onPrivmsg + reservesNick; Server loses _bot, getBot() and every direct Bot reference in command handlers (the constructor still news it up pending the tier registration TUs). BotViaPrivmsg keeps working through the extension path; test_bot owns its own Bot instance.
PlatformBus registers its fds through the public registerExternalFd (the 'friend class PlatformBus' backdoor is gone), listens lazily via onServerStart, and receives events through ownsFd/onFdEvent. AuditLog hangs off the onAudit fan-out. Server loses _bus/_audit members and all bus-specific branches in the event loop; verified end-to-end (AUTH+PUB -> channel PRIVMSG, audit CSV rows written).
Log.cpp is now dependency-free (pure iostream prefixes) so the mandatory tier links no terminal-styling code; the TermWriter renderer moves to src/extras/FancyLogSink.cpp behind a Log::ILogSink seam (Log::setSink, installed by the full tier at startup).
src/tiers/tier_{mandatory,bonus,full}.cpp each define the single
registerExtensions(Server&): mandatory registers nothing, bonus adds
the Bot, full adds Bot + FancyLogSink + the FT_IRC_CONFIG-gated
AuditLog/PlatformBus (the old setupPlatformFeatures body, moved out of
Server). main() calls it between construction and run(); the startup
banner moves to run() so the tier's sink renders it. Server.cpp now
references no concrete extension. Tests link tier_full and the harness
fixture registers it.
Recursive make with TIER={mandatory,bonus,full}: per-tier obj dirs (no
stale-object mixing), a tier marker so switching tiers relinks while a
same-tier repeat stays a no-op, and per-tier source/libcpp groups —
'make mandatory' links neither Bot nor any platform extra nor
libcpp/term. audit.sh now builds all three tiers warning-free and
requires the bonus/mandatory rules.
…riter The epoll lifecycle and ADD/MOD/DEL ops move behind the checked Reactor wrapper; the single epoll_wait() call stays literally in src/Server.cpp (the subject's one poll-equivalent call site, annotated). AuditLog drops its hand-rolled escaping/stream plumbing for libcpp98::CsvWriter (header-once via isNewFile).
Relay-only base64 protocol (never decoded, never touches disk; every line < 512 B by construction): FILE SEND/ACCEPT/REJECT/DATA/END/ABORT with strict validation (casemapped target, filename sanitized against path escapes, strtoul size cap 50 MB, b64 charset + 400-char chunks, running size-overrun check), one active transfer per (sender,recipient), SENDQ/2 backpressure via FILE WAIT, 60 s idle abort via onTick and abort-on-disconnect. Registered by the bonus and full tiers via the extension seam (consumed in the onCommand hook — never shadows an RFC command). Adds Server::findClientByFd. 8 end-to-end tests incl. byte-identical reassembly of a binary payload.
End-to-end tests pin that \x01DCC SEND handshakes (incl. near-512-byte lines) relay byte-for-byte through PRIVMSG after the line sanitizer. DOCUMENTATION.md gains the HexChat DCC demo walkthrough and the server-mediated FILE protocol spec + scripted nc demo.
…ment The PostMan allocation counter was a plain int incremented from both the test thread and the live server thread (lost updates -> phantom 2-3 'leaked' allocations), and assertNoLeaks() constructed its own std::string label argument before comparing, counting itself. g_allocations is now std::atomic<int>, assertNoLeaks takes const char* and captures the delta before anything in the call can allocate, and the churn test polls for convergence (with a warm-up cycle) instead of racing a fixed sleep. Suite is now 125/125, stable across runs.
test_extensions: probe extension pins every hook (onServerStart/Tick/ Registered/Disconnect/Join/Part/onAudit fan-out), onCommand consuming only would-be-421 commands, onPrivmsg interception without 401, reservesNick -> 433, and Server deleting owned extensions. Harness gains an onServerReady() override point. test_libcpp98: LineBuffer framing + flood guard, BufferedSocket CRLF/consume + sticky SENDQ overflow, CsvWriter RFC4180 escaping + header-once across reopens, Reactor ctl ops + a real epoll_wait event through the wrapped fd. Suite: 138/138.
A client that QUITs immediately after registering lost the whole queued 001-005 burst — disconnectClient closed the fd before EPOLLOUT could drain it. One non-blocking send() now flushes whatever the kernel accepts before close (SIGPIPE already ignored). Verified the same scripted nc transcript is byte-identical across all three tier binaries (timestamps normalized).
README documents the three build tiers + the evaluation note (default make includes runtime-gated extras; make mandatory is the strict build). DOCUMENTATION.md gains the tier table and the IServerExtension hook reference. CLAUDE.md rewritten for the new architecture (tiers, seam, libcpp c98 toolkit, casemapping/limits conventions, test harness).
…JOIN A joining client now receives 324/329 right after the NAMES list, mirroring the MODE-query handler, so it sees the channel's +itkl state without having to ask. JoinChannel integration test asserts both.
…rmat
The committed .clang-format claimed to 'match the existing code' but
clang-format 18 cannot reproduce two deliberate house conventions: the
space after a top-level '#' ("# define"/"# include") and the manual
column alignment of one-liner declarations / wrapped continuations. So
normalize.sh --check failed on all 30 files since the config landed.
Fix the config to the closest clang-format CAN do (UseTab:Always,
alignment off) as a baseline for new code, and rework normalize.sh:
the enforced gate is now safe whitespace only (no trailing space, final
newline — the whole tree passes); clang-format diffs are advisory and
never fail the check or rewrite hand-aligned code. clang-format -i is
opt-in via --clang-format. --check now exits 0.
Build stage compiles the full tier from the checked-out submodules; a slim runtime image ships just the binary as a non-root user on 6667. `--target test` runs the 138-assertion suite at build time. Verified: the container serves IRC end-to-end (registration, JOIN, RPL 324).
Multi-stage cargo build to a slim runtime; pure-Rust TLS so no system OpenSSL. Config is entirely via environment variables.
One `docker compose up --build` runs the server and wires the Claude companion to it over the compose network (IRC_HOST=ircserv). Secrets live in a gitignored .env; .env.example documents the variables. Verified end-to-end: the companion connects, joins, and relays a reply into the channel.
One-command stack (docker compose up), server-only run, and the --target test container. Notes the companion is a separate process outside the 42 build.
Pin the dependency graph for reproducible image builds (binaries should commit their lockfile). The Dockerfile now copies Cargo.lock and builds with --locked, so a stale lock fails the build instead of silently resolving new versions.
Two jobs on push-to-main / PR updates:
audit — scripts/audit.sh (builds all 3 tiers + subject checks) and
normalize.sh --check (whitespace gate).
docker — docker build --target test (138-assertion suite in a clean
container), the server runtime image, the ai-assistant image
(cargo --locked), and a compose config validation.
Public submodules are fetched over HTTPS (their .gitmodules URLs are
SSH); only vendor/libcpp + vendor/googletest are initialised. Every
step was reproduced locally before committing.
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.
Builds on the working C++98 IRC server in two arcs. Everything new is config-gated: the graded
./ircserv <port> <password>invocation is byte-for-byte unchanged (audit script passes, tests at baseline, no relink,-std=c++98 -Wall -Wextra -Werrorclean).Refactor + tooling
str/termmodules (already C++98-clean) compiled from source — no external library is linked. Added as a submodule.to_upper/to_string/ostringstreamwithlibcpp::str.Logfacade overlibcpp::term::TermWriterfor markdown-style server-console output (never client-facing protocol).scripts/{audit,normalize,memcheck}.sh+.clang-format;.gitignore; Makefile header-dependency tracking.Real-time platform layer (original feature)
epoll(one poll, more fds — subject-legal). Lets a local backendPUB #chan <type> :<msg>events that get injected into a channel as aplatformparticipant.FT_IRC_CONFIGpoints to a config file.companions/ai-assistant/: a Rust IRC bot that answers via Claude when addressed (separate from the C++98 binary).Verification
make reclean (zero warnings),make testat the pre-existing baseline (1 unrelated leak-check),bash scripts/audit.shgreen, no relink.nc.