ids.rs (3712B)
1 use core::fmt; 2 3 use crate::errors::{GroupError, GroupErrorKind}; 4 5 pub const MIN_GROUP_ID_BYTES: usize = 1; 6 pub const MAX_GROUP_ID_BYTES: usize = 128; 7 8 #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 9 pub struct GroupId(String); 10 11 impl GroupId { 12 pub fn new(value: &str) -> Result<Self, GroupError> { 13 Self::new_with_max_bytes(value, MAX_GROUP_ID_BYTES) 14 } 15 16 pub fn new_with_max_bytes(value: &str, max_bytes: usize) -> Result<Self, GroupError> { 17 validate_group_id(value, max_bytes)?; 18 Ok(Self(value.to_owned())) 19 } 20 21 pub fn as_str(&self) -> &str { 22 &self.0 23 } 24 } 25 26 impl fmt::Debug for GroupId { 27 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 28 formatter.debug_tuple("GroupId").field(&self.0).finish() 29 } 30 } 31 32 impl fmt::Display for GroupId { 33 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 34 formatter.write_str(self.as_str()) 35 } 36 } 37 38 pub fn validate_group_id(value: &str, max_bytes: usize) -> Result<(), GroupError> { 39 let byte_len = value.len(); 40 if byte_len < MIN_GROUP_ID_BYTES { 41 return Err(invalid_group_id("group id must not be empty")); 42 } 43 if byte_len > max_bytes { 44 return Err(invalid_group_id(format!( 45 "group id must be at most {max_bytes} bytes" 46 ))); 47 } 48 if value.trim() != value { 49 return Err(invalid_group_id( 50 "group id must not contain leading or trailing whitespace", 51 )); 52 } 53 for character in value.chars() { 54 if character == '\0' { 55 return Err(invalid_group_id("group id must not contain NUL")); 56 } 57 if character.is_control() { 58 return Err(invalid_group_id( 59 "group id must not contain control characters", 60 )); 61 } 62 if matches!(character, '/' | '\\' | '?' | '#' | ':' | '&' | '=') { 63 return Err(invalid_group_id( 64 "group id must not contain slashes or URL separators", 65 )); 66 } 67 } 68 Ok(()) 69 } 70 71 fn invalid_group_id(message: impl Into<String>) -> GroupError { 72 GroupError::invalid(GroupErrorKind::InvalidGroupId, message) 73 } 74 75 #[cfg(test)] 76 mod tests { 77 use super::GroupId; 78 79 #[test] 80 fn group_id_validation_rejects_forbidden_forms() { 81 assert_eq!( 82 GroupId::new("").expect_err("empty").message(), 83 "group id must not be empty" 84 ); 85 assert_eq!( 86 GroupId::new(&"a".repeat(129)) 87 .expect_err("too long") 88 .message(), 89 "group id must be at most 128 bytes" 90 ); 91 assert_eq!( 92 GroupId::new(" group").expect_err("trim").message(), 93 "group id must not contain leading or trailing whitespace" 94 ); 95 assert_eq!( 96 GroupId::new("group\u{0}id").expect_err("nul").message(), 97 "group id must not contain NUL" 98 ); 99 assert_eq!( 100 GroupId::new("group\nid").expect_err("control").message(), 101 "group id must not contain control characters" 102 ); 103 assert_eq!( 104 GroupId::new("group/id").expect_err("slash").message(), 105 "group id must not contain slashes or URL separators" 106 ); 107 assert_eq!( 108 GroupId::new("group?id").expect_err("url").message(), 109 "group id must not contain slashes or URL separators" 110 ); 111 } 112 113 #[test] 114 fn group_id_is_case_sensitive() { 115 let lower = GroupId::new("farm").expect("lower"); 116 let upper = GroupId::new("Farm").expect("upper"); 117 118 assert_ne!(lower, upper); 119 assert_eq!(lower.as_str(), "farm"); 120 assert_eq!(upper.as_str(), "Farm"); 121 } 122 }