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.

feat(leveva): +Q no-forward user mode (charybdis UMODE_NOFORWARD) — P11 slice 250

The user-side counterpart of the channel +f forwarding feature (slice 242),
closing the +f family. A client that sets +Q on itself is never redirected by
a +f channel: a forwardable JOIN reject (+i/+l/+b/+r) falls through to the
source channel's original reject numeric instead of a 470 RPL_LINKCHANNEL +
re-JOIN of the target.

Client-settable, purely-local umode (letter Q, bit 0x8000) — like +D/+R/+g/+G:
the forward decision runs only at the joining user's home server (a remote
NJOIN is never forwarded), so the bit is not in SEND_UMODES. Decision core
mode::is_noforward; the JOIN forward condition gains !is_noforward(joiner.modes).
(Disjoint from the channel +Q DisableForward, which reuses the letter in its
own table — the channel refuses incoming forwards; this user refuses being
forwarded.)

Tests: boot-golden golden_noforward.rs (a +Q user gets a plain 473, no 470/no
forward; -Q restores the redirect) and proptest noforward_proptest.rs (the
forward composite is false for every reject/allow_forward/co-set-mode combo
when +Q is set, exactly allow_forward && forwardable when clear; bit + letter
round-trip uniquely). Updated the s2s::umode local-only exclusion + the two
004/ISUPPORT literals; regenerated the 5 welcome-burst snapshots (004 usermode
param …GZDR → …GZDRQ).

+378 -20
+1 -1
PLAN.md
··· 90 90 | **P8 — Delete the C + retire the oracle harness ✅ DONE** (per-sub-phase: [`PLAN-P8-progress.md`](PLAN-P8-progress.md) + [`docs/progress-log/p8.md`](docs/progress-log/p8.md)) | remaining C (the ~25 variadic sender trampolines + residual data globals) + `ircd-sys` C build + `ircd-golden` (L2) + `ircd-testkit`/`cref_*` archives (L1) + the frozen reference‑C tree | With all C _logic_ already Rust (P7 exit), delete the last C: replace the variadic sender trampolines with a non‑variadic Rust sender API at every call site (faithful `wire!`/`reply_one!`/`format!` first; leveva typed messages adopted alongside the IRC senders), drop the residual data globals via the data‑symbol seam (bindgen `extern` decls resolve to `#[no_mangle]` Rust defs at link), then drop **all** C compilation from `ircd-sys` (no more `cc::Build`), retire the differential harness (`cref_*`/L1 `ircd-testkit`/`ircd-golden` L2 can no longer be built once the oracle is gone), and **migrate the load‑bearing tests into `ircd-common`** as self‑contained Rust tests (own fixtures, no `cref_` oracle). **Progress (P8a–P8r — full entries in the progress logs):** **every variadic sender trampoline is now Rust** (P8a–P8m, incl. the keystones `sendto_one`/`sendto_flag`/`esendto_*`), and the five data‑global `.o`s are dropped outright (`s_auth.o` iauth globals P8n, `send.o` P8o, `s_bsd.o` `local[]`/`highest_fd`/… P8p, `ircd.o` `me`/`client`/timers/… P8q). **P8r dropped the last C‑logic TU `support.o`** — `dgets`/`make_isupport` ported to Rust + the `ipv6string`/`minus_one` data globals defined as `#[no_mangle]` statics (`snprintf_append` is dead in Rust — WHOX is field‑by‑field — so it died with the `.o`; `irc_sprintf` is deleted not ported, in P11). **Every ircd C TU is now Rust:** the link set holds only the generated `version.o` + the L1 harness's `ctruth.o`. **P8s migrated all 115 L1 `cref_` differentials into self‑contained `ircd-common` `insta` snapshot tests** (drive only the Rust port via `link_anchor()`, no oracle; soundness via the capture chain) — the suite is green + 0 warnings. **P8t ported the last *generated* C TU `version.c` to Rust** (`ircd-common/src/version.rs`): the data globals `generation`/`creation`/`pass_version`/`infotext`/`isupport` are now `#[no_mangle]` statics via the data‑symbol seam — `generation`/`creation` sourced from crate metadata at build time (`CARGO_PKG_VERSION` + an `ircd-common/build.rs` UTC build stamp), `pass_version`/`infotext` copied verbatim. `version.c.SH` generation + the `version.o` compile/archive are gone from `ircd-sys/build.rs`; `libircd_c.a` now holds **only** the L1 harness `ctruth.o`. (`creation` is now genuinely build‑time volatile, so the golden canonicalizer masks RPL_CREATED 003, matching the existing 371 Birth‑Date rule.) **P8u (FINAL) retired the oracle.** Ran the differential suite a last time green — L1 `ircd-testkit` all pass; L2 `ircd-golden` 86 pass + only the documented reference‑C‑garbage `s_serv_stats` flake (where Rust is the *correct* side) — then **dropped all C compilation** from `ircd-sys/build.rs`: the `make` object build, the `cref_*`/`ctruth.o`/`res.o` recompiles, the `libircd_c.a` archive, and the whole‑archive link + `-lz`/`-lm`/`-lcrypt` (none needed by Rust — `pow` resolves from glibc 2.29+). `build.rs` now only runs `configure` + bindgen over the C *headers* (the struct view `ircd-common` still uses until P9–P12) and expands the install‑path Makefile vars. `ircd-testkit` (L1) + `ircd-golden` (L2) are `exclude`d from the workspace (mothballed, git‑recoverable); the `ctruth.c`/`layout.rs` drift‑net is deleted. **The workspace is now 100% Rust — zero C TUs compiled.** **P8v (literal end of C) deleted the C *source tree* itself + retired `ircd-sys`:** froze the generated `bindings.rs` into a committed `ircd-common/src/bindings.rs` (a self‑alias `extern crate self as ircd_sys;` keeps every `ircd_sys::bindings::*` path resolving, zero src churn) + the install‑path consts as literals, then deleted `ircd-sys`, the mothballed `ircd-testkit`/`ircd-golden` oracle crates, the `iauth-rs` C‑iauth differential, and **all** C source (`common/ ircd/ iauth/ support/ contrib/ cbuild/ configure .clang-format* clangformat.yml`). `ircd-rs` calls `ircd_common::ircd::c_ircd_main` directly. **Not one line of C remains in the repo.** | **MET. Final differential run green; oracle mothballed; `cargo build --workspace` 0 warnings links pure Rust; `cargo test --workspace` 695 pass / 0 fail (P8v; was 698 before deleting the C‑oracle tests); the `ircd-common` snapshot tests carry the reference‑C correctness forward. C source tree deleted — repo is 100% Rust at the file level.** | 91 91 | **P9 — std‑library cleanup ❌ RETIRED (2026‑06‑08)** | — (was: whole Rust tree) | **Skipped by decision — superseded by the `leveva` greenfield.** P9 assumed an *in‑place* idiomatic transformation of the mechanical port (`MyMalloc`/`MyFree`→`Box`/`Vec`, `dbuf` freelist→owned, raw `libc`→`std`/`nix`, `[c_char;N]`→`[u8;N]`). Because the idiomatic end‑state is now a separate greenfield crate (`leveva`), `ircd-common` is throwaway code (the oracle, deleted at parity), so polishing it earns nothing. Nothing was implemented; the row is kept for provenance. | n/a — retired. | 92 92 | **P10 — `leveva` greenfield foundations 🔶 IN PROGRESS** ([`docs/progress-log/p10.md`](docs/progress-log/p10.md)) | new `leveva` crate (lib) + `leveva-integration` differential tests | The idiomatic‑Rust product, built from scratch (not transformed from the port). **Done so far:** RFC 1459 case‑folding (`casemap`); typed identifier strings (`IrcStr`/`IrcString` + `Nick`/`ChanName`/`Uid`/`Sid`/`Cid`/`ServerName`/`UserName`/`HostName` validating newtypes via `ident_newtype!`); `Message`/`MessageBuilder`; the full `Numeric` reply/error enum (184 codes, discriminant = wire code); a generic safe `patricia` trie; glob `matching`; `mode`; KDL `config` (model/parse/password/privilege); `rustls`/`tokio` `tls` endpoint with hot reload; `ident`. The async boot path (`main.rs`) binds listeners + watches certs but currently drops accepted connections — no protocol layer yet. Idiomatic Rust allowed everywhere; pulls real crates (tokio/rustls/kdl/nom/clap). | `cargo test -p leveva` green + clippy clean per module; `leveva-integration` pins each greenfield reimplementation against `ircd-common` (the oracle) within their shared domain (documented divergences asserted explicitly). | 93 - | **P11 — `leveva` protocol layer → feature parity 🔶 IN PROGRESS** ([`docs/progress-log/p11.md`](docs/progress-log/p11.md)) | `leveva` (client/server state machine + handlers) | The bulk of the remaining work: the connection + registration state machine, the post‑registration command handlers (JOIN/PRIVMSG/MODE/KICK/TOPIC/WHO/WHOIS/…), channels + channel/user modes, the S2S link/burst protocol (UID‑based: UNICK/NJOIN/SAVE/EOB), and `leveva-iauth` (native async auth — dnsbl/socks/webproxy/pipe/ident). Each slice is built idiomatically (owned per‑connection state, typed identifiers end‑to‑end, `Result`/`thiserror`, `format!`/typed builders — **no** `sprintf`/global `buf`), unit‑tested + boot‑golden + proptest‑fuzzed, and **differentially pinned against `ircd-common`** (or `iauth-rs` for auth) wherever a pure oracle entry point exists — structural skeleton + documented divergences, since leveva emits a clean modern burst rather than a byte copy. **Done: 249 slices.** The slices cover the full client command surface (registration, channels + channel/user modes, WHO/WHOIS/WHOX, OPER/STATS), the UID-based S2S link/burst/netsplit protocol (keepalive, live introduction, NJOIN chunking, TS merge arbitration), `leveva-iauth` (dnsbl/socks/webproxy/pipe/ident), config live-rehash across every block, a channel-mode rework, CertFP, the **IRCv3 track** (message-tags/server-time/msgid/account/bot/oper/echo/labeled-response/CAP, `draft/metadata-2`, caller-id `+g`/`+G`, the **STS/TLS** sub-track), elemental channel modes + extbans, KNOCK, command-rate flood protection (fakelag), the leveva-native channel modes `+C`/`+c`/`+S`/`+j`/`+z`/`+T`/`+g`/`+r` (the `+z` op-moderation track 235–238 — local + network-wide S2S op-redirect for PRIVMSG/NOTICE/TAGMSG/REDACT via the statusmsg `@<chan>` seam — now complete; `+T` no-NOTICE, charybdis `MODE_NONOTICE`, slice 239; `+g` free-invite, charybdis `MODE_FREEINVITE`, slice 240; `+r` registered-only join, charybdis `MODE_REGONLY`, slice 241; `+f` channel forwarding, charybdis `MODE_FORWARD`, slice 242 — a forwardable JOIN reject redirects to the `+f` target with `470`; `+Q`/`+F` forward-target control, charybdis `MODE_DISFORWARD`/`MODE_FREETARGET`, slice 243 — `+F` lifts the op-on-target requirement for setting `+f`, `+Q` makes a channel refuse incoming forwards; `+P` permanent channel, charybdis `MODE_PERMANENT`/`chm_staff`, slice 244 — an oper-only flag that stops an empty channel being destroyed, so its topic/modes persist with zero members; `+L` large ban list, charybdis `MODE_EXLIMIT`/`chm_staff`, slice 245 — an oper-only flag that raises the per-list size cap on `+b`/`+e`/`+I` from 100 to 500, `478 ERR_BANLISTFULL` when a normal list is full — **the last missing standard charybdis channel mode**; the `max-bans`/`max-bans-large` caps `+L` toggles between are promoted to REHASH-able `options { max-bans / max-bans-large }` knobs, slice 246 — the const→knob trajectory `MAX_PENALTY` took); the user mode `+D` deaf, charybdis `UMODE_DEAF`, slice 247 — a client-settable, purely-local umode whose holder receives no channel-addressed messages (the channel fan-out filter runs on the deaf user's home server, so it is not in `SEND_UMODES`); the user mode `+R` registered-only messages, charybdis `UMODE_REGONLYMSG`, slice 248 — a client-settable, purely-local umode whose holder refuses a **private** `PRIVMSG`/`NOTICE` from an unidentified sender (`486 ERR_NONONREG`), bypassed by an identified/operator/accepted sender; the local-sender plane (slice 248); the `+R` remote-sender S2S plane, slice 249 — the inbound `relay::inbound_message` re-runs the same `callerid::regonly_msg_blocks` gate on the target's home server for a *remote* sender (neither `+R` nor `+g`/`+G` is in `SEND_UMODES`), bouncing one `486 ERR_NONONREG` to the sender's uplink for a blocked PRIVMSG (no notify pair, unlike caller-id) and silently dropping a blocked NOTICE — the caller-id 216→217 split repeated, closing the `+R` family; and the native cross-daemon command-skeleton differential harness. **Full per-slice detail (scope, mechanism, divergences, gate) for every slice lives in [`docs/progress-log/p11.md`](docs/progress-log/p11.md); the standing testing plan is [`docs/superpowers/specs/2026-06-10-leveva-testing-plan.md`](docs/superpowers/specs/2026-06-10-leveva-testing-plan.md). This row stays thin.** | Per‑slice: leveva boots and serves the slice; leveva golden snapshot + proptest green; the `leveva-integration` differential agrees with `ircd-common` on the shared skeleton with divergences asserted. Across the eventual matrix: registration, channels, WHO/WHOIS/WHOX, OPER/STATS, server burst, netsplit, rehash, restart; iauth end‑to‑end. | 93 + | **P11 — `leveva` protocol layer → feature parity 🔶 IN PROGRESS** ([`docs/progress-log/p11.md`](docs/progress-log/p11.md)) | `leveva` (client/server state machine + handlers) | The bulk of the remaining work: the connection + registration state machine, the post‑registration command handlers (JOIN/PRIVMSG/MODE/KICK/TOPIC/WHO/WHOIS/…), channels + channel/user modes, the S2S link/burst protocol (UID‑based: UNICK/NJOIN/SAVE/EOB), and `leveva-iauth` (native async auth — dnsbl/socks/webproxy/pipe/ident). Each slice is built idiomatically (owned per‑connection state, typed identifiers end‑to‑end, `Result`/`thiserror`, `format!`/typed builders — **no** `sprintf`/global `buf`), unit‑tested + boot‑golden + proptest‑fuzzed, and **differentially pinned against `ircd-common`** (or `iauth-rs` for auth) wherever a pure oracle entry point exists — structural skeleton + documented divergences, since leveva emits a clean modern burst rather than a byte copy. **Done: 250 slices.** The slices cover the full client command surface (registration, channels + channel/user modes, WHO/WHOIS/WHOX, OPER/STATS), the UID-based S2S link/burst/netsplit protocol (keepalive, live introduction, NJOIN chunking, TS merge arbitration), `leveva-iauth` (dnsbl/socks/webproxy/pipe/ident), config live-rehash across every block, a channel-mode rework, CertFP, the **IRCv3 track** (message-tags/server-time/msgid/account/bot/oper/echo/labeled-response/CAP, `draft/metadata-2`, caller-id `+g`/`+G`, the **STS/TLS** sub-track), elemental channel modes + extbans, KNOCK, command-rate flood protection (fakelag), the leveva-native channel modes `+C`/`+c`/`+S`/`+j`/`+z`/`+T`/`+g`/`+r` (the `+z` op-moderation track 235–238 — local + network-wide S2S op-redirect for PRIVMSG/NOTICE/TAGMSG/REDACT via the statusmsg `@<chan>` seam — now complete; `+T` no-NOTICE, charybdis `MODE_NONOTICE`, slice 239; `+g` free-invite, charybdis `MODE_FREEINVITE`, slice 240; `+r` registered-only join, charybdis `MODE_REGONLY`, slice 241; `+f` channel forwarding, charybdis `MODE_FORWARD`, slice 242 — a forwardable JOIN reject redirects to the `+f` target with `470`; `+Q`/`+F` forward-target control, charybdis `MODE_DISFORWARD`/`MODE_FREETARGET`, slice 243 — `+F` lifts the op-on-target requirement for setting `+f`, `+Q` makes a channel refuse incoming forwards; `+P` permanent channel, charybdis `MODE_PERMANENT`/`chm_staff`, slice 244 — an oper-only flag that stops an empty channel being destroyed, so its topic/modes persist with zero members; `+L` large ban list, charybdis `MODE_EXLIMIT`/`chm_staff`, slice 245 — an oper-only flag that raises the per-list size cap on `+b`/`+e`/`+I` from 100 to 500, `478 ERR_BANLISTFULL` when a normal list is full — **the last missing standard charybdis channel mode**; the `max-bans`/`max-bans-large` caps `+L` toggles between are promoted to REHASH-able `options { max-bans / max-bans-large }` knobs, slice 246 — the const→knob trajectory `MAX_PENALTY` took); the user mode `+D` deaf, charybdis `UMODE_DEAF`, slice 247 — a client-settable, purely-local umode whose holder receives no channel-addressed messages (the channel fan-out filter runs on the deaf user's home server, so it is not in `SEND_UMODES`); the user mode `+R` registered-only messages, charybdis `UMODE_REGONLYMSG`, slice 248 — a client-settable, purely-local umode whose holder refuses a **private** `PRIVMSG`/`NOTICE` from an unidentified sender (`486 ERR_NONONREG`), bypassed by an identified/operator/accepted sender; the local-sender plane (slice 248); the `+R` remote-sender S2S plane, slice 249 — the inbound `relay::inbound_message` re-runs the same `callerid::regonly_msg_blocks` gate on the target's home server for a *remote* sender (neither `+R` nor `+g`/`+G` is in `SEND_UMODES`), bouncing one `486 ERR_NONONREG` to the sender's uplink for a blocked PRIVMSG (no notify pair, unlike caller-id) and silently dropping a blocked NOTICE — the caller-id 216→217 split repeated, closing the `+R` family; the user mode `+Q` no-forward, charybdis `UMODE_NOFORWARD`, slice 250 — the *user*-side counterpart of the channel `+f` forwarding feature: a client-settable, purely-local umode whose holder is never redirected by a `+f` channel (a forwardable JOIN reject falls through to the source's original numeric instead of `470` + redirect; the gate runs on the joiner's home server, so not in `SEND_UMODES`; the disjoint channel `+Q` `MODE_DISFORWARD` reuses the same letter), closing the `+f` family; and the native cross-daemon command-skeleton differential harness. **Full per-slice detail (scope, mechanism, divergences, gate) for every slice lives in [`docs/progress-log/p11.md`](docs/progress-log/p11.md); the standing testing plan is [`docs/superpowers/specs/2026-06-10-leveva-testing-plan.md`](docs/superpowers/specs/2026-06-10-leveva-testing-plan.md). This row stays thin.** | Per‑slice: leveva boots and serves the slice; leveva golden snapshot + proptest green; the `leveva-integration` differential agrees with `ircd-common` on the shared skeleton with divergences asserted. Across the eventual matrix: registration, channels, WHO/WHOIS/WHOX, OPER/STATS, server burst, netsplit, rehash, restart; iauth end‑to‑end. | 94 94 | **P12 — Retire the mechanical port; `leveva` is the product ✅ DONE (2026‑06‑13)** ([`docs/progress-log/p12.md`](docs/progress-log/p12.md)) | deleted `ircd-common`/`ircd-rs` + C‑era `iauth-rs` + `leveva-integration` + the mechanical port's deployment rigging; workspace = `leveva` + `leveva-iauth` | Ran the differential suite a **final time green** (`cargo test -p leveva-integration`, skeleton‑identical to the oracle), then **deleted the mechanical port** (`ircd-common`, `ircd-rs`), its C‑era auth scaffolding (`iauth-rs`, long since subsumed by the native `leveva-iauth`), and the oracle differential crate (`leveva-integration`). The oracle tests retired with their oracle — the load‑bearing behavior was already carried by leveva's self‑contained 212‑file golden + proptest suite (verified: leveva/leveva-iauth have zero Cargo/`use` dep on the deleted crates). Also stripped the mechanical port's **deployment rigging** (`docker/Dockerfile.ircd`, the `ircd` bake target, the entire `docs/k8s/` example network) and updated the README to describe the finished strangler. This is the literal end of the strangler — same lifecycle the C tree had at P8. | **MET. Final differential run green; `ircd-common`/`ircd-rs`/`iauth-rs`/`leveva-integration` deleted; `cargo build --workspace` 0 warnings; `cargo clippy --workspace --tests` clean; `cargo test -p leveva -p leveva-iauth` green — the behavioral guarantees carry forward. The workspace is now `leveva` + `leveva-iauth`.** | 95 95 96 96 ---
+52
docs/progress-log/p11.md
··· 13797 13797 **Plan:** [`docs/superpowers/plans/2026-06-18-p11-slice249-regonly-msg-s2s-remote-sender.md`](../superpowers/plans/2026-06-18-p11-slice249-regonly-msg-s2s-remote-sender.md). 13798 13798 13799 13799 **Gate.** `cargo test -p leveva` green (2213 lib + the new proptest); `cargo clippy -p leveva --tests` clean; `cargo build --workspace` 0 warnings. 13800 + 13801 + ## 2026-06-18 — P11 slice 250: `+Q` no-forward user mode (charybdis `UMODE_NOFORWARD`) 13802 + 13803 + The *user*-side counterpart of the channel `+f` forwarding feature (slice 242), closing the `+f` 13804 + family's last gap. A client that sets `+Q` on itself is **never** redirected by a `+f` channel: a 13805 + forwardable JOIN reject (`+i`/`+l`/`+b`/`+r`) falls through to the source channel's original reject 13806 + numeric instead of a `470 RPL_LINKCHANNEL` + re-JOIN of the target. (Disjoint from the **channel** 13807 + mode `+Q` `DisableForward`, slice 243, which reuses the same letter in its own table — the channel 13808 + refuses *incoming* forwards; this user refuses *being* forwarded. Same letter-reuse pattern as 13809 + channel `+g` FreeInvite vs user `+g` CallerId.) 13810 + 13811 + ### Classification 13812 + Client-settable, **purely local** umode — like `+D`/`+R`/`+g`/`+G`/`+s`. The forward decision runs 13813 + only at the joining user's home server (a remote `NJOIN` is never forwarded), so no remote server 13814 + needs the bit ⇒ **not** in `SEND_UMODES`. Letter `Q`, bit `0x8000` (next free past `+R` `0x4000`). 13815 + No C oracle (leveva-native). 13816 + 13817 + ### Mechanism 13818 + - **Decision core.** `mode::is_noforward(modes) -> bool` = `modes & 0x8000 != 0` — the JOIN-time 13819 + fuzz seam (mirror of `is_deaf`/`is_regonly_msg`/`is_secure`). 13820 + - **Enforcement.** `command::join::join_one`'s forward block now reads 13821 + `allow_forward && !mode::is_noforward(joiner.modes) && channel::forwardable(reject)`. When the 13822 + joiner is `+Q` the redirect is skipped and execution falls through to `join_reject` with the 13823 + source channel's original numeric (`473`/`471`/`474`/`477`), exactly as if no `+f` were set. 13824 + - **Wiring (the umode multi-site checklist).** `mode.rs`: `UserMode::NoForward` appended last — 13825 + enum, `ALL` (15→16), `as_char` `'Q'`, `bit` `0x8000`, `flag_name` `"FLAGS_NOFORWARD"`, 13826 + `from_char`, `from_bit`, `Display`, `is_noforward`. `command::mode::umode`: `'Q'` added to the 13827 + self-settable group + the `umode_diff` table (so `±Q` echoes `:nick MODE nick :+Q`). `s2s::umode`: 13828 + `NoForward` added to the local-only exclusion `matches!` + a dedicated `SEND_UMODES & …NoForward 13829 + == 0` assertion (NOT propagated). The two hardcoded 004/ISUPPORT literals `oOiwraWBxsgGZDR` → 13830 + `oOiwraWBxsgGZDRQ`. 13831 + 13832 + ### Divergences 13833 + None new — leveva-native, no oracle. 13834 + 13835 + ### Tests 13836 + - **Boot-golden `tests/golden_noforward.rs`** (TDD — written failing first): a `+Q` `bob` hitting 13837 + `#secret` (`+i`, forwarding to `#lobby`) gets a plain `473` with no `470` and no `JOIN #lobby`; 13838 + after `-Q` the same JOIN is forwarded (`470 #secret #lobby` + `JOIN #lobby`, never `JOIN #secret`) 13839 + — the inverse, proving the suppression is exactly the `+Q` bit. 13840 + - **Fuzz `tests/noforward_proptest.rs`** (5 tests): `is_noforward` matches an independent model + 13841 + decisive on `0x8000` independent of neighbours; the composite `would_forward(allow_forward, 13842 + modes, reject)` is **false for every** combination of the 7 `JoinReject` variants × `allow_forward` 13843 + × arbitrary co-set modes when `+Q` is set, and exactly `allow_forward && forwardable(reject)` when 13844 + `+Q` is clear (so `+Q` is the *only* umode that gates forwarding); `'Q'`/`0x8000` round-trip 13845 + uniquely (exhaustive over the ASCII letters + `UserMode::ALL` bits). 13846 + - Regenerated the 5 welcome-burst snapshots (`golden_cap` ×2, `golden_registration`, `golden_pass`, 13847 + `golden_user_spaceless`): the only change is the 004 usermode param `…GZDR` → `…GZDRQ`. 13848 + 13849 + **Plan:** [`docs/superpowers/plans/2026-06-18-p11-slice250-noforward-plus-Q.md`](../superpowers/plans/2026-06-18-p11-slice250-noforward-plus-Q.md). 13850 + 13851 + **Gate.** `cargo test -p leveva` green (2213 lib + the new golden/proptest); `cargo clippy -p leveva --tests` clean; `cargo build --workspace` 0 warnings.
+62
docs/superpowers/plans/2026-06-18-p11-slice250-noforward-plus-Q.md
··· 1 + # P11 slice 250 — `+Q` no-forward user mode (charybdis `UMODE_NOFORWARD`) 2 + 3 + ## Goal 4 + 5 + Add the `+Q` user mode: a client that sets `+Q` on itself is **never** redirected by a `+f` 6 + ([`ChanMode::Forward`], slice 242) channel. When such a user's JOIN is refused by a *forwardable* 7 + reject (`+i`/`+l`/`+b`/`+r`), the redirect is suppressed and they receive the source channel's 8 + original reject numeric instead of `470 RPL_LINKCHANNEL` + a re-JOIN of the target. 9 + 10 + This is the *user*-side counterpart of the channel `+f` forwarding feature and closes the 11 + `+f` family's only remaining gap. (The disjoint **channel** mode `+Q` `DisableForward`, slice 243, 12 + reuses the same letter in its own table — channel refuses *incoming* forwards; user refuses *being* 13 + forwarded. Same pattern as channel `+g` FreeInvite vs user `+g` CallerId.) 14 + 15 + ## Classification 16 + 17 + Client-settable, **purely local** umode — like `+D`/`+R`/`+g`/`+G`/`+s`. The forward decision runs 18 + only at the joining user's home server (a remote `NJOIN` is never forwarded), so a remote server 19 + never needs to know the bit ⇒ **not** in `SEND_UMODES`. Letter `Q`, bit `0x8000` (next free past 20 + `+R` `0x4000`). No C oracle (leveva-native). 21 + 22 + ## Decision core 23 + 24 + `mode::is_noforward(modes) -> bool` = `modes & 0x8000 != 0` — the JOIN-time fuzz seam. The JOIN 25 + handler's forward condition becomes `allow_forward && !is_noforward(joiner.modes) && 26 + forwardable(reject)`. 27 + 28 + ## Wiring checklist (the umode multi-site checklist) 29 + 30 + 1. `mode.rs` — `UserMode::NoForward` appended last: enum, `ALL` (15→16), `as_char` `'Q'`, `bit` 31 + `0x8000`, `flag_name` `"FLAGS_NOFORWARD"`, `from_char`, `from_bit`, `Display`, + `is_noforward`. 32 + 2. `command/mode.rs` `umode` — `'Q'` added to the self-settable group + to the `umode_diff` table 33 + (else `±Q` echo is silent). 34 + 3. Enforcement — `command/join.rs` `join_one`: `!is_noforward(joiner.modes)` gates the forward. 35 + 4. `s2s::umode` — NOT in `SEND_UMODES`; `NoForward` added to the local-only exclusion `matches!` in 36 + `send_umodes_excludes_*` + a dedicated assertion. 37 + 5. 004/ISUPPORT — `isupport.rs` `mode_strings_come_from_tables` and `registration.rs` 38 + `myinfo_004_carries_leveva_modes` literal `oOiwraWBxsgGZDR` → `oOiwraWBxsgGZDRQ`. 39 + 6. Snapshots — regenerate the 5 welcome-burst goldens (004 usermode param gains `Q`). 40 + 41 + ## Tests (TDD — failing golden first) 42 + 43 + - Boot-golden `tests/golden_noforward.rs`: a `+Q` user hitting a `+i`-forwarding-to-`#lobby` 44 + `#secret` gets a plain `473` (no `470`, no `JOIN #lobby`); after `-Q` the same JOIN is forwarded 45 + (`470` + `JOIN #lobby`) — the inverse, proving suppression is exactly the `+Q` bit. 46 + - Fuzz `tests/noforward_proptest.rs`: `is_noforward` matches model + decisive on `0x8000` 47 + independent of neighbours; the composite `would_forward` is **false for every** reject / 48 + allow_forward / co-set-mode combination when `+Q` is set, and exactly `allow_forward && 49 + forwardable` when clear; `'Q'`/`0x8000` round-trip uniquely (exhaustive over ASCII letters + ALL 50 + bits). 51 + 52 + ## Gate 53 + 54 + `cargo test -p leveva` green; `cargo clippy -p leveva --tests` clean; `cargo build --workspace` 0 55 + warnings. 56 + 57 + ## Out of scope / follow-ons 58 + 59 + - No S2S plane needed (local-only; the forward only ever happens at the joiner's home server). 60 + - Pre-existing latent quirk noted but NOT fixed here (separate concern): `command/mode.rs` 61 + `umode_diff` omits `SoftCallerId` (`+G`), so a bare `±G` MODE echo is silent. Out of scope for 62 + this slice.
+8 -1
leveva/src/command/join.rs
··· 155 155 // and a bad key never forward (see `channel::forwardable`). If the target *also* 156 156 // refuses, the joiner gets the target's reject numeric (single hop — charybdis chains; 157 157 // documented divergence). 158 - if allow_forward && crate::channel::forwardable(reject) { 158 + // 159 + // `+Q` (charybdis `UMODE_NOFORWARD`, slice 250): a joiner who set `+Q` on themselves is 160 + // never redirected — the forward is suppressed and they fall through to the source 161 + // channel's original reject below, exactly as if no `+f` were set. 162 + if allow_forward 163 + && !crate::mode::is_noforward(joiner.modes) 164 + && crate::channel::forwardable(reject) 165 + { 159 166 if let Some(fwd) = ctx.channels.forward_target(target) { 160 167 // `+Q` (charybdis `MODE_DISFORWARD`, slice 243): a target that refuses incoming 161 168 // forwards makes the joiner fall through to the source's original reject.
+9 -7
leveva/src/command/mode.rs
··· 728 728 match c { 729 729 '+' => adding = true, 730 730 '-' => adding = false, 731 - 'i' | 'w' | 'B' | 'x' | 'g' | 'G' | 'D' | 'R' => { 731 + 'i' | 'w' | 'B' | 'x' | 'g' | 'G' | 'D' | 'R' | 'Q' => { 732 732 // `B` (IRCv3 bot mode), `x` (IP cloak), `g`/`G` (strict/soft caller-id, 733 - // slices 216/218), `D` (deaf, slice 247) and `R` (registered-only messages, 734 - // slice 248) are leveva-native but, unlike `W`, a client *may* set/clear them 735 - // on itself (`MODE <nick> +B`/`+x`/`+g`/`+G`/`+D`/`+R`). The `+x` host change 736 - // itself is applied after the loop (it needs the net old→new diff, not the 737 - // per-char step); the caller-id notify-state reset is likewise applied after. 733 + // slices 216/218), `D` (deaf, slice 247), `R` (registered-only messages, 734 + // slice 248) and `Q` (no-forward, slice 250) are leveva-native but, unlike 735 + // `W`, a client *may* set/clear them on itself (`MODE <nick> 736 + // +B`/`+x`/`+g`/`+G`/`+D`/`+R`/`+Q`). The `+x` host change itself is applied 737 + // after the loop (it needs the net old→new diff, not the per-char step); the 738 + // caller-id notify-state reset is likewise applied after. 738 739 let bit = UserMode::from_char(c) 739 - .expect("i/w/B/x/g/G/D/R are user modes") 740 + .expect("i/w/B/x/g/G/D/R/Q are user modes") 740 741 .bit(); 741 742 if adding { 742 743 modes |= bit; ··· 880 881 UserMode::CallerId, 881 882 UserMode::Deaf, 882 883 UserMode::RegOnlyMsg, 884 + UserMode::NoForward, 883 885 ] { 884 886 let was = old & m.bit() != 0; 885 887 let now = new & m.bit() != 0;
+1 -1
leveva/src/isupport.rs
··· 140 140 141 141 #[test] 142 142 fn mode_strings_come_from_tables() { 143 - assert_eq!(user_modes_string(), "oOiwraWBxsgGZDR"); 143 + assert_eq!(user_modes_string(), "oOiwraWBxsgGZDRQ"); 144 144 // ChanMode::ALL order: p s m n t i O C c S z T g o v q a h Y b e I R k l j (C=no-CTCP slice 145 145 // 220, c=no-color slice 232, S=TLS-only slice 233, z=op-moderation slice 235, 146 146 // j=join-throttle slice 234, T=no-NOTICE slice 239, g=free-invite slice 240,
+34 -1
leveva/src/mode.rs
··· 146 146 /// [`SEND_UMODES`](crate::s2s::umode::SEND_UMODES): a remote server never delivers to the `+R` 147 147 /// user directly and so never needs to know. Appended **last** so all bits above are untouched. 148 148 RegOnlyMsg, 149 + /// `+Q` — no-forward: this user is never redirected by a `+f` channel (P11 slice 250). 150 + /// 151 + /// **leveva-native — there is no C oracle for this** (the charybdis `UMODE_NOFORWARD` 152 + /// convention; the *user*-side counterpart of the channel `+f` forwarding feature, slice 242). 153 + /// When a `+Q` user's JOIN is refused by a *forwardable* reject (`+i`/`+l`/`+b`/`+r`) on a 154 + /// channel with a `+f` target, the forward is **suppressed** — they receive the source 155 + /// channel's original reject numeric instead of a `470 RPL_LINKCHANNEL` + redirect. Like 156 + /// `+B`/`+x`/`+g`/`+D`/`+R` it is **client-settable** (`MODE <nick> +Q`). The gate runs where 157 + /// the JOIN is processed — i.e. the joining user's home server, the only place a forward ever 158 + /// happens (a remote `NJOIN` is never forwarded) — so, like `+g`/`+G`/`+D`/`+R`, it is **purely 159 + /// local** and **not** in [`SEND_UMODES`](crate::s2s::umode::SEND_UMODES). (Note: the disjoint 160 + /// **channel** mode `+Q` [`DisableForward`](ChanMode::DisableForward) reuses this letter in its 161 + /// own table — the channel refuses *incoming* forwards; this user refuses *being* forwarded.) 162 + /// Appended **last** so all bits above are untouched. 163 + NoForward, 149 164 } 150 165 151 166 impl UserMode { 152 167 /// Every user mode, in `user_modes[]` table order (with the leveva-native 153 168 /// `+W` appended last). 154 - pub const ALL: [UserMode; 15] = [ 169 + pub const ALL: [UserMode; 16] = [ 155 170 UserMode::Oper, 156 171 UserMode::LocalOp, 157 172 UserMode::Invisible, ··· 167 182 UserMode::Secure, 168 183 UserMode::Deaf, 169 184 UserMode::RegOnlyMsg, 185 + UserMode::NoForward, 170 186 ]; 171 187 172 188 /// The mode letter, e.g. `'o'`. ··· 188 204 UserMode::Secure => 'Z', 189 205 UserMode::Deaf => 'D', 190 206 UserMode::RegOnlyMsg => 'R', 207 + UserMode::NoForward => 'Q', 191 208 } 192 209 } 193 210 ··· 211 228 UserMode::Secure => 0x1000, // leveva-native (no C oracle, slice 227) 212 229 UserMode::Deaf => 0x2000, // leveva-native (no C oracle, slice 247) 213 230 UserMode::RegOnlyMsg => 0x4000, // leveva-native (no C oracle, slice 248) 231 + UserMode::NoForward => 0x8000, // leveva-native (no C oracle, slice 250) 214 232 } 215 233 } 216 234 ··· 234 252 UserMode::Secure => "FLAGS_SECURE", 235 253 UserMode::Deaf => "FLAGS_DEAF", 236 254 UserMode::RegOnlyMsg => "FLAGS_REGONLYMSG", 255 + UserMode::NoForward => "FLAGS_NOFORWARD", 237 256 } 238 257 } 239 258 ··· 256 275 'Z' => UserMode::Secure, 257 276 'D' => UserMode::Deaf, 258 277 'R' => UserMode::RegOnlyMsg, 278 + 'Q' => UserMode::NoForward, 259 279 _ => return None, 260 280 }) 261 281 } ··· 279 299 0x1000 => UserMode::Secure, 280 300 0x2000 => UserMode::Deaf, 281 301 0x4000 => UserMode::RegOnlyMsg, 302 + 0x8000 => UserMode::NoForward, 282 303 _ => return None, 283 304 }) 284 305 } ··· 331 352 modes & UserMode::RegOnlyMsg.bit() != 0 332 353 } 333 354 355 + /// Whether `modes` carries the no-forward (`+Q`, [`UserMode::NoForward`]) flag — the predicate 356 + /// that suppresses `+f` channel forwarding for a user (P11 slice 250). Exactly the `+Q` bit test, 357 + /// independent of every other mode; the JOIN-time decision core (and its fuzz seam). When set, a 358 + /// *forwardable* JOIN reject falls through to the source channel's original numeric instead of a 359 + /// `470 RPL_LINKCHANNEL` + redirect. (Distinct from the **channel** mode `+Q` 360 + /// [`ChanMode::DisableForward`], which refuses *incoming* forwards.) 361 + #[inline] 362 + pub fn is_noforward(modes: u32) -> bool { 363 + modes & UserMode::NoForward.bit() != 0 364 + } 365 + 334 366 impl core::fmt::Display for UserMode { 335 367 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 336 368 f.write_str(match self { ··· 349 381 UserMode::Secure => "Z", 350 382 UserMode::Deaf => "D", 351 383 UserMode::RegOnlyMsg => "R", 384 + UserMode::NoForward => "Q", 352 385 }) 353 386 } 354 387 }
+3 -2
leveva/src/registration.rs
··· 1256 1256 assert_eq!(p[2], "leveva-test"); 1257 1257 // oOiwra (the oracle set) + W (websocket) + B (bot) + x (cloak) leveva-native umodes 1258 1258 // + s (server notices, re-introduced slice 184) + g/G (strict/soft caller-id, 216/218) 1259 - // + Z (secure/TLS, slice 227) + D (deaf, slice 247) + R (regonly-msg, slice 248). 1260 - assert_eq!(p[3], "oOiwraWBxsgGZDR"); 1259 + // + Z (secure/TLS, slice 227) + D (deaf, slice 247) + R (regonly-msg, slice 248) 1260 + // + Q (no-forward, slice 250). 1261 + assert_eq!(p[3], "oOiwraWBxsgGZDRQ"); 1261 1262 // + the leveva-native elemental membership letters q/a/h (slice 158) and Y (OJOIN, 165) 1262 1263 // + C (no-CTCP, slice 220) + c (no-color, slice 232) + S (TLS-only, slice 233) 1263 1264 // + z (op-moderation, slice 235) + j (join-throttle, slice 234) + T (no-NOTICE, slice 239)
+12 -2
leveva/src/s2s/umode.rs
··· 296 296 0, 297 297 "soft caller-id is local-only" 298 298 ); 299 + // `+Q` (no-forward, slice 250) is purely local — forwarding only ever happens at the 300 + // joining user's home server (a remote NJOIN is never forwarded), so a transit/origin 301 + // server needs no copy — so like `+O`/`+x`/`+s`/`+g`/`+G`/`+D`/`+R` its bit is excluded. 302 + assert_eq!( 303 + SEND_UMODES & UserMode::NoForward.bit(), 304 + 0, 305 + "no-forward is local-only" 306 + ); 299 307 // Every mode except the purely-local `+O` locop, the host-change-carried `+x`, the 300 308 // local-only `+s` server-notice / `+g`/`+G` caller-id flags, `+D` deaf (the channel 301 - // fan-out filter runs on the deaf user's home server) and `+R` registered-only messages 302 - // (the private-message gate runs on the target's home server) is in the mask. 309 + // fan-out filter runs on the deaf user's home server), `+R` registered-only messages 310 + // (the private-message gate runs on the target's home server) and `+Q` no-forward (the 311 + // forward decision runs on the joiner's home server) is in the mask. 303 312 for m in UserMode::ALL { 304 313 if matches!( 305 314 m, ··· 310 319 | UserMode::SoftCallerId 311 320 | UserMode::Deaf 312 321 | UserMode::RegOnlyMsg 322 + | UserMode::NoForward 313 323 ) { 314 324 continue; 315 325 }
+85
leveva/tests/golden_noforward.rs
··· 1 + //! Boot-level golden: the **`+Q` no-forward user mode** (charybdis `UMODE_NOFORWARD`) 2 + //! end-to-end through the real `leveva` binary (P11 slice 250). 3 + //! 4 + //! `+Q` is the *user*-side counterpart of the channel `+f` forwarding feature (slice 242): 5 + //! a user who sets `+Q` on themselves is **never** redirected by a `+f` channel — a 6 + //! forwardable JOIN reject falls through to the source channel's original error numeric 7 + //! instead of a `470` + forward. (This is the disjoint user-mode letter `Q`; the channel 8 + //! mode `+Q` `MODE_DISFORWARD` reuses the same letter in its own table — see slice 243.) 9 + //! 10 + //! `alice` owns `#lobby` (the forward target) and `#secret` (which she makes `+i` forwarding 11 + //! to `#lobby`). Then: 12 + //! 13 + //! - `bob` sets `+Q`, JOINs `#secret`, is refused by `+i`, and is **not** forwarded: he gets 14 + //! a plain `473` with no `470` and no `JOIN #lobby`; 15 + //! - `bob` clears `-Q` and JOINs `#secret` again: now he **is** forwarded (`470` + `JOIN 16 + //! #lobby`) — the inverse, proving the suppression is exactly the `+Q` bit and nothing else. 17 + //! 18 + //! Host/SID are fixed (ident off), so the gate fires deterministically — no canonicalizer. 19 + 20 + mod harness; 21 + use harness::{boot, Client}; 22 + 23 + /// Register a plain client (`NICK`/`USER`), reading through the `422` (no-MOTD) sentinel. 24 + fn register(c: &mut Client, nick: &str) { 25 + c.send(&format!("NICK {nick}")); 26 + c.send(&format!("USER {nick} 0 * :{nick}")); 27 + c.read_until(" 422 "); 28 + } 29 + 30 + #[test] 31 + fn plus_q_user_is_not_forwarded_by_a_plus_f_channel() { 32 + let _srv = boot(); 33 + 34 + // alice: #lobby (forward target) + #secret (+i forwarding to #lobby). 35 + let mut alice = Client::connect(); 36 + register(&mut alice, "alice"); 37 + alice.send("JOIN #lobby"); 38 + alice.read_until(" 366 "); 39 + alice.send("JOIN #secret"); 40 + alice.read_until(" 366 "); 41 + alice.send("MODE #secret +i"); 42 + alice.read_until(" MODE #secret +i"); 43 + alice.send("MODE #secret +f #lobby"); 44 + alice.read_until(" MODE #secret +f #lobby"); 45 + 46 + // bob: sets +Q (no-forward), then JOIN #secret. The +i reject is forwardable but bob's +Q 47 + // suppresses the redirect: a plain 473, no 470, no JOIN #lobby. 48 + let mut bob = Client::connect(); 49 + register(&mut bob, "bob"); 50 + bob.send("MODE bob +Q"); 51 + assert!( 52 + bob.read_until(" MODE bob ").contains("+Q"), 53 + "the +Q self-set is echoed" 54 + ); 55 + bob.send("JOIN #secret"); 56 + bob.send("PING barrier1"); 57 + let blocked = bob.read_until("PONG"); 58 + assert!( 59 + blocked.contains(" 473 "), 60 + "a +Q user gets the source channel's plain 473: {blocked:?}" 61 + ); 62 + assert!( 63 + !blocked.contains(" 470 ") && !blocked.contains(" JOIN #lobby"), 64 + "a +Q user is NOT forwarded: {blocked:?}" 65 + ); 66 + 67 + // Inverse: bob clears -Q and re-JOINs #secret → now forwarded (470 + JOIN #lobby). 68 + bob.send("MODE bob -Q"); 69 + bob.read_until(" MODE bob "); 70 + bob.send("JOIN #secret"); 71 + bob.send("PING barrier2"); 72 + let fwd = bob.read_until("PONG"); 73 + assert!( 74 + fwd.contains(" 470 ") && fwd.contains("#secret") && fwd.contains("#lobby"), 75 + "without +Q the forwardable reject redirects: 470 #secret #lobby: {fwd:?}" 76 + ); 77 + assert!( 78 + fwd.contains(" JOIN #lobby"), 79 + "without +Q bob ends up joined to the forward target #lobby: {fwd:?}" 80 + ); 81 + assert!( 82 + !fwd.contains(" JOIN #secret"), 83 + "bob never joins the +i source #secret: {fwd:?}" 84 + ); 85 + }
+106
leveva/tests/noforward_proptest.rs
··· 1 + //! Property-based fuzzing of the `+Q` (no-forward) user-mode decision seam 2 + //! ([`leveva::mode::is_noforward`]) and its interaction with the `+f` channel-forwarding 3 + //! decision (P11 slice 250, charybdis `UMODE_NOFORWARD`). 4 + //! 5 + //! `+Q` suppresses `+f` forwarding for a user: at JOIN time the redirect fires only when 6 + //! `allow_forward && !is_noforward(modes) && forwardable(reject)` (the condition in 7 + //! [`leveva::command::join`]). The predicate must be **decisive on exactly the `0x8000` bit**, 8 + //! independent of every other umode bit; `'Q'` must round-trip through `from_char` and the bit 9 + //! with no aliasing; and — the property that matters — a `+Q` user must be forwarded for **no** 10 + //! combination of reject kind, `allow_forward`, or co-set modes. 11 + 12 + use leveva::channel::{forwardable, JoinReject}; 13 + use leveva::mode::{is_noforward, UserMode}; 14 + use proptest::prelude::*; 15 + 16 + /// Reference specification for `is_noforward`, independently coded. 17 + fn noforward_model(modes: u32) -> bool { 18 + modes & 0x8000 != 0 19 + } 20 + 21 + /// The forward-decision composite as the JOIN handler computes it: a forwardable reject on a 22 + /// channel the joiner could be redirected from, **unless** the joiner is `+Q`. 23 + fn would_forward(allow_forward: bool, modes: u32, reject: JoinReject) -> bool { 24 + allow_forward && !is_noforward(modes) && forwardable(reject) 25 + } 26 + 27 + /// A strategy over every `JoinReject` variant (the four forwardable + the four hard gates). 28 + fn any_reject() -> impl Strategy<Value = JoinReject> { 29 + prop_oneof![ 30 + Just(JoinReject::BadKey), 31 + Just(JoinReject::Full), 32 + Just(JoinReject::InviteOnly), 33 + Just(JoinReject::Banned), 34 + Just(JoinReject::OperOnly), 35 + Just(JoinReject::SecureOnly), 36 + Just(JoinReject::RegisteredOnly), 37 + ] 38 + } 39 + 40 + proptest! { 41 + /// `is_noforward` matches the model over arbitrary mode words — decisive on the `0x8000` bit 42 + /// alone, regardless of which other bits are set. 43 + #[test] 44 + fn is_noforward_matches_model(modes in any::<u32>()) { 45 + prop_assert_eq!(is_noforward(modes), noforward_model(modes)); 46 + } 47 + 48 + /// Setting `+Q` on any other combination of modes makes `is_noforward` true; clearing it makes 49 + /// it false — the bit is independent of its neighbours. 50 + #[test] 51 + fn noforward_is_independent_of_other_bits(other in any::<u32>()) { 52 + let without = other & !UserMode::NoForward.bit(); 53 + prop_assert!(!is_noforward(without), "no +Q bit → not no-forward"); 54 + prop_assert!(is_noforward(without | UserMode::NoForward.bit()), "with +Q bit → no-forward"); 55 + } 56 + 57 + /// THE invariant: a `+Q` user is **never** forwarded, for any reject kind, any `allow_forward`, 58 + /// and any other modes set alongside `+Q`. 59 + #[test] 60 + fn plus_q_never_forwards( 61 + other in any::<u32>(), 62 + allow_forward in any::<bool>(), 63 + reject in any_reject(), 64 + ) { 65 + let modes = other | UserMode::NoForward.bit(); 66 + prop_assert!(!would_forward(allow_forward, modes, reject), "a +Q user must never forward"); 67 + } 68 + 69 + /// Conversely, when `+Q` is clear the decision is exactly `allow_forward && forwardable` — 70 + /// `+Q` is the only umode that gates forwarding (no other bit suppresses or enables it). 71 + #[test] 72 + fn without_q_forward_is_allow_and_forwardable( 73 + other in any::<u32>(), 74 + allow_forward in any::<bool>(), 75 + reject in any_reject(), 76 + ) { 77 + let modes = other & !UserMode::NoForward.bit(); 78 + prop_assert_eq!( 79 + would_forward(allow_forward, modes, reject), 80 + allow_forward && forwardable(reject), 81 + "without +Q, forwarding depends only on allow_forward and the reject kind" 82 + ); 83 + } 84 + } 85 + 86 + /// `'Q'` round-trips through the letter and the bit, and nothing else aliases onto `NoForward`. 87 + #[test] 88 + fn noforward_letter_and_bit_are_unique() { 89 + assert_eq!(UserMode::from_char('Q'), Some(UserMode::NoForward)); 90 + assert_eq!(UserMode::from_bit(UserMode::NoForward.bit()), Some(UserMode::NoForward)); 91 + assert_eq!(UserMode::NoForward.as_char(), 'Q'); 92 + assert_eq!(UserMode::NoForward.bit(), 0x8000); 93 + 94 + // Exhaustive: over every ASCII letter, only 'Q' maps to NoForward; over every mode in 95 + // UserMode::ALL, only NoForward's bit maps back to NoForward. 96 + for c in ('a'..='z').chain('A'..='Z') { 97 + if c != 'Q' { 98 + assert_ne!(UserMode::from_char(c), Some(UserMode::NoForward), "{c} aliases +Q"); 99 + } 100 + } 101 + for m in UserMode::ALL { 102 + if m != UserMode::NoForward { 103 + assert_ne!(m.bit(), UserMode::NoForward.bit(), "{m} shares the +Q bit"); 104 + } 105 + } 106 + }
+1 -1
leveva/tests/snapshots/golden_cap__cap_302_negotiation_suspends_then_resumes.snap
··· 7 7 :leveva.test 001 dan :Welcome to the Internet Relay Network dan!d@127.0.0.1 8 8 :leveva.test 002 dan :Your host is leveva.test, running version leveva-<VER> 9 9 :leveva.test 003 dan :This server was created <MASKED> 10 - :leveva.test 004 dan leveva.test leveva-<VER> oOiwraWBxsgGZDR psmntiOCcSzTgrQFPLovqahYbeIRkljf 10 + :leveva.test 004 dan leveva.test leveva-<VER> oOiwraWBxsgGZDRQ psmntiOCcSzTgrQFPLovqahYbeIRkljf 11 11 :leveva.test 005 dan CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,ljf,psmntiOCcSzTgrQFPL PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 12 12 :leveva.test 005 dan BOT=B WHOX MONITOR=100 UTF8ONLY CALLERID=g ELIST=CMNTU SAFELIST NETWORK=TestNet :are supported by this server 13 13 :leveva.test 251 dan :There are 1 users and 0 services on 1 servers
+1 -1
leveva/tests/snapshots/golden_cap__cap_ls_without_version_then_end.snap
··· 6 6 :leveva.test 001 jo :Welcome to the Internet Relay Network jo!j@127.0.0.1 7 7 :leveva.test 002 jo :Your host is leveva.test, running version leveva-<VER> 8 8 :leveva.test 003 jo :This server was created <MASKED> 9 - :leveva.test 004 jo leveva.test leveva-<VER> oOiwraWBxsgGZDR psmntiOCcSzTgrQFPLovqahYbeIRkljf 9 + :leveva.test 004 jo leveva.test leveva-<VER> oOiwraWBxsgGZDRQ psmntiOCcSzTgrQFPLovqahYbeIRkljf 10 10 :leveva.test 005 jo CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,ljf,psmntiOCcSzTgrQFPL PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 11 11 :leveva.test 005 jo BOT=B WHOX MONITOR=100 UTF8ONLY CALLERID=g ELIST=CMNTU SAFELIST NETWORK=TestNet :are supported by this server 12 12 :leveva.test 251 jo :There are 1 users and 0 services on 1 servers
+1 -1
leveva/tests/snapshots/golden_pass__correct_password_welcomes.snap
··· 5 5 :leveva.test 001 alice :Welcome to the Internet Relay Network alice!alice@127.0.0.1 6 6 :leveva.test 002 alice :Your host is leveva.test, running version leveva-<VER> 7 7 :leveva.test 003 alice :This server was created <MASKED> 8 - :leveva.test 004 alice leveva.test leveva-<VER> oOiwraWBxsgGZDR psmntiOCcSzTgrQFPLovqahYbeIRkljf 8 + :leveva.test 004 alice leveva.test leveva-<VER> oOiwraWBxsgGZDRQ psmntiOCcSzTgrQFPLovqahYbeIRkljf 9 9 :leveva.test 005 alice CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,ljf,psmntiOCcSzTgrQFPL PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 10 10 :leveva.test 005 alice BOT=B WHOX MONITOR=100 UTF8ONLY CALLERID=g ELIST=CMNTU SAFELIST NETWORK=TestNet :are supported by this server 11 11 :leveva.test 251 alice :There are 1 users and 0 services on 1 servers
+1 -1
leveva/tests/snapshots/golden_registration__registration_welcome_burst.snap
··· 5 5 :leveva.test 001 alice :Welcome to the Internet Relay Network alice!alice@127.0.0.1 6 6 :leveva.test 002 alice :Your host is leveva.test, running version leveva-<VER> 7 7 :leveva.test 003 alice :This server was created <MASKED> 8 - :leveva.test 004 alice leveva.test leveva-<VER> oOiwraWBxsgGZDR psmntiOCcSzTgrQFPLovqahYbeIRkljf 8 + :leveva.test 004 alice leveva.test leveva-<VER> oOiwraWBxsgGZDRQ psmntiOCcSzTgrQFPLovqahYbeIRkljf 9 9 :leveva.test 005 alice CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,ljf,psmntiOCcSzTgrQFPL PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 10 10 :leveva.test 005 alice BOT=B WHOX MONITOR=100 UTF8ONLY CALLERID=g ELIST=CMNTU SAFELIST NETWORK=TestNet :are supported by this server 11 11 :leveva.test 251 alice :There are 1 users and 0 services on 1 servers
+1 -1
leveva/tests/snapshots/golden_user_spaceless__spaceless_user_realname_welcome_burst.snap
··· 5 5 :leveva.test 001 alice :Welcome to the Internet Relay Network alice!alice@127.0.0.1 6 6 :leveva.test 002 alice :Your host is leveva.test, running version leveva-<VER> 7 7 :leveva.test 003 alice :This server was created <MASKED> 8 - :leveva.test 004 alice leveva.test leveva-<VER> oOiwraWBxsgGZDR psmntiOCcSzTgrQFPLovqahYbeIRkljf 8 + :leveva.test 004 alice leveva.test leveva-<VER> oOiwraWBxsgGZDRQ psmntiOCcSzTgrQFPLovqahYbeIRkljf 9 9 :leveva.test 005 alice CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,ljf,psmntiOCcSzTgrQFPL PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 10 10 :leveva.test 005 alice BOT=B WHOX MONITOR=100 UTF8ONLY CALLERID=g ELIST=CMNTU SAFELIST NETWORK=TestNet :are supported by this server 11 11 :leveva.test 251 alice :There are 1 users and 0 services on 1 servers