tangle


git clone https://radroots.dev/git/tangle.git
Log | Files | Refs | README | LICENSE

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 }