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)]
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 &self.as_str()[1..self.colon_idx()]
64 }
65
66 pub fn server_name(&self) -> &ServerName {
68 ServerName::from_borrowed_unchecked(&self.as_str()[self.colon_idx() + 1..])
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(), Some(UriAction::Chat).filter(|_| chat))
156 }
157
158 fn colon_idx(&self) -> usize {
159 self.as_str().find(':').unwrap()
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::{OwnedUserId, UserId};
166 use crate::{IdParseError, server_name};
167
168 #[test]
169 fn valid_user_id_from_str() {
170 let user_id = <&UserId>::try_from("@carl:example.com").expect("Failed to create UserId.");
171 assert_eq!(user_id.as_str(), "@carl:example.com");
172 assert_eq!(user_id.localpart(), "carl");
173 assert_eq!(user_id.server_name(), "example.com");
174 assert!(!user_id.is_historical());
175 user_id.validate_historical().unwrap();
176 user_id.validate_strict().unwrap();
177 }
178
179 #[test]
180 fn parse_valid_user_id() {
181 let server_name = server_name!("example.com");
182 let user_id = UserId::parse_with_server_name("@carl:example.com", server_name)
183 .expect("Failed to create UserId.");
184 assert_eq!(user_id.as_str(), "@carl:example.com");
185 assert_eq!(user_id.localpart(), "carl");
186 assert_eq!(user_id.server_name(), "example.com");
187 assert!(!user_id.is_historical());
188 user_id.validate_historical().unwrap();
189 user_id.validate_strict().unwrap();
190 }
191
192 #[test]
193 fn parse_valid_user_id_parts() {
194 let server_name = server_name!("example.com");
195 let user_id =
196 UserId::parse_with_server_name("carl", server_name).expect("Failed to create UserId.");
197 assert_eq!(user_id.as_str(), "@carl:example.com");
198 assert_eq!(user_id.localpart(), "carl");
199 assert_eq!(user_id.server_name(), "example.com");
200 assert!(!user_id.is_historical());
201 user_id.validate_historical().unwrap();
202 user_id.validate_strict().unwrap();
203 }
204
205 #[test]
206 fn backwards_compatible_user_id() {
207 let localpart = "τ";
208 let user_id_str = "@τ:example.com";
209 let server_name = server_name!("example.com");
210
211 let user_id = <&UserId>::try_from(user_id_str).unwrap();
212 assert_eq!(user_id.as_str(), user_id_str);
213 assert_eq!(user_id.localpart(), localpart);
214 assert_eq!(user_id.server_name(), server_name);
215 assert!(!user_id.is_historical());
216 user_id.validate_historical().unwrap_err();
217 user_id.validate_strict().unwrap_err();
218
219 let user_id = UserId::parse_with_server_name(user_id_str, server_name).unwrap();
220 assert_eq!(user_id.as_str(), user_id_str);
221 assert_eq!(user_id.localpart(), localpart);
222 assert_eq!(user_id.server_name(), server_name);
223 assert!(!user_id.is_historical());
224 user_id.validate_historical().unwrap_err();
225 user_id.validate_strict().unwrap_err();
226
227 let user_id = UserId::parse_with_server_name(localpart, server_name).unwrap();
228 assert_eq!(user_id.as_str(), user_id_str);
229 assert_eq!(user_id.localpart(), localpart);
230 assert_eq!(user_id.server_name(), server_name);
231 assert!(!user_id.is_historical());
232 user_id.validate_historical().unwrap_err();
233 user_id.validate_strict().unwrap_err();
234 }
235
236 #[test]
237 fn definitely_invalid_user_id() {
238 UserId::parse_with_server_name("a:b", server_name!("example.com")).unwrap_err();
239 }
240
241 #[test]
242 fn valid_historical_user_id() {
243 let user_id =
244 <&UserId>::try_from("@a%b[irc]:example.com").expect("Failed to create UserId.");
245 assert_eq!(user_id.as_str(), "@a%b[irc]:example.com");
246 assert_eq!(user_id.localpart(), "a%b[irc]");
247 assert_eq!(user_id.server_name(), "example.com");
248 assert!(user_id.is_historical());
249 user_id.validate_historical().unwrap();
250 user_id.validate_strict().unwrap_err();
251 }
252
253 #[test]
254 fn parse_valid_historical_user_id() {
255 let server_name = server_name!("example.com");
256 let user_id = UserId::parse_with_server_name("@a%b[irc]:example.com", server_name)
257 .expect("Failed to create UserId.");
258 assert_eq!(user_id.as_str(), "@a%b[irc]:example.com");
259 assert_eq!(user_id.localpart(), "a%b[irc]");
260 assert_eq!(user_id.server_name(), "example.com");
261 assert!(user_id.is_historical());
262 user_id.validate_historical().unwrap();
263 user_id.validate_strict().unwrap_err();
264 }
265
266 #[test]
267 fn parse_valid_historical_user_id_parts() {
268 let server_name = server_name!("example.com");
269 let user_id = UserId::parse_with_server_name("a%b[irc]", server_name)
270 .expect("Failed to create UserId.");
271 assert_eq!(user_id.as_str(), "@a%b[irc]:example.com");
272 assert_eq!(user_id.localpart(), "a%b[irc]");
273 assert_eq!(user_id.server_name(), "example.com");
274 assert!(user_id.is_historical());
275 user_id.validate_historical().unwrap();
276 user_id.validate_strict().unwrap_err();
277 }
278
279 #[test]
280 fn uppercase_user_id() {
281 let user_id = <&UserId>::try_from("@CARL:example.com").expect("Failed to create UserId.");
282 assert_eq!(user_id.as_str(), "@CARL:example.com");
283 assert!(user_id.is_historical());
284 user_id.validate_historical().unwrap();
285 user_id.validate_strict().unwrap_err();
286 }
287
288 #[cfg(feature = "rand")]
289 #[test]
290 fn generate_random_valid_user_id() {
291 let server_name = server_name!("example.com");
292 let user_id = UserId::new(server_name);
293 assert_eq!(user_id.localpart().len(), 12);
294 assert_eq!(user_id.server_name(), "example.com");
295 user_id.validate_historical().unwrap();
296 user_id.validate_strict().unwrap();
297
298 let id_str = user_id.as_str();
299
300 assert!(id_str.starts_with('@'));
301 assert_eq!(id_str.len(), 25);
302 }
303
304 #[test]
305 fn serialize_valid_user_id() {
306 assert_eq!(
307 serde_json::to_string(
308 <&UserId>::try_from("@carl:example.com").expect("Failed to create UserId.")
309 )
310 .expect("Failed to convert UserId to JSON."),
311 r#""@carl:example.com""#
312 );
313 }
314
315 #[test]
316 fn deserialize_valid_user_id() {
317 assert_eq!(
318 serde_json::from_str::<OwnedUserId>(r#""@carl:example.com""#)
319 .expect("Failed to convert JSON to UserId"),
320 <&UserId>::try_from("@carl:example.com").expect("Failed to create UserId.")
321 );
322 }
323
324 #[test]
325 fn valid_user_id_with_explicit_standard_port() {
326 assert_eq!(
327 <&UserId>::try_from("@carl:example.com:443")
328 .expect("Failed to create UserId.")
329 .as_str(),
330 "@carl:example.com:443"
331 );
332 }
333
334 #[test]
335 fn valid_user_id_with_non_standard_port() {
336 let user_id =
337 <&UserId>::try_from("@carl:example.com:5000").expect("Failed to create UserId.");
338 assert_eq!(user_id.as_str(), "@carl:example.com:5000");
339 assert!(!user_id.is_historical());
340 }
341
342 #[test]
343 fn invalid_characters_in_user_id_localpart() {
344 let user_id = <&UserId>::try_from("@te\nst:example.com").unwrap();
345 assert_eq!(user_id.validate_historical().unwrap_err(), IdParseError::InvalidCharacters);
346 assert_eq!(user_id.validate_strict().unwrap_err(), IdParseError::InvalidCharacters);
347 }
348
349 #[test]
350 fn missing_user_id_sigil() {
351 assert_eq!(
352 <&UserId>::try_from("carl:example.com").unwrap_err(),
353 IdParseError::MissingLeadingSigil
354 );
355 }
356
357 #[test]
358 fn missing_user_id_delimiter() {
359 assert_eq!(<&UserId>::try_from("@carl").unwrap_err(), IdParseError::MissingColon);
360 }
361
362 #[test]
363 fn invalid_user_id_host() {
364 assert_eq!(<&UserId>::try_from("@carl:/").unwrap_err(), IdParseError::InvalidServerName);
365 }
366
367 #[test]
368 fn invalid_user_id_port() {
369 assert_eq!(
370 <&UserId>::try_from("@carl:example.com:notaport").unwrap_err(),
371 IdParseError::InvalidServerName
372 );
373 }
374}