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}