ruma_common/identifiers/
user_id.rs1pub use ruma_identifiers_validation::user_id::localpart_is_fully_conforming;
4use ruma_identifiers_validation::{ID_MAX_BYTES, localpart_is_backwards_compatible};
5use ruma_macros::IdDst;
6
7use super::{IdParseError, MatrixToUri, MatrixUri, ServerName, matrix_uri::UriAction};
8
9#[repr(transparent)]
21#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdDst)]
22#[ruma_id(validate = ruma_identifiers_validation::user_id::validate, smallvec_inline_bytes = 40)]
23pub struct UserId(str);
24
25impl UserId {
26 #[cfg(feature = "rand")]
31 #[allow(clippy::new_ret_no_self)]
32 pub fn new(server_name: &ServerName) -> OwnedUserId {
33 OwnedUserId::from_string_unchecked(format!(
34 "@{}:{}",
35 super::generate_localpart(12).to_lowercase(),
36 server_name
37 ))
38 }
39
40 pub fn parse_with_server_name(
48 id: impl AsRef<str>,
49 server_name: &ServerName,
50 ) -> Result<OwnedUserId, IdParseError> {
51 let id_str = id.as_ref();
52
53 if id_str.starts_with('@') {
54 Self::parse(id)
55 } else {
56 localpart_is_backwards_compatible(id_str)?;
57 Ok(OwnedUserId::from_string_unchecked(format!("@{id_str}:{server_name}")))
58 }
59 }
60
61 pub fn localpart(&self) -> &str {
63 super::find_localpart(self.as_str())
64 }
65
66 pub fn server_name(&self) -> &ServerName {
68 super::find_server_name_unchecked(self.as_str()).expect("user ID should contain a colon")
69 }
70
71 fn validate_fully_conforming(&self) -> Result<bool, IdParseError> {
76 if self.as_bytes().len() > ID_MAX_BYTES {
79 return Err(IdParseError::MaximumLengthExceeded);
80 }
81
82 localpart_is_fully_conforming(self.localpart())
83 }
84
85 pub fn validate_strict(&self) -> Result<(), IdParseError> {
92 let is_fully_conforming = self.validate_fully_conforming()?;
93
94 if is_fully_conforming { Ok(()) } else { Err(IdParseError::InvalidCharacters) }
95 }
96
97 pub fn validate_historical(&self) -> Result<(), IdParseError> {
107 self.validate_fully_conforming()?;
108 Ok(())
109 }
110
111 pub fn is_historical(&self) -> bool {
118 self.validate_fully_conforming().is_ok_and(|is_fully_conforming| !is_fully_conforming)
119 }
120
121 pub fn matrix_to_uri(&self) -> MatrixToUri {
135 MatrixToUri::new(self.into(), Vec::new())
136 }
137
138 pub fn matrix_uri(&self, chat: bool) -> MatrixUri {
155 MatrixUri::new(self.into(), Vec::new(), chat.then_some(UriAction::Chat))
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::{OwnedUserId, UserId};
162 use crate::{IdParseError, server_name};
163
164 #[test]
165 fn valid_user_id_from_str() {
166 let user_id = <&UserId>::try_from("@carl:example.com").expect("Failed to create UserId.");
167 assert_eq!(user_id, "@carl:example.com");
168 assert_eq!(user_id.localpart(), "carl");
169 assert_eq!(user_id.server_name(), "example.com");
170 assert!(!user_id.is_historical());
171 user_id.validate_historical().unwrap();
172 user_id.validate_strict().unwrap();
173 }
174
175 #[test]
176 fn parse_valid_user_id() {
177 let server_name = server_name!("example.com");
178 let user_id = UserId::parse_with_server_name("@carl:example.com", server_name)
179 .expect("Failed to create UserId.");
180 assert_eq!(user_id, "@carl:example.com");
181 assert_eq!(user_id.localpart(), "carl");
182 assert_eq!(user_id.server_name(), "example.com");
183 assert!(!user_id.is_historical());
184 user_id.validate_historical().unwrap();
185 user_id.validate_strict().unwrap();
186 }
187
188 #[test]
189 fn parse_valid_user_id_parts() {
190 let server_name = server_name!("example.com");
191 let user_id =
192 UserId::parse_with_server_name("carl", server_name).expect("Failed to create UserId.");
193 assert_eq!(user_id, "@carl:example.com");
194 assert_eq!(user_id.localpart(), "carl");
195 assert_eq!(user_id.server_name(), "example.com");
196 assert!(!user_id.is_historical());
197 user_id.validate_historical().unwrap();
198 user_id.validate_strict().unwrap();
199 }
200
201 #[test]
202 fn backwards_compatible_user_id() {
203 let localpart = "τ";
204 let user_id_str = "@τ:example.com";
205 let server_name = server_name!("example.com");
206
207 let user_id = <&UserId>::try_from(user_id_str).unwrap();
208 assert_eq!(user_id, user_id_str);
209 assert_eq!(user_id.localpart(), localpart);
210 assert_eq!(user_id.server_name(), server_name);
211 assert!(!user_id.is_historical());
212 user_id.validate_historical().unwrap_err();
213 user_id.validate_strict().unwrap_err();
214
215 let user_id = UserId::parse_with_server_name(user_id_str, server_name).unwrap();
216 assert_eq!(user_id, user_id_str);
217 assert_eq!(user_id.localpart(), localpart);
218 assert_eq!(user_id.server_name(), server_name);
219 assert!(!user_id.is_historical());
220 user_id.validate_historical().unwrap_err();
221 user_id.validate_strict().unwrap_err();
222
223 let user_id = UserId::parse_with_server_name(localpart, server_name).unwrap();
224 assert_eq!(user_id, user_id_str);
225 assert_eq!(user_id.localpart(), localpart);
226 assert_eq!(user_id.server_name(), server_name);
227 assert!(!user_id.is_historical());
228 user_id.validate_historical().unwrap_err();
229 user_id.validate_strict().unwrap_err();
230 }
231
232 #[test]
233 fn definitely_invalid_user_id() {
234 UserId::parse_with_server_name("a:b", server_name!("example.com")).unwrap_err();
235 }
236
237 #[test]
238 fn valid_historical_user_id() {
239 let user_id =
240 <&UserId>::try_from("@a%b[irc]:example.com").expect("Failed to create UserId.");
241 assert_eq!(user_id, "@a%b[irc]:example.com");
242 assert_eq!(user_id.localpart(), "a%b[irc]");
243 assert_eq!(user_id.server_name(), "example.com");
244 assert!(user_id.is_historical());
245 user_id.validate_historical().unwrap();
246 user_id.validate_strict().unwrap_err();
247 }
248
249 #[test]
250 fn parse_valid_historical_user_id() {
251 let server_name = server_name!("example.com");
252 let user_id = UserId::parse_with_server_name("@a%b[irc]:example.com", server_name)
253 .expect("Failed to create UserId.");
254 assert_eq!(user_id, "@a%b[irc]:example.com");
255 assert_eq!(user_id.localpart(), "a%b[irc]");
256 assert_eq!(user_id.server_name(), "example.com");
257 assert!(user_id.is_historical());
258 user_id.validate_historical().unwrap();
259 user_id.validate_strict().unwrap_err();
260 }
261
262 #[test]
263 fn parse_valid_historical_user_id_parts() {
264 let server_name = server_name!("example.com");
265 let user_id = UserId::parse_with_server_name("a%b[irc]", server_name)
266 .expect("Failed to create UserId.");
267 assert_eq!(user_id, "@a%b[irc]:example.com");
268 assert_eq!(user_id.localpart(), "a%b[irc]");
269 assert_eq!(user_id.server_name(), "example.com");
270 assert!(user_id.is_historical());
271 user_id.validate_historical().unwrap();
272 user_id.validate_strict().unwrap_err();
273 }
274
275 #[test]
276 fn uppercase_user_id() {
277 let user_id = <&UserId>::try_from("@CARL:example.com").expect("Failed to create UserId.");
278 assert_eq!(user_id, "@CARL:example.com");
279 assert!(user_id.is_historical());
280 user_id.validate_historical().unwrap();
281 user_id.validate_strict().unwrap_err();
282 }
283
284 #[cfg(feature = "rand")]
285 #[test]
286 fn generate_random_valid_user_id() {
287 let server_name = server_name!("example.com");
288 let user_id = UserId::new(server_name);
289 assert_eq!(user_id.localpart().len(), 12);
290 assert_eq!(user_id.server_name(), "example.com");
291 user_id.validate_historical().unwrap();
292 user_id.validate_strict().unwrap();
293
294 let id_str = user_id.as_str();
295
296 assert!(id_str.starts_with('@'));
297 assert_eq!(id_str.len(), 25);
298 }
299
300 #[test]
301 fn serialize_valid_user_id() {
302 assert_eq!(
303 serde_json::to_string(
304 <&UserId>::try_from("@carl:example.com").expect("Failed to create UserId.")
305 )
306 .expect("Failed to convert UserId to JSON."),
307 r#""@carl:example.com""#
308 );
309 }
310
311 #[test]
312 fn deserialize_valid_user_id() {
313 assert_eq!(
314 serde_json::from_str::<OwnedUserId>(r#""@carl:example.com""#)
315 .expect("Failed to convert JSON to UserId"),
316 "@carl:example.com"
317 );
318 }
319
320 #[test]
321 fn valid_user_id_with_explicit_standard_port() {
322 assert_eq!(
323 <&UserId>::try_from("@carl:example.com:443").expect("Failed to create UserId."),
324 "@carl:example.com:443"
325 );
326 }
327
328 #[test]
329 fn valid_user_id_with_non_standard_port() {
330 let user_id =
331 <&UserId>::try_from("@carl:example.com:5000").expect("Failed to create UserId.");
332 assert_eq!(user_id, "@carl:example.com:5000");
333 assert!(!user_id.is_historical());
334 }
335
336 #[test]
337 fn invalid_characters_in_user_id_localpart() {
338 let user_id = <&UserId>::try_from("@te\nst:example.com").unwrap();
339 assert_eq!(user_id.validate_historical().unwrap_err(), IdParseError::InvalidCharacters);
340 assert_eq!(user_id.validate_strict().unwrap_err(), IdParseError::InvalidCharacters);
341 }
342
343 #[test]
344 fn missing_user_id_sigil() {
345 assert_eq!(
346 <&UserId>::try_from("carl:example.com").unwrap_err(),
347 IdParseError::MissingLeadingSigil
348 );
349 }
350
351 #[test]
352 fn missing_user_id_delimiter() {
353 assert_eq!(<&UserId>::try_from("@carl").unwrap_err(), IdParseError::MissingColon);
354 }
355
356 #[test]
357 fn invalid_user_id_host() {
358 assert_eq!(<&UserId>::try_from("@carl:/").unwrap_err(), IdParseError::InvalidServerName);
359 }
360
361 #[test]
362 fn invalid_user_id_port() {
363 assert_eq!(
364 <&UserId>::try_from("@carl:example.com:notaport").unwrap_err(),
365 IdParseError::InvalidServerName
366 );
367 }
368}