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.

refactor: extract leveva-cidr leaf crate (+ proptest fuzzing)

Carve the pure CIDR network-mask helpers (parse_cidr / contains /
network_base / cidr_match) out of leveva into a self-contained
leveva-cidr leaf crate, re-exported as `leveva::cidr` — continuing the
post-P12 leaf-crate carve-out after casemap/string/message/patricia.

cidr is a genuinely pure leaf: zero workspace deps, only std::net. This
corrects the earlier leveva-string survey, which filed matching+cidr as
"mutually referential, not a clean single leaf yet" — cidr's references
to matching/connlimit are doc-comment-only; the real edge is one-way
matching -> cidr. Extracting cidr therefore *unblocks* matching, whose
only remaining intra-crate deps are now casemap + cidr (both crates).

git mv leveva/src/cidr.rs -> leveva-cidr/src/lib.rs verbatim except the
doctest import and two cross-crate doc links; leveva/src/lib.rs swaps
`pub mod cidr;` for `pub use leveva_cidr as cidr;`. All four call sites
use fully-qualified crate::cidr::*, so zero consumer churn.

Fuzzing (the user ask): 10 proptest invariants over the full v4/v6
address space and arbitrary in-range prefixes, all consistency/inverse
properties — parse round-trip + out-of-range reject, contains reflexive
through network_base, network_base idempotent, contains prefix-monotone,
/0 universal + full-length exact, family mismatch never contains, and
cidr_match == parse + contains.

Next extraction candidates: matching (now a clean leaf, the C match.c
cluster), cloak (zero intra-crate deps), uid (blocked on ident).

Gate: cargo build -p leveva 0 warnings; cargo clippy -p leveva-cidr
--tests clean; cargo test -p leveva-cidr 18 (8 unit + 10 fuzz) + 1
doctest green; leveva cidr-consumer tests (matching/connlimit/websocket,
54) green.

+297 -13
+8
Cargo.lock
··· 1204 1204 "insta", 1205 1205 "kdl 6.7.1", 1206 1206 "leveva-casemap", 1207 + "leveva-cidr", 1207 1208 "leveva-iauth", 1208 1209 "leveva-message", 1209 1210 "leveva-patricia", ··· 1234 1235 1235 1236 [[package]] 1236 1237 name = "leveva-casemap" 1238 + version = "0.0.0" 1239 + dependencies = [ 1240 + "proptest", 1241 + ] 1242 + 1243 + [[package]] 1244 + name = "leveva-cidr" 1237 1245 version = "0.0.0" 1238 1246 dependencies = [ 1239 1247 "proptest",
+1
Cargo.toml
··· 4 4 "leveva", 5 5 "leveva-iauth", 6 6 "leveva-casemap", 7 + "leveva-cidr", 7 8 "leveva-message", 8 9 "leveva-patricia", 9 10 "leveva-string",
+22 -9
docs/claude-memory/p12-complete.md
··· 52 52 consistent+antisymmetric, hash-agrees-with-eq, `Borrow<IrcStr>` round-trip, 53 53 validation soundness, builder≡try_from). 54 54 55 + - **`leveva-cidr`** — pure CIDR network-mask helpers 56 + (`parse_cidr`/`contains`/`network_base`/`cidr_match`; `leveva-cidr/src/lib.rs`, was 57 + `leveva/src/cidr.rs`). A **pure** leaf: **zero** workspace deps (only `std::net`). 58 + This corrected the prior survey's claim that `matching`+`cidr` were "mutually 59 + referential" — `cidr`'s references to `matching`/`connlimit` are doc-comment-only; 60 + the real edge is one-way `matching` → `cidr`. Extracting `cidr` therefore **unblocks 61 + `matching`** (its only intra-crate deps are now `casemap` + `cidr`, both crates). 62 + Shipped with a 10-property **proptest fuzz** module (parse round-trip + out-of-range 63 + reject, contains reflexive-through-network_base, network_base idempotent, contains 64 + prefix-monotone, `/0` universal + full-length exact, family mismatch never contains, 65 + `cidr_match ≡ parse + contains`). 66 + 55 67 `leveva` re-exports each as `leveva::message` / `leveva::patricia` / `leveva::casemap` 56 - / `leveva::string` via `pub use leveva_message as message;` (etc.) in 68 + / `leveva::string` / `leveva::cidr` via `pub use leveva_message as message;` (etc.) in 57 69 `leveva/src/lib.rs`, so every `crate::message::*` / `crate::patricia::*` / 58 - `crate::casemap::*` / `crate::string::*` path and the crate-root re-exports are 59 - unchanged — no consumer churn. `Cargo.toml` members now = `["leveva", "leveva-iauth", 60 - "leveva-casemap", "leveva-message", "leveva-patricia", "leveva-string"]`. 70 + `crate::casemap::*` / `crate::string::*` / `crate::cidr::*` path and the crate-root 71 + re-exports are unchanged — no consumer churn. `Cargo.toml` members now = `["leveva", 72 + "leveva-iauth", "leveva-casemap", "leveva-cidr", "leveva-message", "leveva-patricia", 73 + "leveva-string"]`. 61 74 **Next extraction candidates** (surveyed by intra-crate dep surface, NOT done): 62 - `cloak` (ZERO intra-crate deps — perfect standalone leaf, strongest next pick); 63 - `uid` (only `crate::ident`, blocked on extracting `ident`); `matching`+`cidr` are a 64 - mutually-referential coupled pair (not a single clean leaf); `msgid` pulls 65 - `cap`/`clock`/`ident`. Plans: 66 - `docs/superpowers/plans/2026-06-21-extract-leveva-{casemap,string}.md`. 75 + `matching` (NOW a clean leaf — deps `casemap` + `cidr`, both crates; pulls `nom`; the 76 + C `match.c` cluster, natural next pick); `cloak` (ZERO intra-crate deps — perfect 77 + standalone leaf, independent of the matching cluster); `uid` (only `crate::ident`, 78 + blocked on extracting `ident`); `msgid` pulls `cap`/`clock`/`ident`. Plans: 79 + `docs/superpowers/plans/2026-06-21-extract-leveva-{casemap,string,cidr}.md`. 67 80 68 81 **Implications for future work:** there is no more oracle and no `ircd-common` — 69 82 the differential-testing era is over. Any new leveva work is **feature work on a
+48
docs/progress-log/p12.md
··· 148 148 `docs/superpowers/plans/2026-06-21-extract-leveva-string.md`. Workspace members are 149 149 now `leveva` + `leveva-iauth` + `leveva-casemap` + `leveva-message` + 150 150 `leveva-patricia` + `leveva-string`. 151 + 152 + - **2026-06-21 — refactor: extract `leveva-cidr` leaf crate (+ proptest fuzzing).** 153 + Continues the post-P12 leaf-crate carve-out after 154 + `casemap`/`string`/`message`/`patricia`. Standalone feature work — no oracle, no 155 + differential. 156 + 157 + - **Why / a correction.** `cidr` is a **pure** leaf — every function depends only on 158 + `std::net::IpAddr`. The earlier `leveva-string` survey filed `matching`+`cidr` as 159 + "a coupled pair (mutually referential), not a clean single leaf yet"; that was 160 + **wrong**. `cidr.rs`'s only references to `crate::matching`/`crate::connlimit` are 161 + in *doc comments*, not code. The real edge is one-way: `matching` → `cidr` 162 + (`matching.rs:176` calls `crate::cidr::cidr_match`), with `connlimit`/`websocket` 163 + also calling in, while `cidr` calls nothing intra-crate. So `cidr` extracts cleanly 164 + on its own — and **unblocks `matching`**, whose only remaining intra-crate deps are 165 + now the two already-extracted crates `casemap` + `cidr`. 166 + 167 + - **Mechanism (identical to casemap/string/message/patricia).** `git mv 168 + leveva/src/cidr.rs → leveva-cidr/src/lib.rs`; new `leveva-cidr` crate 169 + (`publish = false`, std lib, **no** lib deps, `proptest` dev-dep); 170 + `leveva/src/lib.rs` now does `pub use leveva_cidr as cidr;` (a doc'd re-export 171 + beside the others). Body moved verbatim except the doctest's 172 + `use leveva::cidr::cidr_match;` → `use leveva_cidr::cidr_match;` and two cross-crate 173 + intra-doc links repointed to plain text. All four call sites use fully-qualified 174 + `crate::cidr::*` (`parse_cidr`/`contains`/`network_base`/`cidr_match`), so zero 175 + consumer churn — the full `leveva` build + the matching/connlimit/websocket tests 176 + confirm it. 177 + 178 + - **Fuzzing (user ask).** Stable toolchain → proptest, matching casemap/string. Added 179 + 10 `proptest!` invariants over the full v4/v6 address space (`u32::ANY`/`u128::ANY`) 180 + and arbitrary in-range prefixes, all *consistency/inverse* properties: parse_cidr 181 + round-trips (+ out-of-range prefix always rejected), contains reflexive through 182 + network_base, network_base idempotent, contains prefix-monotone (membership at `p` 183 + ⇒ membership at every wider `q ≤ p`), `/0` universal + full-length exact, family 184 + mismatch never contains, and `cidr_match ≡ parse + contains`. 185 + 186 + - **Other crates that can still move out (the user's question).** `cidr` cleared and 187 + `matching` now unblocked: **`matching`** (498 LoC) is now a clean leaf (deps 188 + `casemap` + `cidr`, both crates; pulls `nom`) — the natural next extraction, the C 189 + `match.c` cluster; **`cloak`** (zero intra-crate deps — a perfect standalone leaf, 190 + independent of the matching cluster); **`uid`** (only `crate::ident`, extractable 191 + once `ident` is a crate); **`msgid`** pulls `cap`/`clock`/`ident`, not a leaf. 192 + 193 + - **Gate.** `cargo build -p leveva` 0 warnings; `cargo test -p leveva-cidr` 18 green 194 + (8 unit + 10 fuzz) + 1 doctest; `leveva` cidr-consumer tests 195 + (matching/connlimit/websocket, 54) green. Plan: 196 + `docs/superpowers/plans/2026-06-21-extract-leveva-cidr.md`. Workspace members are 197 + now `leveva` + `leveva-iauth` + `leveva-casemap` + `leveva-cidr` + `leveva-message` 198 + + `leveva-patricia` + `leveva-string`.
+84
docs/superpowers/plans/2026-06-21-extract-leveva-cidr.md
··· 1 + # Extract `leveva-cidr` leaf crate (+ proptest fuzzing) 2 + 3 + **Date:** 2026-06-21. **Type:** refactor (post-P12 standalone feature work — no oracle, 4 + no differential). Continues the leaf-crate carve-out after 5 + `leveva-casemap` / `leveva-string` / `leveva-message` / `leveva-patricia`. 6 + 7 + ## Why 8 + 9 + `cidr` is a **pure** leaf: every function depends only on `std::net::IpAddr`, with 10 + zero IRC-specific coupling. The prior `leveva-string` survey listed `matching`+`cidr` 11 + as "a coupled pair (mutually referential), not a clean single leaf yet" — **that was 12 + wrong**. `cidr.rs`'s only references to `crate::matching` / `crate::connlimit` are in 13 + **doc comments**, not code. The actual dependency edge runs one way: 14 + `matching` → `cidr` (`matching.rs:176` calls `crate::cidr::cidr_match`), and 15 + `connlimit` / `websocket` also call into `cidr`, but `cidr` itself calls nothing 16 + intra-crate. So `cidr` extracts cleanly on its own — and doing so **unblocks 17 + `matching`**, whose only remaining intra-crate deps are now two already-extracted 18 + crates (`casemap` + `cidr`). 19 + 20 + No dependency cycle: `leveva-cidr` depends on nothing in the workspace; `matching`, 21 + `connlimit`, and `websocket` depend on *it*. 22 + 23 + ## Mechanism (identical to the casemap/string/message/patricia extraction) 24 + 25 + 1. New crate `leveva-cidr` (workspace member, `publish = false`, std lib). 26 + - `leveva-cidr/Cargo.toml` — no lib deps; `[dev-dependencies] proptest = "1"`. 27 + - `leveva-cidr/src/lib.rs` — the verbatim body of `leveva/src/cidr.rs` (`git mv`), 28 + with: 29 + - the doctest's `use leveva::cidr::cidr_match;` → `use leveva_cidr::cidr_match;`, 30 + - the two cross-crate intra-doc links repointed to plain text 31 + (`[`crate::matching::…`]` → `` `leveva::matching::…` ``, 32 + `[`crate::connlimit`]` → `` `leveva::connlimit` ``) so rustdoc has no broken 33 + intra-crate link, 34 + - a `proptest!` fuzz module appended to the existing `#[cfg(test)] mod tests`. 35 + 2. `leveva/Cargo.toml` — add `leveva-cidr = { path = "../leveva-cidr" }`. 36 + 3. `leveva/src/lib.rs` — replace `pub mod cidr;` with `pub use leveva_cidr as cidr;` 37 + (a doc'd re-export, like the others). 38 + 4. Workspace `Cargo.toml` — add `"leveva-cidr"` to `members`. 39 + 40 + No consumer churn: all four call sites use fully-qualified `crate::cidr::*` 41 + (`parse_cidr` / `contains` / `network_base` / `cidr_match`), which keep resolving 42 + through the re-export line. 43 + 44 + ## Fuzzing (the user ask — proptest tier) 45 + 46 + Stable toolchain only → proptest, matching `leveva-casemap`/`leveva-string`. Fed the 47 + full v4/v6 address space (`u32::ANY` / `u128::ANY` → `Ipv4Addr`/`Ipv6Addr::from`) and 48 + arbitrary in-range prefixes. All consistency/inverse properties, not happy-path echoes: 49 + 50 + - **parse_cidr round-trips** — `parse_cidr(format!("{ip}/{p}"))` == `Some((ip, p))` 51 + for both families, and an out-of-range prefix (`33..=255` for v4) is always rejected 52 + (the "degrade to an unmatchable glob" guard). 53 + - **contains is reflexive through network_base** — a host is always inside its own 54 + masked network (`contains(network_base(ip,p), p, ip)`), every prefix, both families. 55 + - **network_base idempotent** — masking an already-masked address to the same prefix 56 + is a no-op (host bits already zero). 57 + - **contains is prefix-monotone** — membership at prefix `p` implies membership at 58 + every shorter (wider) prefix `q ≤ p`. 59 + - **/0 universal, full-length exact** — `/0` contains every same-family address; a 60 + `/32` contains only the byte-equal address. 61 + - **family mismatch never contains** — cross-family membership is never true, 62 + regardless of bits/prefix. 63 + - **cidr_match ≡ parse + contains** — the convenience wrapper never diverges from its 64 + two components for an IP host. 65 + 66 + ## Other crates that can still move out (the user's question) 67 + 68 + Surveyed by intra-crate dependency surface; `cidr` cleared, `matching` now unblocked: 69 + 70 + - **`matching`** (498 LoC) — NOW a clean leaf: its only intra-crate deps are 71 + `crate::casemap` + `crate::cidr`, both already crates. The natural next extraction 72 + (it pulls `nom` as a lib dep). Pairs with this slice — the C `match.c` cluster. 73 + - **`cloak`** (241 LoC) — zero intra-crate deps (only `std::net::IpAddr`); a perfect 74 + standalone leaf (the `+x` host-cloak transform). Independent of the matching cluster. 75 + - **`uid`** (≈230 LoC) — depends only on `crate::ident`; extractable once `ident` is a 76 + crate (`ident` is blocked only by a `crate::command` reference worth auditing). 77 + - **`msgid`** depends on `cap`/`clock`/`ident`; not a leaf. 78 + 79 + ## Gate 80 + 81 + `cargo build -p leveva` (0 warnings) · `cargo test -p leveva-cidr` (18 = 8 unit + 10 82 + fuzz + 1 doctest) green · `cargo test -p leveva` cidr-consumer tests 83 + (matching/connlimit/websocket, 54) green. Record in the progress log and bump the 84 + in-repo p12 memory note.
+22
leveva-cidr/Cargo.toml
··· 1 + [package] 2 + name = "leveva-cidr" 3 + version = "0.0.0" 4 + edition = "2021" 5 + publish = false 6 + description = "Pure CIDR network-mask helpers: parse `addr/prefix`, test network membership, and compute the masked network base — IPv4 and IPv6, no v4-mapped normalization." 7 + 8 + # Extracted from `leveva` (the IRC daemon) as a self-contained leaf library: every 9 + # function is pure and depends only on `std::net`, with zero IRC-specific coupling, 10 + # while `matching` (host masks), `connlimit` (per-CIDR accounting), and the 11 + # `websocket` proxy-trust list all depend on *it*. It is the safe successor of the C 12 + # `ircd`'s `match_ipmask`. `leveva` re-exports it as `leveva::cidr`. 13 + [lib] 14 + path = "src/lib.rs" 15 + 16 + [dependencies] 17 + 18 + [dev-dependencies] 19 + # Property/fuzz tests: the parse/contains/network_base/cidr_match invariants are fed 20 + # arbitrary IP addresses and prefix lengths. Stable-toolchain fuzz tier — the project 21 + # has no nightly/cargo-fuzz, so proptest is the runnable fuzzer. 22 + proptest = "1"
+1
leveva/Cargo.toml
··· 20 20 # `leveva::patricia` / `leveva::string` so the rest of the crate (and external 21 21 # consumers) keep their existing paths. 22 22 leveva-casemap = { path = "../leveva-casemap" } 23 + leveva-cidr = { path = "../leveva-cidr" } 23 24 leveva-message = { path = "../leveva-message" } 24 25 leveva-patricia = { path = "../leveva-patricia" } 25 26 leveva-string = { path = "../leveva-string" }
+108 -3
leveva/src/cidr.rs leveva-cidr/src/lib.rs
··· 6 6 //! authorizes by network membership. 7 7 //! 8 8 //! These functions are **pure**. [`cidr_match`] is the seam the host matcher 9 - //! ([`crate::matching::host_component_matches`]) calls: it returns `Some(result)` 9 + //! (`leveva::matching::host_component_matches`) calls: it returns `Some(result)` 10 10 //! when the mask is a CIDR spec and `None` when it is not (so the caller falls 11 11 //! back to glob matching). A mask is "a CIDR spec" only in the `addr/prefix` 12 12 //! form — a bare IP or a `*`/`?` glob is **not** CIDR, keeping every existing ··· 18 18 //! [`parse_cidr`] so the mask degrades to a glob that cannot match a real IP. 19 19 //! 20 20 //! ``` 21 - //! use leveva::cidr::cidr_match; 21 + //! use leveva_cidr::cidr_match; 22 22 //! 23 23 //! assert_eq!(cidr_match("10.0.0.0/24", "10.0.0.5"), Some(true)); 24 24 //! assert_eq!(cidr_match("10.0.0.0/24", "10.0.1.5"), Some(false)); ··· 70 70 } 71 71 72 72 /// The canonical network address of `ip` masked to `prefix` bits — the grouping key for 73 - /// per-CIDR connection accounting ([`crate::connlimit`]). Two hosts in the same `/prefix` 73 + /// per-CIDR connection accounting (`leveva::connlimit`). Two hosts in the same `/prefix` 74 74 /// block map to the same `network_base`, so a counter keyed on it counts the block. The host 75 75 /// bits below `prefix` are cleared. A `prefix` of `0` collapses to the all-zero address (one 76 76 /// global bucket); a `/32` (v4) or `/128` (v6) yields the address unchanged. ··· 218 218 assert_eq!(cidr_match("10.0.0.0/24", "host.example.net"), Some(false)); 219 219 // family mismatch 220 220 assert_eq!(cidr_match("10.0.0.0/8", "::1"), Some(false)); 221 + } 222 + 223 + // ---- proptest fuzz tier ------------------------------------------------- 224 + // 225 + // Stable toolchain (no nightly / no cargo-fuzz), so proptest is the runnable 226 + // fuzzer: the parse/contains/network_base invariants are fed arbitrary IP 227 + // addresses (the full v4/v6 space) and arbitrary in-range prefix lengths. 228 + 229 + fn v4_addr(bits: u32) -> IpAddr { 230 + IpAddr::V4(Ipv4Addr::from(bits)) 231 + } 232 + fn v6_addr(bits: u128) -> IpAddr { 233 + IpAddr::V6(Ipv6Addr::from(bits)) 234 + } 235 + 236 + proptest::proptest! { 237 + /// `parse_cidr` round-trips any in-range `addr/prefix` back to its parts, 238 + /// and the canonical text it parses is the text `Display` would emit. 239 + #[test] 240 + fn parse_cidr_roundtrips_v4(bits in proptest::num::u32::ANY, prefix in 0u8..=32) { 241 + let ip = v4_addr(bits); 242 + let spec = format!("{ip}/{prefix}"); 243 + proptest::prop_assert_eq!(parse_cidr(&spec), Some((ip, prefix))); 244 + } 245 + 246 + #[test] 247 + fn parse_cidr_roundtrips_v6(bits in proptest::num::u128::ANY, prefix in 0u8..=128) { 248 + let ip = v6_addr(bits); 249 + let spec = format!("{ip}/{prefix}"); 250 + proptest::prop_assert_eq!(parse_cidr(&spec), Some((ip, prefix))); 251 + } 252 + 253 + /// An out-of-range prefix is always rejected (the "degrade to an 254 + /// unmatchable glob" guard), for both families. 255 + #[test] 256 + fn parse_cidr_rejects_out_of_range(bits in proptest::num::u32::ANY, prefix in 33u8..=255) { 257 + let spec = format!("{}/{}", v4_addr(bits), prefix); 258 + proptest::prop_assert_eq!(parse_cidr(&spec), None); 259 + } 260 + 261 + /// A host is always inside its own masked network — `contains` is 262 + /// reflexive through `network_base`, for every prefix. 263 + #[test] 264 + fn contains_own_network_base_v4(bits in proptest::num::u32::ANY, prefix in 0u8..=32) { 265 + let ip = v4_addr(bits); 266 + let base = network_base(ip, prefix); 267 + proptest::prop_assert!(contains(base, prefix, ip)); 268 + } 269 + 270 + #[test] 271 + fn contains_own_network_base_v6(bits in proptest::num::u128::ANY, prefix in 0u8..=128) { 272 + let ip = v6_addr(bits); 273 + let base = network_base(ip, prefix); 274 + proptest::prop_assert!(contains(base, prefix, ip)); 275 + } 276 + 277 + /// `network_base` is idempotent: masking an already-masked address to the 278 + /// same prefix is a no-op (the host bits are already zero). 279 + #[test] 280 + fn network_base_idempotent(bits in proptest::num::u128::ANY, prefix in 0u8..=128) { 281 + let ip = v6_addr(bits); 282 + let once = network_base(ip, prefix); 283 + proptest::prop_assert_eq!(once, network_base(once, prefix)); 284 + } 285 + 286 + /// `contains` widens monotonically: if a host is in a network at prefix 287 + /// `p`, it is also in that network at every shorter (wider) prefix `q ≤ p`. 288 + #[test] 289 + fn contains_is_prefix_monotone(bits in proptest::num::u32::ANY, 290 + p in 0u8..=32, q in 0u8..=32) { 291 + let ip = v4_addr(bits); 292 + let (lo, hi) = if p <= q { (p, q) } else { (q, p) }; 293 + let base = network_base(ip, hi); 294 + // host is in base/hi by construction; therefore also in base/lo. 295 + proptest::prop_assert!(contains(base, lo, ip)); 296 + } 297 + 298 + /// A `/0` mask contains every same-family address; a full-length prefix is 299 + /// exact (contains only addresses byte-equal to the network). 300 + #[test] 301 + fn prefix_zero_is_universal_full_is_exact(a in proptest::num::u32::ANY, 302 + b in proptest::num::u32::ANY) { 303 + proptest::prop_assert!(contains(v4_addr(a), 0, v4_addr(b))); 304 + proptest::prop_assert_eq!(contains(v4_addr(a), 32, v4_addr(b)), a == b); 305 + } 306 + 307 + /// Cross-family membership is never true, regardless of bits or prefix. 308 + #[test] 309 + fn family_mismatch_never_contains(n in proptest::num::u32::ANY, 310 + h in proptest::num::u128::ANY, prefix in 0u8..=32) { 311 + proptest::prop_assert!(!contains(v4_addr(n), prefix, v6_addr(h))); 312 + proptest::prop_assert!(!contains(v6_addr(h), prefix, v4_addr(n))); 313 + } 314 + 315 + /// `cidr_match` is exactly `parse_cidr` then `contains` against an IP host: 316 + /// the convenience wrapper never diverges from its two components. 317 + #[test] 318 + fn cidr_match_agrees_with_parse_and_contains(bits in proptest::num::u32::ANY, 319 + host in proptest::num::u32::ANY, 320 + prefix in 0u8..=32) { 321 + let mask = format!("{}/{}", v4_addr(bits), prefix); 322 + let host_ip = v4_addr(host); 323 + let expect = contains(v4_addr(bits), prefix, host_ip); 324 + proptest::prop_assert_eq!(cidr_match(&mask, &host_ip.to_string()), Some(expect)); 325 + } 221 326 } 222 327 }
+3 -1
leveva/src/lib.rs
··· 55 55 pub use leveva_casemap as casemap; 56 56 pub mod certfp; 57 57 pub mod channel; 58 - pub mod cidr; 58 + /// Pure CIDR network-mask helpers — extracted into the [`leveva-cidr`](leveva_cidr) 59 + /// leaf crate, re-exported here so `leveva::cidr::…` paths are unchanged. 60 + pub use leveva_cidr as cidr; 59 61 pub mod cloak; 60 62 pub mod clock; 61 63 pub mod command;