Proof of concept mechanical port of ircnet/ircd to Rust as part of a bit about C being insecure for network services
15 kB
191 lines
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.