1use std::{rc::Rc, sync::Arc};
4
5pub use ruma_identifiers_validation::user_id::localpart_is_fully_conforming;
6use ruma_identifiers_validation::{ID_MAX_BYTES, localpart_is_backwards_compatible};
7use ruma_macros::IdDst;
8
9use super::{IdParseError, MatrixToUri, MatrixUri, ServerName, matrix_uri::UriAction};
10
11#[repr(transparent)]
23#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdDst)]
24#[ruma_id(validate = ruma_identifiers_validation::user_id::validate)]
25pub struct UserId(str);
26
27impl UserId {
28 #[cfg(feature = "rand")]
33 #[allow(clippy::new_ret_no_self)]
34 pub fn new(server_name: &ServerName) -> OwnedUserId {
35 Self::from_borrowed(&format!(
36 "@{}:{}",
37 super::generate_localpart(12).to_lowercase(),
38 server_name
39 ))
40 .to_owned()
41 }
42
43 pub fn parse_with_server_name(
51 id: impl AsRef<str> + Into<Box<str>>,
52 server_name: &ServerName,
53 ) -> Result<OwnedUserId, IdParseError> {
54 let id_str = id.as_ref();
55
56 if id_str.starts_with('@') {
57 Self::parse(id)
58 } else {
59 localpart_is_backwards_compatible(id_str)?;
60 Ok(Self::from_borrowed(&format!("@{id_str}:{server_name}")).to_owned())
61 }
62 }
63
64 pub fn parse_with_server_name_rc(
68 id: impl AsRef<str> + Into<Rc<str>>,
69 server_name: &ServerName,
70 ) -> Result<Rc<Self>, IdParseError> {
71 let id_str = id.as_ref();
72
73 if id_str.starts_with('@') {
74 Self::parse_rc(id)
75 } else {
76 localpart_is_backwards_compatible(id_str)?;
77 Ok(Self::from_rc(format!("@{id_str}:{server_name}").into()))
78 }
79 }
80
81 pub fn parse_with_server_name_arc(
85 id: impl AsRef<str> + Into<Arc<str>>,
86 server_name: &ServerName,
87 ) -> Result<Arc<Self>, IdParseError> {
88 let id_str = id.as_ref();
89
90 if id_str.starts_with('@') {
91 Self::parse_arc(id)
92 } else {
93 localpart_is_backwards_compatible(id_str)?;
94 Ok(Self::from_arc(format!("@{id_str}:{server_name}").into()))
95 }
96 }
97
98 pub fn localpart(&self) -> &str {
100 &self.as_str()[1..self.colon_idx()]
101 }
102
103 pub fn server_name(&self) -> &ServerName {
105 ServerName::from_borrowed(&self.as_str()[self.colon_idx() + 1..])
106 }
107
108 fn validate_fully_conforming(&self) -> Result<bool, IdParseError> {
113 if self.as_bytes().len() > ID_MAX_BYTES {
116 return Err(IdParseError::MaximumLengthExceeded);
117 }
118
119 localpart_is_fully_conforming(self.localpart())
120 }
121
122 pub fn validate_strict(&self) -> Result<(), IdParseError> {
129 let is_fully_conforming = self.validate_fully_conforming()?;
130
131 if is_fully_conforming { Ok(()) } else { Err(IdParseError::InvalidCharacters) }
132 }
133
134 pub fn validate_historical(&self) -> Result<(), IdParseError> {
144 self.validate_fully_conforming()?;
145 Ok(())
146 }
147
148 pub fn is_historical(&self) -> bool {
155 self.validate_fully_conforming().is_ok_and(|is_fully_conforming| !is_fully_conforming)
156 }
157
158 pub fn matrix_to_uri(&self) -> MatrixToUri {
172 MatrixToUri::new(self.into(), Vec::new())
173 }
174
175 pub fn matrix_uri(&self, chat: bool) -> MatrixUri {
192 MatrixUri::new(self.into(), Vec::new(), Some(UriAction::Chat).filter(|_| chat))
193 }
194
195 fn colon_idx(&self) -> usize {
196 self.as_str().find(':').unwrap()
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::{OwnedUserId, UserId};
203 use crate::{IdParseError, server_name};
204
205 #[test]
206 fn valid_user_id_from_str() {
207 let user_id = <&UserId>::try_from("@carl:example.com").expect("Failed to create UserId.");
208 assert_eq!(user_id.as_str(), "@carl:example.com");
209 assert_eq!(user_id.localpart(), "carl");
210 assert_eq!(user_id.server_name(), "example.com");
211 assert!(!user_id.is_historical());
212 user_id.validate_historical().unwrap();
213 user_id.validate_strict().unwrap();
214 }
215
216 #[test]
217 fn parse_valid_user_id() {
218 let server_name = server_name!("example.com");
219 let user_id = UserId::parse_with_server_name("@carl:example.com", server_name)
220 .expect("Failed to create UserId.");
221 assert_eq!(user_id.as_str(), "@carl:example.com");
222 assert_eq!(user_id.localpart(), "carl");
223 assert_eq!(user_id.server_name(), "example.com");
224 assert!(!user_id.is_historical());
225 user_id.validate_historical().unwrap();
226 user_id.validate_strict().unwrap();
227 }
228
229 #[test]
230 fn parse_valid_user_id_parts() {
231 let server_name = server_name!("example.com");
232 let user_id =
233 UserId::parse_with_server_name("carl", server_name).expect("Failed to create UserId.");
234 assert_eq!(user_id.as_str(), "@carl:example.com");
235 assert_eq!(user_id.localpart(), "carl");
236 assert_eq!(user_id.server_name(), "example.com");
237 assert!(!user_id.is_historical());
238 user_id.validate_historical().unwrap();
239 user_id.validate_strict().unwrap();
240 }
241
242 #[test]
243 fn backwards_compatible_user_id() {
244 let localpart = "τ";
245 let user_id_str = "@τ:example.com";
246 let server_name = server_name!("example.com");
247
248 let user_id = <&UserId>::try_from(user_id_str).unwrap();
249 assert_eq!(user_id.as_str(), user_id_str);
250 assert_eq!(user_id.localpart(), localpart);
251 assert_eq!(user_id.server_name(), server_name);
252 assert!(!user_id.is_historical());
253 user_id.validate_historical().unwrap_err();
254 user_id.validate_strict().unwrap_err();
255
256 let user_id = UserId::parse_with_server_name(user_id_str, server_name).unwrap();
257 assert_eq!(user_id.as_str(), user_id_str);
258 assert_eq!(user_id.localpart(), localpart);
259 assert_eq!(user_id.server_name(), server_name);
260 assert!(!user_id.is_historical());
261 user_id.validate_historical().unwrap_err();
262 user_id.validate_strict().unwrap_err();
263
264 let user_id = UserId::parse_with_server_name(localpart, server_name).unwrap();
265 assert_eq!(user_id.as_str(), user_id_str);
266 assert_eq!(user_id.localpart(), localpart);
267 assert_eq!(user_id.server_name(), server_name);
268 assert!(!user_id.is_historical());
269 user_id.validate_historical().unwrap_err();
270 user_id.validate_strict().unwrap_err();
271
272 let user_id = UserId::parse_with_server_name_rc(user_id_str, server_name).unwrap();
273 assert_eq!(user_id.as_str(), user_id_str);
274 assert_eq!(user_id.localpart(), localpart);
275 assert_eq!(user_id.server_name(), server_name);
276 assert!(!user_id.is_historical());
277 user_id.validate_historical().unwrap_err();
278 user_id.validate_strict().unwrap_err();
279
280 let user_id = UserId::parse_with_server_name_rc(localpart, server_name).unwrap();
281 assert_eq!(user_id.as_str(), user_id_str);
282 assert_eq!(user_id.localpart(), localpart);
283 assert_eq!(user_id.server_name(), server_name);
284 assert!(!user_id.is_historical());
285 user_id.validate_historical().unwrap_err();
286 user_id.validate_strict().unwrap_err();
287
288 let user_id = UserId::parse_with_server_name_arc(user_id_str, server_name).unwrap();
289 assert_eq!(user_id.as_str(), user_id_str);
290 assert_eq!(user_id.localpart(), localpart);
291 assert_eq!(user_id.server_name(), server_name);
292 assert!(!user_id.is_historical());
293 user_id.validate_historical().unwrap_err();
294 user_id.validate_strict().unwrap_err();
295
296 let user_id = UserId::parse_with_server_name_arc(localpart, server_name).unwrap();
297 assert_eq!(user_id.as_str(), user_id_str);
298 assert_eq!(user_id.localpart(), localpart);
299 assert_eq!(user_id.server_name(), server_name);
300 assert!(!user_id.is_historical());
301 user_id.validate_historical().unwrap_err();
302 user_id.validate_strict().unwrap_err();
303
304 let user_id = UserId::parse_rc(user_id_str).unwrap();
305 assert_eq!(user_id.as_str(), user_id_str);
306 assert_eq!(user_id.localpart(), localpart);
307 assert_eq!(user_id.server_name(), server_name);
308 assert!(!user_id.is_historical());
309 user_id.validate_historical().unwrap_err();
310 user_id.validate_strict().unwrap_err();
311
312 let user_id = UserId::parse_arc(user_id_str).unwrap();
313 assert_eq!(user_id.as_str(), user_id_str);
314 assert_eq!(user_id.localpart(), localpart);
315 assert_eq!(user_id.server_name(), server_name);
316 assert!(!user_id.is_historical());
317 user_id.validate_historical().unwrap_err();
318 user_id.validate_strict().unwrap_err();
319 }
320
321 #[test]
322 fn definitely_invalid_user_id() {
323 UserId::parse_with_server_name("a:b", server_name!("example.com")).unwrap_err();
324 }
325
326 #[test]
327 fn valid_historical_user_id() {
328 let user_id =
329 <&UserId>::try_from("@a%b[irc]:example.com").expect("Failed to create UserId.");
330 assert_eq!(user_id.as_str(), "@a%b[irc]:example.com");
331 assert_eq!(user_id.localpart(), "a%b[irc]");
332 assert_eq!(user_id.server_name(), "example.com");
333 assert!(user_id.is_historical());
334 user_id.validate_historical().unwrap();
335 user_id.validate_strict().unwrap_err();
336 }
337
338 #[test]
339 fn parse_valid_historical_user_id() {
340 let server_name = server_name!("example.com");
341 let user_id = UserId::parse_with_server_name("@a%b[irc]:example.com", server_name)
342 .expect("Failed to create UserId.");
343 assert_eq!(user_id.as_str(), "@a%b[irc]:example.com");
344 assert_eq!(user_id.localpart(), "a%b[irc]");
345 assert_eq!(user_id.server_name(), "example.com");
346 assert!(user_id.is_historical());
347 user_id.validate_historical().unwrap();
348 user_id.validate_strict().unwrap_err();
349 }
350
351 #[test]
352 fn parse_valid_historical_user_id_parts() {
353 let server_name = server_name!("example.com");
354 let user_id = UserId::parse_with_server_name("a%b[irc]", server_name)
355 .expect("Failed to create UserId.");
356 assert_eq!(user_id.as_str(), "@a%b[irc]:example.com");
357 assert_eq!(user_id.localpart(), "a%b[irc]");
358 assert_eq!(user_id.server_name(), "example.com");
359 assert!(user_id.is_historical());
360 user_id.validate_historical().unwrap();
361 user_id.validate_strict().unwrap_err();
362 }
363
364 #[test]
365 fn uppercase_user_id() {
366 let user_id = <&UserId>::try_from("@CARL:example.com").expect("Failed to create UserId.");
367 assert_eq!(user_id.as_str(), "@CARL:example.com");
368 assert!(user_id.is_historical());
369 user_id.validate_historical().unwrap();
370 user_id.validate_strict().unwrap_err();
371 }
372
373 #[cfg(feature = "rand")]
374 #[test]
375 fn generate_random_valid_user_id() {
376 let server_name = server_name!("example.com");
377 let user_id = UserId::new(server_name);
378 assert_eq!(user_id.localpart().len(), 12);
379 assert_eq!(user_id.server_name(), "example.com");
380 user_id.validate_historical().unwrap();
381 user_id.validate_strict().unwrap();
382
383 let id_str = user_id.as_str();
384
385 assert!(id_str.starts_with('@'));
386 assert_eq!(id_str.len(), 25);
387 }
388
389 #[test]
390 fn serialize_valid_user_id() {
391 assert_eq!(
392 serde_json::to_string(
393 <&UserId>::try_from("@carl:example.com").expect("Failed to create UserId.")
394 )
395 .expect("Failed to convert UserId to JSON."),
396 r#""@carl:example.com""#
397 );
398 }
399
400 #[test]
401 fn deserialize_valid_user_id() {
402 assert_eq!(
403 serde_json::from_str::<OwnedUserId>(r#""@carl:example.com""#)
404 .expect("Failed to convert JSON to UserId"),
405 <&UserId>::try_from("@carl:example.com").expect("Failed to create UserId.")
406 );
407 }
408
409 #[test]
410 fn valid_user_id_with_explicit_standard_port() {
411 assert_eq!(
412 <&UserId>::try_from("@carl:example.com:443")
413 .expect("Failed to create UserId.")
414 .as_str(),
415 "@carl:example.com:443"
416 );
417 }
418
419 #[test]
420 fn valid_user_id_with_non_standard_port() {
421 let user_id =
422 <&UserId>::try_from("@carl:example.com:5000").expect("Failed to create UserId.");
423 assert_eq!(user_id.as_str(), "@carl:example.com:5000");
424 assert!(!user_id.is_historical());
425 }
426
427 #[test]
428 fn invalid_characters_in_user_id_localpart() {
429 let user_id = <&UserId>::try_from("@te\nst:example.com").unwrap();
430 assert_eq!(user_id.validate_historical().unwrap_err(), IdParseError::InvalidCharacters);
431 assert_eq!(user_id.validate_strict().unwrap_err(), IdParseError::InvalidCharacters);
432 }
433
434 #[test]
435 fn missing_user_id_sigil() {
436 assert_eq!(
437 <&UserId>::try_from("carl:example.com").unwrap_err(),
438 IdParseError::MissingLeadingSigil
439 );
440 }
441
442 #[test]
443 fn missing_user_id_delimiter() {
444 assert_eq!(<&UserId>::try_from("@carl").unwrap_err(), IdParseError::MissingColon);
445 }
446
447 #[test]
448 fn invalid_user_id_host() {
449 assert_eq!(<&UserId>::try_from("@carl:/").unwrap_err(), IdParseError::InvalidServerName);
450 }
451
452 #[test]
453 fn invalid_user_id_port() {
454 assert_eq!(
455 <&UserId>::try_from("@carl:example.com:notaport").unwrap_err(),
456 IdParseError::InvalidServerName
457 );
458 }
459}