Something went wrong. Try again.
Proof of concept mechanical port of ircnet/ircd to Rust as part of a bit about C being insecure for network services
Something went wrong. Try again.
14 kB · 486 lines
Rust
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487//! Casemapping-aware IRC string types.//!//! [`IrcStr`] (borrowed) and [`IrcString`] (owned) wrap UTF-8 text but compare,//! order, and hash under RFC 1459 case folding (see [`leveva_casemap`]). They are to//! `str`/`String` what [`std::path::Path`]/[`PathBuf`] are to `str`/`String`: thin//! wrappers that change comparison semantics, not storage.//!//! ```//! use leveva_string::IrcString;//! let a = IrcString::try_from("Nick[]").unwrap();//! let b = IrcString::try_from("nick{}").unwrap();//! assert_eq!(a, b); // RFC 1459: [] are the uppercase of {}//! ```//!//! This crate was extracted from `leveva` (the IRC daemon) as a self-contained leaf://! it depends only on [`leveva_casemap`] and is re-exported there as `leveva::string`.//!//! [`PathBuf`]: std::path::PathBuf
use core::borrow::Borrow;use core::cmp::Ordering;use core::fmt;use core::hash::{Hash, Hasher};use core::ops::Deref;use core::str::FromStr;use leveva_casemap as casemap;
/// Error constructing an [`IrcString`] from raw text.#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum IrcError { /// The text contained a byte that can never appear inside an IRC message field: /// NUL (`\0`), CR (`\r`), or LF (`\n`). These are protocol framing bytes. Forbidden(char),}
impl fmt::Display for IrcError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { IrcError::Forbidden(c) => { write!(f, "forbidden control byte {:?} in IRC string", c) } } }}
impl std::error::Error for IrcError {}
/// Reject the three bytes that may never appear in any IRC message field.fn validate_field(s: &str) -> Result<(), IrcError> { for c in s.chars() { if matches!(c, '\0' | '\r' | '\n') { return Err(IrcError::Forbidden(c)); } } Ok(())}
/// A borrowed IRC string slice: `str` with RFC 1459 case-insensitive/// equality/ordering/hashing.////// Construct one with [`IrcStr::new`] or by dereferencing an [`IrcString`].#[repr(transparent)]pub struct IrcStr(str);
impl IrcStr { /// Wrap a `&str` as a `&IrcStr` (zero-copy). #[inline] pub fn new(s: &str) -> &IrcStr { // SAFETY: IrcStr is #[repr(transparent)] over str, so the two have // identical layout; this is the standard unsized-newtype cast (cf. // std's `Path::new`). unsafe { &*(s as *const str as *const IrcStr) } }
/// The underlying text, with its original (un-folded) casing preserved. #[inline] pub fn as_str(&self) -> &str { &self.0 }
/// The underlying bytes. #[inline] pub fn as_bytes(&self) -> &[u8] { self.0.as_bytes() }}
impl Deref for IrcStr { type Target = str; #[inline] fn deref(&self) -> &str { &self.0 }}
impl PartialEq for IrcStr { #[inline] fn eq(&self, other: &Self) -> bool { casemap::eq_ignore_case(self.as_bytes(), other.as_bytes()) }}impl Eq for IrcStr {}
impl PartialOrd for IrcStr { #[inline] fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }}impl Ord for IrcStr { #[inline] fn cmp(&self, other: &Self) -> Ordering { casemap::cmp(self.as_bytes(), other.as_bytes()) }}
impl Hash for IrcStr { #[inline] fn hash<H: Hasher>(&self, state: &mut H) { casemap::hash(self.as_bytes(), state); }}
impl fmt::Display for IrcStr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.0) }}impl fmt::Debug for IrcStr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(&self.0, f) }}
impl AsRef<str> for IrcStr { #[inline] fn as_ref(&self) -> &str { &self.0 }}
impl ToOwned for IrcStr { type Owned = IrcString; #[inline] fn to_owned(&self) -> IrcString { IrcString(self.0.to_owned()) }}
/// An owned IRC string: `String` with RFC 1459 case-insensitive semantics.////// Construction is fallible — the text may not contain NUL/CR/LF (see [`IrcError`]).#[derive(Clone)]pub struct IrcString(String);
impl IrcString { /// Borrow as an [`IrcStr`]. #[inline] pub fn as_irc_str(&self) -> &IrcStr { IrcStr::new(&self.0) }
/// The underlying text, original casing preserved. #[inline] pub fn as_str(&self) -> &str { &self.0 }
/// Consume into the inner `String`. #[inline] pub fn into_string(self) -> String { self.0 }}
impl Deref for IrcString { type Target = IrcStr; #[inline] fn deref(&self) -> &IrcStr { self.as_irc_str() }}
impl Borrow<IrcStr> for IrcString { #[inline] fn borrow(&self) -> &IrcStr { self.as_irc_str() }}
impl PartialEq for IrcString { #[inline] fn eq(&self, other: &Self) -> bool { self.as_irc_str() == other.as_irc_str() }}impl Eq for IrcString {}
impl PartialOrd for IrcString { #[inline] fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }}impl Ord for IrcString { #[inline] fn cmp(&self, other: &Self) -> Ordering { self.as_irc_str().cmp(other.as_irc_str()) }}
impl Hash for IrcString { #[inline] fn hash<H: Hasher>(&self, state: &mut H) { self.as_irc_str().hash(state) }}
impl fmt::Display for IrcString { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.0) }}impl fmt::Debug for IrcString { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(&self.0, f) }}
impl AsRef<str> for IrcString { #[inline] fn as_ref(&self) -> &str { &self.0 }}
impl TryFrom<String> for IrcString { type Error = IrcError; fn try_from(s: String) -> Result<Self, IrcError> { validate_field(&s)?; Ok(IrcString(s)) }}
impl TryFrom<&str> for IrcString { type Error = IrcError; fn try_from(s: &str) -> Result<Self, IrcError> { validate_field(s)?; Ok(IrcString(s.to_owned())) }}
impl FromStr for IrcString { type Err = IrcError; fn from_str(s: &str) -> Result<Self, IrcError> { IrcString::try_from(s) }}
/// Incrementally assemble an [`IrcString`], validating on [`build`](Self::build).////// ```/// use leveva_string::IrcStringBuilder;/// let s = IrcStringBuilder::new().push_str("ni").push('c').push_str("k").build().unwrap();/// assert_eq!(s.as_str(), "nick");/// ```#[derive(Default, Clone)]pub struct IrcStringBuilder { buf: String,}
impl IrcStringBuilder { /// A new, empty builder. #[inline] pub fn new() -> Self { IrcStringBuilder { buf: String::new() } }
/// A new builder with reserved capacity. #[inline] pub fn with_capacity(cap: usize) -> Self { IrcStringBuilder { buf: String::with_capacity(cap), } }
/// Append a string slice. #[inline] pub fn push_str(mut self, s: &str) -> Self { self.buf.push_str(s); self }
/// Append a single character. #[inline] pub fn push(mut self, c: char) -> Self { self.buf.push(c); self }
/// Current length in bytes. #[inline] pub fn len(&self) -> usize { self.buf.len() }
/// Whether nothing has been pushed yet. #[inline] pub fn is_empty(&self) -> bool { self.buf.is_empty() }
/// Current contents (un-validated). #[inline] pub fn as_str(&self) -> &str { &self.buf }
/// Finalize, validating that no forbidden control byte (NUL/CR/LF) was pushed. #[inline] pub fn build(self) -> Result<IrcString, IrcError> { IrcString::try_from(self.buf) }}
#[cfg(test)]mod tests { use super::*; use std::collections::HashSet;
#[test] fn casefold_equality() { let a = IrcString::try_from("Nick[]\\~").unwrap(); let b = IrcString::try_from("nick{}|^").unwrap(); assert_eq!(a, b); assert_eq!(a.as_irc_str(), b.as_irc_str()); }
#[test] fn display_preserves_original_case() { let a = IrcString::try_from("AbC{}").unwrap(); assert_eq!(a.to_string(), "AbC{}"); assert_eq!(a.as_str(), "AbC{}"); }
#[test] fn hash_agrees_with_eq() { let mut set: HashSet<IrcString> = HashSet::new(); set.insert(IrcString::try_from("Foo[Bar]").unwrap()); // Same name under RFC 1459 folding → already present, set stays size 1. assert!(!set.insert(IrcString::try_from("foo{bar}").unwrap())); assert_eq!(set.len(), 1); // A genuinely different name does get inserted. assert!(set.insert(IrcString::try_from("other").unwrap())); assert_eq!(set.len(), 2); }
#[test] fn borrow_lookup_by_irc_str() { use std::collections::HashMap; let mut m: HashMap<IrcString, u32> = HashMap::new(); m.insert(IrcString::try_from("Channel[A]").unwrap(), 7); // Look up via a borrowed &IrcStr with different casing. assert_eq!(m.get(IrcStr::new("channel{a}")), Some(&7)); assert_eq!(m.get(IrcStr::new("nope")), None); }
#[test] fn ordering_is_case_insensitive() { let mut v = [ IrcString::try_from("Zebra").unwrap(), IrcString::try_from("apple").unwrap(), IrcString::try_from("Mango").unwrap(), ]; v.sort(); let names: Vec<&str> = v.iter().map(|s| s.as_str()).collect(); assert_eq!(names, ["apple", "Mango", "Zebra"]); }
#[test] fn rejects_framing_bytes() { assert_eq!(IrcString::try_from("a\0b"), Err(IrcError::Forbidden('\0'))); assert_eq!(IrcString::try_from("a\rb"), Err(IrcError::Forbidden('\r'))); assert_eq!(IrcString::try_from("a\nb"), Err(IrcError::Forbidden('\n'))); assert!(IrcString::try_from("normal text with spaces").is_ok()); }
#[test] fn builder_happy_and_sad() { let ok = IrcStringBuilder::new() .push_str("hello") .push(' ') .push_str("world") .build() .unwrap(); assert_eq!(ok.as_str(), "hello world");
let bad = IrcStringBuilder::new().push_str("oops\n").build(); assert_eq!(bad, Err(IrcError::Forbidden('\n'))); }
// ----- Property / fuzz tier (stable-toolchain proptest, matching leveva-casemap) -----
use std::collections::hash_map::DefaultHasher;
/// One-shot `Hash` → `u64`, for the hash-agrees-with-eq invariant. fn hash_of<T: Hash>(v: &T) -> u64 { let mut h = DefaultHasher::new(); v.hash(&mut h); h.finish() }
/// A `String` made wire-legal by stripping the three framing bytes, so the /// casemap-lift invariants exercise `IrcString` construction over arbitrary text. fn sanitize(s: &str) -> String { s.chars().filter(|c| !matches!(c, '\0' | '\r' | '\n')).collect() }
proptest::proptest! { /// The wrapper adds no equality of its own: two slices are `IrcStr`-equal /// exactly when `casemap::eq_ignore_case` holds. Fed arbitrary strings. #[test] fn eq_lifts_casemap(a in ".*", b in ".*") { let lifted = IrcStr::new(&a) == IrcStr::new(&b); proptest::prop_assert_eq!(lifted, casemap::eq_ignore_case(a.as_bytes(), b.as_bytes())); }
/// `Ord` is consistent with `Eq` (`cmp == Equal` iff `==`) and antisymmetric. #[test] fn ord_is_consistent_and_antisymmetric(a in ".*", b in ".*") { let (sa, sb) = (IrcStr::new(&a), IrcStr::new(&b)); proptest::prop_assert_eq!(sa.cmp(sb) == Ordering::Equal, sa == sb); proptest::prop_assert_eq!(sa.cmp(sb), sb.cmp(sa).reverse()); }
/// The `HashMap`-key contract the daemon's nick/channel tables rely on: /// equal `IrcString`s hash equally. #[test] fn hash_agrees_with_eq_prop(a in ".*", b in ".*") { let (sa, sb) = (sanitize(&a), sanitize(&b)); let (ka, kb) = (IrcString::try_from(sa).unwrap(), IrcString::try_from(sb).unwrap()); if ka == kb { proptest::prop_assert_eq!(hash_of(&ka), hash_of(&kb)); } }
/// `Borrow<IrcStr>` + matching `Hash`/`Eq`: a value inserted under one casing /// is found by a case-different borrowed key (`map.get(IrcStr::new(..))`). #[test] fn borrow_round_trip(s in ".*") { use std::collections::HashMap; let clean = sanitize(&s); let key = IrcString::try_from(clean.clone()).unwrap(); let mut m: HashMap<IrcString, u32> = HashMap::new(); m.insert(key, 42); // The same text (any casing folds equal to itself) must be found. proptest::prop_assert_eq!(m.get(IrcStr::new(&clean)), Some(&42)); }
/// Validation soundness: `try_from` succeeds iff the text has no NUL/CR/LF, /// `FromStr` agrees, and on success storage is byte-identical (casing kept). #[test] fn validation_is_sound(s in ".*") { let has_framing = s.chars().any(|c| matches!(c, '\0' | '\r' | '\n')); let parsed: Result<IrcString, _> = s.parse(); proptest::prop_assert_eq!(IrcString::try_from(s.as_str()).is_ok(), !has_framing); proptest::prop_assert_eq!(parsed.is_ok(), !has_framing); if let Ok(v) = IrcString::try_from(s.as_str()) { proptest::prop_assert_eq!(v.as_str(), s.as_str()); } }
/// `IrcStringBuilder` is a deferred validate: concatenating pieces and /// building equals validating the joined string, and rejects iff any piece /// carried a framing byte. #[test] fn builder_equals_try_from(parts in proptest::collection::vec(".*", 0..6)) { let mut b = IrcStringBuilder::new(); for p in &parts { b = b.push_str(p); } let joined: String = parts.concat(); proptest::prop_assert_eq!(b.build(), IrcString::try_from(joined)); } }}