ruma_common/identifiers/
event_id.rs

1//! Matrix event identifiers.
2
3use ruma_macros::IdDst;
4
5use super::ServerName;
6
7/// A Matrix [event ID].
8///
9/// An `EventId` is generated randomly or converted from a string slice, and can be converted back
10/// into a string as needed.
11///
12/// # Room versions
13///
14/// Matrix specifies multiple [room versions] and the format of event identifiers differ between
15/// them. The original format used by room versions 1 and 2 uses a short pseudorandom "localpart"
16/// followed by the hostname and port of the originating homeserver. Later room versions change
17/// event identifiers to be a hash of the event encoded with Base64. Some of the methods provided by
18/// `EventId` are only relevant to the original event format.
19///
20/// ```
21/// # use ruma_common::EventId;
22/// // Original format
23/// assert_eq!(<&EventId>::try_from("$h29iv0s8:example.com").unwrap(), "$h29iv0s8:example.com");
24/// // Room version 3 format
25/// assert_eq!(
26///     <&EventId>::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk").unwrap(),
27///     "$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk"
28/// );
29/// // Room version 4 format
30/// assert_eq!(
31///     <&EventId>::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg").unwrap(),
32///     "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg"
33/// );
34/// ```
35///
36/// [event ID]: https://spec.matrix.org/latest/appendices/#event-ids
37/// [room versions]: https://spec.matrix.org/latest/rooms/#complete-list-of-room-versions
38#[repr(transparent)]
39#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdDst)]
40#[ruma_id(validate = ruma_identifiers_validation::event_id::validate)]
41pub struct EventId(str);
42
43impl EventId {
44    /// Attempts to generate an `EventId` for the given origin server with a localpart consisting
45    /// of 18 random ASCII characters.
46    ///
47    /// This should only be used for events in the original format  as used by Matrix room versions
48    /// 1 and 2.
49    #[cfg(feature = "rand")]
50    #[allow(clippy::new_ret_no_self)]
51    pub fn new(server_name: &ServerName) -> OwnedEventId {
52        OwnedEventId::from_string_unchecked(format!(
53            "${}:{server_name}",
54            super::generate_localpart(18)
55        ))
56    }
57
58    /// Returns the event's unique ID.
59    ///
60    /// For the original event format as used by Matrix room versions 1 and 2, this is the
61    /// "localpart" that precedes the homeserver. For later formats, this is the entire ID without
62    /// the leading `$` sigil.
63    pub fn localpart(&self) -> &str {
64        let idx = self.colon_idx().unwrap_or_else(|| self.as_str().len());
65        &self.as_str()[1..idx]
66    }
67
68    /// Returns the server name of the event ID.
69    ///
70    /// Only applicable to events in the original format as used by Matrix room versions 1 and 2.
71    pub fn server_name(&self) -> Option<&ServerName> {
72        self.colon_idx().map(|idx| ServerName::from_borrowed_unchecked(&self.as_str()[idx + 1..]))
73    }
74
75    fn colon_idx(&self) -> Option<usize> {
76        self.as_str().find(':')
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::{EventId, OwnedEventId};
83    use crate::IdParseError;
84
85    #[test]
86    fn valid_original_event_id() {
87        assert_eq!(
88            <&EventId>::try_from("$39hvsi03hlne:example.com").expect("Failed to create EventId."),
89            "$39hvsi03hlne:example.com"
90        );
91    }
92
93    #[test]
94    fn valid_base64_event_id() {
95        assert_eq!(
96            <&EventId>::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk")
97                .expect("Failed to create EventId."),
98            "$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk"
99        );
100    }
101
102    #[test]
103    fn valid_url_safe_base64_event_id() {
104        assert_eq!(
105            <&EventId>::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg")
106                .expect("Failed to create EventId."),
107            "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg"
108        );
109    }
110
111    #[cfg(feature = "rand")]
112    #[test]
113    fn generate_random_valid_event_id() {
114        use crate::server_name;
115
116        let event_id = EventId::new(server_name!("example.com"));
117        let id_str = event_id.as_str();
118
119        assert!(id_str.starts_with('$'));
120        assert_eq!(id_str.len(), 31);
121    }
122
123    #[test]
124    fn serialize_valid_original_event_id() {
125        assert_eq!(
126            serde_json::to_string(
127                <&EventId>::try_from("$39hvsi03hlne:example.com")
128                    .expect("Failed to create EventId.")
129            )
130            .expect("Failed to convert EventId to JSON."),
131            r#""$39hvsi03hlne:example.com""#
132        );
133    }
134
135    #[test]
136    fn serialize_valid_base64_event_id() {
137        assert_eq!(
138            serde_json::to_string(
139                <&EventId>::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk")
140                    .expect("Failed to create EventId.")
141            )
142            .expect("Failed to convert EventId to JSON."),
143            r#""$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk""#
144        );
145    }
146
147    #[test]
148    fn serialize_valid_url_safe_base64_event_id() {
149        assert_eq!(
150            serde_json::to_string(
151                <&EventId>::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg")
152                    .expect("Failed to create EventId.")
153            )
154            .expect("Failed to convert EventId to JSON."),
155            r#""$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg""#
156        );
157    }
158
159    #[test]
160    fn deserialize_valid_original_event_id() {
161        assert_eq!(
162            serde_json::from_str::<OwnedEventId>(r#""$39hvsi03hlne:example.com""#)
163                .expect("Failed to convert JSON to EventId"),
164            <&EventId>::try_from("$39hvsi03hlne:example.com").expect("Failed to create EventId.")
165        );
166    }
167
168    #[test]
169    fn deserialize_valid_base64_event_id() {
170        assert_eq!(
171            serde_json::from_str::<OwnedEventId>(
172                r#""$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk""#
173            )
174            .expect("Failed to convert JSON to EventId"),
175            <&EventId>::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk")
176                .expect("Failed to create EventId.")
177        );
178    }
179
180    #[test]
181    fn deserialize_valid_url_safe_base64_event_id() {
182        assert_eq!(
183            serde_json::from_str::<OwnedEventId>(
184                r#""$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg""#
185            )
186            .expect("Failed to convert JSON to EventId"),
187            <&EventId>::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg")
188                .expect("Failed to create EventId.")
189        );
190    }
191
192    #[test]
193    fn valid_original_event_id_with_explicit_standard_port() {
194        assert_eq!(
195            <&EventId>::try_from("$39hvsi03hlne:example.com:443")
196                .expect("Failed to create EventId."),
197            "$39hvsi03hlne:example.com:443"
198        );
199    }
200
201    #[test]
202    fn valid_original_event_id_with_non_standard_port() {
203        assert_eq!(
204            <&EventId>::try_from("$39hvsi03hlne:example.com:5000")
205                .expect("Failed to create EventId."),
206            "$39hvsi03hlne:example.com:5000"
207        );
208    }
209
210    #[test]
211    fn missing_original_event_id_sigil() {
212        assert_eq!(
213            <&EventId>::try_from("39hvsi03hlne:example.com").unwrap_err(),
214            IdParseError::MissingLeadingSigil
215        );
216    }
217
218    #[test]
219    fn missing_base64_event_id_sigil() {
220        assert_eq!(
221            <&EventId>::try_from("acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk").unwrap_err(),
222            IdParseError::MissingLeadingSigil
223        );
224    }
225
226    #[test]
227    fn missing_url_safe_base64_event_id_sigil() {
228        assert_eq!(
229            <&EventId>::try_from("Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg").unwrap_err(),
230            IdParseError::MissingLeadingSigil
231        );
232    }
233
234    #[test]
235    fn invalid_event_id_host() {
236        assert_eq!(
237            <&EventId>::try_from("$39hvsi03hlne:/").unwrap_err(),
238            IdParseError::InvalidServerName
239        );
240    }
241
242    #[test]
243    fn invalid_event_id_port() {
244        assert_eq!(
245            <&EventId>::try_from("$39hvsi03hlne:example.com:notaport").unwrap_err(),
246            IdParseError::InvalidServerName
247        );
248    }
249}