1 //! Matrix user identifiers.
2 
3 use std::{convert::TryFrom, num::NonZeroU8};
4 
5 use crate::{error::Error, parse_id, ServerName};
6 
7 /// A Matrix user ID.
8 ///
9 /// A `UserId` is generated randomly or converted from a string slice, and can be converted back
10 /// into a string as needed.
11 ///
12 /// ```
13 /// # use std::convert::TryFrom;
14 /// # use ruma_identifiers::UserId;
15 /// assert_eq!(
16 ///     UserId::try_from("@carl:example.com").unwrap().as_ref(),
17 ///     "@carl:example.com"
18 /// );
19 /// ```
20 #[derive(Clone, Debug)]
21 pub struct UserId {
22     full_id: Box<str>,
23     colon_idx: NonZeroU8,
24     /// Whether this user id is a historical one.
25     ///
26     /// A historical user id is one that is not legal per the regular user id rules, but was
27     /// accepted by previous versions of the spec and thus has to be supported because users with
28     /// these kinds of ids still exist.
29     is_historical: bool,
30 }
31 
32 impl UserId {
33     /// Attempts to generate a `UserId` for the given origin server with a localpart consisting of
34     /// 12 random ASCII characters.
35     #[cfg(feature = "rand")]
36     #[cfg_attr(docsrs, doc(cfg(feature = "rand")))]
new(server_name: &ServerName) -> Self37     pub fn new(server_name: &ServerName) -> Self {
38         use crate::generate_localpart;
39 
40         let full_id = format!("@{}:{}", generate_localpart(12).to_lowercase(), server_name).into();
41 
42         Self { full_id, colon_idx: NonZeroU8::new(13).unwrap(), is_historical: false }
43     }
44 
45     /// Attempts to complete a user ID, by adding the colon + server name and `@` prefix, if not
46     /// present already.
47     ///
48     /// This is a convenience function for the login API, where a user can supply either their full
49     /// user ID or just the localpart. It only supports a valid user ID or a valid user ID
50     /// localpart, not the localpart plus the `@` prefix, or the localpart plus server name without
51     /// the `@` prefix.
parse_with_server_name( id: impl AsRef<str> + Into<Box<str>>, server_name: &ServerName, ) -> Result<Self, Error>52     pub fn parse_with_server_name(
53         id: impl AsRef<str> + Into<Box<str>>,
54         server_name: &ServerName,
55     ) -> Result<Self, Error> {
56         let id_str = id.as_ref();
57 
58         if id_str.starts_with('@') {
59             try_from(id.into())
60         } else {
61             let is_fully_conforming = localpart_is_fully_comforming(id_str)?;
62 
63             Ok(Self {
64                 full_id: format!("@{}:{}", id_str, server_name).into(),
65                 colon_idx: NonZeroU8::new(id_str.len() as u8 + 1).unwrap(),
66                 is_historical: !is_fully_conforming,
67             })
68         }
69     }
70 }
71 
72 impl UserId {
73     /// Returns the user's localpart.
localpart(&self) -> &str74     pub fn localpart(&self) -> &str {
75         &self.full_id[1..self.colon_idx.get() as usize]
76     }
77 
78     /// Returns the server name of the user ID.
server_name(&self) -> &ServerName79     pub fn server_name(&self) -> &ServerName {
80         <&ServerName>::try_from(&self.full_id[self.colon_idx.get() as usize + 1..]).unwrap()
81     }
82 
83     /// Whether this user ID is a historical one, i.e. one that doesn't conform to the latest
84     /// specification of the user ID grammar but is still accepted because it was previously
85     /// allowed.
is_historical(&self) -> bool86     pub fn is_historical(&self) -> bool {
87         self.is_historical
88     }
89 }
90 
91 /// Attempts to create a new Matrix user ID from a string representation.
92 ///
93 /// The string must include the leading @ sigil, the localpart, a literal colon, and a server name.
try_from<S>(user_id: S) -> Result<UserId, Error> where S: AsRef<str> + Into<Box<str>>,94 fn try_from<S>(user_id: S) -> Result<UserId, Error>
95 where
96     S: AsRef<str> + Into<Box<str>>,
97 {
98     let user_id_str = user_id.as_ref();
99 
100     let colon_idx = parse_id(user_id_str, &['@'])?;
101     let localpart = &user_id_str[1..colon_idx.get() as usize];
102 
103     let is_historical = localpart_is_fully_comforming(localpart)?;
104 
105     Ok(UserId { full_id: user_id.into(), colon_idx, is_historical: !is_historical })
106 }
107 
108 common_impls!(UserId, try_from, "a Matrix user ID");
109 
110 /// Check whether the given user id localpart is valid and fully conforming
111 ///
112 /// Returns an `Err` for invalid user ID localparts, `Ok(false)` for historical user ID localparts
113 /// and `Ok(true)` for fully conforming user ID localparts.
localpart_is_fully_comforming(localpart: &str) -> Result<bool, Error>114 pub fn localpart_is_fully_comforming(localpart: &str) -> Result<bool, Error> {
115     if localpart.is_empty() {
116         return Err(Error::InvalidLocalPart);
117     }
118 
119     // See https://matrix.org/docs/spec/appendices#user-identifiers
120     let is_fully_conforming = localpart
121         .bytes()
122         .all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-' | b'.' | b'=' | b'_' | b'/'));
123 
124     // If it's not fully conforming, check if it contains characters that are also disallowed
125     // for historical user IDs. If there are, return an error.
126     // See https://matrix.org/docs/spec/appendices#historical-user-ids
127     if !is_fully_conforming && localpart.bytes().any(|b| b < 0x21 || b == b':' || b > 0x7E) {
128         Err(Error::InvalidCharacters)
129     } else {
130         Ok(is_fully_conforming)
131     }
132 }
133 
134 #[cfg(test)]
135 mod tests {
136     use std::convert::TryFrom;
137 
138     #[cfg(feature = "serde")]
139     use serde_json::{from_str, to_string};
140 
141     use super::UserId;
142     use crate::{error::Error, ServerName};
143 
144     #[test]
valid_user_id_from_str()145     fn valid_user_id_from_str() {
146         let user_id = UserId::try_from("@carl:example.com").expect("Failed to create UserId.");
147         assert_eq!(user_id.as_ref(), "@carl:example.com");
148         assert_eq!(user_id.localpart(), "carl");
149         assert_eq!(user_id.server_name(), "example.com");
150         assert!(!user_id.is_historical());
151     }
152 
153     #[test]
parse_valid_user_id()154     fn parse_valid_user_id() {
155         let server_name = <&ServerName>::try_from("example.com").unwrap();
156         let user_id = UserId::parse_with_server_name("@carl:example.com", server_name)
157             .expect("Failed to create UserId.");
158         assert_eq!(user_id.as_ref(), "@carl:example.com");
159         assert_eq!(user_id.localpart(), "carl");
160         assert_eq!(user_id.server_name(), "example.com");
161         assert!(!user_id.is_historical());
162     }
163 
164     #[test]
parse_valid_user_id_parts()165     fn parse_valid_user_id_parts() {
166         let server_name = <&ServerName>::try_from("example.com").unwrap();
167         let user_id =
168             UserId::parse_with_server_name("carl", server_name).expect("Failed to create UserId.");
169         assert_eq!(user_id.as_ref(), "@carl:example.com");
170         assert_eq!(user_id.localpart(), "carl");
171         assert_eq!(user_id.server_name(), "example.com");
172         assert!(!user_id.is_historical());
173     }
174 
175     #[test]
valid_historical_user_id()176     fn valid_historical_user_id() {
177         let user_id = UserId::try_from("@a%b[irc]:example.com").expect("Failed to create UserId.");
178         assert_eq!(user_id.as_ref(), "@a%b[irc]:example.com");
179         assert_eq!(user_id.localpart(), "a%b[irc]");
180         assert_eq!(user_id.server_name(), "example.com");
181         assert!(user_id.is_historical());
182     }
183 
184     #[test]
parse_valid_historical_user_id()185     fn parse_valid_historical_user_id() {
186         let server_name = <&ServerName>::try_from("example.com").unwrap();
187         let user_id = UserId::parse_with_server_name("@a%b[irc]:example.com", server_name)
188             .expect("Failed to create UserId.");
189         assert_eq!(user_id.as_ref(), "@a%b[irc]:example.com");
190         assert_eq!(user_id.localpart(), "a%b[irc]");
191         assert_eq!(user_id.server_name(), "example.com");
192         assert!(user_id.is_historical());
193     }
194 
195     #[test]
parse_valid_historical_user_id_parts()196     fn parse_valid_historical_user_id_parts() {
197         let server_name = <&ServerName>::try_from("example.com").unwrap();
198         let user_id = UserId::parse_with_server_name("a%b[irc]", server_name)
199             .expect("Failed to create UserId.");
200         assert_eq!(user_id.as_ref(), "@a%b[irc]:example.com");
201         assert_eq!(user_id.localpart(), "a%b[irc]");
202         assert_eq!(user_id.server_name(), "example.com");
203         assert!(user_id.is_historical());
204     }
205 
206     #[test]
uppercase_user_id()207     fn uppercase_user_id() {
208         let user_id = UserId::try_from("@CARL:example.com").expect("Failed to create UserId.");
209         assert_eq!(user_id.as_ref(), "@CARL:example.com");
210         assert!(user_id.is_historical());
211     }
212 
213     #[cfg(feature = "rand")]
214     #[test]
generate_random_valid_user_id()215     fn generate_random_valid_user_id() {
216         let server_name = <&ServerName>::try_from("example.com").unwrap();
217         let user_id = UserId::new(server_name);
218         assert_eq!(user_id.localpart().len(), 12);
219         assert_eq!(user_id.server_name(), "example.com");
220 
221         let id_str = user_id.as_str();
222 
223         assert!(id_str.starts_with('@'));
224         assert_eq!(id_str.len(), 25);
225     }
226 
227     #[cfg(feature = "serde")]
228     #[test]
serialize_valid_user_id()229     fn serialize_valid_user_id() {
230         assert_eq!(
231             to_string(&UserId::try_from("@carl:example.com").expect("Failed to create UserId."))
232                 .expect("Failed to convert UserId to JSON."),
233             r#""@carl:example.com""#
234         );
235     }
236 
237     #[cfg(feature = "serde")]
238     #[test]
deserialize_valid_user_id()239     fn deserialize_valid_user_id() {
240         assert_eq!(
241             from_str::<UserId>(r#""@carl:example.com""#).expect("Failed to convert JSON to UserId"),
242             UserId::try_from("@carl:example.com").expect("Failed to create UserId.")
243         );
244     }
245 
246     #[test]
valid_user_id_with_explicit_standard_port()247     fn valid_user_id_with_explicit_standard_port() {
248         assert_eq!(
249             UserId::try_from("@carl:example.com:443").expect("Failed to create UserId.").as_ref(),
250             "@carl:example.com:443"
251         );
252     }
253 
254     #[test]
valid_user_id_with_non_standard_port()255     fn valid_user_id_with_non_standard_port() {
256         let user_id = UserId::try_from("@carl:example.com:5000").expect("Failed to create UserId.");
257         assert_eq!(user_id.as_ref(), "@carl:example.com:5000");
258         assert!(!user_id.is_historical());
259     }
260 
261     #[test]
invalid_characters_in_user_id_localpart()262     fn invalid_characters_in_user_id_localpart() {
263         assert_eq!(UserId::try_from("@te\nst:example.com").unwrap_err(), Error::InvalidCharacters);
264     }
265 
266     #[test]
missing_user_id_sigil()267     fn missing_user_id_sigil() {
268         assert_eq!(UserId::try_from("carl:example.com").unwrap_err(), Error::MissingSigil);
269     }
270 
271     #[test]
missing_localpart()272     fn missing_localpart() {
273         assert_eq!(UserId::try_from("@:example.com").unwrap_err(), Error::InvalidLocalPart);
274     }
275 
276     #[test]
missing_user_id_delimiter()277     fn missing_user_id_delimiter() {
278         assert_eq!(UserId::try_from("@carl").unwrap_err(), Error::MissingDelimiter);
279     }
280 
281     #[test]
invalid_user_id_host()282     fn invalid_user_id_host() {
283         assert_eq!(UserId::try_from("@carl:/").unwrap_err(), Error::InvalidServerName);
284     }
285 
286     #[test]
invalid_user_id_port()287     fn invalid_user_id_port() {
288         assert_eq!(
289             UserId::try_from("@carl:example.com:notaport").unwrap_err(),
290             Error::InvalidServerName
291         );
292     }
293 }
294