1#![allow(clippy::module_name_repetitions)]
13
14use std::{
15 borrow::Cow,
16 collections::BTreeSet,
17 iter::FromIterator,
18 ops::{Deref, DerefMut},
19 str::FromStr,
20};
21
22use serde::{Deserialize, Serialize};
23use thiserror::Error;
24
25#[derive(Debug, Error, PartialEq, Eq, PartialOrd, Ord, Hash)]
27#[error("Invalid scope format")]
28pub struct InvalidScope;
29
30#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
32pub struct ScopeToken(Cow<'static, str>);
33
34impl ScopeToken {
35 #[must_use]
38 pub const fn from_static(token: &'static str) -> Self {
39 Self(Cow::Borrowed(token))
40 }
41
42 #[must_use]
44 pub fn as_str(&self) -> &str {
45 self.0.as_ref()
46 }
47}
48
49pub const OPENID: ScopeToken = ScopeToken::from_static("openid");
53
54pub const PROFILE: ScopeToken = ScopeToken::from_static("profile");
58
59pub const EMAIL: ScopeToken = ScopeToken::from_static("email");
63
64pub const ADDRESS: ScopeToken = ScopeToken::from_static("address");
68
69pub const PHONE: ScopeToken = ScopeToken::from_static("phone");
73
74pub const OFFLINE_ACCESS: ScopeToken = ScopeToken::from_static("offline_access");
80
81fn nqchar(c: char) -> bool {
88 '\x21' == c || ('\x23'..='\x5B').contains(&c) || ('\x5D'..='\x7E').contains(&c)
89}
90
91impl FromStr for ScopeToken {
92 type Err = InvalidScope;
93
94 fn from_str(s: &str) -> Result<Self, Self::Err> {
95 if !s.is_empty() && s.chars().all(nqchar) {
100 Ok(ScopeToken(Cow::Owned(s.into())))
101 } else {
102 Err(InvalidScope)
103 }
104 }
105}
106
107impl Deref for ScopeToken {
108 type Target = str;
109
110 fn deref(&self) -> &Self::Target {
111 &self.0
112 }
113}
114
115impl std::fmt::Display for ScopeToken {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 self.0.fmt(f)
118 }
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct Scope(BTreeSet<ScopeToken>);
124
125impl Deref for Scope {
126 type Target = BTreeSet<ScopeToken>;
127
128 fn deref(&self) -> &Self::Target {
129 &self.0
130 }
131}
132
133impl DerefMut for Scope {
134 fn deref_mut(&mut self) -> &mut Self::Target {
135 &mut self.0
136 }
137}
138
139impl FromStr for Scope {
140 type Err = InvalidScope;
141
142 fn from_str(s: &str) -> Result<Self, Self::Err> {
143 let scopes: Result<BTreeSet<ScopeToken>, InvalidScope> =
148 s.split(' ').map(ScopeToken::from_str).collect();
149
150 Ok(Self(scopes?))
151 }
152}
153
154impl Scope {
155 #[must_use]
157 pub fn is_empty(&self) -> bool {
158 self.0.is_empty()
160 }
161
162 #[must_use]
164 pub fn len(&self) -> usize {
165 self.0.len()
166 }
167
168 #[must_use]
170 pub fn contains(&self, token: &str) -> bool {
171 ScopeToken::from_str(token).is_ok_and(|token| self.0.contains(&token))
172 }
173
174 pub fn insert(&mut self, value: ScopeToken) -> bool {
178 self.0.insert(value)
179 }
180}
181
182impl std::fmt::Display for Scope {
183 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184 for (index, token) in self.0.iter().enumerate() {
185 if index == 0 {
186 write!(f, "{token}")?;
187 } else {
188 write!(f, " {token}")?;
189 }
190 }
191
192 Ok(())
193 }
194}
195
196impl Serialize for Scope {
197 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
198 where
199 S: serde::Serializer,
200 {
201 self.to_string().serialize(serializer)
202 }
203}
204
205impl<'de> Deserialize<'de> for Scope {
206 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
207 where
208 D: serde::Deserializer<'de>,
209 {
210 let scope: String = Deserialize::deserialize(deserializer)?;
212 Scope::from_str(&scope).map_err(serde::de::Error::custom)
213 }
214}
215
216impl FromIterator<ScopeToken> for Scope {
217 fn from_iter<T: IntoIterator<Item = ScopeToken>>(iter: T) -> Self {
218 Self(BTreeSet::from_iter(iter))
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn parse_scope_token() {
228 assert_eq!(ScopeToken::from_str("openid"), Ok(OPENID));
229
230 assert_eq!(ScopeToken::from_str("invalid\\scope"), Err(InvalidScope));
231
232 assert!(ScopeToken::from_str("urn:matrix:client:device:AA~bb1234_-.").is_ok());
235 }
236
237 #[test]
238 fn parse_scope_token_characters() {
239 for c in '\x21'..='\x7E' {
241 if c == '\x22' || c == '\x5C' {
242 continue;
243 }
244 assert!(
245 ScopeToken::from_str(&c.to_string()).is_ok(),
246 "expected {c:?} (0x{:02X}) to be a valid NQCHAR",
247 c as u32
248 );
249 }
250
251 for s in [" ", "\"", "\\", "\x7F"] {
257 assert_eq!(ScopeToken::from_str(s), Err(InvalidScope));
258 }
259 }
260
261 #[test]
262 fn parse_scope() {
263 let scope = Scope::from_str("openid profile address").unwrap();
264 assert_eq!(scope.len(), 3);
265 assert!(scope.contains("openid"));
266 assert!(scope.contains("profile"));
267 assert!(scope.contains("address"));
268 assert!(!scope.contains("unknown"));
269
270 assert!(
271 Scope::from_str("").is_err(),
272 "there should always be at least one token in the scope"
273 );
274
275 assert!(Scope::from_str("invalid\\scope").is_err());
276 assert!(Scope::from_str("no double space").is_err());
277 assert!(Scope::from_str(" no leading space").is_err());
278 assert!(Scope::from_str("no trailing space ").is_err());
279
280 let scope = Scope::from_str("openid").unwrap();
281 assert_eq!(scope.len(), 1);
282 assert!(scope.contains("openid"));
283 assert!(!scope.contains("profile"));
284 assert!(!scope.contains("address"));
285
286 assert_eq!(
287 Scope::from_str("order does not matter"),
288 Scope::from_str("matter not order does"),
289 );
290
291 assert!(Scope::from_str("http://example.com").is_ok());
292 assert!(Scope::from_str("urn:matrix:client:api:*").is_ok());
293 assert!(Scope::from_str("urn:matrix:org.matrix.msc2967.client:api:*").is_ok());
294 }
295}