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.

1# P10 — Rename to `leveva` + typed identifier string types 2 3P10 is the idiomatic-Rust end state: the workspace is renamed to **`leveva`** and gains 4validating newtype strings for every IRC identifier class. Greenfield Rust — no `cref_` 5oracle, no `extern "C"` seam, no `ircd-sys` dependency. Tests are ordinary Rust unit / 6doc tests. 7 8> Note: P10 is being *started early*, before P8/P9, at the user's request — the string 9> foundation is useful to have in place. The crate/binary **rename** of the `ircd-*` 10> crates is still deferred to the real P10 slot (after P8 deletes the C and P9 does the 11> std-lib cleanup). 12 13## Entries 14 15- **2026-06-07 — `leveva` crate bootstrapped: RFC 1459 string layer.** New workspace 16 member `leveva/` (std-only, no deps). Foundation modules: 17 - **`casemap`** — textbook RFC 1459 case folding: `A-Z↔a-z` **plus** the Scandinavian 18 set `[↔{ \↔| ]↔} ~↔^` (`to_lower`/`to_upper` `const fn`s + `eq_ignore_case`/`cmp`/ 19 `hash` byte-slice helpers). Deliberately differs from the faithful-port `tolowertab` 20 in `common/match.c`, which is ASCII-only — confirmed by reading the C table; `leveva` 21 implements the rule the RFC actually specifies (user decision). 22 - **`string`** — `IrcStr` (`#[repr(transparent)]` over `str`, one localized `unsafe` 23 cast like std `Path`) and `IrcString` (`String`), whose `Eq`/`Ord`/`Hash` are RFC 24 1459 case-insensitive while `Display` preserves original casing. `IrcString: Borrow<IrcStr>` 25 so it keys a `HashMap` looked up by `&IrcStr`. Construction rejects the framing bytes 26 NUL/CR/LF (`IrcError::Forbidden`). Plus `IrcStringBuilder`. 27 - **`ident`** — validating newtypes around `IrcString`, generated by an `ident_newtype!` 28 macro: `Nick` (do_nick_name rule: no leading `-`/digit, not `anonymous`, NVALID char 29 class, NICKLEN 15), `ChanName` (`#&+!` prefix + chanstring, CHANNELLEN 50), `Sid`/`Uid`/ 30 `Cid` (exact-length base-36 `A-Z0-9`: 4/9/5), `ServerName`, `UserName` (USERLEN 10), 31 `HostName` (HOSTLEN 63). `IdentError` enum. Grammars grounded in `common/struct_def.h`, 32 `common/match.c char_atribs`, `ircd/s_user.c do_nick_name`, `ircd/s_id.c`. 33 - **`message`** — `Message`/`MessageBuilder`: `prefix`/`param(s)`/`trailing` (each `impl 34 Display`, so typed idents pass through) → `to_line()` and 512-byte-safe CRLF 35 `to_wire()` (truncation respects UTF-8 char boundaries). 36 - **Gate:** `cargo test -p leveva` — 27 unit + 4 doc tests green; `cargo clippy -p leveva` 37 clean; 0 warnings. Each identifier type round-trips its legal inputs **and** rejects 38 every illegal class (the inverse-invariant discipline). Plan: 39 `docs/superpowers/plans/2026-06-07-leveva-rfc1459-string.md`. 40 41- **2026-06-07 — `leveva::patricia`: generic longest-prefix-match trie.** Idiomatic, 42 100% safe Rust descendant of `ircd/patricia.c` (which was a CIDR-only, `void*`-valued, 43 raw-pointer/`MyMalloc` trie). User decisions: **generic core + IP helpers**, **idiomatic 44 Rust API**. 45 - **Generic over the value** `V` (no `void*`) **and over the key** — a `Prefix` is just 46 *bytes + a bit length*, so the trie serves IP CIDRs, MAC ranges, routing labels, any 47 hierarchical bit key. `Prefix` masks insignificant trailing bits on construction, so 48 `10.5.0.0/8 == 10.0.0.0/8` (correct network identity for `Eq`/`Hash`). 49 - **No `unsafe`, no raw pointers.** Nodes live in a `Vec<Option<Node>>` arena with a 50 free list; children/parent are `Option<usize>` indices. The C `node->bit < maxbits` 51 OOB guard is subsumed by `bit_test` reading past-end bits as `0`, which also makes 52 mixed-width keys safe. Removed slots are recycled. 53 - **API:** `new`/`insert`(→old `Option<V>`)/`get`/`get_mut`/`contains`/`remove`/ 54 `longest_match`/`iter` (+ `Default`/`FromIterator`/`Extend`/`IntoIterator`/`Debug`). 55 IP layer: `Prefix::{from_ipv4,from_ipv6,from_ip,host}`, `IpCidr` (`FromStr`: 56 `"10.0.0.0/8"` or bare host; `CidrParseError`), `insert_cidr`/`get_cidr`/`remove_cidr`/ 57 `longest_match_ip(impl Into<IpAddr>)`. 58 - **Faithfulness:** `insert`/`search_exact`/`search_best`(inclusive)/`remove` ported from 59 the C algorithms incl. glue-node creation and the three-case removal (two-children → 60 demote to glue; leaf → unlink + fold glue parent; one-child → splice up). Iteration is 61 deterministic left-first pre-order (not claimed byte-identical to the C `PATRICIA_WALK`). 62 - **Gate:** `cargo test -p leveva` — 17 patricia unit tests + 1 doc test green (full 63 crate 41 unit + 5 doc); `cargo clippy -p leveva --all-targets` clean; 0 warnings. 64 Tests pair every positive with its inverse (insert→remove→gone, glue fold + slot reuse, 65 remove-all → empty, longest-match fallback after removal, 256-prefix stress with 66 odd-removal). 67 68- **2026-06-07 — `leveva::numeric`: the `Numeric` reply/error enum.** Pure-data port of 69 the complete numeric table in `common/numeric_def.h` (184 codes, 001–759) into a single 70 `#[repr(u16)] #[non_exhaustive]` enum whose **discriminant is the three-digit code**, so 71 `n as u16` / `Numeric::code()` is the wire value directly. 72 - **Naming:** variants keep the `Rpl`/`Err` prefix with the remainder title-cased 73 (`RplWelcome`, `ErrNosuchnick`). The prefix is load-bearing — dropping it would collide 74 `RPL_STATSKLINE` (216) with `ERR_STATSKLINE` (499), the only such clash in the table. 75 The canonical C symbol is always recoverable via `Numeric::name()` (`"RPL_WELCOME"`). 76 - **API:** `code()`, `name()`, `is_error()` (the `400`–`599` convention), `From<Numeric> 77 for u16`, `TryFrom<u16>` (→ `UnknownNumeric(u16)`), and a `Display` that renders the 78 zero-padded three-digit wire token (`001`). `RPL_WHOISTLS` is exposed as an associated 79 `const` aliasing `RplWhoisextra` (320), mirroring the C `#define` (a repr enum can't 80 carry a duplicate discriminant). 81 - **Gate:** `cargo test -p leveva` — 8 numeric unit + 1 doc test green; clippy clean; 82 0 warnings. Tests cover code==discriminant, three-digit padding, name round-trip, 83 known/unknown `TryFrom`, the STATSKLINE 216-vs-499 distinction, and the WHOISTLS alias. 84 85- **2026-06-07 — `leveva::mode`: the `UserMode` and `ChanMode` enums.** Pure-data port of 86 the mode tables, grounded in `ircd/s_user.c`, `ircd/channel.c`, and `common/struct_def.h`. 87 - **`UserMode`** — the six modes in `user_modes[]` (`o O i w r a`), bits from the 88 `FLAGS_*` constants. `as_char`/`TryFrom<char>`, `bit`/`from_bit`, `flag_name()` 89 (`"FLAGS_OPER"`), `ALL` in table order, and `render(bits)` (set letters, table order). 90 - **`ChanMode`** — all 17 channel mode letters, categorised by `ChanModeKind` into 91 `Flag` (`p s m n t i a r`), `Membership` (`o v O`), `List` (`b e I R`), and `Param` 92 (`k l`). `bit()` returns the `MODE_*` value (membership bits double as the low `CHFL_*` 93 bits); `mode_name()` recovers the C symbol; `kind()`/`takes_parameter()`; `render(bits)` 94 emits **only** flag-kind letters in `flags[]` order (membership/list/param can't be 95 reconstructed from a channel bitmask alone). `MODE_QUIET` (0x02000) is **omitted** — it 96 has no mode letter and never appears on the wire. 97 - **Grounding caveats:** `+I` (`MODE_INVITE`, invite-exception list) is kept distinct from 98 `+i` (`MODE_INVITEONLY`); `takes_parameter()` documents that the oracle still omits the 99 parameter specifically when *removing* `+l` (parse behaviour, left to the handler port). 100 - **Gate:** `cargo test -p leveva` — 8 mode unit + 1 doc test green (full crate 56 unit + 101 7 doc); clippy clean; 0 warnings. Tests cover char/bit round-trips against the oracle 102 values, the four `ChanModeKind` categories, and table-ordered `render`. 103 104- **2026-06-07 — `leveva::config`: KDL-based, typed ircd configuration.** The first leveva 105 module with **dependencies** (`kdl` v6 `+v1-fallback` and `thiserror` v2 — the exact 106 versions `iauth-rs` pins, so the lockfile is shared). Replaces the C daemon's 107 pipe-delimited single-letter config (the `M`/`A`/`Y`/`P`/`I`/`O`/`C`/`N`/`H`/`L`/`K` 108 lines parsed in `ircd/config_read.c` + `ircd/s_conf.c`) with a KDL document parsed into a 109 typed `Config`. Plan: `docs/superpowers/plans/2026-06-07-leveva-config-kdl.md`. User 110 decisions: **lives in `leveva`** (deps are fine), **idiomatic redesign** (not a faithful 111 line-mirror), **pragmatic core** scope. 112 - **Idiomatic schema, not a line-mirror:** `C`+`N` merge into one `connect` block 113 (`send-password`/`accept-password`) with `H`/`L` folded into a `role "hub" "*"` / 114 `role "leaf"` property; `I`/`O`-line flag letters become named booleans; the `O`-line 115 `ACL_*` flag string becomes an `OperPrivilege` enum (20 granular variants, grounded in 116 `struct_def.h:1043-1070` + the letter table in `s_conf.c:306-383`). Tokens `"kill"`/ 117 `"squit"`/`"connect"` expand to local+remote; `"all"` matches the C `A` letter 118 (everything bar clients/no-penalty/can-flood); `ACL_LOCOP` is the `local` field, not a 119 privilege. 120 - **Typed by construction:** identifier fields (`server.name`, `server.sid`, `connect` 121 name) parse through leveva's `ServerName`/`Sid` newtypes, so a malformed value is a 122 `ConfigError::BadField` at parse time. Glob/CIDR patterns (`user@host`, `*.x`) stay 123 `String`. 124 - **Passwords classified, not carried verbatim:** `operator`/`connect`/`allow` passwords 125 are a `Password` enum (`Hashed`|`Plain`). You can't reliably distinguish a hash from 126 plaintext (a 13-char alnum plaintext == a DES `crypt(3)` hash), so classification is 127 conservative: a `(hash)`/`(plain)` **KDL type annotation** (read via `entry.ty()`) 128 forces the choice — and a declared `(hash)` is structurally validated, so a typo'd hash 129 is a `BadField` at load, not a failed login; with no annotation, a well-formed Modular 130 Crypt Format value (`$id$…`) is auto-detected as a hash and everything else (incl. bare 131 DES-13) is `Plain`. `warnings()` flags plaintext operator/link passwords. (The C makes 132 this a *compile-time* choice — `CRYPT_OPER_PASSWORD`, off in this build — and uses the 133 config password as the `crypt()` salt; per-password classification is a deliberate 134 leveva improvement.) 135 - **Parsing** mirrors `iauth-rs/src/config.rs`: `src.parse::<KdlDocument>()`, walk 136 `doc.nodes()` by name, read each leaf as a child node's single positional arg; typed 137 helpers (`req_string`/`opt_string`/`flag`/`req_u32`/…). Note kdl v6 `as_integer()` 138 returns **`i128`** (range-checked into `u16`/`u32`/`u64`). Scope: `server`, `admin`, 139 `options`, `class`, `listen`, `allow`, `operator`, `connect`, `ban`. Cross-reference 140 validation (every `class` named by allow/operator/connect is defined; singleton 141 `server`/`admin`; unique class names) + non-fatal `warnings()`. 142 - **KDL v2 gotcha:** bare `true`/`false` is **illegal** in KDL v2 — it only parses via the 143 fragile v1-fallback (which requires the *whole* document be valid v1, so it works 144 intermittently). Canonical form is `#true`/`#false`; a bare node name also means true. 145 The `flag()` helper + the example doc this. 146 - **Deferred** (out of this cut): the rare line types (bounce `B` / version `V` / 147 quarantine `Q` / deny `D` / gecos-`X` / service `S` / zip-connect `c`), a KDL **writer** 148 for rehash round-trip, a `Mask` newtype, and wiring into the daemon boot path (belongs 149 with the connection/rehash port — this module just parses → typed `Config`). 150 - **Gate:** `cargo test -p leveva` — 26 config unit + 1 doc test green (full crate 82 unit 151 + 8 doc); `cargo clippy -p leveva --all-targets` clean; 0 warnings. Worked example at 152 `leveva/examples/ircd.kdl` (also the happy-path test fixture via `include_str!`); each 153 error variant has a negative test, plus password classification (MCF auto-detect, 154 `(plain)` override, malformed declared hash, unknown annotation). 155 156- **2026-06-07 — `leveva::message` gains a parser + `leveva::matching` glob/hostmask.** The 157 two halves of the protocol-text layer that were missing. 158 - **`Message::parse` / `FromStr`** — the inverse of the existing builder: a raw line → 159 optional `:`-prefix, command, space-separated middle params, optional `:`-trailing. 160 Strips a trailing CRLF, tolerates runs of spaces, keeps a bare `:` as `Some("")` 161 (distinct from no trailing), preserves the command verbatim (case left to a future 162 `Command` enum), and round-trips with `to_line()`. `ParseError::{Empty, MissingCommand}`. 163 - **`leveva::matching`** — descendant of `match()`/`collapse()` (`common/match.c`). 164 `matches(mask, name)` supports `*`, `?`, `#` (the IRCnet **digit** wildcard — so a 165 literal `#` in a mask matches a digit; use `\#`), and `\`-escaping, case-insensitive 166 under leveva's **RFC 1459** casemapping (deliberate divergence from `match.c`'s 167 ASCII-only `tolowertab`, the same call the rest of leveva makes). Greedy `*` backtracking 168 is linear-time, so the C `MAX_ITERATIONS` cap is unnecessary. `collapse()` minimizes 169 redundant wildcards (`*?*?``*??`). 170 - **`HostMask`** — the `nick!user@host` pattern whose three components are matched 171 **independently** (faithful to `BanMatch`, `ircd/channel.c:49`). `parse()` mirrors 172 `parseNUH` (`channel.c:1815`): split on the **first** `!`, then the **last** `@`; with 173 no `!`, the text before `@` is the *nick* (a real gotcha — `foo@bar` ⇒ nick=`foo`, 174 host=`bar`), and empty components default to `*`. `matches_parts(nick,user,host)` is the 175 primary API (the server already has the three components); `matches_target("n!u@h")` is 176 the convenience. 177 - **Gate:** `cargo test -p leveva` — 8 message + 13 matching unit tests + their doc tests 178 green (full crate 101 unit + 10 doc); clippy `--all-targets` clean; 0 warnings. Covers 179 the parse round-trip, empty/extra-space/error cases, each wildcard + escape, the 180 no-catastrophic-backtracking shapes, `collapse` minimization, and the `parseNUH` split 181 edge cases. 182 183- **2026-06-07 — `leveva::matching` glob parsing moved to `nom` + a compiled `Pattern`.** 184 Replaced the hand-rolled byte-walker tokenizer with a `nom` parser (`nom` 7, the version 185 `ircd-common` already pins) that compiles a mask into a `Vec<Tok>`. The token `alt`'s final 186 any-byte arm makes the parser total, so `many0` consumes the whole mask without looping. New 187 public `Pattern` type (`compile` once, `is_match` many) so a ban/host mask checked against 188 many users isn't re-parsed per call; `matches()` is now the compile-once-match-once 189 convenience over it. Matching **semantics are unchanged** (`*`/`?`/`#`/`\`, RFC 1459 190 case-insensitive, linear greedy-`*` backtracking); `collapse()` and `HostMask` untouched. 191 Gate: full crate 103 unit + 11 doc green; clippy clean.