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-string leaf crate (+ proptest fuzzing)

Continue the leaf-crate carve-out (after leveva-message / leveva-patricia /
leveva-casemap) by moving the casemapping-aware IRC string types out of the
monolith into their own workspace crate:

- leveva-string: IrcStr / IrcString (str/String wrappers whose equality,
ordering, and hashing fold under RFC 1459) + IrcError + IrcStringBuilder. The
architectural #2 leaf in the string cluster — now that casemap is its own crate,
string depends on exactly one workspace crate (leveva-casemap) plus core/std,
while ident/channel/registry/whowas all depend on it. No cycle: leveva-message
uses neither, leveva-casemap depends only on core.

leveva re-exports it as leveva::string (`pub use leveva_string as string;`), and
the existing `pub use string::{IrcError, IrcStr, IrcString, IrcStringBuilder};`
re-exports straight out of the new crate — so every crate::string::* /
leveva::string::* path and the four flat re-exports are unchanged across all
consumers (the full dependent build confirms zero churn). Body moved verbatim
except `use crate::casemap;` -> `use leveva_casemap as casemap;` and the two
doctests' `use leveva::...;` -> `use leveva_string::...;`.

Fuzzing: stable toolchain only (no nightly / no cargo-fuzz), so proptest is the
runnable fuzz tier, matching casemap/message. Added 6 consistency/inverse
invariants fed arbitrary (incl. non-ASCII) strings: eq lifts casemap (the wrapper
adds no equality of its own), Ord consistent-with-Eq + antisymmetric, Hash agrees
with Eq (the HashMap-key contract behind the nick/channel tables), Borrow<IrcStr>
round-trip (insert under one casing, find by a case-different borrowed key),
validation soundness (accept iff no NUL/CR/LF, FromStr agrees, storage
byte-identical on success), and builder = deferred try_from.

What else can move out: cloak (zero intra-crate deps — perfect standalone leaf,
strongest next pick); uid (only crate::ident); matching+cidr are a coupled pair;
msgid pulls cap/clock/ident. Recorded in the plan + progress log.

Gate: cargo build --workspace 0 warnings; cargo clippy --workspace --tests clean;
leveva-string 13 (7 unit + 6 fuzz) + 2 doctests green; full leveva suite (413
binaries) green.

+290 -16
+9
Cargo.lock
··· 1207 1207 "leveva-iauth", 1208 1208 "leveva-message", 1209 1209 "leveva-patricia", 1210 + "leveva-string", 1210 1211 "libc", 1211 1212 "nom", 1212 1213 "notify", ··· 1261 1262 [[package]] 1262 1263 name = "leveva-patricia" 1263 1264 version = "0.0.0" 1265 + 1266 + [[package]] 1267 + name = "leveva-string" 1268 + version = "0.0.0" 1269 + dependencies = [ 1270 + "leveva-casemap", 1271 + "proptest", 1272 + ] 1264 1273 1265 1274 [[package]] 1266 1275 name = "libc"
+1
Cargo.toml
··· 6 6 "leveva-casemap", 7 7 "leveva-message", 8 8 "leveva-patricia", 9 + "leveva-string", 9 10 ] 10 11 # The strangler is complete. The faithful mechanical port (`ircd-common`/`ircd-rs`), 11 12 # its C-era auth scaffolding (`iauth-rs`), and the oracle differential crate
+18 -9
docs/claude-memory/p12-complete.md
··· 44 44 on nothing. Shipped with a 7-property **proptest fuzz** module (fold⇔eq, cmp⇔eq, 45 45 antisymmetry, hash-agrees-with-eq, fold idempotent+UTF-8-safe, high-byte identity) 46 46 — proptest is the stable fuzz tier (no nightly/cargo-fuzz here, same as message). 47 + - **`leveva-string`** — casemapping-aware `IrcStr`/`IrcString`/`IrcStringBuilder` 48 + (`leveva-string/src/lib.rs`, was `leveva/src/string.rs`). The architectural #2 leaf: 49 + now that `casemap` is a crate, `string` depends on exactly one workspace crate 50 + (`leveva-casemap`) + std, while `ident`/`channel`/`registry`/`whowas` depend on it. 51 + Shipped with a 6-property **proptest fuzz** module (eq lifts casemap, Ord 52 + consistent+antisymmetric, hash-agrees-with-eq, `Borrow<IrcStr>` round-trip, 53 + validation soundness, builder≡try_from). 47 54 48 55 `leveva` re-exports each as `leveva::message` / `leveva::patricia` / `leveva::casemap` 49 - via `pub use leveva_message as message;` (etc.) in `leveva/src/lib.rs`, so every 50 - `crate::message::*` / `crate::patricia::*` / `crate::casemap::*` path and the 51 - crate-root re-exports are unchanged — no consumer churn. `Cargo.toml` members now = 52 - `["leveva", "leveva-iauth", "leveva-casemap", "leveva-message", "leveva-patricia"]`. 53 - The remaining runtime-free leaf cluster that COULD also be extracted (not done): 54 - `string`/`matching`/`ident`/`cidr` (all touch no `ServerContext`/registry/tokio, but 55 - `string`/`ident`/`matching` still cross-reference each other; `whowas` looks like a 56 - leaf but reaches the runtime). `casemap` extraction plan: 57 - `docs/superpowers/plans/2026-06-21-extract-leveva-casemap.md`. 56 + / `leveva::string` via `pub use leveva_message as message;` (etc.) in 57 + `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"]`. 61 + **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`. 58 67 59 68 **Implications for future work:** there is no more oracle and no `ircd-common` — 60 69 the differential-testing era is over. Any new leveva work is **feature work on a
+50
docs/progress-log/p12.md
··· 98 98 suite green. Plan: `docs/superpowers/plans/2026-06-21-extract-leveva-casemap.md`. 99 99 Workspace members are now `leveva` + `leveva-iauth` + `leveva-casemap` + 100 100 `leveva-message` + `leveva-patricia`. 101 + 102 + --- 103 + 104 + - **2026-06-21 — post-P12 refactor: `leveva-string` leaf crate extracted (+ proptest fuzz).** 105 + The next leaf-crate carve-out after `leveva-casemap` (which followed 106 + `leveva-message`/`leveva-patricia`). The casemapping-aware string types moved out of 107 + the monolith into their own workspace crate. Still **not** a migration slice — the 108 + strangler is finished; this is standalone refactoring of the finished product. 109 + 110 + - **Why string (the architectural #2).** With `casemap` now its own crate, `string` 111 + depends on exactly one intra-crate module — `crate::casemap`, already the extracted 112 + `leveva-casemap` — and otherwise only `core`/`std`. That makes it the cleanest next 113 + leaf: `leveva-string` depends on `leveva-casemap` and nothing else in the workspace, 114 + while `ident`/`channel`/`registry`/`whowas` depend on *it* (via the re-exported 115 + `IrcStr`/`IrcString`/`IrcStringBuilder`/`IrcError` names). No cycle — `leveva-message` 116 + uses neither, and `leveva-casemap` depends only on `core`. 117 + 118 + - **Mechanism (identical to message/patricia/casemap).** New `leveva-string` crate 119 + (`publish = false`, std lib, one lib dep `leveva-casemap`, `proptest` dev-dep); 120 + `leveva/src/string.rs` deleted; `leveva/src/lib.rs` now does 121 + `pub use leveva_string as string;` (a doc'd re-export beside the others) and the 122 + existing `pub use string::{IrcError, IrcStr, IrcString, IrcStringBuilder};` then 123 + re-exports straight out of the new crate. The body moved verbatim except 124 + `use crate::casemap;` → `use leveva_casemap as casemap;` and the two doctests' 125 + `use leveva::…;` → `use leveva_string::…;`. Every `crate::string::*` / 126 + `leveva::string::*` path and the four flat re-exports are unchanged — zero consumer 127 + churn (the full build of every dependent crate confirms it). 128 + 129 + - **Fuzzing (user ask).** Stable toolchain → proptest, matching casemap/message. Added 130 + 6 `proptest!` invariants fed arbitrary (incl. non-ASCII) strings, all 131 + *consistency/inverse* properties: eq lifts casemap (the wrapper adds no equality of 132 + its own), Ord consistent-with-Eq + antisymmetric, Hash agrees with Eq (the 133 + HashMap-key contract for nick/channel tables), `Borrow<IrcStr>` round-trip (insert 134 + under one casing, find by a case-different borrowed key), validation soundness 135 + (accept iff no NUL/CR/LF, FromStr agrees, storage byte-identical on success), and 136 + builder ≡ try_from (build is a deferred validate, rejects iff a piece carried a 137 + framing byte). 138 + 139 + - **Other crates that can still move out (the user's question).** Surveyed by 140 + intra-crate dep surface: `cloak` (zero intra-crate deps — a perfect standalone leaf, 141 + the strongest next candidate); `uid` (only `crate::ident`, extractable once `ident` 142 + is a crate); `matching`+`cidr` are a coupled pair (mutually referential) so not a 143 + clean single leaf yet; `msgid` pulls `cap`/`clock`/`ident`, not a leaf. 144 + 145 + - **Gate.** `cargo build --workspace` 0 warnings; `cargo clippy --workspace --tests` 146 + clean; `cargo test -p leveva-string` 13 green (7 unit + 6 fuzz) + 2 doctests; full 147 + `leveva` suite green. Plan: 148 + `docs/superpowers/plans/2026-06-21-extract-leveva-string.md`. Workspace members are 149 + now `leveva` + `leveva-iauth` + `leveva-casemap` + `leveva-message` + 150 + `leveva-patricia` + `leveva-string`.
+91
docs/superpowers/plans/2026-06-21-extract-leveva-string.md
··· 1 + # Extract `leveva-string` 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-message` / `leveva-patricia` / `leveva-casemap`. 6 + 7 + ## Why 8 + 9 + `string` is the architectural #2 in the string-foundation cluster (the lib.rs doc 10 + order is casemap → string → ident → …). Now that `casemap` is its own crate, 11 + `string` depends on **exactly one** intra-crate module — `crate::casemap`, which is 12 + already the extracted `leveva-casemap` crate — and otherwise only `core` + `std` 13 + (`std::error::Error`). That makes it the cleanest next leaf: `leveva-string` will 14 + depend on `leveva-casemap` and nothing else in the workspace, while `ident`, 15 + `channel`, `registry`, `whowas`, etc. all depend on *it* (via the re-exported 16 + `IrcStr`/`IrcString`/`IrcStringBuilder`/`IrcError` names). 17 + 18 + No dependency cycle: `leveva-message` does not use `string`/`casemap`, and 19 + `leveva-casemap` depends only on `core`. 20 + 21 + ## Mechanism (identical to the message/patricia/casemap extraction) 22 + 23 + 1. New crate `leveva-string` (workspace member, `publish = false`, std lib). 24 + - `leveva-string/Cargo.toml` — one lib dep `leveva-casemap = { path = ".." }`; 25 + `[dev-dependencies] proptest = "1"`. 26 + - `leveva-string/src/lib.rs` — the verbatim body of `leveva/src/string.rs`, with: 27 + - `use crate::casemap;` → `use leveva_casemap as casemap;` 28 + - the two doctests' `use leveva::IrcString;` / `use leveva::IrcStringBuilder;` 29 + → `use leveva_string::…;` (the crate's own name), 30 + - the module doc-comment lifted to crate-level docs (`//!`), with the 31 + `[`crate::casemap`]` intra-doc link repointed to `[`leveva_casemap`]`, 32 + - a new `proptest!` fuzz module appended to the existing `#[cfg(test)] mod tests`. 33 + 2. `leveva/Cargo.toml` — add `leveva-string = { path = "../leveva-string" }`. 34 + 3. `leveva/src/lib.rs` — replace `pub mod string;` with 35 + `pub use leveva_string as string;` (a doc'd re-export, like the others). The 36 + existing `pub use string::{IrcError, IrcStr, IrcString, IrcStringBuilder};` then 37 + re-exports out of the new crate unchanged. 38 + 4. Delete `leveva/src/string.rs`. 39 + 5. Workspace `Cargo.toml` — add `"leveva-string"` to `members`. 40 + 41 + No consumer churn: every `crate::string::*` / `leveva::string::*` path and the four 42 + flat re-exports keep resolving through the two re-export lines. 43 + 44 + ## Fuzzing (the user ask — proptest tier) 45 + 46 + Stable toolchain only → proptest, matching `leveva-casemap`/`leveva-message`. The 47 + `string` types are casemapping wrappers, so the invariants assert the wrapper 48 + *faithfully lifts* the casemap contract into `Eq`/`Ord`/`Hash` and that validation is 49 + sound. Fed arbitrary (incl. non-ASCII) `String`s and arbitrary byte-derived text: 50 + 51 + - **eq lifts casemap** — `IrcStr::new(a) == IrcStr::new(b)` iff 52 + `casemap::eq_ignore_case(a,b)` (the wrapper adds no equality of its own). 53 + - **Ord total-order consistency** — `cmp(a,b) == Equal` iff the two are `==`; 54 + antisymmetry `cmp(a,b) == cmp(b,a).reverse()`. 55 + - **Hash agrees with Eq** — equal `IrcString`s hash equally (the `HashMap`-key 56 + contract the daemon relies on for nick/channel tables); verified via a 57 + `DefaultHasher`. 58 + - **`Borrow` round-trip** — an `IrcString` inserted into a `HashMap` is found by a 59 + case-different `&IrcStr` key (the `Borrow<IrcStr>` + matching `Hash`/`Eq` law that 60 + makes `map.get(IrcStr::new(...))` sound). 61 + - **validation soundness** — `try_from` accepts a string iff it contains no 62 + NUL/CR/LF; on success `as_str()` is byte-identical to the input (storage is 63 + casing-preserving, only comparison folds); `FromStr` agrees with `TryFrom<&str>`. 64 + - **builder ≡ try_from** — concatenating pieces through `IrcStringBuilder` and 65 + validating equals `IrcString::try_from(concatenation)` (build is just a deferred 66 + validate), and rejects iff any piece carried a framing byte. 67 + 68 + These are inverse/consistency invariants (equal-iff-equal both directions, accept-iff-valid, 69 + found-iff-inserted), not happy-path echoes. 70 + 71 + ## Other crates that can still move out (the user's question) 72 + 73 + Surveyed by intra-crate dependency surface: 74 + 75 + - **`cloak`** (241 LoC) — zero intra-crate deps (only `std::net::IpAddr`); a perfect 76 + standalone leaf (the `+x` host-cloak transform). Strong next candidate after this. 77 + - **`uid`** (≈230 LoC) — depends only on `crate::ident`; extractable once `ident` is a 78 + crate (and `ident` itself is a leaf-after-`string`-and-`command`, blocked only by a 79 + `crate::command` reference worth auditing). 80 + - **`matching`** — depends on `casemap` + `cidr`; `cidr` depends back on 81 + `matching` + `connlimit`, so the two are a coupled pair, not a clean single leaf 82 + yet. 83 + - **`msgid`** depends on `cap`/`clock`/`ident`; not a leaf. 84 + 85 + Recorded so the carve-out has a visible backlog; this slice does **`string`** only. 86 + 87 + ## Gate 88 + 89 + `cargo build --workspace` (0 warnings) · `cargo clippy --workspace --tests` (clean) · 90 + `cargo test -p leveva-string` · `cargo test -p leveva` green. Record in the progress 91 + log and bump MEMORY.
+22
leveva-string/Cargo.toml
··· 1 + [package] 2 + name = "leveva-string" 3 + version = "0.0.0" 4 + edition = "2021" 5 + publish = false 6 + description = "Casemapping-aware IRC string types: IrcStr/IrcString (str/String wrappers whose equality, ordering, and hashing fold under RFC 1459) and an IrcStringBuilder." 7 + 8 + # Extracted from `leveva` (the IRC daemon) as a self-contained leaf library: the #2 9 + # leaf in the string cluster — it depends only on `leveva-casemap` (RFC 1459 folding) 10 + # and `core`/`std`, while `ident`/`channel`/`registry`/`whowas` all depend on *it*. 11 + # `leveva` re-exports it as `leveva::string`. 12 + [lib] 13 + path = "src/lib.rs" 14 + 15 + [dependencies] 16 + leveva-casemap = { path = "../leveva-casemap" } 17 + 18 + [dev-dependencies] 19 + # Property/fuzz tests: the casemap-lift, ordering, hashing, Borrow round-trip, and 20 + # validation invariants are fed arbitrary (incl. non-ASCII) strings. Stable-toolchain 21 + # fuzz tier — the project has no nightly/cargo-fuzz, so proptest is the runnable fuzzer. 22 + proptest = "1"
+3 -2
leveva/Cargo.toml
··· 17 17 [dependencies] 18 18 leveva-iauth = { path = "../leveva-iauth" } 19 19 # Extracted leaf libraries, re-exported as `leveva::casemap` / `leveva::message` / 20 - # `leveva::patricia` so the rest of the crate (and external consumers) keep their 21 - # existing paths. 20 + # `leveva::patricia` / `leveva::string` so the rest of the crate (and external 21 + # consumers) keep their existing paths. 22 22 leveva-casemap = { path = "../leveva-casemap" } 23 23 leveva-message = { path = "../leveva-message" } 24 24 leveva-patricia = { path = "../leveva-patricia" } 25 + leveva-string = { path = "../leveva-string" } 25 26 kdl = { version = "6", features = ["v1-fallback"] } 26 27 nom = "7" 27 28 thiserror = "2"
+4 -1
leveva/src/lib.rs
··· 107 107 pub mod snomask; 108 108 pub mod snotice; 109 109 pub mod stdreply; 110 - pub mod string; 110 + /// Casemapping-aware IRC string types ([`IrcStr`]/[`IrcString`]) — extracted into 111 + /// the [`leveva-string`](leveva_string) leaf crate, re-exported here so 112 + /// `leveva::string::…` paths are unchanged. 113 + pub use leveva_string as string; 111 114 pub mod sts; 112 115 pub mod tls; 113 116 pub mod uid;
+92 -4
leveva/src/string.rs leveva-string/src/lib.rs
··· 1 1 //! Casemapping-aware IRC string types. 2 2 //! 3 3 //! [`IrcStr`] (borrowed) and [`IrcString`] (owned) wrap UTF-8 text but compare, 4 - //! order, and hash under RFC 1459 case folding (see [`crate::casemap`]). They are to 4 + //! order, and hash under RFC 1459 case folding (see [`leveva_casemap`]). They are to 5 5 //! `str`/`String` what [`std::path::Path`]/[`PathBuf`] are to `str`/`String`: thin 6 6 //! wrappers that change comparison semantics, not storage. 7 7 //! 8 8 //! ``` 9 - //! use leveva::IrcString; 9 + //! use leveva_string::IrcString; 10 10 //! let a = IrcString::try_from("Nick[]").unwrap(); 11 11 //! let b = IrcString::try_from("nick{}").unwrap(); 12 12 //! assert_eq!(a, b); // RFC 1459: [] are the uppercase of {} 13 13 //! ``` 14 14 //! 15 + //! This crate was extracted from `leveva` (the IRC daemon) as a self-contained leaf: 16 + //! it depends only on [`leveva_casemap`] and is re-exported there as `leveva::string`. 17 + //! 15 18 //! [`PathBuf`]: std::path::PathBuf 16 19 17 - use crate::casemap; 18 20 use core::borrow::Borrow; 19 21 use core::cmp::Ordering; 20 22 use core::fmt; 21 23 use core::hash::{Hash, Hasher}; 22 24 use core::ops::Deref; 23 25 use core::str::FromStr; 26 + use leveva_casemap as casemap; 24 27 25 28 /// Error constructing an [`IrcString`] from raw text. 26 29 #[derive(Debug, Clone, Copy, PartialEq, Eq)] ··· 257 260 /// Incrementally assemble an [`IrcString`], validating on [`build`](Self::build). 258 261 /// 259 262 /// ``` 260 - /// use leveva::IrcStringBuilder; 263 + /// use leveva_string::IrcStringBuilder; 261 264 /// let s = IrcStringBuilder::new().push_str("ni").push('c').push_str("k").build().unwrap(); 262 265 /// assert_eq!(s.as_str(), "nick"); 263 266 /// ``` ··· 394 397 395 398 let bad = IrcStringBuilder::new().push_str("oops\n").build(); 396 399 assert_eq!(bad, Err(IrcError::Forbidden('\n'))); 400 + } 401 + 402 + // ----- Property / fuzz tier (stable-toolchain proptest, matching leveva-casemap) ----- 403 + 404 + use std::collections::hash_map::DefaultHasher; 405 + 406 + /// One-shot `Hash` → `u64`, for the hash-agrees-with-eq invariant. 407 + fn hash_of<T: Hash>(v: &T) -> u64 { 408 + let mut h = DefaultHasher::new(); 409 + v.hash(&mut h); 410 + h.finish() 411 + } 412 + 413 + /// A `String` made wire-legal by stripping the three framing bytes, so the 414 + /// casemap-lift invariants exercise `IrcString` construction over arbitrary text. 415 + fn sanitize(s: &str) -> String { 416 + s.chars().filter(|c| !matches!(c, '\0' | '\r' | '\n')).collect() 417 + } 418 + 419 + proptest::proptest! { 420 + /// The wrapper adds no equality of its own: two slices are `IrcStr`-equal 421 + /// exactly when `casemap::eq_ignore_case` holds. Fed arbitrary strings. 422 + #[test] 423 + fn eq_lifts_casemap(a in ".*", b in ".*") { 424 + let lifted = IrcStr::new(&a) == IrcStr::new(&b); 425 + proptest::prop_assert_eq!(lifted, casemap::eq_ignore_case(a.as_bytes(), b.as_bytes())); 426 + } 427 + 428 + /// `Ord` is consistent with `Eq` (`cmp == Equal` iff `==`) and antisymmetric. 429 + #[test] 430 + fn ord_is_consistent_and_antisymmetric(a in ".*", b in ".*") { 431 + let (sa, sb) = (IrcStr::new(&a), IrcStr::new(&b)); 432 + proptest::prop_assert_eq!(sa.cmp(sb) == Ordering::Equal, sa == sb); 433 + proptest::prop_assert_eq!(sa.cmp(sb), sb.cmp(sa).reverse()); 434 + } 435 + 436 + /// The `HashMap`-key contract the daemon's nick/channel tables rely on: 437 + /// equal `IrcString`s hash equally. 438 + #[test] 439 + fn hash_agrees_with_eq_prop(a in ".*", b in ".*") { 440 + let (sa, sb) = (sanitize(&a), sanitize(&b)); 441 + let (ka, kb) = (IrcString::try_from(sa).unwrap(), IrcString::try_from(sb).unwrap()); 442 + if ka == kb { 443 + proptest::prop_assert_eq!(hash_of(&ka), hash_of(&kb)); 444 + } 445 + } 446 + 447 + /// `Borrow<IrcStr>` + matching `Hash`/`Eq`: a value inserted under one casing 448 + /// is found by a case-different borrowed key (`map.get(IrcStr::new(..))`). 449 + #[test] 450 + fn borrow_round_trip(s in ".*") { 451 + use std::collections::HashMap; 452 + let clean = sanitize(&s); 453 + let key = IrcString::try_from(clean.clone()).unwrap(); 454 + let mut m: HashMap<IrcString, u32> = HashMap::new(); 455 + m.insert(key, 42); 456 + // The same text (any casing folds equal to itself) must be found. 457 + proptest::prop_assert_eq!(m.get(IrcStr::new(&clean)), Some(&42)); 458 + } 459 + 460 + /// Validation soundness: `try_from` succeeds iff the text has no NUL/CR/LF, 461 + /// `FromStr` agrees, and on success storage is byte-identical (casing kept). 462 + #[test] 463 + fn validation_is_sound(s in ".*") { 464 + let has_framing = s.chars().any(|c| matches!(c, '\0' | '\r' | '\n')); 465 + let parsed: Result<IrcString, _> = s.parse(); 466 + proptest::prop_assert_eq!(IrcString::try_from(s.as_str()).is_ok(), !has_framing); 467 + proptest::prop_assert_eq!(parsed.is_ok(), !has_framing); 468 + if let Ok(v) = IrcString::try_from(s.as_str()) { 469 + proptest::prop_assert_eq!(v.as_str(), s.as_str()); 470 + } 471 + } 472 + 473 + /// `IrcStringBuilder` is a deferred validate: concatenating pieces and 474 + /// building equals validating the joined string, and rejects iff any piece 475 + /// carried a framing byte. 476 + #[test] 477 + fn builder_equals_try_from(parts in proptest::collection::vec(".*", 0..6)) { 478 + let mut b = IrcStringBuilder::new(); 479 + for p in &parts { 480 + b = b.push_str(p); 481 + } 482 + let joined: String = parts.concat(); 483 + proptest::prop_assert_eq!(b.build(), IrcString::try_from(joined)); 484 + } 397 485 } 398 486 }