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