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            "$39hvsi03hlne:example.com"
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            "$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk"
215        );
216    }
217
218    #[test]
219    fn deserialize_valid_url_safe_base64_event_id() {
220        assert_eq!(
221            serde_json::from_str::<OwnedEventId>(
222                r#""$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg""#
223            )
224            .expect("Failed to convert JSON to EventId"),
225            "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg"
226        );
227    }
228
229    #[test]
230    fn valid_original_event_id_with_explicit_standard_port() {
231        assert_eq!(
232            <&EventId>::try_from("$39hvsi03hlne:example.com:443")
233                .expect("Failed to create EventId."),
234            "$39hvsi03hlne:example.com:443"
235        );
236    }
237
238    #[test]
239    fn valid_original_event_id_with_non_standard_port() {
240        assert_eq!(
241            <&EventId>::try_from("$39hvsi03hlne:example.com:5000")
242                .expect("Failed to create EventId."),
243            "$39hvsi03hlne:example.com:5000"
244        );
245    }
246
247    #[test]
248    fn missing_original_event_id_sigil() {
249        assert_eq!(
250            <&EventId>::try_from("39hvsi03hlne:example.com").unwrap_err(),
251            IdParseError::MissingLeadingSigil
252        );
253    }
254
255    #[test]
256    fn missing_base64_event_id_sigil() {
257        assert_eq!(
258            <&EventId>::try_from("acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk").unwrap_err(),
259            IdParseError::MissingLeadingSigil
260        );
261    }
262
263    #[test]
264    fn missing_url_safe_base64_event_id_sigil() {
265        assert_eq!(
266            <&EventId>::try_from("Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg").unwrap_err(),
267            IdParseError::MissingLeadingSigil
268        );
269    }
270
271    #[test]
272    fn invalid_event_id_host() {
273        assert_eq!(
274            <&EventId>::try_from("$39hvsi03hlne:/").unwrap_err(),
275            IdParseError::InvalidServerName
276        );
277    }
278
279    #[test]
280    fn invalid_event_id_port() {
281        assert_eq!(
282            <&EventId>::try_from("$39hvsi03hlne:example.com:notaport").unwrap_err(),
283            IdParseError::InvalidServerName
284        );
285    }
286
287    #[test]
288    fn construct_v2_or_v3_event_id() {
289        assert_eq!(
290            EventId::new_v2_or_v3("Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg").unwrap(),
291            "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg"
292        );
293    }
294}