username.rs (6542B)
1 //! Canonical public usernames. 2 //! 3 //! [`Username`] trims surrounding whitespace, lowercases ASCII input, enforces 4 //! [`MIN_LENGTH`] and [`MAX_LENGTH`], and accepts only the documented public 5 //! character set and dot placement. Parsing and serde decoding use the same 6 //! validator and always produce canonical text. 7 8 use alloc::string::String; 9 use core::{fmt, str::FromStr}; 10 11 use crate::Error; 12 13 /// Minimum canonical username length in ASCII bytes. 14 pub const MIN_LENGTH: usize = 3; 15 16 /// Maximum canonical username length in ASCII bytes. 17 pub const MAX_LENGTH: usize = 30; 18 19 /// A normalized public Radroots username. 20 /// 21 /// Usernames are lowercase ASCII and may contain letters, digits, `.`, `_`, 22 /// and `-`. A dot cannot occur first, last, or consecutively. Parsing trims 23 /// surrounding whitespace and canonicalizes ASCII uppercase letters. 24 #[repr(transparent)] 25 #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 26 pub struct Username(String); 27 28 impl Username { 29 /// Parses and normalizes a public username. 30 pub fn parse(value: &str) -> Result<Self, Error> { 31 let canonical = value.trim().to_ascii_lowercase(); 32 validate(&canonical)?; 33 Ok(Self(canonical)) 34 } 35 36 /// Borrows the canonical username text. 37 #[must_use] 38 pub fn as_str(&self) -> &str { 39 &self.0 40 } 41 42 /// Returns the canonical username text. 43 #[must_use] 44 pub fn into_string(self) -> String { 45 self.0 46 } 47 } 48 49 fn validate(value: &str) -> Result<(), Error> { 50 let length = value.len(); 51 if !(MIN_LENGTH..=MAX_LENGTH).contains(&length) { 52 return Err(Error::InvalidUsernameLength { 53 min: MIN_LENGTH, 54 max: MAX_LENGTH, 55 actual: length, 56 }); 57 } 58 59 let bytes = value.as_bytes(); 60 if bytes.first() == Some(&b'.') || bytes.last() == Some(&b'.') { 61 return Err(Error::InvalidUsernameDotPlacement); 62 } 63 64 let mut previous_dot = false; 65 for (index, byte) in bytes.iter().copied().enumerate() { 66 if byte == b'.' { 67 if previous_dot { 68 return Err(Error::InvalidUsernameDotPlacement); 69 } 70 previous_dot = true; 71 continue; 72 } 73 previous_dot = false; 74 if !(byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'-')) { 75 return Err(Error::InvalidUsernameCharacter { index }); 76 } 77 } 78 Ok(()) 79 } 80 81 impl fmt::Debug for Username { 82 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 83 formatter 84 .debug_tuple("Username") 85 .field(&self.as_str()) 86 .finish() 87 } 88 } 89 90 impl fmt::Display for Username { 91 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 92 formatter.write_str(self.as_str()) 93 } 94 } 95 96 impl FromStr for Username { 97 type Err = Error; 98 99 fn from_str(value: &str) -> Result<Self, Self::Err> { 100 Self::parse(value) 101 } 102 } 103 104 impl TryFrom<&str> for Username { 105 type Error = Error; 106 107 fn try_from(value: &str) -> Result<Self, Self::Error> { 108 Self::parse(value) 109 } 110 } 111 112 impl TryFrom<String> for Username { 113 type Error = Error; 114 115 fn try_from(value: String) -> Result<Self, Self::Error> { 116 Self::parse(&value) 117 } 118 } 119 120 impl AsRef<str> for Username { 121 fn as_ref(&self) -> &str { 122 self.as_str() 123 } 124 } 125 126 #[cfg(feature = "serde")] 127 impl serde::Serialize for Username { 128 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 129 where 130 S: serde::Serializer, 131 { 132 serializer.serialize_str(self.as_str()) 133 } 134 } 135 136 #[cfg(feature = "serde")] 137 impl<'de> serde::Deserialize<'de> for Username { 138 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 139 where 140 D: serde::Deserializer<'de>, 141 { 142 struct UsernameVisitor; 143 144 impl serde::de::Visitor<'_> for UsernameVisitor { 145 type Value = Username; 146 147 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 148 formatter.write_str("a valid public Radroots username") 149 } 150 151 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> 152 where 153 E: serde::de::Error, 154 { 155 Username::parse(value).map_err(E::custom) 156 } 157 } 158 159 deserializer.deserialize_str(UsernameVisitor) 160 } 161 } 162 163 #[cfg(test)] 164 mod tests { 165 use alloc::{format, string::ToString}; 166 167 use super::*; 168 169 #[test] 170 fn usernames_normalize_to_one_canonical_form() { 171 let username = Username::parse(" RadRoots.Test ").unwrap(); 172 assert_eq!(username.as_str(), "radroots.test"); 173 assert_eq!(username.as_ref(), "radroots.test"); 174 assert_eq!(username.to_string(), "radroots.test"); 175 assert_eq!(format!("{username:?}"), "Username(\"radroots.test\")"); 176 assert_eq!(Username::from_str("radroots.test").unwrap(), username); 177 assert_eq!(Username::try_from("radroots.test").unwrap(), username); 178 assert_eq!( 179 Username::try_from(String::from("radroots.test")).unwrap(), 180 username 181 ); 182 assert_eq!(username.clone().into_string(), "radroots.test"); 183 } 184 185 #[test] 186 fn usernames_reject_invalid_lengths_characters_and_dots() { 187 assert!(matches!( 188 Username::parse("rr"), 189 Err(Error::InvalidUsernameLength { actual: 2, .. }) 190 )); 191 assert!(matches!( 192 Username::parse(&"r".repeat(MAX_LENGTH + 1)), 193 Err(Error::InvalidUsernameLength { actual, .. }) if actual == MAX_LENGTH + 1 194 )); 195 assert!(matches!( 196 Username::parse("rad roots"), 197 Err(Error::InvalidUsernameCharacter { index: 3 }) 198 )); 199 for value in [".radroots", "radroots.", "radroots..test"] { 200 assert!(matches!( 201 Username::parse(value), 202 Err(Error::InvalidUsernameDotPlacement) 203 )); 204 } 205 assert!(matches!( 206 Username::parse("rädroots"), 207 Err(Error::InvalidUsernameCharacter { index: 1 }) 208 )); 209 } 210 211 #[cfg(feature = "serde")] 212 #[test] 213 fn username_serde_is_checked_and_canonical() { 214 let username: Username = serde_json::from_str("\" RadRoots \"").unwrap(); 215 assert_eq!(username.as_str(), "radroots"); 216 assert_eq!(serde_json::to_string(&username).unwrap(), "\"radroots\""); 217 assert!(serde_json::from_str::<Username>("\"rr\"").is_err()); 218 assert!(serde_json::from_str::<Username>("42").is_err()); 219 } 220 }