1use std::collections::HashSet;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum InputType {
26 EntityName,
28 DisplayName,
30 Message,
32 ThreadReply,
34 Password,
36}
37
38pub type ValidationResult<T> = Result<T, ValidationError>;
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct ValidationError {
44 pub field: String,
45 pub message: String,
46 pub code: ValidationErrorCode,
47}
48
49impl std::error::Error for ValidationError {}
50
51impl std::fmt::Display for ValidationError {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 write!(f, "{}: {}", self.field, self.message)
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum ValidationErrorCode {
60 Required,
61 TooShort,
62 TooLong,
63 InvalidFormat,
64 ReservedWord,
65 ContainsInvalidChars,
66 Empty,
67 OnlyWhitespace,
68}
69
70pub struct ValidationService {
72 reserved_words: HashSet<String>,
73}
74
75impl Default for ValidationService {
76 fn default() -> Self {
77 Self::new()
78 }
79}
80
81impl ValidationService {
82 pub fn new() -> Self {
84 let mut reserved_words = HashSet::new();
85 for word in [
87 "admin",
88 "system",
89 "root",
90 "null",
91 "undefined",
92 "none",
93 "default",
94 "public",
95 "private",
96 "internal",
97 "external",
98 "test",
99 "testing",
100 "example",
101 "sample",
102 "demo",
103 "temp",
104 "temporary",
105 ] {
106 reserved_words.insert(word.to_string());
107 }
108
109 Self { reserved_words }
110 }
111
112 pub fn validate(&self, input: &str, input_type: InputType) -> ValidationResult<()> {
114 match input_type {
115 InputType::EntityName => self.validate_entity_name(input),
116 InputType::DisplayName => self.validate_display_name(input),
117 InputType::Message => self.validate_message(input),
118 InputType::ThreadReply => self.validate_thread_reply(input),
119 InputType::Password => self.validate_password(input),
120 }
121 }
122
123 pub fn validate_entity_name(&self, name: &str) -> ValidationResult<()> {
125 if name.trim().is_empty() {
127 return Err(ValidationError {
128 field: "entity_name".to_string(),
129 message: "Entity name cannot be empty".to_string(),
130 code: ValidationErrorCode::Required,
131 });
132 }
133
134 let trimmed = name.trim();
136 if trimmed.len() < 2 {
137 return Err(ValidationError {
138 field: "entity_name".to_string(),
139 message: "Entity name must be at least 2 characters long".to_string(),
140 code: ValidationErrorCode::TooShort,
141 });
142 }
143
144 if trimmed.len() > 50 {
146 return Err(ValidationError {
147 field: "entity_name".to_string(),
148 message: "Entity name cannot exceed 50 characters".to_string(),
149 code: ValidationErrorCode::TooLong,
150 });
151 }
152
153 let lower_trimmed = trimmed.to_lowercase();
155 if self.reserved_words.contains(&lower_trimmed) {
156 return Err(ValidationError {
157 field: "entity_name".to_string(),
158 message: format!("'{}' is a reserved word and cannot be used", trimmed),
159 code: ValidationErrorCode::ReservedWord,
160 });
161 }
162
163 if !trimmed
165 .chars()
166 .all(|c| c.is_alphanumeric() || c.is_whitespace() || c == '-' || c == '_')
167 {
168 return Err(ValidationError {
169 field: "entity_name".to_string(),
170 message: "Entity name can only contain letters, numbers, spaces, hyphens, and underscores".to_string(),
171 code: ValidationErrorCode::ContainsInvalidChars,
172 });
173 }
174
175 if trimmed.contains(" ") || trimmed.contains("--") || trimmed.contains("__") {
177 return Err(ValidationError {
178 field: "entity_name".to_string(),
179 message: "Entity name cannot contain consecutive spaces or special characters"
180 .to_string(),
181 code: ValidationErrorCode::InvalidFormat,
182 });
183 }
184
185 Ok(())
186 }
187
188 pub fn validate_display_name(&self, name: &str) -> ValidationResult<()> {
190 let trimmed = name.trim();
191
192 if trimmed.is_empty() {
194 return Err(ValidationError {
195 field: "display_name".to_string(),
196 message: "Display name cannot be empty".to_string(),
197 code: ValidationErrorCode::Required,
198 });
199 }
200
201 if trimmed.len() > 100 {
203 return Err(ValidationError {
204 field: "display_name".to_string(),
205 message: "Display name cannot exceed 100 characters".to_string(),
206 code: ValidationErrorCode::TooLong,
207 });
208 }
209
210 if trimmed.chars().any(|c| c.is_control()) {
213 return Err(ValidationError {
214 field: "display_name".to_string(),
215 message: "Display name cannot contain control characters".to_string(),
216 code: ValidationErrorCode::ContainsInvalidChars,
217 });
218 }
219
220 Ok(())
221 }
222
223 pub fn validate_message(&self, message: &str) -> ValidationResult<()> {
225 let trimmed = message.trim();
227
228 if !message.is_empty() && trimmed.is_empty() {
229 return Err(ValidationError {
230 field: "message".to_string(),
231 message: "Message cannot be only whitespace".to_string(),
232 code: ValidationErrorCode::OnlyWhitespace,
233 });
234 }
235
236 if message.len() > 10000 {
238 return Err(ValidationError {
239 field: "message".to_string(),
240 message: "Message cannot exceed 10,000 characters".to_string(),
241 code: ValidationErrorCode::TooLong,
242 });
243 }
244
245 if message.chars().any(|c| c == '\0') {
248 return Err(ValidationError {
249 field: "message".to_string(),
250 message: "Message cannot contain null characters".to_string(),
251 code: ValidationErrorCode::ContainsInvalidChars,
252 });
253 }
254
255 Ok(())
256 }
257
258 pub fn validate_thread_reply(&self, reply: &str) -> ValidationResult<()> {
260 self.validate_message(reply)
261 }
262
263 pub fn validate_password(&self, password: &str) -> ValidationResult<()> {
265 if password.len() < 8 {
267 return Err(ValidationError {
268 field: "password".to_string(),
269 message: "Password must be at least 8 characters long".to_string(),
270 code: ValidationErrorCode::TooShort,
271 });
272 }
273
274 if password.len() > 128 {
276 return Err(ValidationError {
277 field: "password".to_string(),
278 message: "Password cannot exceed 128 characters".to_string(),
279 code: ValidationErrorCode::TooLong,
280 });
281 }
282
283 if password.chars().any(|c| c.is_control()) {
285 return Err(ValidationError {
286 field: "password".to_string(),
287 message: "Password cannot contain control characters".to_string(),
288 code: ValidationErrorCode::ContainsInvalidChars,
289 });
290 }
291
292 Ok(())
293 }
294
295 pub fn sanitize(&self, input: &str, input_type: InputType) -> String {
297 match input_type {
298 InputType::EntityName | InputType::DisplayName => input.trim().to_string(),
299 InputType::Message | InputType::ThreadReply | InputType::Password => input.to_string(),
300 }
301 }
302
303 pub fn validate_and_sanitize(
305 &self,
306 input: &str,
307 input_type: InputType,
308 ) -> ValidationResult<String> {
309 let sanitized = self.sanitize(input, input_type);
310 self.validate(&sanitized, input_type)?;
311 Ok(sanitized)
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 fn create_validator() -> ValidationService {
320 ValidationService::new()
321 }
322
323 mod entity_name_validation {
324 use super::*;
325
326 #[test]
327 fn test_valid_entity_names() {
328 let validator = create_validator();
329
330 assert!(validator.validate_entity_name("My Channel").is_ok());
332 assert!(validator.validate_entity_name("project-alpha").is_ok());
333 assert!(validator.validate_entity_name("Group_123").is_ok());
334 assert!(validator.validate_entity_name("AB").is_ok()); assert!(
336 validator
337 .validate_entity_name("Valid Name With Spaces")
338 .is_ok()
339 );
340 }
341
342 #[test]
343 fn test_empty_entity_names() {
344 let validator = create_validator();
345
346 let result = validator.validate_entity_name("");
347 assert!(result.is_err());
348 assert_eq!(result.unwrap_err().code, ValidationErrorCode::Required);
349
350 let result = validator.validate_entity_name(" ");
351 assert!(result.is_err());
352 assert_eq!(result.unwrap_err().code, ValidationErrorCode::Required);
353 }
354
355 #[test]
356 fn test_too_short_entity_names() {
357 let validator = create_validator();
358
359 let result = validator.validate_entity_name("A");
360 assert!(result.is_err());
361 assert_eq!(result.unwrap_err().code, ValidationErrorCode::TooShort);
362 }
363
364 #[test]
365 fn test_too_long_entity_names() {
366 let validator = create_validator();
367
368 let long_name = "A".repeat(51);
369 let result = validator.validate_entity_name(&long_name);
370 assert!(result.is_err());
371 assert_eq!(result.unwrap_err().code, ValidationErrorCode::TooLong);
372 }
373
374 #[test]
375 fn test_reserved_words() {
376 let validator = create_validator();
377
378 let result = validator.validate_entity_name("admin");
379 assert!(result.is_err());
380 assert_eq!(result.unwrap_err().code, ValidationErrorCode::ReservedWord);
381
382 let result = validator.validate_entity_name("ADMIN"); assert!(result.is_err());
384 assert_eq!(result.unwrap_err().code, ValidationErrorCode::ReservedWord);
385 }
386
387 #[test]
388 fn test_invalid_characters() {
389 let validator = create_validator();
390
391 let result = validator.validate_entity_name("Channel@Name");
392 assert!(result.is_err());
393 assert_eq!(
394 result.unwrap_err().code,
395 ValidationErrorCode::ContainsInvalidChars
396 );
397
398 let result = validator.validate_entity_name("Channel.Name");
399 assert!(result.is_err());
400 assert_eq!(
401 result.unwrap_err().code,
402 ValidationErrorCode::ContainsInvalidChars
403 );
404 }
405
406 #[test]
407 fn test_consecutive_special_chars() {
408 let validator = create_validator();
409
410 let result = validator.validate_entity_name("Channel--Name");
411 assert!(result.is_err());
412 assert_eq!(result.unwrap_err().code, ValidationErrorCode::InvalidFormat);
413
414 let result = validator.validate_entity_name("Channel Name");
415 assert!(result.is_err());
416 assert_eq!(result.unwrap_err().code, ValidationErrorCode::InvalidFormat);
417 }
418 }
419
420 mod display_name_validation {
421 use super::*;
422
423 #[test]
424 fn test_valid_display_names() {
425 let validator = create_validator();
426
427 assert!(validator.validate_display_name("John Doe").is_ok());
428 assert!(validator.validate_display_name("Alice").is_ok());
429 assert!(validator.validate_display_name("用户123").is_ok()); assert!(
431 validator
432 .validate_display_name("Name with émojis 🎉")
433 .is_ok()
434 );
435 }
436
437 #[test]
438 fn test_empty_display_names() {
439 let validator = create_validator();
440
441 let result = validator.validate_display_name("");
442 assert!(result.is_err());
443 assert_eq!(result.unwrap_err().code, ValidationErrorCode::Required);
444 }
445
446 #[test]
447 fn test_too_long_display_names() {
448 let validator = create_validator();
449
450 let long_name = "A".repeat(101);
451 let result = validator.validate_display_name(&long_name);
452 assert!(result.is_err());
453 assert_eq!(result.unwrap_err().code, ValidationErrorCode::TooLong);
454 }
455
456 #[test]
457 fn test_control_characters_in_display_names() {
458 let validator = create_validator();
459
460 let result = validator.validate_display_name("Name\nwith\nnewlines");
461 assert!(result.is_err());
462 assert_eq!(
463 result.unwrap_err().code,
464 ValidationErrorCode::ContainsInvalidChars
465 );
466 }
467 }
468
469 mod message_validation {
470 use super::*;
471
472 #[test]
473 fn test_valid_messages() {
474 let validator = create_validator();
475
476 assert!(validator.validate_message("").is_ok()); assert!(validator.validate_message("Hello world!").is_ok());
478 assert!(validator.validate_message("Multi\nline\nmessage").is_ok());
479 assert!(validator.validate_message("Message with émojis 🎉").is_ok());
480 }
481
482 #[test]
483 fn test_whitespace_only_messages() {
484 let validator = create_validator();
485
486 let result = validator.validate_message(" ");
487 assert!(result.is_err());
488 assert_eq!(
489 result.unwrap_err().code,
490 ValidationErrorCode::OnlyWhitespace
491 );
492
493 let result = validator.validate_message("\t\n \t");
494 assert!(result.is_err());
495 assert_eq!(
496 result.unwrap_err().code,
497 ValidationErrorCode::OnlyWhitespace
498 );
499 }
500
501 #[test]
502 fn test_too_long_messages() {
503 let validator = create_validator();
504
505 let long_message = "A".repeat(10001);
506 let result = validator.validate_message(&long_message);
507 assert!(result.is_err());
508 assert_eq!(result.unwrap_err().code, ValidationErrorCode::TooLong);
509 }
510
511 #[test]
512 fn test_null_characters_in_messages() {
513 let validator = create_validator();
514
515 let result = validator.validate_message("Message with \0 null");
516 assert!(result.is_err());
517 assert_eq!(
518 result.unwrap_err().code,
519 ValidationErrorCode::ContainsInvalidChars
520 );
521 }
522 }
523
524 mod password_validation {
525 use super::*;
526
527 #[test]
528 fn test_valid_passwords() {
529 let validator = create_validator();
530
531 assert!(validator.validate_password("password123").is_ok());
532 assert!(validator.validate_password("A".repeat(8).as_str()).is_ok());
533 assert!(validator.validate_password("Complex!@#$%^&*()").is_ok());
534 }
535
536 #[test]
537 fn test_too_short_passwords() {
538 let validator = create_validator();
539
540 let result = validator.validate_password("short");
541 assert!(result.is_err());
542 assert_eq!(result.unwrap_err().code, ValidationErrorCode::TooShort);
543 }
544
545 #[test]
546 fn test_too_long_passwords() {
547 let validator = create_validator();
548
549 let long_password = "A".repeat(129);
550 let result = validator.validate_password(&long_password);
551 assert!(result.is_err());
552 assert_eq!(result.unwrap_err().code, ValidationErrorCode::TooLong);
553 }
554
555 #[test]
556 fn test_control_characters_in_passwords() {
557 let validator = create_validator();
558
559 let result = validator.validate_password("password\n123");
560 assert!(result.is_err());
561 assert_eq!(
562 result.unwrap_err().code,
563 ValidationErrorCode::ContainsInvalidChars
564 );
565 }
566 }
567
568 mod sanitization {
569 use super::*;
570
571 #[test]
572 fn test_entity_name_sanitization() {
573 let validator = create_validator();
574
575 assert_eq!(
576 validator.sanitize(" My Channel ", InputType::EntityName),
577 "My Channel"
578 );
579 assert_eq!(
580 validator.sanitize("project", InputType::EntityName),
581 "project"
582 );
583 }
584
585 #[test]
586 fn test_message_sanitization() {
587 let validator = create_validator();
588
589 assert_eq!(
591 validator.sanitize(" Hello ", InputType::Message),
592 " Hello "
593 );
594 assert_eq!(
595 validator.sanitize("Multi\nline", InputType::Message),
596 "Multi\nline"
597 );
598 }
599
600 #[test]
601 fn test_validate_and_sanitize() {
602 let validator = create_validator();
603
604 let result = validator.validate_and_sanitize(" My Channel ", InputType::EntityName);
605 assert!(result.is_ok());
606 assert_eq!(result.unwrap(), "My Channel");
607
608 let result = validator.validate_and_sanitize("", InputType::EntityName);
609 assert!(result.is_err());
610 }
611 }
612}