Skip to main content

steel_core/permission/
key.rs

1use std::{error::Error, fmt};
2
3/// One validated dotted permission key.
4#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
5pub struct PermissionKey(String);
6
7impl PermissionKey {
8    /// Parses a dotted permission key with an optional trailing wildcard.
9    ///
10    /// # Errors
11    ///
12    /// Returns an error for empty or invalid segments and misplaced wildcards.
13    pub fn parse(value: impl Into<String>) -> Result<Self, PermissionKeyError> {
14        let value = value.into();
15        if value.is_empty() {
16            return Err(PermissionKeyError::Empty);
17        }
18
19        let segments = value.split('.').collect::<Vec<_>>();
20        for (index, segment) in segments.iter().enumerate() {
21            if segment.is_empty() {
22                return Err(PermissionKeyError::EmptySegment);
23            }
24            if *segment == "*" {
25                if index + 1 != segments.len() {
26                    return Err(PermissionKeyError::WildcardNotFinal);
27                }
28                continue;
29            }
30            if segment.contains('*') {
31                return Err(PermissionKeyError::InvalidWildcardSegment);
32            }
33            validate_permission_segment(segment)?;
34        }
35
36        Ok(Self(value))
37    }
38
39    /// Builds a permission key from validated non-wildcard segments.
40    ///
41    /// # Errors
42    ///
43    /// Returns an error when no segments are supplied.
44    pub fn from_segments(
45        segments: impl IntoIterator<Item = PermissionSegment>,
46    ) -> Result<Self, PermissionKeyError> {
47        let mut value = String::new();
48        for segment in segments {
49            if !value.is_empty() {
50                value.push('.');
51            }
52            value.push_str(segment.as_str());
53        }
54        if value.is_empty() {
55            return Err(PermissionKeyError::Empty);
56        }
57        Ok(Self(value))
58    }
59
60    /// Returns the validated textual key.
61    #[must_use]
62    pub fn as_str(&self) -> &str {
63        &self.0
64    }
65
66    /// Appends one non-wildcard child segment.
67    ///
68    /// # Errors
69    ///
70    /// Returns an error when this key ends in a wildcard.
71    pub fn child(&self, segment: &PermissionSegment) -> Result<Self, PermissionKeyError> {
72        Self::parse(format!("{}.{}", self.0, segment.as_str()))
73    }
74
75    /// Returns whether this key pattern matches `other`.
76    #[must_use]
77    pub fn matches(&self, other: &Self) -> bool {
78        if self.0 == "*" {
79            return true;
80        }
81        let Some(prefix) = self.0.strip_suffix(".*") else {
82            return self == other;
83        };
84        other
85            .0
86            .strip_prefix(prefix)
87            .is_some_and(|remaining| remaining.starts_with('.'))
88    }
89
90    pub(super) fn specificity(&self) -> usize {
91        if self.0 == "*" {
92            return 0;
93        }
94        self.0
95            .strip_suffix(".*")
96            .map_or(self.0.as_str(), |prefix| prefix)
97            .split('.')
98            .count()
99    }
100
101    pub(super) fn scopes(&self, key: &Self) -> bool {
102        if self == key {
103            return true;
104        }
105        key.0
106            .strip_prefix(self.as_str())
107            .is_some_and(|remaining| remaining.starts_with('.'))
108    }
109}
110
111/// One validated non-wildcard permission key segment.
112#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
113pub struct PermissionSegment(String);
114
115impl PermissionSegment {
116    /// Parses one permission key segment.
117    ///
118    /// # Errors
119    ///
120    /// Returns an error when the segment is empty, contains a wildcard, or
121    /// contains characters outside lowercase ASCII letters, digits, `_`, and `-`.
122    pub fn parse(value: impl Into<String>) -> Result<Self, PermissionKeyError> {
123        let value = value.into();
124        if value.is_empty() {
125            return Err(PermissionKeyError::EmptySegment);
126        }
127        if value.contains('*') {
128            return Err(PermissionKeyError::InvalidWildcardSegment);
129        }
130        validate_permission_segment(&value)?;
131        Ok(Self(value))
132    }
133
134    /// Returns the validated segment text.
135    #[must_use]
136    pub fn as_str(&self) -> &str {
137        &self.0
138    }
139}
140
141fn validate_permission_segment(segment: &str) -> Result<(), PermissionKeyError> {
142    if segment.bytes().all(|byte| {
143        byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_' || byte == b'-'
144    }) {
145        Ok(())
146    } else {
147        Err(PermissionKeyError::InvalidSegment)
148    }
149}
150
151/// Why a permission key failed validation.
152#[derive(Clone, Copy, Debug, PartialEq, Eq)]
153pub enum PermissionKeyError {
154    /// The complete key is empty.
155    Empty,
156    /// A dotted segment is empty.
157    EmptySegment,
158    /// A wildcard appears before the final segment.
159    WildcardNotFinal,
160    /// A wildcard is embedded within a non-wildcard segment.
161    InvalidWildcardSegment,
162    /// A segment contains unsupported characters.
163    InvalidSegment,
164}
165
166impl fmt::Display for PermissionKeyError {
167    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
168        match self {
169            Self::Empty => formatter.write_str("permission key is empty"),
170            Self::EmptySegment => formatter.write_str("permission key contains an empty segment"),
171            Self::WildcardNotFinal => {
172                formatter.write_str("permission wildcard must be the final segment")
173            }
174            Self::InvalidWildcardSegment => {
175                formatter.write_str("permission wildcard must occupy the full segment")
176            }
177            Self::InvalidSegment => formatter.write_str(
178                "permission segment must contain only lowercase letters, numbers, '_' or '-'",
179            ),
180        }
181    }
182}
183
184impl Error for PermissionKeyError {}