Skip to main content

ruma_common/identifiers/
event_id.rs

1//! Matrix event identifiers.
2
3use ruma_macros::IdDst;
4
5use super::{IdParseError, 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::{server_name, EventId};
22/// // Room versions 1 and 2
23/// assert_eq!(<&EventId>::try_from("$h29iv0s8:example.com").unwrap(), "$h29iv0s8:example.com");
24///
25/// # #[cfg(feature = "rand")]
26/// # {
27/// let server_name = server_name!("example.com");
28/// let event_id = EventId::new_v1(server_name);
29/// assert_eq!(event_id.localpart().len(), 18);
30/// assert_eq!(event_id.server_name(), Some(server_name));
31/// # }
32///
33/// // Room version 3
34/// assert_eq!(
35///     <&EventId>::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk").unwrap(),
36///     "$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk"
37/// );
38/// assert_eq!(
39///     EventId::new_v2_or_v3("acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk").unwrap(),
40///     "$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk"
41/// );
42///
43/// // Room version 4 and later
44/// assert_eq!(
45///     <&EventId>::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg").unwrap(),
46///     "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg"
47/// );
48/// assert_eq!(
49///     EventId::new_v2_or_v3("Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg").unwrap(),
50///     "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg"
51/// );
52/// ```
53///
54/// [event ID]: https://spec.matrix.org/v1.19/appendices/#event-ids
55/// [room versions]: https://spec.matrix.org/v1.19/rooms/
56#[repr(transparent)]
57#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdDst)]
58#[ruma_id(validate = ruma_identifiers_validation::event_id::validate, smallvec_inline_bytes = 48)]
59pub struct EventId(str);
60
61impl EventId {
62    /// Attempts to generate an `OwnedEventId` for the given origin server with a localpart
63    /// consisting of 18 random ASCII characters.
64    ///
65    /// This generates an event ID matching the [`EventIdFormatVersion::V1`] variant of the
66    /// `event_id_format` field of [`RoomVersionRules`]. To construct an event ID matching the
67    /// [`EventIdFormatVersion::V2`] or [`EventIdFormatVersion::V3`] variants, use
68    /// [`EventId::new_v2_or_v3()`] instead.
69    ///
70    /// [`EventIdFormatVersion::V1`]: crate::room_version_rules::EventIdFormatVersion::V1
71    /// [`EventIdFormatVersion::V2`]: crate::room_version_rules::EventIdFormatVersion::V2
72    /// [`EventIdFormatVersion::V3`]: crate::room_version_rules::EventIdFormatVersion::V3
73    /// [`RoomVersionRules`]: crate::room_version_rules::RoomVersionRules
74    #[cfg(feature = "rand")]
75    #[allow(clippy::new_ret_no_self)]
76    pub fn new_v1(server_name: &ServerName) -> OwnedEventId {
77        OwnedEventId::from_string_unchecked(format!(
78            "${}:{server_name}",
79            super::generate_localpart(18)
80        ))
81    }
82
83    /// Construct an `OwnedEventId` using the reference hash of the event.
84    ///
85    /// This generates a room ID matching the [`EventIdFormatVersion::V2`] or
86    /// [`EventIdFormatVersion::V3`] variants of the `event_id_format` field of
87    /// [`RoomVersionRules`]. To construct an event ID matching the [`EventIdFormatVersion::V1`]
88    /// variant, use [`EventId::new_v1()`] instead.
89    ///
90    /// Returns an error if the given string contains a NUL byte or is too long.
91    ///
92    /// [`EventIdFormatVersion::V1`]: crate::room_version_rules::EventIdFormatVersion::V1
93    /// [`EventIdFormatVersion::V2`]: crate::room_version_rules::EventIdFormatVersion::V2
94    /// [`EventIdFormatVersion::V3`]: crate::room_version_rules::EventIdFormatVersion::V3
95    /// [`RoomVersionRules`]: crate::room_version_rules::RoomVersionRules
96    pub fn new_v2_or_v3(reference_hash: &str) -> Result<OwnedEventId, IdParseError> {
97        OwnedEventId::try_from(format!("${reference_hash}"))
98    }
99
100    /// Returns the event's unique ID.
101    ///
102    /// For the original event format as used by Matrix room versions 1 and 2, this is the
103    /// "localpart" that precedes the homeserver. For later formats, this is the entire ID without
104    /// the leading `$` sigil.
105    pub fn localpart(&self) -> &str {
106        super::find_localpart(self.as_str())
107    }
108
109    /// Returns the server name of the event ID.
110    ///
111    /// Only applicable to events in the original format as used by Matrix room versions 1 and 2.
112    pub fn server_name(&self) -> Option<&ServerName> {
113        super::find_server_name_unchecked(self.as_str())
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::{EventId, OwnedEventId};
120    use crate::IdParseError;
121
122    #[test]
123    fn valid_original_event_id() {
124        assert_eq!(
125            <&EventId>::try_from("$39hvsi03hlne:example.com").expect("Failed to create EventId."),
126            "$39hvsi03hlne:example.com"
127        );
128    }
129
130    #[test]
131    fn valid_base64_event_id() {
132        assert_eq!(
133            <&EventId>::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk")
134                .expect("Failed to create EventId."),
135            "$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk"
136        );
137    }
138
139    #[test]
140    fn valid_url_safe_base64_event_id() {
141        assert_eq!(
142            <&EventId>::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg")
143                .expect("Failed to create EventId."),
144            "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg"
145        );
146    }
147
148    #[cfg(feature = "rand")]
149    #[test]
150    fn generate_random_valid_event_id() {
151        use crate::server_name;
152
153        let server_name = server_name!("example.com");
154        let event_id = EventId::new_v1(server_name);
155        let id_str = event_id.as_str();
156
157        assert!(id_str.starts_with('$'));
158        assert_eq!(id_str.len(), 31);
159        assert_eq!(event_id.server_name(), Some(server_name));
160    }
161
162    #[test]
163    fn serialize_valid_original_event_id() {
164        assert_eq!(
165            serde_json::to_string(
166                <&EventId>::try_from("$39hvsi03hlne:example.com")
167                    .expect("Failed to create EventId.")
168            )
169            .expect("Failed to convert EventId to JSON."),
170            r#""$39hvsi03hlne:example.com""#
171        );
172    }
173
174    #[test]
175    fn serialize_valid_base64_event_id() {
176        assert_eq!(
177            serde_json::to_string(
178                <&EventId>::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk")
179                    .expect("Failed to create EventId.")
180            )
181            .expect("Failed to convert EventId to JSON."),
182            r#""$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk""#
183        );
184    }
185
186    #[test]
187    fn serialize_valid_url_safe_base64_event_id() {
188        assert_eq!(
189            serde_json::to_string(
190                <&EventId>::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg")
191                    .expect("Failed to create EventId.")
192            )
193            .expect("Failed to convert EventId to JSON."),
194            r#""$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg""#
195        );
196    }
197
198    #[test]
199    fn deserialize_valid_original_event_id() {
200        assert_eq!(
201            serde_json::from_str::<OwnedEventId>(r#""$39hvsi03hlne:example.com""#)
202                .expect("Failed to convert JSON to EventId"),
203            <&EventId>::try_from("$39hvsi03hlne:example.com").expect("Failed to create EventId.")
204        );
205    }
206
207    #[test]
208    fn deserialize_valid_base64_event_id() {
209        assert_eq!(
210            serde_json::from_str::<OwnedEventId>(
211                r#""$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk""#
212            )
213            .expect("Failed to convert JSON to EventId"),
214            <&EventId>::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk")
215                .expect("Failed to create EventId.")
216        );
217    }
218
219    #[test]
220    fn deserialize_valid_url_safe_base64_event_id() {
221        assert_eq!(
222            serde_json::from_str::<OwnedEventId>(
223                r#""$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg""#
224            )
225            .expect("Failed to convert JSON to EventId"),
226            <&EventId>::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg")
227                .expect("Failed to create EventId.")
228        );
229    }
230
231    #[test]
232    fn valid_original_event_id_with_explicit_standard_port() {
233        assert_eq!(
234            <&EventId>::try_from("$39hvsi03hlne:example.com:443")
235                .expect("Failed to create EventId."),
236            "$39hvsi03hlne:example.com:443"
237        );
238    }
239
240    #[test]
241    fn valid_original_event_id_with_non_standard_port() {
242        assert_eq!(
243            <&EventId>::try_from("$39hvsi03hlne:example.com:5000")
244                .expect("Failed to create EventId."),
245            "$39hvsi03hlne:example.com:5000"
246        );
247    }
248
249    #[test]
250    fn missing_original_event_id_sigil() {
251        assert_eq!(
252            <&EventId>::try_from("39hvsi03hlne:example.com").unwrap_err(),
253            IdParseError::MissingLeadingSigil
254        );
255    }
256
257    #[test]
258    fn missing_base64_event_id_sigil() {
259        assert_eq!(
260            <&EventId>::try_from("acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk").unwrap_err(),
261            IdParseError::MissingLeadingSigil
262        );
263    }
264
265    #[test]
266    fn missing_url_safe_base64_event_id_sigil() {
267        assert_eq!(
268            <&EventId>::try_from("Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg").unwrap_err(),
269            IdParseError::MissingLeadingSigil
270        );
271    }
272
273    #[test]
274    fn invalid_event_id_host() {
275        assert_eq!(
276            <&EventId>::try_from("$39hvsi03hlne:/").unwrap_err(),
277            IdParseError::InvalidServerName
278        );
279    }
280
281    #[test]
282    fn invalid_event_id_port() {
283        assert_eq!(
284            <&EventId>::try_from("$39hvsi03hlne:example.com:notaport").unwrap_err(),
285            IdParseError::InvalidServerName
286        );
287    }
288
289    #[test]
290    fn construct_v2_or_v3_event_id() {
291        assert_eq!(
292            EventId::new_v2_or_v3("Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg").unwrap(),
293            "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg"
294        );
295    }
296}