Proof of concept mechanical port of ircnet/ircd to Rust as part of a bit about C being insecure for network services
0

Configure Feed

Select the types of activity you want to include in your feed.

docs(PLAN): mark P4 (Parser & dispatch) DONE; add P4 spec/plan + P5 cluster map

+288 -1
+8 -1
PLAN.md
··· 81 81 | **P1 — Leaf / pure utils ✅ DONE** | `common/match.c` (+`tolowertab/touppertab/char_atribs`), `dbuf.c`, **pure** subset of `support.c` (`mystrdup/strtoken/myctime/mybasename/inetntop/inetpton/irc_memcmp`). ~~`irc_sprintf.c`~~ → **deferred to P8** (variadic `va_arg` engine; stable Rust can't consume `va_list`, and it has no post-format core to hand a string to Rust → stays C, no trampoline). `snprintf_append`, `dgets`, `make_isupport`, `ipv6string` also stay C for now. | Faithful; exact `char*` signatures; tables `#[no_mangle] static [u8;256]` (byte-equality asserted); `dbuf` owns `poolsize/freelist`. **`support.c` partial port** via inert `#ifndef PORT_SUPPORT_P1` guards + a `support_link.o` second-compile substituted into the link set; `MyMalloc/MyRealloc/MyFree` + `outofmemory()` **defer to P2**. | **MET:** L1 zero-diff over all corpora (match incl. tables + `MAX_ITERATIONS` cap; dbuf incl. `getmsg` framing + over-poolsize rollback; support incl. 8-case inet round-trip). Rust-driven `ircd` binary links; layout net green; 0 warnings. (L3 fuzz / L2/L4 not built yet — deferred from P0.) | 82 82 | **P2 — Data & lookup ✅ DONE** | `patricia.c`, `class.c`, `whowas.c` (ring; `m_whowas` stays C), `hash.c` (6 tables; `m_hash` stays C), `list.c` (allocators, `make_client`, `svrtop/numclients`) + `MyMalloc`/`MyRealloc` + `outofmemory` | Faithful; preserved intrusive pointers + refcounts (raw‑pointer invariant); replicated `CLIENT_REMOTE_SIZE` branch + hash **iteration order** exactly. **Verified findings:** `ENABLE_CIDR_LIMITS` ON (patricia live, `add_class` carries `cidrlen_s`); `USE_HOSTHASH`+`USE_IPHASH` ON (6 tables); **`MyFree` is a macro** (trio = `MyMalloc`+`MyRealloc`+`outofmemory`). `m_whowas`/`m_hash` kept in C via `whowas_link.o`/`hash_link.o` partial‑ports (`-DPORT_*_P2`). | **MET:** L1 zero‑diff over all corpora (patricia tree walk; class list + CIDR; whowas ring/index + uwas; hash client/channel/uid bucket chains + counters + stored hashv; `make_client` local/remote self‑pointers). Rust‑driven `ircd` binary links with `list.o` dropped; layout net green; 0 warnings. **whowas/class notification (`m_whowas`/`report_classes`) `sendto_*` path deferred to post‑P3 (L2).** | 83 83 | **P3 — Transport & format ✅ DONE** | `s_err.c` (`replies[]`), `s_id.c` (UID/SID/CID), `s_numeric.c` (`do_numeric`), `send.c` **delivery core** (`send_message`/`send_queued`/`flush_*`). **Finding:** the whole `sendto_*`/`esendto_*` family is irreducibly C trampolines (variadic; `vsendpreprep` consumes `va_arg` per‑recipient) → no non‑variadic cores extractable; senders + `sendto_flog`/`logfiles`/`setup_svchans` stay C (move to P7/P8). | Faithful; `replies[]` as Rust static (dumped from the oracle so config‑gated `#if` entries resolve); exact UID/SID base‑N; `send.c` partial‑ported via `send_link.o` (`-DPORT_SEND_P3`, `dead_link` exposed). ZIP/USE_SERVICES/LOG_SERVER_CHANNELS/etc. all OFF. | **MET (L1):** zero‑diff vs `cref_` on `replies[]` (all 1000), `ltoid`/`idtol`/`sid_valid`/`cid_ok`/`check_uid`, `do_numeric` guards, `send_message` sendQ buffering + `send_queued` early returns. **L2 (post‑P3):** `do_numeric`/sender wire broadcasts + `deliver_it` write path. | 84 - | **P4 — Parser & dispatch** | `common/parse.c` (tokenizer, `msgtab[]`, `find_*` wrappers) | Faithful; `msgtab` as `static mut` (mutable counters); handlers stay C via `extern`; exact tokenize semantics (512 trunc, leading/trailing colon, empty trailing param, `MPAR` cap). **Deliverable:** per‑handler‑file **static‑symbol cluster analysis** (prereq for P5). | L3 parser fuzz: zero (cmd/parv/dispatch/post‑state) divergence; L2/L4 identical. | 84 + | **P4 — Parser & dispatch ✅ DONE** | `common/parse.c` (tokenizer, `msgtab[]`, all 11 `find_*` wrappers, `getfield`, `m_nop`/`m_nopriv`/`m_unreg`/`m_reg`, `find_sender`/`cancel_clients`/`remove_unknown`) | Faithful; `msgtab` as `#[no_mangle] static mut [Message; 66]` (config‑resolved: TKLINE on, SERVSET/KLINE/SIDTRACE off, MOTD→m_unreg; mutable counters); the ~60 real `m_*` handlers stay C and are referenced as fn pointers; **`parse.o` dropped outright** (every symbol ported — no `parse_link.o`). Rust **calls** the C variadic senders (`sendto_one`/`sendto_flag`/`sendto_serv_butone`) via bindgen decls (the s_numeric.rs pattern). Exact tokenize semantics (512 trunc, leading/trailing colon, empty trailing param, `MPAR` cap). **Deliverable:** per‑handler‑file static‑symbol cluster map (`specs/2026-06-02-p5-handler-cluster-map.md`). | **MET (L1):** zero‑diff vs `cref_` on the `msgtab[]` structure (cmd/min/max per row + row count), `getfield` (delimiter/escape/trailing/empty), `find_mask`/`find_name` fallbacks, and `parse()`'s no‑server paths (IsDead, empty, unknown‑cmd, numeric, ENCAP→m_nop dispatch — return code + `cptr.flags` + `ircstp` deltas). **Full `parse()` post‑state + hash‑populated `find_*` = L3/L2** (need the server fixture). | 85 85 | **P5 — Command handlers (bulk)** | `channel.c`, `s_user.c`+`s_auth.c`, `s_serv.c`+`s_service.c`+`s_zip.c` (zlib→flate2, **bit‑exact**), `s_misc.c` (`exit_client`), `s_debug.c` | Faithful, but **cluster‑based, not per‑function**: `channel.c` has 37 file‑statics (`set_mode`, `can_join`, `get_channel`, …); migrate each **connected component over shared statics** as a unit via the `msgtab` fn‑pointer seam. | Per‑cluster L2 golden + L4 conformance byte‑identical incl. WHO/WHOIS/WHOX, full server‑burst/netsplit; zlib bytes match. | 86 86 | **P6 — Config & DNS** | `s_conf.c`+`config_read.c` (rehash; `conf/kconf/networkname`), `chkconf.c`, `res.c`+`res_comp.c`+`res_init.c`+`res_mkquery.c` | Faithful config grammar + rehash. Resolver: faithful port **or** hickory‑dns behind `res_ext.h`, but golden runs use a **fixture nameserver** for determinism. | Config‑parse + chkconf L1/L3 zero‑diff on fixtures; resolver encode/decode identical; L2/L4 identical with fixture DNS. | 87 87 | **P7 — I/O core + event loop + glue (last)** | `s_bsd.c` (select/poll loop, `local[]`, `highest_fd`, `timeofday`, `FdAry`), `fileio.c`, `packet.c`, `bsd.c`, `ircd.c` (main, io_loop, signals, rehash/restart, tune file) | Faithful: keep **single‑threaded select/poll via mio** (not tokio); preserve per‑client state machine; globals become Rust‑owned statics; signals via signal‑hook/nix + atomics. Removes `c_ircd_main`. | All C _logic_ gone (only variadic trampolines remain); full L2+L3+L4 + soak byte‑identical under connect storms, partial reads, sendQ backpressure, netsplit, rehash, restart. | ··· 110 110 - **Faithfulness notes (caught in review):** `replies[]` is **not** mechanically text‑extractable — it contains `#if/#else/#endif` config branches (WHOIS_SIGNON_TIME/USERS_RFC1459/ENABLE_SUMMON/TXT_NOSTATSK), two bare‑`NULL` entries, and an escaped `\r\n` (entry 384) that Rust source‑level CRLF normalization would silently corrupt → it is **dumped from the compiled `cref_replies` oracle** with byte‑level escaping. `alphabet_id[256]` in s_id.c is partially initialized (249 explicit values) so indices 249..=255 default to **0** (reproduced verbatim). `init_sid` is `#if 0` (not ported). `poolsize *= 1.1` reproduced as `(poolsize as f64 * 1.1) as u_int`. 111 111 - **L1 coverage / L2 deferrals:** L1 zero‑diff vs `cref_` on the `replies[]` table (all 1000 slots), `ltoid`/`idtol` round‑trip + `sid_valid`/`cid_ok`/`check_uid`, `do_numeric`'s early‑out guards, and `send_message` sendQ buffering (bytes + `sendM` + `lastsq`) + `send_queued`'s dead/empty early returns. **Deferred to L2** (need full server state — initialized hash tables, registered `me`, `cref_`'s separate globals, a live fd): the `do_numeric` broadcast wire output and the `send_queued` `deliver_it` write path. 112 112 - **Verification:** `cargo build` warning‑free; Rust‑driven `ircd` links (`send_link.o`'s variadic senders call the Rust `send_message`; the Rust core calls C `dead_link`/`deliver_it`); all `*_diff` L1 tests zero‑diff; layout net green. 113 + - **2026‑06‑02 — P4 (Parser & dispatch) merged.** All of `common/parse.c` ported into `ircd-common/src/parse.rs` and **`parse.o` dropped outright** (added to `PORTED`, like `list.o`/`match.o`) — no `parse_link.o`, because **every** symbol parse.c defined is portable now: the `msgtab[]` table, all 11 `find_*` wrappers, `parse`, `getfield`, the four stub handlers (`m_nop`/`m_nopriv`/`m_unreg`/`m_reg`), and the three file‑statics (`find_sender`/`cancel_clients`/`remove_unknown`). Spec: `docs/superpowers/specs/2026-06-02-p4-parse-dispatch-design.md`; plan: `docs/superpowers/plans/2026-06-02-p4-parse-dispatch.md`. 114 + - **Refines the hazard table (variadic dispatch):** the ~14 `sendto_*` senders parse.c **calls** stay C, but Rust **calls** them through bindgen variadic declarations (`sendto_one(to, c"…".as_ptr(), …)`) — the exact s_numeric.rs pattern. *Calling* a C variadic is stable Rust; only *defining* one is not. So no trampoline is needed on the call side; the senders themselves still vanish in P8. 115 + - **`msgtab[]` as a Rust `#[no_mangle] static mut [Message; 66]`:** built with a `row!`/`m!` macro; handler fn‑pointers reference the still‑C `m_*` (and the four Rust stubs) and coerce in the `static` initializer; the mutable `count`/`rcount`/`bytes`/`rbytes` counters mutate in place exactly as C (read by STATS in still‑C `s_serv.o`). **Config‑resolved** for this build: `TKLINE` **on** (TKLINE+UNTKLINE rows), `USE_SERVICES`/`KLINE`/`ENABLE_SIDTRACE` **off** (no SERVSET/KLINE/SIDTRACE rows), `MOTD_UNREG` **off** (MOTD unreg slot = `m_unreg`), `IDLE_FROM_MSG` **on** (idle guard = `fhandler == m_private`), `DEBUGMODE` **off** (every `Debug(())` omitted). The msgtab L1 test compares cmd/min/max/row‑count, **not** handler pointers (Rust rows point at `m_join`, the oracle at `cref_m_join`). 116 + - **Bug caught by L1:** `IRCDCONF_DELIMITER` is `'|'` (config.h:500), **not** `':'` — the first `getfield` draft used `':'` and the differential failed immediately. (find_mask/find_name L1 also surfaced that the hash tables are null until `inithashtables()`; the test now inits both sets.) 117 + - **L1 coverage / L2‑L3 deferrals:** L1 zero‑diff vs `cref_` on the `msgtab[]` structure, `getfield` (delimiter/escape/trailing/empty), `find_mask`/`find_name` empty‑hash fallbacks, and `parse()`'s no‑`me`/no‑hash/no‑sender paths (IsDead early‑out, empty message, unknown‑command‑from‑non‑person, 3‑digit numeric, and an `ENCAP→m_nop` dispatch — asserting return code + `cptr.flags` + `ircstp` counter deltas). **Deferred to L3 (parser fuzz) / L2:** the full `parse()` post‑state differential (dispatch into the still‑C `m_*`, mutated `aClient` + handler counters) and hash‑populated `find_*` — both need the populated‑server fixture. 118 + - **Deliverable:** the per‑handler‑file static‑symbol **cluster map** for P5 (`docs/superpowers/specs/2026-06-02-p5-handler-cluster-map.md`): clusters + entry points + suggested porting order (s_zip/s_debug → s_auth/s_service → s_user → s_misc → channel.c → s_serv.c). 119 + - **Verification:** `cargo build` warning‑free; the Rust‑driven `ircd` binary links with `parse.o` dropped (`parse`/`msgtab`/`find_*`/`getfield`/`m_nop` resolve to Rust); full `cargo test` green (all prior `*_diff` + `parse_diff` 4/4 + layout); 0 warnings. 113 120 114 121 --- 115 122
+70
docs/superpowers/plans/2026-06-02-p4-parse-dispatch.md
··· 1 + # P4 — Parser & dispatch Implementation Plan 2 + 3 + > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. 4 + 5 + **Goal:** Port all of `common/parse.c` (tokenizer, `msgtab[]`, `find_*` wrappers, `getfield`, stub handlers) to `ircd-common/src/parse.rs`, dropping `parse.o` from the C link set, with an L1 differential test green vs the `cref_*` oracle. 6 + 7 + **Architecture:** Faithful mechanical translation following P1–P3 convention: `#[no_mangle] pub extern "C"` with exact `parse_ext.h` signatures; `msgtab` as a `static mut` whose handler pointers reference still-C `m_*`; Rust calls the C variadic senders (`sendto_one`/`sendto_flag`/`sendto_serv_butone`) via bindgen declarations (the `s_numeric.rs` pattern). `parse.o` is added to `PORTED` (clean drop, no `parse_link.o`). 8 + 9 + **Tech Stack:** Rust (ircd-common crate), bindgen ABI (ircd-sys), C oracle archive (ircd-testkit L1). 10 + 11 + --- 12 + 13 + ### Task 1: Module skeleton + build drop 14 + 15 + **Files:** 16 + - Create: `ircd-common/src/parse.rs` 17 + - Modify: `ircd-common/src/lib.rs` (add `pub mod parse;` + anchor) 18 + - Modify: `ircd-sys/build.rs` (add `"parse.o"` to `PORTED`) 19 + 20 + - [ ] **Step 1:** Create `parse.rs` with the module doc-comment, `use ircd_sys::bindings::{…}`, the macro-crib `#[inline] unsafe fn is_*` helpers (copy the proven set from `s_numeric.rs`/`send.rs`, add `is_registered`/`is_registered_user`/`is_dead`/`my_connect`/`bad_to`), extern decls for the C variadics + libc (`sendto_one`/`sendto_flag`/`sendto_serv_butone`/`sprintf`/`strcpy`/`strlen`/`strchr`/`isdigit`), and `#[no_mangle] pub static PARSE_LINK_ANCHOR: u8 = 0;`. 21 + - [ ] **Step 2:** In `lib.rs` add `pub mod parse;` and `std::hint::black_box(&parse::PARSE_LINK_ANCHOR as *const u8 as usize);` inside `link_anchor()`. 22 + - [ ] **Step 3:** In `build.rs` add `"parse.o"` to the `PORTED` slice. 23 + - [ ] **Step 4:** `cargo build -p ircd-common` — Expected: compiles (empty module ok). Do NOT build the binary yet (symbols missing → link error until Task 2–4 land). 24 + - [ ] **Step 5:** Commit: `feat(ircd-common): scaffold parse.rs module + drop parse.o from link set`. 25 + 26 + ### Task 2: `msgtab[]` static + stub handlers + `getfield` 27 + 28 + **Files:** Modify `ircd-common/src/parse.rs`; Test `ircd-testkit/tests/parse_diff.rs`. 29 + 30 + - [ ] **Step 1 (test first):** Create `parse_diff.rs` with a `msgtab_structure_matches_cref` test: extern `msgtab`/`cref_msgtab` as `[Message; 256]`, walk both to the NULL-`cmd` terminator, assert equal row count and per-row `strcmp(cmd)==0` + equal `minparams`/`maxparams`; assert every counter field == 0. Plus `getfield_matches_cref` over a delimiter/escape/trailing/empty corpus. 31 + - [ ] **Step 2:** Run `cargo test -p ircd-testkit --test parse_diff` — Expected: FAIL to link (`msgtab`/`getfield` undefined). 32 + - [ ] **Step 3:** Implement in `parse.rs`: the `MSGTAB` static mut (config-resolved: TKLINE on; SERVSET/KLINE/SIDTRACE off; MOTD→m_unreg), `#[no_mangle] pub static mut msgtab` aliasing it; `m_nop`/`m_nopriv`/`m_unreg`/`m_reg`; `getfield`. extern-declare every `m_*` handler the table names. 33 + - [ ] **Step 4:** `cargo test -p ircd-testkit --test parse_diff` — Expected: PASS. 34 + - [ ] **Step 5:** Commit: `feat(ircd-common): port parse.c msgtab[] + getfield + stub handlers`. 35 + 36 + ### Task 3: `find_*` wrappers 37 + 38 + **Files:** Modify `ircd-common/src/parse.rs`, `ircd-testkit/tests/parse_diff.rs`. 39 + 40 + - [ ] **Step 1 (test):** Add `find_mask_name_match_cref` — drive `find_mask`/`find_name` (no hash state) on `*`-wildcard + plain inputs vs `cref_*`; assert identical result pointer class (NULL / passed-cptr). 41 + - [ ] **Step 2:** Run — Expected: FAIL (undefined `find_mask`/`find_name`). 42 + - [ ] **Step 3:** Implement all 11 `find_*` faithfully (`find_client/uid/sid/service/server/mask/name/person/matching_client/userhost/target`), the `para` module static, and `find_sender` (private). 43 + - [ ] **Step 4:** Run — Expected: PASS. 44 + - [ ] **Step 5:** Commit: `feat(ircd-common): port parse.c find_* lookup wrappers`. 45 + 46 + ### Task 4: `parse()` + `cancel_clients`/`remove_unknown` + final drop 47 + 48 + **Files:** Modify `ircd-common/src/parse.rs`, `ircd-testkit/tests/parse_diff.rs`. 49 + 50 + - [ ] **Step 1 (test):** Add `parse_tokenizer_matches_cref` over the no-dispatch corpus (empty msg, numeric-with-null-target, unknown-command-from-non-person, MPAR cap, trailing colon) asserting identical return code + `para[]` split where observable without a live server. 51 + - [ ] **Step 2:** Run — Expected: FAIL (undefined `parse`). 52 + - [ ] **Step 3:** Implement `parse`, `cancel_clients`, `remove_unknown`, the `sender` static. Inline-translate the macros; omit `Debug()` (DEBUGMODE off); use the `IDLE_FROM_MSG` (== m_private) idle branch. 53 + - [ ] **Step 4:** `cargo test -p ircd-testkit --test parse_diff` then `cargo build` (full workspace, ircd binary must link with `parse.o` dropped) — Expected: both PASS, 0 warnings. 54 + - [ ] **Step 5:** Commit: `feat(ircd-common): port parse.c parse()/find_sender; drop parse.o`. 55 + 56 + ### Task 5: P5 cluster-analysis deliverable + PLAN update 57 + 58 + **Files:** Create `docs/superpowers/specs/2026-06-02-p5-handler-cluster-map.md`; Modify `PLAN.md`. 59 + 60 + - [ ] **Step 1:** Write the handler-file static-symbol cluster map (from the parallel Explore agent's report). 61 + - [ ] **Step 2:** Mark P4 ✅ DONE in `PLAN.md`'s phase table + add a Progress-log entry. 62 + - [ ] **Step 3:** Commit: `docs(PLAN): mark P4 (Parser & dispatch) DONE; add P5 cluster map`. 63 + 64 + --- 65 + 66 + ## Self-Review notes 67 + 68 + - **Spec coverage:** msgtab (T2), all find_* (T3), parse/getfield/handlers (T2–T4), build drop (T1/T4), L1 (T2–T4), cluster deliverable (T5). ✓ 69 + - **Deferred (honest, like P2/P3):** full `parse()` post-state + hash-populated `find_*` = L3/L2 (need a populated server fixture). Stated in the spec. 70 + - **Handler-pointer comparison** intentionally skipped in the msgtab test (Rust vs `cref_` symbols differ); structural fields compared instead.
+110
docs/superpowers/specs/2026-06-02-p4-parse-dispatch-design.md
··· 1 + # P4 — Parser & dispatch: design 2 + 3 + **Status:** design locked 2026-06-02. Scope = port **all of `common/parse.c`** to Rust 4 + (`ircd-common/src/parse.rs`) and **drop `parse.o` outright** (no `parse_link.o`): every 5 + symbol parse.c defines is ported, so nothing C-side remains in that translation unit. 6 + 7 + ## Why a clean drop (not a `_link.o` partial port) 8 + 9 + P1/P2/P3 used `<mod>_link.o` because each of those modules kept *some* function in C 10 + (`MyMalloc`, `m_whowas`, `m_hash`, the variadic senders). parse.c is different: **every** 11 + function it defines — the 11 public `find_*` wrappers, `parse`, `getfield`, the 4 exported 12 + stub handlers (`m_nop`/`m_nopriv`/`m_unreg`/`m_reg`), the global `msgtab[]`, and the 3 13 + file-statics (`find_sender`/`cancel_clients`/`remove_unknown`) — is portable to Rust now. 14 + The variadic *senders* it **calls** (`sendto_one`/`sendto_flag`/`sendto_serv_butone`) live in 15 + send.c/s_send.c and stay C; Rust **calls** them through bindgen variadic declarations exactly 16 + as `s_numeric.rs` already does (`c"..."` literal patterns). Calling a C variadic from Rust is 17 + stable; only *defining* one is not. So `parse.o` joins `PORTED` like `list.o`/`match.o`. 18 + 19 + ## What P4 ports (Rust) vs keeps (C) 20 + 21 + | parse.c symbol | Disposition | 22 + | --- | --- | 23 + | `msgtab[]` | Rust `#[no_mangle] pub static mut msgtab: [Message; N]`, config-resolved table (see gates). Counters (`count`/`bytes`/…) mutate in place exactly as C. | 24 + | `find_client/find_uid/find_sid/find_service/find_server/find_mask/find_name/find_person/find_matching_client/find_userhost/find_target` | Rust. Thin wrappers over Rust `hash_find_*` (P2) + `match`/`mycmp`/`collapse` (P1). | 25 + | `parse` | Rust. The tokenizer + dispatcher. Calls C variadic senders + C `get_client_name`/`exit_client`/`is_allowed`; Rust `do_numeric`/`mycmp`/`replies[]`. | 26 + | `getfield` | Rust. Pure config-field splitter (static `line`). | 27 + | `m_nop/m_nopriv/m_unreg/m_reg` | Rust. Trivial stubs (3 call `sendto_one(replies[...])`). | 28 + | `find_sender/cancel_clients/remove_unknown` | Rust **private** fns (were file-static). Call C senders + `exit_client`. | 29 + | `para[MPAR+1]`, `sender[HOSTLEN+1]` | Rust module statics. `para` is shared between `find_sender` and `parse` (faithful: `find_sender` writes `para[0]`). | 30 + 31 + `m_*` handlers referenced by `msgtab` (≈60: `m_private`, `m_join`, …) **stay C** (P5) and are 32 + `extern`-declared; the Rust `msgtab` initializer takes their function pointers 33 + (`Some(m_join as CmdHandler-fn)`), which is legal in a `static` initializer. 34 + 35 + ## Verified config gates (cbuild/config.h) — affect the compiled `msgtab` 36 + 37 + | Gate | State | Effect on `msgtab` / `parse` | 38 + | --- | --- | --- | 39 + | `USE_SERVICES` | **off** | no `SERVSET` row | 40 + | `TKLINE` | **on** | `TKLINE` + `UNTKLINE` rows **present** | 41 + | `KLINE` | **off** | no `KLINE` row | 42 + | `ENABLE_SIDTRACE` | **off** | no `SIDTRACE` row | 43 + | `MOTD_UNREG` | **off** | `MOTD` unregistered-slot handler = `m_unreg` | 44 + | `IDLE_FROM_MSG` | **on** | `parse` idle-update guard uses `fhandler == m_private` (not the `!= m_ping && != m_pong` branch) | 45 + | `DEBUGMODE` | **off** | every `Debug((...))` is a no-op → omitted from the Rust port | 46 + 47 + Numeric constants (struct_def.h): `MPAR`=15 (parse.c local), `HOSTLEN`=63, `SIDLEN`=4, 48 + `BOOT_PROT`=0x100, `FLUSH_BUFFER`=-2, `ACL_NOPENALTY`=0x20000. `STAT_MAX`=5 (handlers per row). 49 + 50 + ## Macro crib (inline-translate at use sites; no macros.rs — matches P1/P2/P3) 51 + 52 + `status` is `i16`, compare as `c_int`. From struct_def.h: 53 + - `IsDead(x)` = `flags & FLAGS_DEADSOCK` 54 + - `IsServer(x)` = `status == STAT_SERVER`; `IsMe(x)` = `STAT_ME`; `IsClient(x)` = `STAT_CLIENT || STAT_OPER` 55 + - `IsPerson(x)` = `!user.is_null() && IsClient(x)`; `IsService(x)` = `status == STAT_SERVICE && !service.is_null()` 56 + - `IsRegistered(x)` = `status >= STAT_SERVER || status == STAT_ME` 57 + - `IsRegisteredUser(x)` = `(status == STAT_CLIENT || status == STAT_OPER) && !user.is_null()` 58 + - `MyConnect(x)` = `fd >= 0` 59 + - `BadPtr(x)` = `x.is_null() || *x == 0`; `BadTo(x)` = `if BadPtr(x) {c"*"} else {x}` 60 + - `ME` = `me.name` 61 + 62 + ## Rust→C variadic call surface (stable; pattern proven in s_numeric.rs) 63 + 64 + `parse`/`cancel_clients`/`remove_unknown`/`m_*` call: `sendto_one(to, pat, ...)`, 65 + `sendto_flag(sch, pat, ...)`, `sendto_serv_butone(one, pat, ...)` — all bindgen-declared 66 + variadics. Plus libc `sprintf`/`strcpy`/`strlen`/`strchr`(for `index`)/`isdigit`. `replies[i]` 67 + is the Rust `s_err` static (`[*const c_char; 1000]`); pass `replies[ERR_*]` as the pattern. 68 + 69 + ## Build wiring 70 + 71 + `ircd-sys/build.rs`: add `"parse.o"` to `PORTED`. `parse.o` stays in `CREF_OBJS` (the oracle 72 + still compiles the pristine `parse.c` → `cref_parse`/`cref_msgtab`/`cref_find_client`/…). No new 73 + second-compile. `ircd-common/src/lib.rs`: add `pub mod parse;` + `parse::PARSE_LINK_ANCHOR`. 74 + 75 + ## L1 test strategy (zero-diff vs `cref_*`) 76 + 77 + `ircd-testkit/tests/parse_diff.rs`. The cref oracle exports `cref_msgtab`, `cref_find_client`, 78 + `cref_find_mask`, `cref_find_name`, `cref_getfield`, `cref_parse`, etc. 79 + 80 + - **msgtab structure**: extern both `msgtab` and `cref_msgtab` as `[Message; N]`; walk until the 81 + NULL-`cmd` terminator on BOTH; assert equal length and, per row, `strcmp(cmd)==0` + 82 + identical `minparams`/`maxparams`. Handler **pointers are NOT compared** (Rust row points at 83 + `m_join`, oracle row at `cref_m_join`); structural identity + the parse fuzz/dispatch test 84 + below cover dispatch. Assert all counters start 0. 85 + - **getfield**: drive identical `ircd.conf`-style lines (IRCDCONF_DELIMITER is `'|'`, 86 + config.h:500 — NOT `':'`, an L1-caught bug; escaped `\|`, trailing field, empty 87 + field) through `getfield`/`cref_getfield`; assert identical returned fields and 88 + identical re-entrant continuation (`getfield(NULL)` sequence). 89 + - **find_mask / find_name**: build with no hash state (returns the cptr/fallback paths) — assert 90 + identical pointer-class results on `*`-containing and plain names. (Hash-populated lookups are 91 + L2: they need a live hash table shared between the two symbol sets.) 92 + - **tokenizer (the core dispatch path)**: this is the L3-fuzz gate per PLAN. For L1, call 93 + `parse(cptr, buf, bufend)` vs `cref_parse` on a corpus of raw lines exercising: leading/trailing 94 + colon prefix, empty message, numeric (3-digit) command, unknown command, `MPAR` parameter cap, 95 + empty trailing param, 512-byte truncation framing. Because `parse` mutates `cptr`/`ircstp` and 96 + dispatches into still-C `m_*`, **full** differential here needs a populated server (L2). L1 97 + asserts the **pure tokenizer-visible** outcomes that don't require a live `me`/hash: the 98 + `para[]` split + return code on the no-prefix / non-dispatching paths (numeric with null target, 99 + empty, unknown-command-from-non-person). The full post-state (`aClient` bytes + `ircstp` 100 + counters + handler identity) diff is the **L3 fuzz** gate, run when the harness has a populated 101 + server fixture. 102 + 103 + The complete `parse()` post-state differential (mutated `aClient`, `ircstp`, dispatch identity) 104 + and the hash-populated `find_*` lookups are the **L3/L2** gates (post-P4), matching how P2/P3 105 + deferred sender-wire and live-state checks. 106 + 107 + ## Deliverable: P5 handler-cluster analysis 108 + 109 + PLAN lists "per-handler-file static-symbol cluster analysis" as a P4 deliverable (P5 prereq). 110 + Produced as `docs/superpowers/specs/2026-06-02-p5-handler-cluster-map.md`.
+100
docs/superpowers/specs/2026-06-02-p5-handler-cluster-map.md
··· 1 + # P5 handler-file static-symbol cluster map (P4 deliverable / P5 prereq) 2 + 3 + **Produced:** 2026-06-02 as the P4 "per-handler-file static-symbol cluster analysis" 4 + deliverable. Purpose: identify the connected components of file-`static` functions + 5 + file-scope variables in each command-handler TU, so P5 ports each shared-state cluster 6 + **as a unit** through the `msgtab` fn-pointer seam (a cluster can't be split across the 7 + C/Rust boundary because the statics are private to the TU). 8 + 9 + > Method: `grep -nE "^static|^\s*static\b"` per file to enumerate statics, then grep each 10 + > static name across its own file to establish call/reference edges. Counts are 11 + > approximate (some forward declarations inflate raw grep counts). 12 + 13 + ## channel.c (~4019 lines) — difficulty: HIGH 14 + 15 + **Static fns:** `list_length`, `find_chasing`, `check_string`, `free_bei`, `make_bei`, 16 + `add_modeid`, `del_modeid`, `match_modeid`, `add_user_to_channel`, `change_chan_flag`, 17 + `send_mode_list`, `set_mode`, `can_join`, `check_channelmask`, `get_channel`, `add_invite`, 18 + `free_channel`, `names_channel`, `reop_channel`. 19 + **Static vars:** `asterix[2]`, `PartFmt`, `buf[BUFSIZE]`, `modebuf[MODEBUFLEN]`, 20 + `parabuf[MODEBUFLEN]`, `uparabuf[MODEBUFLEN]`. 21 + 22 + **Clusters:** 23 + 1. **Mode manipulation (core)** — `set_mode`, `add_modeid`/`del_modeid`/`match_modeid`, 24 + `send_mode_list`, `change_chan_flag`; shared vars `modebuf`/`parabuf`/`uparabuf`. 25 + Entry: `m_mode`. 26 + 2. **Join / access control** — `can_join`, `check_channelmask`, `get_channel`, 27 + `reop_channel`, `add_user_to_channel`. Entry: `m_join`, `m_njoin`. 28 + 3. **Channel struct lifecycle** — `get_channel`, `free_channel`, `make_bei`/`free_bei`. 29 + (shared with cluster 2 via `get_channel`.) Entry: `m_join`. 30 + 4. **Invite list** — `add_invite`, `list_length`. Entry: `m_invite`. 31 + 5. **Display** — `names_channel`, `find_chasing`, `check_string`; uses `buf`. Entry: 32 + `m_names`, plus `find_chasing` is used by `m_kick`/`m_mode`/`m_topic`. 33 + 34 + ## s_user.c (~3565 lines) — difficulty: MEDIUM 35 + 36 + **Static fns:** `save_user`, `m_message` (internal PRIVMSG/NOTICE core), `who_one`, 37 + `who_channel`, `who_find`, `send_whois`. 38 + **Static vars:** `buf[BUFSIZE]`, `buf2[BUFSIZE]`, `user_modes[]`. 39 + 40 + **Clusters:** 41 + 1. **WHOIS/WHO** — `who_find`, `who_channel`, `who_one`, `send_whois`; shared `buf`. 42 + Entry: `m_who`, `m_whois`. 43 + 2. **Message routing** — `m_message`; isolated (calls external fns only). Entry: 44 + `m_private`/`m_notice` (msgtab `PRIVMSG`/`NOTICE`). 45 + 3. **User persistence** — `save_user`; standalone. Entry: SAVE / nick-collision path. 46 + 47 + ## s_auth.c (~799 lines) — difficulty: LOW 48 + 49 + **Static fns:** `set_clean_username`. **Static vars:** `iauth_conf`, `iauth_stats` (USE_IAUTH). 50 + **Cluster:** username sanitization (`set_clean_username`, pure). Entry: `m_user` / auth path. 51 + 52 + ## s_serv.c (~3884 lines) — difficulty: MEDIUM-HIGH 53 + 54 + **Static fns:** `get_version`, `send_server`, `introduce_server`, `send_server_burst`, 55 + `report_myservers`, `report_x_lines`, `report_configured_links`, `report_ping`, `report_fd`, 56 + `trace_one`, `check_link`, `dump_map_sid`, `dump_map`, `report_listeners`. 57 + **Static vars:** `buf[BUFSIZE]`, `report_array[18][3]`, `server_name`, `server_max`, 58 + `server_num`. 59 + 60 + **Clusters:** 61 + 1. **Server introduction / burst (protocol-critical, recursive)** — `introduce_server`, 62 + `send_server`, `send_server_burst` (recurses). Entry: `m_server`. 63 + 2. **Map display (recursive)** — `dump_map_sid`, `dump_map`; shared `buf`. Entry: `m_map`. 64 + 3. **Stats / reporting** — `report_{myservers,x_lines,configured_links,ping,fd,listeners}`, 65 + `trace_one`; shared `report_array`, `buf`. Entry: `m_stats`, `m_trace`. 66 + 4. **Version / link validation** — `get_version`, `check_link`. Entry: `m_server`. 67 + 68 + ## s_service.c (~720 lines) — difficulty: LOW 69 + 70 + **Static fns:** `best_service`. **Cluster:** service selection (standalone). Entry: `m_service`. 71 + 72 + ## s_zip.c (~254 lines) — difficulty: VERY LOW 73 + 74 + **Static vars:** `unzipbuf[UNZIP_BUFFER_SIZE]`, `zipbuf[ZIP_BUFFER_SIZE]`. No static fns. 75 + Compression buffers; the public zip API (zlib→flate2, bit-exact) wraps them. 76 + 77 + ## s_misc.c (~1217 lines) — difficulty: MEDIUM-HIGH 78 + 79 + **Static fns:** `exit_server`, `exit_one_client` (both recursive). **Static vars:** `months[]`, 80 + `weekdays[]` (date() tables). 81 + **Cluster:** client/server exit cascade — `exit_server`↔`exit_one_client`, mutually/self 82 + recursive, complex flag state (FLAGS_SPLIT/HIDDEN/SQUIT). Public entry: `exit_client`. 83 + **Critical network path** — port together, with care for the recursion. 84 + 85 + ## s_debug.c (~693 lines) — difficulty: VERY LOW 86 + 87 + **Static vars:** `debugbuf[2*READBUF_SIZE]`. No static fns. Debug output buffer. 88 + 89 + ## Porting-order suggestion (easiest → hardest) 90 + 91 + 1. `s_zip.c`, `s_debug.c` (buffers only) — foundation. 92 + 2. `s_auth.c` (1 utility fn), `s_service.c` (1 lookup fn). 93 + 3. `s_user.c` (3 loosely-coupled clusters). 94 + 4. `s_misc.c` (recursive but isolated `exit_client` cascade — critical, port carefully). 95 + 5. `channel.c` (largest; 5 clusters; shared mode buffers). 96 + 6. `s_serv.c` (last; protocol-critical recursion in `send_server_burst`/`dump_map`). 97 + 98 + **P8 notes (not P5):** shared static buffers (`modebuf`/`parabuf`/`uparabuf`, `buf`) become 99 + owned struct fields; the `exit_server`/`exit_one_client` and `send_server_burst`/`dump_map` 100 + recursions may want explicit-stack/trampoline reshaping. P5 keeps them faithful (recursive).