this repo has no description
1pub const MAX_EMAIL_LENGTH: usize = 254; 2pub const MAX_LOCAL_PART_LENGTH: usize = 64; 3pub const MAX_DOMAIN_LENGTH: usize = 253; 4pub const MAX_DOMAIN_LABEL_LENGTH: usize = 63; 5const EMAIL_LOCAL_SPECIAL_CHARS: &str = ".!#$%&'*+/=?^_`{|}~-"; 6 7pub const MIN_HANDLE_LENGTH: usize = 3; 8pub const MAX_HANDLE_LENGTH: usize = 253; 9pub const MAX_SERVICE_HANDLE_LOCAL_PART: usize = 18; 10 11#[derive(Debug, PartialEq)] 12pub enum HandleValidationError { 13 Empty, 14 TooShort, 15 TooLong, 16 InvalidCharacters, 17 StartsWithInvalidChar, 18 EndsWithInvalidChar, 19 ContainsSpaces, 20 BannedWord, 21 Reserved, 22} 23 24impl std::fmt::Display for HandleValidationError { 25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 26 match self { 27 Self::Empty => write!(f, "Handle cannot be empty"), 28 Self::TooShort => write!( 29 f, 30 "Handle must be at least {} characters", 31 MIN_HANDLE_LENGTH 32 ), 33 Self::TooLong => write!( 34 f, 35 "Handle exceeds maximum length of {} characters", 36 MAX_SERVICE_HANDLE_LOCAL_PART 37 ), 38 Self::InvalidCharacters => write!( 39 f, 40 "Handle contains invalid characters. Only alphanumeric characters and hyphens are allowed" 41 ), 42 Self::StartsWithInvalidChar => { 43 write!(f, "Handle cannot start with a hyphen") 44 } 45 Self::EndsWithInvalidChar => write!(f, "Handle cannot end with a hyphen"), 46 Self::ContainsSpaces => write!(f, "Handle cannot contain spaces"), 47 Self::BannedWord => write!(f, "Inappropriate language in handle"), 48 Self::Reserved => write!(f, "Reserved handle"), 49 } 50 } 51} 52 53pub fn validate_short_handle(handle: &str) -> Result<String, HandleValidationError> { 54 validate_service_handle(handle, false) 55} 56 57pub fn validate_service_handle( 58 handle: &str, 59 allow_reserved: bool, 60) -> Result<String, HandleValidationError> { 61 let handle = handle.trim(); 62 63 if handle.is_empty() { 64 return Err(HandleValidationError::Empty); 65 } 66 67 if handle.contains(' ') || handle.contains('\t') || handle.contains('\n') { 68 return Err(HandleValidationError::ContainsSpaces); 69 } 70 71 if handle.len() < MIN_HANDLE_LENGTH { 72 return Err(HandleValidationError::TooShort); 73 } 74 75 if handle.len() > MAX_SERVICE_HANDLE_LOCAL_PART { 76 return Err(HandleValidationError::TooLong); 77 } 78 79 if let Some(first_char) = handle.chars().next() 80 && first_char == '-' 81 { 82 return Err(HandleValidationError::StartsWithInvalidChar); 83 } 84 85 if let Some(last_char) = handle.chars().last() 86 && last_char == '-' 87 { 88 return Err(HandleValidationError::EndsWithInvalidChar); 89 } 90 91 for c in handle.chars() { 92 if !c.is_ascii_alphanumeric() && c != '-' { 93 return Err(HandleValidationError::InvalidCharacters); 94 } 95 } 96 97 if crate::moderation::has_explicit_slur(handle) { 98 return Err(HandleValidationError::BannedWord); 99 } 100 101 if !allow_reserved && crate::handle::reserved::is_reserved_subdomain(handle) { 102 return Err(HandleValidationError::Reserved); 103 } 104 105 Ok(handle.to_lowercase()) 106} 107 108pub fn is_valid_email(email: &str) -> bool { 109 let email = email.trim(); 110 if email.is_empty() || email.len() > MAX_EMAIL_LENGTH { 111 return false; 112 } 113 let parts: Vec<&str> = email.rsplitn(2, '@').collect(); 114 if parts.len() != 2 { 115 return false; 116 } 117 let domain = parts[0]; 118 let local = parts[1]; 119 if local.is_empty() || local.len() > MAX_LOCAL_PART_LENGTH { 120 return false; 121 } 122 if local.starts_with('.') || local.ends_with('.') { 123 return false; 124 } 125 if local.contains("..") { 126 return false; 127 } 128 for c in local.chars() { 129 if !c.is_ascii_alphanumeric() && !EMAIL_LOCAL_SPECIAL_CHARS.contains(c) { 130 return false; 131 } 132 } 133 if domain.is_empty() || domain.len() > MAX_DOMAIN_LENGTH { 134 return false; 135 } 136 if !domain.contains('.') { 137 return false; 138 } 139 for label in domain.split('.') { 140 if label.is_empty() || label.len() > MAX_DOMAIN_LABEL_LENGTH { 141 return false; 142 } 143 if label.starts_with('-') || label.ends_with('-') { 144 return false; 145 } 146 for c in label.chars() { 147 if !c.is_ascii_alphanumeric() && c != '-' { 148 return false; 149 } 150 } 151 } 152 true 153} 154 155#[cfg(test)] 156mod tests { 157 use super::*; 158 159 #[test] 160 fn test_valid_handles() { 161 assert_eq!(validate_short_handle("alice"), Ok("alice".to_string())); 162 assert_eq!(validate_short_handle("bob123"), Ok("bob123".to_string())); 163 assert_eq!( 164 validate_short_handle("user-name"), 165 Ok("user-name".to_string()) 166 ); 167 assert_eq!( 168 validate_short_handle("UPPERCASE"), 169 Ok("uppercase".to_string()) 170 ); 171 assert_eq!( 172 validate_short_handle("MixedCase123"), 173 Ok("mixedcase123".to_string()) 174 ); 175 assert_eq!(validate_short_handle("abc"), Ok("abc".to_string())); 176 } 177 178 #[test] 179 fn test_invalid_handles() { 180 assert_eq!(validate_short_handle(""), Err(HandleValidationError::Empty)); 181 assert_eq!( 182 validate_short_handle(" "), 183 Err(HandleValidationError::Empty) 184 ); 185 assert_eq!( 186 validate_short_handle("ab"), 187 Err(HandleValidationError::TooShort) 188 ); 189 assert_eq!( 190 validate_short_handle("a"), 191 Err(HandleValidationError::TooShort) 192 ); 193 assert_eq!( 194 validate_short_handle("test spaces"), 195 Err(HandleValidationError::ContainsSpaces) 196 ); 197 assert_eq!( 198 validate_short_handle("test\ttab"), 199 Err(HandleValidationError::ContainsSpaces) 200 ); 201 assert_eq!( 202 validate_short_handle("-starts"), 203 Err(HandleValidationError::StartsWithInvalidChar) 204 ); 205 assert_eq!( 206 validate_short_handle("_starts"), 207 Err(HandleValidationError::InvalidCharacters) 208 ); 209 assert_eq!( 210 validate_short_handle("ends-"), 211 Err(HandleValidationError::EndsWithInvalidChar) 212 ); 213 assert_eq!( 214 validate_short_handle("ends_"), 215 Err(HandleValidationError::InvalidCharacters) 216 ); 217 assert_eq!( 218 validate_short_handle("user_name"), 219 Err(HandleValidationError::InvalidCharacters) 220 ); 221 assert_eq!( 222 validate_short_handle("test@user"), 223 Err(HandleValidationError::InvalidCharacters) 224 ); 225 assert_eq!( 226 validate_short_handle("test!user"), 227 Err(HandleValidationError::InvalidCharacters) 228 ); 229 assert_eq!( 230 validate_short_handle("test.user"), 231 Err(HandleValidationError::InvalidCharacters) 232 ); 233 } 234 235 #[test] 236 fn test_handle_trimming() { 237 assert_eq!(validate_short_handle(" alice "), Ok("alice".to_string())); 238 } 239 240 #[test] 241 fn test_handle_max_length() { 242 assert_eq!( 243 validate_short_handle("exactly18charslol"), 244 Ok("exactly18charslol".to_string()) 245 ); 246 assert_eq!( 247 validate_short_handle("exactly18charslol1"), 248 Ok("exactly18charslol1".to_string()) 249 ); 250 assert_eq!( 251 validate_short_handle("exactly19characters"), 252 Err(HandleValidationError::TooLong) 253 ); 254 assert_eq!( 255 validate_short_handle("waytoolongusername123456789"), 256 Err(HandleValidationError::TooLong) 257 ); 258 } 259 260 #[test] 261 fn test_reserved_subdomains() { 262 assert_eq!( 263 validate_short_handle("admin"), 264 Err(HandleValidationError::Reserved) 265 ); 266 assert_eq!( 267 validate_short_handle("api"), 268 Err(HandleValidationError::Reserved) 269 ); 270 assert_eq!( 271 validate_short_handle("bsky"), 272 Err(HandleValidationError::Reserved) 273 ); 274 assert_eq!( 275 validate_short_handle("barackobama"), 276 Err(HandleValidationError::Reserved) 277 ); 278 assert_eq!( 279 validate_short_handle("ADMIN"), 280 Err(HandleValidationError::Reserved) 281 ); 282 assert_eq!(validate_short_handle("alice"), Ok("alice".to_string())); 283 assert_eq!( 284 validate_short_handle("notreserved"), 285 Ok("notreserved".to_string()) 286 ); 287 } 288 289 #[test] 290 fn test_allow_reserved() { 291 assert_eq!( 292 validate_service_handle("admin", true), 293 Ok("admin".to_string()) 294 ); 295 assert_eq!(validate_service_handle("api", true), Ok("api".to_string())); 296 assert_eq!( 297 validate_service_handle("admin", false), 298 Err(HandleValidationError::Reserved) 299 ); 300 } 301 302 #[test] 303 fn test_valid_emails() { 304 assert!(is_valid_email("user@example.com")); 305 assert!(is_valid_email("user.name@example.com")); 306 assert!(is_valid_email("user+tag@example.com")); 307 assert!(is_valid_email("user@sub.example.com")); 308 assert!(is_valid_email("USER@EXAMPLE.COM")); 309 assert!(is_valid_email("user123@example123.com")); 310 assert!(is_valid_email("a@b.co")); 311 } 312 #[test] 313 fn test_invalid_emails() { 314 assert!(!is_valid_email("")); 315 assert!(!is_valid_email("user")); 316 assert!(!is_valid_email("user@")); 317 assert!(!is_valid_email("@example.com")); 318 assert!(!is_valid_email("user@example")); 319 assert!(!is_valid_email("user@@example.com")); 320 assert!(!is_valid_email("user@.example.com")); 321 assert!(!is_valid_email("user@example..com")); 322 assert!(!is_valid_email(".user@example.com")); 323 assert!(!is_valid_email("user.@example.com")); 324 assert!(!is_valid_email("user..name@example.com")); 325 assert!(!is_valid_email("user@-example.com")); 326 assert!(!is_valid_email("user@example-.com")); 327 } 328 #[test] 329 fn test_trimmed_whitespace() { 330 assert!(is_valid_email(" user@example.com ")); 331 } 332}