Skip to content

libcpp reuse + audit/normalize tooling + real-time platform layer#1

Merged
LESdylan merged 46 commits into
mainfrom
refactor/libcpp-c98-tier
Jun 9, 2026
Merged

libcpp reuse + audit/normalize tooling + real-time platform layer#1
LESdylan merged 46 commits into
mainfrom
refactor/libcpp-c98-tier

Conversation

@LESdylan

@LESdylan LESdylan commented Jun 9, 2026

Copy link
Copy Markdown
Member

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 -Werror clean).

Refactor + tooling

  • Reuse the project's own libcpp str/term modules (already C++98-clean) compiled from source — no external library is linked. Added as a submodule.
  • Replaced hand-rolled to_upper/to_string/ostringstream with libcpp::str.
  • Log facade over libcpp::term::TermWriter for 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)

  • PlatformBus: a loopback socket multiplexed in the same epoll (one poll, more fds — subject-legal). Lets a local backend PUB #chan <type> :<msg> events that get injected into a channel as a platform participant.
  • Presence + audit: timestamped CSV trail (register/join/publish/part/disconnect).
  • Both activate only when FT_IRC_CONFIG points 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 re clean (zero warnings), make test at the pre-existing baseline (1 unrelated leak-check), bash scripts/audit.sh green, no relink.
  • Platform bus + audit demoed end-to-end; raw IRC protocol confirmed unchanged via nc.

LESdylan added 30 commits June 9, 2026 14:37
…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.
LESdylan added 16 commits June 9, 2026 23:04
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.
@LESdylan
LESdylan merged commit ab93142 into main Jun 9, 2026
2 checks passed
@LESdylan
LESdylan deleted the refactor/libcpp-c98-tier branch June 9, 2026 23:12
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.

1 participant